feat(provider): add support for disable cooling feature and enhance model configuration options

This commit is contained in:
LTbinglingfeng
2026-06-13 06:02:15 +08:00
Unverified
parent b75d4084aa
commit e62ed4dbac
10 changed files with 326 additions and 60 deletions
@@ -56,6 +56,11 @@ const emptyApiKeyEntry = (): ApiKeyEntryInput => ({
const stripDisableAllRule = (list?: string[]): string[] => const stripDisableAllRule = (list?: string[]): string[] =>
(list ?? []).filter((s) => s.trim() !== '*'); (list ?? []).filter((s) => s.trim() !== '*');
const formatJsonObject = (value?: Record<string, unknown>): string => {
if (!value || Object.keys(value).length === 0) return '';
return JSON.stringify(value, null, 2);
};
function buildInitialForm( function buildInitialForm(
brand: Exclude<ProviderBrand, 'ampcode'>, brand: Exclude<ProviderBrand, 'ampcode'>,
resource: ProviderResource | null, resource: ProviderResource | null,
@@ -69,13 +74,17 @@ function buildInitialForm(
proxyUrl: '', proxyUrl: '',
prefix: '', prefix: '',
disabled: false, disabled: false,
disableCooling: false,
priority: undefined, priority: undefined,
models: [emptyModel()], models: [emptyModel()],
headers: [emptyHeader()], headers: [emptyHeader()],
excludedModelsText: '', excludedModelsText: '',
websockets: brand === 'codex' ? false : undefined, websockets: brand === 'codex' ? false : undefined,
cloak: cloak:
brand === 'claude' ? { mode: '', strictMode: false, sensitiveWordsText: '' } : undefined, brand === 'claude'
? { mode: '', strictMode: false, sensitiveWordsText: '', cacheUserId: false }
: undefined,
experimentalCchSigning: brand === 'claude' ? false : undefined,
testModel: testModel:
brand === 'openaiCompatibility' || brand === 'claude' || brand === 'gemini' brand === 'openaiCompatibility' || brand === 'claude' || brand === 'gemini'
? '' ? ''
@@ -94,6 +103,7 @@ function buildInitialForm(
proxyUrl: '', proxyUrl: '',
prefix: cfg.prefix ?? '', prefix: cfg.prefix ?? '',
disabled: cfg.disabled === true, disabled: cfg.disabled === true,
disableCooling: cfg.disableCooling === true,
priority: cfg.priority, priority: cfg.priority,
models: cfg.models?.length models: cfg.models?.length
? cfg.models.map((m) => ({ ? cfg.models.map((m) => ({
@@ -101,6 +111,8 @@ function buildInitialForm(
alias: m.alias ?? '', alias: m.alias ?? '',
priority: m.priority, priority: m.priority,
testModel: m.testModel, testModel: m.testModel,
image: m.image === true,
thinkingJson: formatJsonObject(m.thinking),
})) }))
: [emptyModel()], : [emptyModel()],
headers: cfg.headers headers: cfg.headers
@@ -133,6 +145,7 @@ function buildInitialForm(
proxyUrl: cfg.proxyUrl ?? '', proxyUrl: cfg.proxyUrl ?? '',
prefix: cfg.prefix ?? '', prefix: cfg.prefix ?? '',
disabled, disabled,
disableCooling: cfg.disableCooling === true,
priority: cfg.priority, priority: cfg.priority,
models: cfg.models?.length models: cfg.models?.length
? cfg.models.map((m) => ({ ? cfg.models.map((m) => ({
@@ -153,8 +166,13 @@ function buildInitialForm(
mode: (cfg as ProviderKeyConfig).cloak?.mode ?? '', mode: (cfg as ProviderKeyConfig).cloak?.mode ?? '',
strictMode: (cfg as ProviderKeyConfig).cloak?.strictMode === true, strictMode: (cfg as ProviderKeyConfig).cloak?.strictMode === true,
sensitiveWordsText: (cfg as ProviderKeyConfig).cloak?.sensitiveWords?.join('\n') ?? '', sensitiveWordsText: (cfg as ProviderKeyConfig).cloak?.sensitiveWords?.join('\n') ?? '',
cacheUserId: (cfg as ProviderKeyConfig).cloak?.cacheUserId === true,
} }
: undefined, : undefined,
experimentalCchSigning:
brand === 'claude'
? (cfg as ProviderKeyConfig).experimentalCchSigning === true
: undefined,
testModel: brand === 'claude' || brand === 'gemini' ? '' : undefined, testModel: brand === 'claude' || brand === 'gemini' ? '' : undefined,
}; };
} }
@@ -368,7 +386,12 @@ export function BaseProviderForm({
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
cloak: { cloak: {
...(prev.cloak ?? { mode: '', strictMode: false, sensitiveWordsText: '' }), ...(prev.cloak ?? {
mode: '',
strictMode: false,
sensitiveWordsText: '',
cacheUserId: false,
}),
[key]: value, [key]: value,
}, },
})); }));
@@ -418,6 +441,12 @@ export function BaseProviderForm({
[form.apiKeyEntries] [form.apiKeyEntries]
); );
const actualApiKeyEntries = form.apiKeyEntries ?? []; const actualApiKeyEntries = form.apiKeyEntries ?? [];
const supportsDisableCooling =
brand === 'gemini' ||
brand === 'codex' ||
brand === 'claude' ||
brand === 'openaiCompatibility';
const supportsOpenAIModelOptions = brand === 'openaiCompatibility';
const singleConnectivity = const singleConnectivity =
brand === 'gemini' brand === 'gemini'
? { status: connectivity.geminiStatus, run: connectivity.runGemini } ? { status: connectivity.geminiStatus, run: connectivity.runGemini }
@@ -444,6 +473,20 @@ export function BaseProviderForm({
); );
}; };
const updateModelEntry = (idx: number, patch: Partial<ModelEntryInput>) => {
updateField(
'models',
modelsList.map((it, i) => (i === idx ? { ...it, ...patch } : it))
);
};
const removeModelEntry = (idx: number) => {
updateField(
'models',
modelsList.filter((_, i) => i !== idx)
);
};
return ( return (
<form id={formId} className={styles.form} onSubmit={handleSubmit} noValidate> <form id={formId} className={styles.form} onSubmit={handleSubmit} noValidate>
{/* 基础字段 */} {/* 基础字段 */}
@@ -661,6 +704,22 @@ export function BaseProviderForm({
</span> </span>
</label> </label>
) : null} ) : null}
{supportsDisableCooling ? (
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={form.disableCooling ?? false}
disabled={mutating}
onChange={(e) => updateField('disableCooling', e.target.checked)}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.disableCooling')}</span>
<small>{t('providersPage.form.disableCoolingHint')}</small>
</span>
</label>
) : null}
</div> </div>
{/* 高级折叠区 */} {/* 高级折叠区 */}
@@ -906,50 +965,97 @@ export function BaseProviderForm({
onClose={closeDiscovery} onClose={closeDiscovery}
/> />
) : null} ) : null}
{modelsList.map((entry, idx) => ( {modelsList.map((entry, idx) =>
<div supportsOpenAIModelOptions ? (
key={idx} <div key={idx} className={styles.entryCard}>
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 8 }} <div className={styles.entryCardHeader}>
> <span>{t('providersPage.form.modelEntry', { index: idx + 1 })}</span>
<input <button
className={styles.input} type="button"
placeholder="model-name" className={styles.removeBtn}
value={entry.name} disabled={mutating || modelsList.length <= 1}
onChange={(e) => onClick={() => removeModelEntry(idx)}
updateField( >
'models', <IconX size={12} />
modelsList.map((it, i) => (i === idx ? { ...it, name: e.target.value } : it)) </button>
) </div>
} <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
disabled={mutating} <input
/> className={styles.input}
<input placeholder="model-name"
className={styles.input} value={entry.name}
placeholder="alias (optional)" onChange={(e) => updateModelEntry(idx, { name: e.target.value })}
value={entry.alias ?? ''} disabled={mutating}
onChange={(e) => />
updateField( <input
'models', className={styles.input}
modelsList.map((it, i) => (i === idx ? { ...it, alias: e.target.value } : it)) placeholder="alias (optional)"
) value={entry.alias ?? ''}
} onChange={(e) => updateModelEntry(idx, { alias: e.target.value })}
disabled={mutating} disabled={mutating}
/> />
<button </div>
type="button" <label className={styles.checkboxRow}>
className={styles.removeBtn} <input
disabled={mutating || modelsList.length <= 1} type="checkbox"
onClick={() => className={styles.checkboxBox}
updateField( checked={entry.image === true}
'models', disabled={mutating}
modelsList.filter((_, i) => i !== idx) onChange={(e) => updateModelEntry(idx, { image: e.target.checked })}
) />
} <span className={styles.checkboxText}>
<span>{t('providersPage.form.modelImage')}</span>
<small>{t('providersPage.form.modelImageHint')}</small>
</span>
</label>
<div className={styles.field}>
<label className={styles.label}>
{t('providersPage.form.thinkingConfig')}
<span className={styles.labelHint}>
{' '}
· {t('providersPage.form.thinkingConfigHint')}
</span>
</label>
<textarea
className={styles.textarea}
rows={4}
value={entry.thinkingJson ?? ''}
onChange={(e) => updateModelEntry(idx, { thinkingJson: e.target.value })}
disabled={mutating}
placeholder={'{"levels":["low","medium","high"]}'}
/>
</div>
</div>
) : (
<div
key={idx}
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 8 }}
> >
<IconX size={12} /> <input
</button> className={styles.input}
</div> placeholder="model-name"
))} value={entry.name}
onChange={(e) => updateModelEntry(idx, { name: e.target.value })}
disabled={mutating}
/>
<input
className={styles.input}
placeholder="alias (optional)"
value={entry.alias ?? ''}
onChange={(e) => updateModelEntry(idx, { alias: e.target.value })}
disabled={mutating}
/>
<button
type="button"
className={styles.removeBtn}
disabled={mutating || modelsList.length <= 1}
onClick={() => removeModelEntry(idx)}
>
<IconX size={12} />
</button>
</div>
)
)}
<button <button
type="button" type="button"
className={styles.addBtn} className={styles.addBtn}
@@ -1004,6 +1110,32 @@ export function BaseProviderForm({
<span>{t('providersPage.form.cloakStrict')}</span> <span>{t('providersPage.form.cloakStrict')}</span>
</span> </span>
</label> </label>
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={form.cloak.cacheUserId}
disabled={mutating}
onChange={(e) => updateCloak('cacheUserId', e.target.checked)}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.cloakCacheUserId')}</span>
<small>{t('providersPage.form.cloakCacheUserIdHint')}</small>
</span>
</label>
<label className={styles.checkboxRow}>
<input
type="checkbox"
className={styles.checkboxBox}
checked={form.experimentalCchSigning ?? false}
disabled={mutating}
onChange={(e) => updateField('experimentalCchSigning', e.target.checked)}
/>
<span className={styles.checkboxText}>
<span>{t('providersPage.form.experimentalCchSigning')}</span>
<small>{t('providersPage.form.experimentalCchSigningHint')}</small>
</span>
</label>
<div className={styles.field}> <div className={styles.field}>
<label className={styles.label}>{t('providersPage.form.cloakSensitiveWords')}</label> <label className={styles.label}>{t('providersPage.form.cloakSensitiveWords')}</label>
<textarea <textarea
+5
View File
@@ -87,6 +87,8 @@ export interface ModelEntryInput {
alias?: string; alias?: string;
priority?: number; priority?: number;
testModel?: string; testModel?: string;
image?: boolean;
thinkingJson?: string;
} }
export interface ApiKeyEntryInput { export interface ApiKeyEntryInput {
@@ -100,6 +102,7 @@ export interface CloakInput {
mode: string; mode: string;
strictMode: boolean; strictMode: boolean;
sensitiveWordsText: string; sensitiveWordsText: string;
cacheUserId: boolean;
} }
export interface ProviderEntryFormInput { export interface ProviderEntryFormInput {
@@ -111,6 +114,7 @@ export interface ProviderEntryFormInput {
proxyUrl: string; proxyUrl: string;
prefix: string; prefix: string;
disabled: boolean; disabled: boolean;
disableCooling?: boolean;
priority?: number; priority?: number;
/** 高级折叠区 */ /** 高级折叠区 */
@@ -122,6 +126,7 @@ export interface ProviderEntryFormInput {
websockets?: boolean; websockets?: boolean;
/** Claude 专属 */ /** Claude 专属 */
cloak?: CloakInput; cloak?: CloakInput;
experimentalCchSigning?: boolean;
/** OpenAI persists this; Gemini/Claude use it for one-off connectivity tests. */ /** OpenAI persists this; Gemini/Claude use it for one-off connectivity tests. */
testModel?: string; testModel?: string;
apiKeyEntries?: ApiKeyEntryInput[]; apiKeyEntries?: ApiKeyEntryInput[];
@@ -69,6 +69,16 @@ const headersFromEntries = (
return out; return out;
}; };
const parseThinkingJson = (value: string | undefined): Record<string, unknown> | undefined => {
const trimmed = (value ?? '').trim();
if (!trimmed) return undefined;
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Thinking config must be a JSON object');
}
return parsed as Record<string, unknown>;
};
const buildExcludedModels = ( const buildExcludedModels = (
textValue: string, textValue: string,
disabled: boolean, disabled: boolean,
@@ -110,6 +120,7 @@ const buildProviderKeyConfig = (
models: models.length ? models : undefined, models: models.length ? models : undefined,
headers: Object.keys(headers).length ? headers : undefined, headers: Object.keys(headers).length ? headers : undefined,
excludedModels: excluded, excludedModels: excluded,
disableCooling: input.disableCooling === true,
authIndex: existing?.authIndex, authIndex: existing?.authIndex,
}; };
if (brand === 'codex' && input.websockets !== undefined) { if (brand === 'codex' && input.websockets !== undefined) {
@@ -120,8 +131,12 @@ const buildProviderKeyConfig = (
mode: input.cloak.mode.trim() || undefined, mode: input.cloak.mode.trim() || undefined,
strictMode: input.cloak.strictMode, strictMode: input.cloak.strictMode,
sensitiveWords: parseTextList(input.cloak.sensitiveWordsText), sensitiveWords: parseTextList(input.cloak.sensitiveWordsText),
cacheUserId: input.cloak.cacheUserId === true,
}; };
} }
if (brand === 'claude') {
next.experimentalCchSigning = input.experimentalCchSigning === true;
}
return next; return next;
}; };
@@ -136,6 +151,8 @@ const buildOpenAIConfig = (
alias: m.alias?.trim() || undefined, alias: m.alias?.trim() || undefined,
priority: m.priority, priority: m.priority,
testModel: m.testModel, testModel: m.testModel,
image: m.image === true,
thinking: parseThinkingJson(m.thinkingJson),
})) }))
.filter((m) => m.name); .filter((m) => m.name);
const apiKeyEntries = const apiKeyEntries =
@@ -158,6 +175,7 @@ const buildOpenAIConfig = (
prefix: input.prefix.trim() || undefined, prefix: input.prefix.trim() || undefined,
apiKeyEntries, apiKeyEntries,
disabled: input.disabled, disabled: input.disabled,
disableCooling: input.disableCooling === true,
headers: Object.keys(headers).length ? headers : undefined, headers: Object.keys(headers).length ? headers : undefined,
models: models.length ? models : undefined, models: models.length ? models : undefined,
priority: input.priority, priority: input.priority,
+11
View File
@@ -1470,6 +1470,17 @@
"addApiKeyEntry": "Add key entry", "addApiKeyEntry": "Add key entry",
"showApiKey": "Show API key", "showApiKey": "Show API key",
"hideApiKey": "Hide API key", "hideApiKey": "Hide API key",
"disableCooling": "Disable cooling",
"disableCoolingHint": "Disable failure cooldown windows only for this credential or provider",
"modelEntry": "Model #{{index}}",
"modelImage": "Allow image endpoints",
"modelImageHint": "Allow this model on /v1/images/generations and /v1/images/edits",
"thinkingConfig": "Thinking config (JSON)",
"thinkingConfigHint": "Supports levels, min, max, zero_allowed, dynamic_allowed",
"cloakCacheUserId": "Cache user_id",
"cloakCacheUserIdHint": "Reuse the Claude cloak user_id per API key",
"experimentalCchSigning": "Experimental CCH signing",
"experimentalCchSigningHint": "Sign the final cloaked Claude /v1/messages body with CCH",
"validation": { "validation": {
"nameRequired": "Name is required", "nameRequired": "Name is required",
"apiKeyRequired": "At least one API key is required", "apiKeyRequired": "At least one API key is required",
+11
View File
@@ -1445,6 +1445,17 @@
"addApiKeyEntry": "Добавить ключ", "addApiKeyEntry": "Добавить ключ",
"showApiKey": "Показать ключ API", "showApiKey": "Показать ключ API",
"hideApiKey": "Скрыть ключ API", "hideApiKey": "Скрыть ключ API",
"disableCooling": "Отключить cooldown",
"disableCoolingHint": "Отключает окна охлаждения после ошибок только для этой записи",
"modelEntry": "Модель #{{index}}",
"modelImage": "Разрешить image endpoints",
"modelImageHint": "Разрешить модель для /v1/images/generations и /v1/images/edits",
"thinkingConfig": "Thinking config (JSON)",
"thinkingConfigHint": "Поддерживает levels, min, max, zero_allowed, dynamic_allowed",
"cloakCacheUserId": "Кэшировать user_id",
"cloakCacheUserIdHint": "Переиспользовать Claude cloak user_id для каждого API-ключа",
"experimentalCchSigning": "Экспериментальная CCH-подпись",
"experimentalCchSigningHint": "Подписывать финальное тело cloaked Claude /v1/messages через CCH",
"validation": { "validation": {
"nameRequired": "Название обязательно", "nameRequired": "Название обязательно",
"apiKeyRequired": "Нужен хотя бы один API-ключ", "apiKeyRequired": "Нужен хотя бы один API-ключ",
+11
View File
@@ -1470,6 +1470,17 @@
"addApiKeyEntry": "添加密钥条目", "addApiKeyEntry": "添加密钥条目",
"showApiKey": "显示密钥", "showApiKey": "显示密钥",
"hideApiKey": "隐藏密钥", "hideApiKey": "隐藏密钥",
"disableCooling": "禁用冷却调度",
"disableCoolingHint": "仅对当前凭据或提供商禁用失败后的冷却窗口",
"modelEntry": "模型 #{{index}}",
"modelImage": "允许图片端点",
"modelImageHint": "允许该模型用于 /v1/images/generations 和 /v1/images/edits",
"thinkingConfig": "Thinking 配置(JSON)",
"thinkingConfigHint": "可配置 levels、min、max、zero_allowed、dynamic_allowed",
"cloakCacheUserId": "缓存 user_id",
"cloakCacheUserIdHint": "按 API key 复用 Claude cloak 生成的 user_id",
"experimentalCchSigning": "实验性 CCH 签名",
"experimentalCchSigningHint": "对 cloaked Claude /v1/messages 最终请求体启用 CCH 签名",
"validation": { "validation": {
"nameRequired": "名称必填", "nameRequired": "名称必填",
"apiKeyRequired": "至少填写一个 API 密钥", "apiKeyRequired": "至少填写一个 API 密钥",
+11
View File
@@ -1496,6 +1496,17 @@
"addApiKeyEntry": "新增金鑰條目", "addApiKeyEntry": "新增金鑰條目",
"showApiKey": "顯示金鑰", "showApiKey": "顯示金鑰",
"hideApiKey": "隱藏金鑰", "hideApiKey": "隱藏金鑰",
"disableCooling": "停用冷卻調度",
"disableCoolingHint": "僅對目前憑證或提供商停用失敗後的冷卻視窗",
"modelEntry": "模型 #{{index}}",
"modelImage": "允許圖片端點",
"modelImageHint": "允許該模型用於 /v1/images/generations 和 /v1/images/edits",
"thinkingConfig": "Thinking 設定(JSON)",
"thinkingConfigHint": "可設定 levels、min、max、zero_allowed、dynamic_allowed",
"cloakCacheUserId": "快取 user_id",
"cloakCacheUserIdHint": "按 API key 複用 Claude cloak 產生的 user_id",
"experimentalCchSigning": "實驗性 CCH 簽名",
"experimentalCchSigningHint": "對 cloaked Claude /v1/messages 最終請求體啟用 CCH 簽名",
"validation": { "validation": {
"nameRequired": "名稱必填", "nameRequired": "名稱必填",
"apiKeyRequired": "至少填寫一個 API 金鑰", "apiKeyRequired": "至少填寫一個 API 金鑰",
+50 -15
View File
@@ -22,23 +22,35 @@ const serializeHeaders = (headers?: Record<string, string>) =>
const RESPONSE_ONLY_FIELDS = ['auth-index'] as const; const RESPONSE_ONLY_FIELDS = ['auth-index'] as const;
const PROVIDER_KEY_FIELDS = [ const PROVIDER_COMMON_KEY_FIELDS = [
'api-key', 'api-key',
'priority', 'priority',
'prefix', 'prefix',
'base-url', 'base-url',
'websockets',
'proxy-url', 'proxy-url',
'headers', 'headers',
'models', 'models',
'excluded-models', 'excluded-models',
'cloak', 'disable-cooling',
] as const; ] as const;
const GEMINI_KEY_FIELDS = PROVIDER_KEY_FIELDS.filter( const GEMINI_KEY_FIELDS = PROVIDER_COMMON_KEY_FIELDS;
(field) => field !== 'websockets' && field !== 'cloak' const CODEX_KEY_FIELDS = [...PROVIDER_COMMON_KEY_FIELDS, 'websockets'] as const;
); const CLAUDE_KEY_FIELDS = [
const VERTEX_KEY_FIELDS = GEMINI_KEY_FIELDS; ...PROVIDER_COMMON_KEY_FIELDS,
'cloak',
'experimental-cch-signing',
] as const;
const VERTEX_KEY_FIELDS = [
'api-key',
'priority',
'prefix',
'base-url',
'proxy-url',
'headers',
'models',
'excluded-models',
] as const;
const OPENAI_PROVIDER_FIELDS = [ const OPENAI_PROVIDER_FIELDS = [
'name', 'name',
@@ -50,13 +62,15 @@ const OPENAI_PROVIDER_FIELDS = [
'headers', 'headers',
'models', 'models',
'test-model', 'test-model',
'disable-cooling',
] as const; ] as const;
const MODEL_ALIAS_FIELDS = ['name', 'alias', 'priority', 'test-model'] as const; const MODEL_ALIAS_FIELDS = ['name', 'alias', 'priority', 'test-model'] as const;
const OPENAI_MODEL_ALIAS_FIELDS = [...MODEL_ALIAS_FIELDS, 'image', 'thinking'] as const;
const API_KEY_ENTRY_FIELDS = ['api-key', 'proxy-url'] as const; const API_KEY_ENTRY_FIELDS = ['api-key', 'proxy-url'] as const;
const CLOAK_FIELDS = ['mode', 'strict-mode', 'sensitive-words'] as const; const CLOAK_FIELDS = ['mode', 'strict-mode', 'sensitive-words', 'cache-user-id'] as const;
const getStringField = (record: Record<string, unknown>, keys: readonly string[]) => { const getStringField = (record: Record<string, unknown>, keys: readonly string[]) => {
for (const key of keys) { for (const key of keys) {
@@ -170,12 +184,16 @@ const getRawSectionList = (rawConfig: unknown, section: string): unknown[] => {
return Array.isArray(value) ? value : []; return Array.isArray(value) ? value : [];
}; };
const mergeModelPayloads = (raw: unknown, models: unknown) => const mergeModelPayloads = (
raw: unknown,
models: unknown,
knownFields: readonly string[] = MODEL_ALIAS_FIELDS
) =>
Array.isArray(models) Array.isArray(models)
? mergeKnownRecordList( ? mergeKnownRecordList(
isRecord(raw) ? raw.models : undefined, isRecord(raw) ? raw.models : undefined,
models.filter(isRecord), models.filter(isRecord),
MODEL_ALIAS_FIELDS, knownFields,
modelIdentity, modelIdentity,
false false
) )
@@ -211,7 +229,7 @@ const mergeOpenAIProviderPayload = (raw: unknown, payload: Record<string, unknow
apiKeyEntryIdentity apiKeyEntryIdentity
); );
} }
const models = mergeModelPayloads(raw, payload.models); const models = mergeModelPayloads(raw, payload.models, OPENAI_MODEL_ALIAS_FIELDS);
if (models) next.models = models; if (models) next.models = models;
return next; return next;
}; };
@@ -252,7 +270,7 @@ const buildProviderDeleteQuery = (apiKey: string, baseUrl?: string) => {
return `?${params.toString()}`; return `?${params.toString()}`;
}; };
const serializeModelAliases = (models?: ModelAlias[]) => const serializeModelAliases = (models?: ModelAlias[], includeOpenAIFields = false) =>
Array.isArray(models) Array.isArray(models)
? models ? models
.map((model) => { .map((model) => {
@@ -267,6 +285,14 @@ const serializeModelAliases = (models?: ModelAlias[]) =>
if (model.testModel) { if (model.testModel) {
payload['test-model'] = model.testModel; payload['test-model'] = model.testModel;
} }
if (includeOpenAIFields) {
if (model.image) {
payload.image = true;
}
if (model.thinking) {
payload.thinking = model.thinking;
}
}
return payload; return payload;
}) })
.filter(Boolean) .filter(Boolean)
@@ -285,6 +311,7 @@ const serializeProviderKey = (config: ProviderKeyConfig) => {
if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.baseUrl) payload['base-url'] = config.baseUrl;
if (config.websockets !== undefined) payload.websockets = config.websockets; if (config.websockets !== undefined) payload.websockets = config.websockets;
if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl; if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl;
if (config.disableCooling) payload['disable-cooling'] = true;
const headers = serializeHeaders(config.headers); const headers = serializeHeaders(config.headers);
if (headers) payload.headers = headers; if (headers) payload.headers = headers;
const models = serializeModelAliases(config.models); const models = serializeModelAliases(config.models);
@@ -301,10 +328,16 @@ const serializeProviderKey = (config: ProviderKeyConfig) => {
if (config.cloak.sensitiveWords && config.cloak.sensitiveWords.length) { if (config.cloak.sensitiveWords && config.cloak.sensitiveWords.length) {
cloakPayload['sensitive-words'] = config.cloak.sensitiveWords; cloakPayload['sensitive-words'] = config.cloak.sensitiveWords;
} }
if (config.cloak.cacheUserId) {
cloakPayload['cache-user-id'] = true;
}
if (Object.keys(cloakPayload).length) { if (Object.keys(cloakPayload).length) {
payload.cloak = cloakPayload; payload.cloak = cloakPayload;
} }
} }
if (config.experimentalCchSigning) {
payload['experimental-cch-signing'] = true;
}
return payload; return payload;
}; };
@@ -342,6 +375,7 @@ const serializeGeminiKey = (config: GeminiKeyConfig) => {
if (config.prefix?.trim()) payload.prefix = config.prefix.trim(); if (config.prefix?.trim()) payload.prefix = config.prefix.trim();
if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.baseUrl) payload['base-url'] = config.baseUrl;
if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl; if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl;
if (config.disableCooling) payload['disable-cooling'] = true;
const headers = serializeHeaders(config.headers); const headers = serializeHeaders(config.headers);
if (headers) payload.headers = headers; if (headers) payload.headers = headers;
const models = serializeModelAliases(config.models); const models = serializeModelAliases(config.models);
@@ -364,10 +398,11 @@ const serializeOpenAIProvider = (provider: OpenAIProviderConfig) => {
if (provider.disabled !== undefined) payload.disabled = provider.disabled; if (provider.disabled !== undefined) payload.disabled = provider.disabled;
const headers = serializeHeaders(provider.headers); const headers = serializeHeaders(provider.headers);
if (headers) payload.headers = headers; if (headers) payload.headers = headers;
const models = serializeModelAliases(provider.models); const models = serializeModelAliases(provider.models, true);
if (models && models.length) payload.models = models; if (models && models.length) payload.models = models;
if (provider.priority !== undefined) payload.priority = provider.priority; if (provider.priority !== undefined) payload.priority = provider.priority;
if (provider.testModel) payload['test-model'] = provider.testModel; if (provider.testModel) payload['test-model'] = provider.testModel;
if (provider.disableCooling) payload['disable-cooling'] = true;
return payload; return payload;
}; };
@@ -408,7 +443,7 @@ export const providersApi = {
'codex-api-key', 'codex-api-key',
configs, configs,
serializeProviderKey, serializeProviderKey,
(raw, payload) => mergeProviderKeyPayload(raw, payload, PROVIDER_KEY_FIELDS), (raw, payload) => mergeProviderKeyPayload(raw, payload, CODEX_KEY_FIELDS),
providerKeyIdentity providerKeyIdentity
) )
), ),
@@ -431,7 +466,7 @@ export const providersApi = {
'claude-api-key', 'claude-api-key',
configs, configs,
serializeProviderKey, serializeProviderKey,
(raw, payload) => mergeProviderKeyPayload(raw, payload, PROVIDER_KEY_FIELDS), (raw, payload) => mergeProviderKeyPayload(raw, payload, CLAUDE_KEY_FIELDS),
providerKeyIdentity providerKeyIdentity
) )
), ),
+25
View File
@@ -16,6 +16,9 @@ import { isRecord } from '@/utils/helpers';
const normalizeBoolean = (value: unknown): boolean | undefined => const normalizeBoolean = (value: unknown): boolean | undefined =>
typeof value === 'boolean' ? value : undefined; typeof value === 'boolean' ? value : undefined;
const normalizeRecord = (value: unknown): Record<string, unknown> | undefined =>
isRecord(value) ? value : undefined;
const normalizeModelAliases = (models: unknown): ModelAlias[] => { const normalizeModelAliases = (models: unknown): ModelAlias[] => {
if (!Array.isArray(models)) return []; if (!Array.isArray(models)) return [];
return models return models
@@ -32,6 +35,8 @@ const normalizeModelAliases = (models: unknown): ModelAlias[] => {
const alias = item.alias; const alias = item.alias;
const priority = item.priority; const priority = item.priority;
const testModel = item['test-model']; const testModel = item['test-model'];
const image = normalizeBoolean(item.image);
const thinking = normalizeRecord(item.thinking);
const entry: ModelAlias = { name: String(name) }; const entry: ModelAlias = { name: String(name) };
if (alias && alias !== name) { if (alias && alias !== name) {
entry.alias = String(alias); entry.alias = String(alias);
@@ -45,6 +50,12 @@ const normalizeModelAliases = (models: unknown): ModelAlias[] => {
if (testModel) { if (testModel) {
entry.testModel = String(testModel); entry.testModel = String(testModel);
} }
if (image !== undefined) {
entry.image = image;
}
if (thinking) {
entry.thinking = thinking;
}
return entry; return entry;
}) })
.filter(Boolean) as ModelAlias[]; .filter(Boolean) as ModelAlias[];
@@ -130,6 +141,8 @@ const normalizeProviderKeyConfig = (item: unknown): ProviderKeyConfig | null =>
const websockets = normalizeBoolean(record?.websockets); const websockets = normalizeBoolean(record?.websockets);
if (websockets !== undefined) config.websockets = websockets; if (websockets !== undefined) config.websockets = websockets;
if (proxyUrl) config.proxyUrl = String(proxyUrl); if (proxyUrl) config.proxyUrl = String(proxyUrl);
const disableCooling = normalizeBoolean(record?.['disable-cooling']);
if (disableCooling !== undefined) config.disableCooling = disableCooling;
const headers = normalizeHeaders(record?.headers); const headers = normalizeHeaders(record?.headers);
if (headers) config.headers = headers; if (headers) config.headers = headers;
const models = normalizeModelAliases(record?.models); const models = normalizeModelAliases(record?.models);
@@ -154,10 +167,18 @@ const normalizeProviderKeyConfig = (item: unknown): ProviderKeyConfig | null =>
if (sensitiveWords.length) { if (sensitiveWords.length) {
cloak.sensitiveWords = sensitiveWords; cloak.sensitiveWords = sensitiveWords;
} }
const cacheUserId = normalizeBoolean(cloakRaw['cache-user-id']);
if (cacheUserId !== undefined) {
cloak.cacheUserId = cacheUserId;
}
if (Object.keys(cloak).length) { if (Object.keys(cloak).length) {
config.cloak = cloak; config.cloak = cloak;
} }
} }
const experimentalCchSigning = normalizeBoolean(record?.['experimental-cch-signing']);
if (experimentalCchSigning !== undefined) {
config.experimentalCchSigning = experimentalCchSigning;
}
return config; return config;
}; };
@@ -186,6 +207,8 @@ const normalizeGeminiKeyConfig = (item: unknown): GeminiKeyConfig | null => {
if (baseUrl) config.baseUrl = String(baseUrl); if (baseUrl) config.baseUrl = String(baseUrl);
const proxyUrl = record?.['proxy-url']; const proxyUrl = record?.['proxy-url'];
if (proxyUrl) config.proxyUrl = String(proxyUrl); if (proxyUrl) config.proxyUrl = String(proxyUrl);
const disableCooling = normalizeBoolean(record?.['disable-cooling']);
if (disableCooling !== undefined) config.disableCooling = disableCooling;
const models = normalizeModelAliases(record?.models); const models = normalizeModelAliases(record?.models);
if (models.length) config.models = models; if (models.length) config.models = models;
const headers = normalizeHeaders(record?.headers); const headers = normalizeHeaders(record?.headers);
@@ -222,6 +245,8 @@ const normalizeOpenAIProvider = (provider: unknown): OpenAIProviderConfig | null
const disabled = normalizeBoolean(provider.disabled); const disabled = normalizeBoolean(provider.disabled);
if (disabled !== undefined) result.disabled = disabled; if (disabled !== undefined) result.disabled = disabled;
const disableCooling = normalizeBoolean(provider['disable-cooling']);
if (disableCooling !== undefined) result.disableCooling = disableCooling;
const prefix = normalizePrefix(provider.prefix); const prefix = normalizePrefix(provider.prefix);
if (prefix) result.prefix = prefix; if (prefix) result.prefix = prefix;
if (headers) result.headers = headers; if (headers) result.headers = headers;
+7
View File
@@ -8,6 +8,8 @@ export interface ModelAlias {
alias?: string; alias?: string;
priority?: number; priority?: number;
testModel?: string; testModel?: string;
image?: boolean;
thinking?: Record<string, unknown>;
} }
export interface ApiKeyEntry { export interface ApiKeyEntry {
@@ -20,6 +22,7 @@ export interface CloakConfig {
mode?: string; mode?: string;
strictMode?: boolean; strictMode?: boolean;
sensitiveWords?: string[]; sensitiveWords?: string[];
cacheUserId?: boolean;
} }
export interface GeminiKeyConfig { export interface GeminiKeyConfig {
@@ -31,6 +34,7 @@ export interface GeminiKeyConfig {
models?: ModelAlias[]; models?: ModelAlias[];
headers?: Record<string, string>; headers?: Record<string, string>;
excludedModels?: string[]; excludedModels?: string[];
disableCooling?: boolean;
authIndex?: string; authIndex?: string;
} }
@@ -44,7 +48,9 @@ export interface ProviderKeyConfig {
headers?: Record<string, string>; headers?: Record<string, string>;
models?: ModelAlias[]; models?: ModelAlias[];
excludedModels?: string[]; excludedModels?: string[];
disableCooling?: boolean;
cloak?: CloakConfig; cloak?: CloakConfig;
experimentalCchSigning?: boolean;
authIndex?: string; authIndex?: string;
} }
@@ -58,6 +64,7 @@ export interface OpenAIProviderConfig {
models?: ModelAlias[]; models?: ModelAlias[];
priority?: number; priority?: number;
testModel?: string; testModel?: string;
disableCooling?: boolean;
authIndex?: string; authIndex?: string;
[key: string]: unknown; [key: string]: unknown;
} }