feat(authFiles): add disable cooling feature with related UI and logic updates

This commit is contained in:
Supra4E8C
2026-08-01 17:18:56 +08:00
parent 7976b16f6c
commit 6394584b99
9 changed files with 113 additions and 0 deletions
@@ -208,6 +208,16 @@ export function AuthFileDetailsSheet(props: AuthFileDetailsSheetProps) {
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('weight', e.target.value)}
/>
<div className="form-group">
<label>{t('auth_files.disable_cooling_label')}</label>
<ToggleSwitch
checked={editor.disableCooling}
onChange={(value) => onChange('disableCooling', value)}
disabled={disableControls || editor.saving || !editor.json}
ariaLabel={t('auth_files.disable_cooling_label')}
/>
<div className="hint">{t('auth_files.disable_cooling_hint')}</div>
</div>
{supportsAuthFileWebsockets(editor.providerKey) && (
<div className="form-group">
<label>{t('auth_files.websockets_label')}</label>
+6
View File
@@ -194,6 +194,12 @@ export const parseDisableCoolingValue = (value: unknown): boolean | undefined =>
return undefined;
};
export const readAuthFileDisableCooling = (value: Record<string, unknown>): boolean => {
const canonical = parseDisableCoolingValue(value.disable_cooling);
if (canonical !== undefined) return canonical;
return parseDisableCoolingValue(value['disable-cooling']) ?? false;
};
export const supportsAuthFileWebsockets = (providerKey: string): boolean =>
AUTH_FILE_WEBSOCKET_PROVIDERS.has(normalizeProviderKey(providerKey));
@@ -8,6 +8,7 @@ import {
applyAuthFileUsingApi,
normalizeProviderKey,
parsePriorityValue,
readAuthFileDisableCooling,
readAuthFileWebsockets,
readAuthFileUsingApi,
supportsAuthFileWebsockets,
@@ -35,6 +36,7 @@ export type PrefixProxyEditorField =
| 'proxyUrl'
| 'priority'
| 'weight'
| 'disableCooling'
| 'websockets'
| 'usingApi'
| 'note'
@@ -59,6 +61,8 @@ export type PrefixProxyEditorState = {
priority: string;
weight: string;
weightError: string | null;
disableCooling: boolean;
disableCoolingTouched: boolean;
websockets: boolean;
websocketsTouched: boolean;
usingApi: boolean;
@@ -313,6 +317,18 @@ export const buildAuthFileFieldsPatch = (
patch.weight = nextWeight;
}
if (editor.disableCoolingTouched) {
const originalDisableCooling = readAuthFileDisableCooling(original);
const nextDisableCooling = Boolean(editor.disableCooling);
if (nextDisableCooling !== originalDisableCooling) {
const field =
original.disable_cooling === undefined && original['disable-cooling'] !== undefined
? 'disable-cooling'
: 'disable_cooling';
patch[field] = nextDisableCooling;
}
}
if (editor.noteTouched) {
const originalNote = normalizeTextField(original.note);
const nextNote = editor.note.trim();
@@ -400,6 +416,13 @@ const buildPrefixProxyUpdatedText = (
}
}
if (patch.disable_cooling !== undefined) {
next.disable_cooling = patch.disable_cooling;
}
if (patch['disable-cooling'] !== undefined) {
next['disable-cooling'] = patch['disable-cooling'];
}
if (patch.note !== undefined) {
if (patch.note) {
next.note = patch.note;
@@ -483,6 +506,8 @@ export function useAuthFilesPrefixProxyEditor(
priority: '',
weight: '',
weightError: null,
disableCooling: false,
disableCoolingTouched: false,
websockets: false,
websocketsTouched: false,
usingApi: false,
@@ -534,6 +559,7 @@ export function useAuthFilesPrefixProxyEditor(
const proxyUrl = typeof json.proxy_url === 'string' ? json.proxy_url : '';
const priority = parsePriorityValue(json.priority);
const weight = readCredentialWeight(json.weight);
const disableCooling = readAuthFileDisableCooling(json);
const websockets = supportsAuthFileWebsockets(providerKey)
? readAuthFileWebsockets(json)
: false;
@@ -564,6 +590,8 @@ export function useAuthFilesPrefixProxyEditor(
priority: priority !== undefined ? String(priority) : '',
weight: weight !== undefined ? String(weight) : '',
weightError: null,
disableCooling,
disableCoolingTouched: false,
websockets,
websocketsTouched: false,
usingApi,
@@ -606,6 +634,13 @@ export function useAuthFilesPrefixProxyEditor(
weightError: error ? t(credentialWeightErrorKey(error)) : null,
};
}
if (field === 'disableCooling') {
return {
...prev,
disableCooling: Boolean(value),
disableCoolingTouched: true,
};
}
if (field === 'websockets') {
return { ...prev, websockets: Boolean(value), websocketsTouched: true };
}
+2
View File
@@ -343,6 +343,8 @@
"weight_hint": "Defaults to 1. Values at or below 0 exclude this credential from weighted scheduling; maximum 1,000,000.",
"weight_invalid_integer": "Weight must be an integer.",
"weight_invalid_max": "Weight cannot exceed 1,000,000.",
"disable_cooling_label": "Disable exponential cooldown (disable_cooling)",
"disable_cooling_hint": "Skip failure cooldown and exponential backoff only for this credential; other credentials are unaffected.",
"websockets_label": "WebSockets (websockets)",
"websockets_hint": "Enable Responses API websocket transport for this credential.",
"using_api_label": "Use official API (using_api)",
+2
View File
@@ -342,6 +342,8 @@
"weight_hint": "По умолчанию 1. Значения ≤ 0 исключают эти учётные данные из взвешенного планирования; максимум 1 000 000.",
"weight_invalid_integer": "Вес должен быть целым числом.",
"weight_invalid_max": "Вес не может превышать 1 000 000.",
"disable_cooling_label": "Отключить экспоненциальный cooldown (disable_cooling)",
"disable_cooling_hint": "Отключает cooldown после ошибок и экспоненциальную задержку только для этих учётных данных; остальные не затрагиваются.",
"websockets_label": "WebSockets (websockets)",
"websockets_hint": "Включает websocket-транспорт Responses API для этих учётных данных.",
"using_api_label": "Использовать официальный API (using_api)",
+2
View File
@@ -343,6 +343,8 @@
"weight_hint": "默认值为 1;小于或等于 0 时不参与加权调度;最大值为 1,000,000。",
"weight_invalid_integer": "权重必须是整数。",
"weight_invalid_max": "权重不能超过 1,000,000。",
"disable_cooling_label": "禁用指数冷却(disable_cooling",
"disable_cooling_hint": "开启后,仅当前凭证跳过失败后的冷却与指数退避;其他凭证不受影响。",
"websockets_label": "WebSocketswebsockets",
"websockets_hint": "为该凭证开启 Responses API 的 websocket 传输。",
"using_api_label": "使用官方 APIusing_api",
+2
View File
@@ -343,6 +343,8 @@
"weight_hint": "預設值為 1;小於或等於 0 時不參與加權調度;最大值為 1,000,000。",
"weight_invalid_integer": "權重必須是整數。",
"weight_invalid_max": "權重不能超過 1,000,000。",
"disable_cooling_label": "停用指數冷卻(disable_cooling",
"disable_cooling_hint": "開啟後,僅目前憑證會略過失敗後的冷卻與指數退避;其他憑證不受影響。",
"websockets_label": "WebSocketswebsockets",
"websockets_hint": "為該憑證開啟 Responses API 的 websocket 傳輸。",
"using_api_label": "使用官方 APIusing_api",
+2
View File
@@ -22,6 +22,8 @@ export type AuthFileFieldsPatch = {
headers?: Record<string, string>;
priority?: number;
weight?: number | null;
disable_cooling?: boolean;
'disable-cooling'?: boolean;
websockets?: boolean;
using_api?: boolean;
note?: string;
+52
View File
@@ -3,6 +3,7 @@ import {
buildAuthFileFieldsPatch,
type PrefixProxyEditorState,
} from '../src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
import { readAuthFileDisableCooling } from '../src/features/authFiles/constants';
const makeEditor = (json: Record<string, unknown>, weight: string): PrefixProxyEditorState => ({
fileName: 'credential.json',
@@ -20,6 +21,8 @@ const makeEditor = (json: Record<string, unknown>, weight: string): PrefixProxyE
priority: '',
weight,
weightError: null,
disableCooling: false,
disableCoolingTouched: false,
websockets: false,
websocketsTouched: false,
usingApi: false,
@@ -60,6 +63,55 @@ describe('auth-file credential weight patch', () => {
});
});
describe('auth-file disable cooling patch', () => {
test('reads canonical and legacy boolean-compatible metadata', () => {
expect(readAuthFileDisableCooling({ disable_cooling: 'true' })).toBe(true);
expect(readAuthFileDisableCooling({ 'disable-cooling': 1 })).toBe(true);
expect(
readAuthFileDisableCooling({ disable_cooling: 'invalid', 'disable-cooling': true })
).toBe(true);
expect(readAuthFileDisableCooling({ disable_cooling: false, 'disable-cooling': true })).toBe(
false
);
});
test('writes the canonical field when enabling the per-credential override', () => {
const editor = {
...makeEditor({}, ''),
disableCooling: true,
disableCoolingTouched: true,
};
expect(buildAuthFileFieldsPatch(editor, resolveError)).toEqual({ disable_cooling: true });
});
test('preserves the legacy field name and writes false when disabling it', () => {
const editor = {
...makeEditor({ 'disable-cooling': true }, ''),
disableCooling: false,
disableCoolingTouched: true,
};
expect(buildAuthFileFieldsPatch(editor, resolveError)).toEqual({ 'disable-cooling': false });
});
test('does not patch an untouched or unchanged override', () => {
expect(buildAuthFileFieldsPatch(makeEditor({ disable_cooling: true }, ''), resolveError)).toEqual(
{}
);
expect(
buildAuthFileFieldsPatch(
{
...makeEditor({ disable_cooling: 'true' }, ''),
disableCooling: true,
disableCoolingTouched: true,
},
resolveError
)
).toEqual({});
});
});
describe('auth-file excluded models patch', () => {
test('writes normalized model patterns to the canonical field', () => {
const editor = {