diff --git a/src/features/authFiles/components/AuthFileDetailsSheet.tsx b/src/features/authFiles/components/AuthFileDetailsSheet.tsx index 5ca106ca..b162d3ca 100644 --- a/src/features/authFiles/components/AuthFileDetailsSheet.tsx +++ b/src/features/authFiles/components/AuthFileDetailsSheet.tsx @@ -208,6 +208,16 @@ export function AuthFileDetailsSheet(props: AuthFileDetailsSheetProps) { disabled={disableControls || editor.saving || !editor.json} onChange={(e) => onChange('weight', e.target.value)} /> +
+ + onChange('disableCooling', value)} + disabled={disableControls || editor.saving || !editor.json} + ariaLabel={t('auth_files.disable_cooling_label')} + /> +
{t('auth_files.disable_cooling_hint')}
+
{supportsAuthFileWebsockets(editor.providerKey) && (
diff --git a/src/features/authFiles/constants.ts b/src/features/authFiles/constants.ts index be1db86c..8d72494a 100644 --- a/src/features/authFiles/constants.ts +++ b/src/features/authFiles/constants.ts @@ -194,6 +194,12 @@ export const parseDisableCoolingValue = (value: unknown): boolean | undefined => return undefined; }; +export const readAuthFileDisableCooling = (value: Record): 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)); diff --git a/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts b/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts index 8a25244b..d6f286f2 100644 --- a/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts +++ b/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts @@ -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 }; } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 03755f8e..a0cf1d78 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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)", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index f3c9fe71..5389368e 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -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)", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index f68998ae..859ec319 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -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": "WebSockets(websockets)", "websockets_hint": "为该凭证开启 Responses API 的 websocket 传输。", "using_api_label": "使用官方 API(using_api)", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index f17318c2..d7c90561 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -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": "WebSockets(websockets)", "websockets_hint": "為該憑證開啟 Responses API 的 websocket 傳輸。", "using_api_label": "使用官方 API(using_api)", diff --git a/src/services/api/authFiles.ts b/src/services/api/authFiles.ts index 8a0121a8..74f67c2c 100644 --- a/src/services/api/authFiles.ts +++ b/src/services/api/authFiles.ts @@ -22,6 +22,8 @@ export type AuthFileFieldsPatch = { headers?: Record; priority?: number; weight?: number | null; + disable_cooling?: boolean; + 'disable-cooling'?: boolean; websockets?: boolean; using_api?: boolean; note?: string; diff --git a/tests/authFileWeight.test.ts b/tests/authFileWeight.test.ts index 79f2cdc5..3ef49064 100644 --- a/tests/authFileWeight.test.ts +++ b/tests/authFileWeight.test.ts @@ -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, weight: string): PrefixProxyEditorState => ({ fileName: 'credential.json', @@ -20,6 +21,8 @@ const makeEditor = (json: Record, 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 = {