Enhance support for the v8 kernel

This commit is contained in:
lzt404 committed 2026-09-28 16:42:22 +08:00
1 parent 9991aea818
commit 59b05e9281
25 files changed
+1463 -185

No files matched your search

+7 -3
View File
@@ -683,6 +683,7 @@ export const jaOverrides = {
'aliases.unboundEffort': 'レベル未設定',
'aliases.delete': '{alias} を削除',
'config.routing.roundRobin': 'ラウンドロビン',
'config.routing.weightedRoundRobin': '重み付きラウンドロビン',
'config.routing.fillFirst': '優先充填',
'config.tabs.label': '詳細設定ページ',
'config.tabs.general': '基本設定',
@@ -1387,12 +1388,12 @@ export const jaOverrides = {
'apiAccess.baseRequiredPlaceholder': '必須、例:https://api.example.com',
'apiAccess.baseOptionalPlaceholder': '任意。空欄の場合は既定のアドレスを使用',
'apiAccess.thinking.title': '推論レベル',
'apiAccess.thinking.description': '上流の対応状況に合わせて追加してください',
'apiAccess.thinking.description': '変更はこの接続の全モデルに適用されます。未変更なら各モデルの設定を保持します',
'apiAccess.thinking.placeholder': '例:low、medium、またはカスタムレベル',
'apiAccess.thinking.add': '追加',
'apiAccess.thinking.delete': '{level} を削除',
'apiAccess.thinking.deleteAria': '推論レベル {level} を削除',
'apiAccess.thinking.empty': '未追加の場合 thinking.levels は書き込まれません',
'apiAccess.thinking.empty': 'レベルを編集するまでは、各モデルの推論設定を保持します',
'apiAccess.models.title': 'モデル',
'apiAccess.models.description': '選択したモデルを公開します。選択解除するとモデル一覧から非表示になります',
'apiAccess.models.fetch': 'モデルを取得',
@@ -1407,7 +1408,10 @@ export const jaOverrides = {
'apiAccess.prefixPlaceholder': '任意、例:team-a',
'apiAccess.field.headers': 'カスタムリクエストヘッダー(1 行に Name: Value)',
'apiAccess.field.excludedModels': '除外モデル(1 行に 1 件、ワイルドカード対応)',
'apiAccess.field.testModel': 'テストモデル',
'apiAccess.option.inherit': 'デフォルトを使用',
'apiAccess.error.priorityInteger': '優先度は安全な範囲の整数で指定してください。',
'apiAccess.cooling.disable': 'クールダウンを無効化',
'apiAccess.cooling.enable': 'クールダウンを有効化',
'apiAccess.cloak.mode': 'Claude 偽装モード',
'apiAccess.cloak.default': '既定(自動)',
'apiAccess.cloak.words': '偽装する機密語(1 行に 1 件)',
+7 -3
View File
@@ -680,6 +680,7 @@ export const en: Record<MessageKey, string> = {
'speedAliases.fast.title': 'Fast',
'speedAliases.aliasName.example': 'For example: {model}-fast',
'config.routing.roundRobin': 'Round Robin',
'config.routing.weightedRoundRobin': 'Weighted Round Robin',
'config.routing.fillFirst': 'Fill First',
'config.tabs.label': 'Advanced features sections',
'config.tabs.general': 'General Settings',
@@ -1388,12 +1389,12 @@ export const en: Record<MessageKey, string> = {
'apiAccess.baseRequiredPlaceholder': 'Required, for example https://api.example.com',
'apiAccess.baseOptionalPlaceholder': 'Optional; leave blank to use the default address',
'apiAccess.thinking.title': 'Reasoning Levels',
'apiAccess.thinking.description': 'Add levels supported by the upstream provider',
'apiAccess.thinking.description': 'Edits apply to all models in this connection; otherwise each model keeps its settings',
'apiAccess.thinking.placeholder': 'For example: low, medium, or a custom level',
'apiAccess.thinking.add': 'Add',
'apiAccess.thinking.delete': 'Delete {level}',
'apiAccess.thinking.deleteAria': 'Delete reasoning level {level}',
'apiAccess.thinking.empty': 'thinking.levels is omitted when no levels are added',
'apiAccess.thinking.empty': 'Each model keeps its reasoning settings until you edit the levels',
'apiAccess.models.title': 'Models',
'apiAccess.models.description': 'Checked models are open; uncheck a model to hide it from the list',
'apiAccess.models.fetch': 'Fetch Models',
@@ -1407,7 +1408,10 @@ export const en: Record<MessageKey, string> = {
'apiAccess.prefixPlaceholder': 'Optional, for example team-a',
'apiAccess.field.headers': 'Custom Headers (one Name: Value per line)',
'apiAccess.field.excludedModels': 'Excluded Models (one per line; wildcards supported)',
'apiAccess.field.testModel': 'Test Model',
'apiAccess.option.inherit': 'Use default',
'apiAccess.error.priorityInteger': 'Priority must be a safe integer.',
'apiAccess.cooling.disable': 'Disable cooldown',
'apiAccess.cooling.enable': 'Enable cooldown',
'apiAccess.cloak.mode': 'Claude Cloaking Mode',
'apiAccess.cloak.default': 'Default (auto)',
'apiAccess.cloak.words': 'Cloaking Keywords (one per line)',
+7 -3
View File
@@ -679,6 +679,7 @@ export const zhCN = {
'speedAliases.fast.title': 'Fast',
'speedAliases.aliasName.example': '例如 {model}-fast',
'config.routing.roundRobin': '轮询',
'config.routing.weightedRoundRobin': '加权轮询',
'config.routing.fillFirst': '优先填充',
'config.tabs.label': '高级功能子页面',
'config.tabs.general': '基础设置',
@@ -1387,12 +1388,12 @@ export const zhCN = {
'apiAccess.baseRequiredPlaceholder': '必填,例如 https://api.example.com',
'apiAccess.baseOptionalPlaceholder': '可选,留空使用默认地址',
'apiAccess.thinking.title': '思考等级',
'apiAccess.thinking.description': '按上游支持情况自行添加',
'apiAccess.thinking.description': '修改后应用于此接入的所有模型;未修改时保留各模型配置',
'apiAccess.thinking.placeholder': '例如 low、medium 或自定义等级',
'apiAccess.thinking.add': '添加',
'apiAccess.thinking.delete': '删除 {level}',
'apiAccess.thinking.deleteAria': '删除思考等级 {level}',
'apiAccess.thinking.empty': '未添加时不写入 thinking.levels',
'apiAccess.thinking.empty': '未手动修改时保留模型各自的思考能力设置',
'apiAccess.models.title': '模型',
'apiAccess.models.description': '勾选表示当前开放,取消勾选后将从模型列表隐藏',
'apiAccess.models.fetch': '获取模型',
@@ -1406,7 +1407,10 @@ export const zhCN = {
'apiAccess.prefixPlaceholder': '可选,例如 team-a',
'apiAccess.field.headers': '自定义请求头(每行 Name: Value)',
'apiAccess.field.excludedModels': '排除模型(每行一个,支持通配符)',
'apiAccess.field.testModel': '测试模型',
'apiAccess.option.inherit': '使用默认值',
'apiAccess.error.priorityInteger': '优先级必须是安全范围内的整数。',
'apiAccess.cooling.disable': '禁用冷却',
'apiAccess.cooling.enable': '启用冷却',
'apiAccess.cloak.mode': 'Claude 伪装模式',
'apiAccess.cloak.default': '默认(自动)',
'apiAccess.cloak.words': '伪装敏感词(每行一个)',
+75 -47
View File
@@ -1,4 +1,5 @@
import { useConfirmation } from '../components/ConfirmationDialog';
import { normalizeProviderModels } from '../services/providerModels';
import { ModelSelectionPanel } from '../components/ModelSelectionPanel';
import { useDialogFocusTrap } from '../components/useDialogFocusTrap';
import {
@@ -219,15 +220,15 @@ export type ProviderDraft = {
prefix?: string;
headersText?: string;
excludedModelsText?: string;
disableCooling?: boolean;
disableCooling?: boolean | null;
websockets?: boolean;
testModel?: string;
thinkingLevels?: string[];
thinkingLevelsEdited?: boolean;
disabled?: boolean;
cloakMode?: string;
cloakStrictMode?: boolean;
cloakSensitiveWordsText?: string;
cloakCacheUserId?: boolean;
cloakCacheUserId?: boolean | null;
};
const providerDefinitions: ProviderDefinition[] = [
@@ -362,7 +363,6 @@ const providerHealthIdentity = (row: ProviderRow) => [
row.name,
row.baseUrl,
row.authIndex,
readString(row.record, 'test-model', 'testModel'),
row.apiKeys.join('\u0000'),
row.models.map((model) => model.name).join('\u0000'),
].join('\u0001');
@@ -384,7 +384,9 @@ const providerHeadersFromRecord = (record: Record<string, unknown>) =>
: {};
export const stripResponseFields = (record: Record<string, unknown>) => {
const next = { ...record };
const next = normalizeProviderModels(record);
delete next['test-model'];
delete next.testModel;
delete next['auth-index'];
delete next.authIndex;
delete next.auth_index;
@@ -445,7 +447,9 @@ export const hasDuplicateProviderRecord = (
return readString(record, 'api-key', 'apiKey') === readString(candidate, 'api-key', 'apiKey')
&& readString(record, 'base-url', 'baseUrl') === readString(candidate, 'base-url', 'baseUrl');
}
return providerConfigIdentity(record) === providerConfigIdentity(candidate);
const { name: _recordName, ...recordConfig } = record;
const { name: _candidateName, ...candidateConfig } = candidate;
return providerConfigIdentity(recordConfig) === providerConfigIdentity(candidateConfig);
}));
const mergeModelRecords = (current: unknown, selected: ModelOption[]) => {
@@ -644,12 +648,12 @@ const draftFromRow = (row: ProviderRow): ProviderDraft => {
excludedModelsText: Array.isArray(row.record['excluded-models'])
? row.record['excluded-models'].map(String).filter((model) => model.trim() !== '*').join('\n')
: '',
disableCooling: readBoolean(row.record, 'disable-cooling', 'disableCooling'),
disableCooling: typeof row.record['disable-cooling'] === 'boolean' ? row.record['disable-cooling'] : null,
websockets: readBoolean(row.record, 'websockets'),
testModel: readString(row.record, 'test-model', 'testModel'),
thinkingLevels: definition.openAi
? thinkingLevelsFromModels(row.models)
: undefined,
thinkingLevelsEdited: false,
disabled: row.disabled,
cloakMode: isRecord(row.record.cloak) ? readString(row.record.cloak, 'mode') : '',
cloakStrictMode: isRecord(row.record.cloak)
@@ -659,9 +663,8 @@ const draftFromRow = (row: ProviderRow): ProviderDraft => {
isRecord(row.record.cloak) && Array.isArray(row.record.cloak['sensitive-words'])
? row.record.cloak['sensitive-words'].map(String).join('\n')
: '',
cloakCacheUserId: isRecord(row.record.cloak)
? readBoolean(row.record.cloak, 'cache-user-id', 'cacheUserId')
: false,
cloakCacheUserId: isRecord(row.record.cloak) && typeof row.record.cloak['cache-user-id'] === 'boolean'
? row.record.cloak['cache-user-id'] : null,
};
};
@@ -676,19 +679,18 @@ const emptyProviderDraft = (): ProviderDraft => ({
prefix: '',
headersText: '',
excludedModelsText: '',
disableCooling: false,
disableCooling: null,
websockets: false,
testModel: '',
disabled: false,
cloakMode: '',
cloakStrictMode: false,
cloakSensitiveWordsText: '',
cloakCacheUserId: false,
cloakCacheUserId: null,
});
export const createProviderDraft = (category: ProviderCategory): ProviderDraft => {
const draft = emptyProviderDraft();
if (category === 'openai-compatibility') return { ...draft, thinkingLevels: [] };
if (category === 'openai-compatibility') return { ...draft, thinkingLevels: [], thinkingLevelsEdited: false };
if (category !== 'deepseek') return draft;
return {
...draft,
@@ -711,7 +713,7 @@ export const applyProviderPreset = (
category: ProviderCategory,
draft: ProviderDraft,
): ProviderDraft => {
if (!definitionFor(category).openAi || draft.thinkingLevels === undefined) return draft;
if (!definitionFor(category).openAi || draft.thinkingLevels === undefined || draft.thinkingLevelsEdited === false) return draft;
const levels = draft.thinkingLevels;
return {
...draft,
@@ -785,17 +787,12 @@ const applyAdvancedFields = (
else delete next['excluded-models'];
}
if (draft.disableCooling !== undefined) {
if (draft.disableCooling) next['disable-cooling'] = true;
else delete next['disable-cooling'];
if (draft.disableCooling === null) delete next['disable-cooling'];
else next['disable-cooling'] = draft.disableCooling;
}
if (draft.websockets !== undefined && section === 'codex-api-key') {
next.websockets = draft.websockets;
}
if (draft.testModel !== undefined && section === 'openai-compatibility') {
const testModel = draft.testModel.trim();
if (testModel) next['test-model'] = testModel;
else delete next['test-model'];
}
if (
section === 'claude-api-key'
&& (
@@ -806,22 +803,29 @@ const applyAdvancedFields = (
)
) {
const cloak: Record<string, unknown> = isRecord(next.cloak) ? { ...next.cloak } : {};
const mode = draft.cloakMode?.trim();
if (mode) cloak.mode = mode;
else delete cloak.mode;
delete cloak.strictMode;
if (draft.cloakStrictMode) cloak['strict-mode'] = true;
else delete cloak['strict-mode'];
const sensitiveWords = (draft.cloakSensitiveWordsText ?? '')
.split(/[,\n]/)
.map((value) => value.trim())
.filter((value, index, values) => value && values.indexOf(value) === index);
if (sensitiveWords.length > 0) cloak['sensitive-words'] = sensitiveWords;
else delete cloak['sensitive-words'];
delete cloak.sensitiveWords;
delete cloak.cacheUserId;
if (draft.cloakCacheUserId) cloak['cache-user-id'] = true;
else delete cloak['cache-user-id'];
if (draft.cloakMode !== undefined) {
const mode = draft.cloakMode.trim();
if (mode) cloak.mode = mode;
else delete cloak.mode;
}
if (draft.cloakStrictMode !== undefined) {
delete cloak.strictMode;
if (draft.cloakStrictMode) cloak['strict-mode'] = true;
else delete cloak['strict-mode'];
}
if (draft.cloakSensitiveWordsText !== undefined) {
const sensitiveWords = draft.cloakSensitiveWordsText.split(/[,\n]/)
.map((value) => value.trim())
.filter((value, index, values) => value && values.indexOf(value) === index);
delete cloak.sensitiveWords;
if (sensitiveWords.length > 0) cloak['sensitive-words'] = sensitiveWords;
else delete cloak['sensitive-words'];
}
if (draft.cloakCacheUserId !== undefined) {
delete cloak.cacheUserId;
if (draft.cloakCacheUserId === null) delete cloak['cache-user-id'];
else cloak['cache-user-id'] = draft.cloakCacheUserId;
}
if (Object.keys(cloak).length > 0) next.cloak = cloak;
else delete next.cloak;
}
@@ -836,8 +840,20 @@ export const buildProviderRecord = (
const record = current ? stripResponseFields(current) : {};
const priorityText = draft.priority.trim();
const priority = priorityText ? Number(priorityText) : null;
if (priority !== null && !Number.isSafeInteger(priority)) {
throw new Error(translate(getCurrentLocale(), 'apiAccess.error.priorityInteger'));
}
const models = mergeModelRecords(record.models, draft.models);
if (definitionFor(section).openAi) {
if (draft.thinkingLevels !== undefined && draft.thinkingLevelsEdited !== false) {
for (const model of models) {
const thinking = isRecord(model.thinking) ? { ...model.thinking } : {};
if (draft.thinkingLevels.length) thinking.levels = [...draft.thinkingLevels];
else delete thinking.levels;
if (Object.keys(thinking).length) model.thinking = thinking;
else delete model.thinking;
}
}
const entries = mergeOpenAiApiKeyEntries(record['api-key-entries'], draft.apiKey.trim());
if (draft.proxyUrlEdited !== false && draft.proxyUrl !== undefined) {
const proxyUrl = draft.proxyUrl.trim();
@@ -1842,7 +1858,7 @@ export function ApiProviderDialog({
), [modelOptions, selectedModelNames]);
const updateTextField = (
field: 'apiKey' | 'remark' | 'baseUrl' | 'proxyUrl' | 'priority' | 'prefix' | 'headersText' | 'excludedModelsText' | 'testModel' | 'cloakMode' | 'cloakSensitiveWordsText',
field: 'apiKey' | 'remark' | 'baseUrl' | 'proxyUrl' | 'priority' | 'prefix' | 'headersText' | 'excludedModelsText' | 'cloakMode' | 'cloakSensitiveWordsText',
value: string,
) => {
setFormError('');
@@ -1861,13 +1877,18 @@ export function ApiProviderDialog({
};
const updateBooleanField = (
field: 'disableCooling' | 'websockets' | 'cloakStrictMode' | 'cloakCacheUserId',
field: 'websockets' | 'cloakStrictMode',
value: boolean,
) => {
setFormError('');
setDraft((current) => ({ ...current, [field]: value }));
};
const updateOptionalBooleanField = (field: 'disableCooling' | 'cloakCacheUserId', value: string) => {
setFormError('');
setDraft((current) => ({ ...current, [field]: value === '' ? null : value === 'true' }));
};
const updateModels = (update: (models: ModelOption[]) => ModelOption[]) => {
setDraft((current) => {
const models = update(current.models);
@@ -1904,6 +1925,7 @@ export function ApiProviderDialog({
return {
...current,
thinkingLevels: [...levels, level],
thinkingLevelsEdited: true,
};
});
setThinkingLevelInput('');
@@ -1913,6 +1935,7 @@ export function ApiProviderDialog({
setDraft((current) => ({
...current,
thinkingLevels: (current.thinkingLevels ?? []).filter((item) => item !== level),
thinkingLevelsEdited: true,
}));
};
@@ -2159,7 +2182,7 @@ export function ApiProviderDialog({
</div>
{modelError && !modelDiscoveryOpen ? <MessageNotice message={modelError} onDismiss={() => setModelError('')} /> : null}
</div>
<label><span>{t('apiAccess.field.priority')}</span><input inputMode="numeric" value={draft.priority} onChange={(event) => updateTextField('priority', event.currentTarget.value.replace(/\D/g, ''))} placeholder={t('common.optional')} /></label>
<label><span>{t('apiAccess.field.priority')}</span><input type="number" step="1" value={draft.priority} onChange={(event) => updateTextField('priority', event.currentTarget.value)} placeholder={t('common.optional')} /></label>
<details className="provider-advanced-settings">
<summary>{t('apiAccess.advanced')}</summary>
<div className="provider-advanced-fields">
@@ -2174,9 +2197,6 @@ export function ApiProviderDialog({
<textarea value={draft.excludedModelsText ?? ''} onChange={(event) => updateTextField('excludedModelsText', event.currentTarget.value)} rows={3} placeholder={'model-old-*\nmodel-preview'} />
</label>
) : null}
{activeSection === 'openai-compatibility' ? (
<label><span>{t('apiAccess.field.testModel')}</span><input value={draft.testModel ?? ''} onChange={(event) => updateTextField('testModel', event.currentTarget.value)} placeholder={t('common.optional')} /></label>
) : null}
{activeSection === 'claude-api-key' ? (
<div className="provider-cloak-settings">
<label>
@@ -2198,7 +2218,11 @@ export function ApiProviderDialog({
</div>
<div className="provider-advanced-toggle">
<div><strong>{t('apiAccess.cloak.cacheUser')}</strong><span>{t('apiAccess.cloak.cacheUserDescription')}</span></div>
<label className="switch-control" title={t('apiAccess.cloak.cacheUser')}><input type="checkbox" checked={Boolean(draft.cloakCacheUserId)} onChange={(event) => updateBooleanField('cloakCacheUserId', event.currentTarget.checked)} /><span className="switch-track" /></label>
<select aria-label={t('apiAccess.cloak.cacheUser')} value={draft.cloakCacheUserId == null ? '' : String(draft.cloakCacheUserId)} onChange={(event) => updateOptionalBooleanField('cloakCacheUserId', event.currentTarget.value)}>
<option value="">{t('apiAccess.option.inherit')}</option>
<option value="true">{t('common.enabled')}</option>
<option value="false">{t('common.disabled')}</option>
</select>
</div>
</div>
) : null}
@@ -2210,7 +2234,11 @@ export function ApiProviderDialog({
) : null}
<div className="provider-advanced-toggle">
<div><strong>{t('apiAccess.cooling.title')}</strong><span>{t('apiAccess.cooling.description')}</span></div>
<label className="switch-control" title={t('apiAccess.cooling.title')}><input type="checkbox" checked={Boolean(draft.disableCooling)} onChange={(event) => updateBooleanField('disableCooling', event.currentTarget.checked)} /><span className="switch-track" /></label>
<select aria-label={t('apiAccess.cooling.title')} value={draft.disableCooling == null ? '' : String(draft.disableCooling)} onChange={(event) => updateOptionalBooleanField('disableCooling', event.currentTarget.value)}>
<option value="">{t('apiAccess.option.inherit')}</option>
<option value="true">{t('apiAccess.cooling.disable')}</option>
<option value="false">{t('apiAccess.cooling.enable')}</option>
</select>
</div>
</div>
</details>
+1
View File
@@ -136,6 +136,7 @@ const cleanNetworkDraft = (): NetworkDraftDirty => ({
const ROUTING_OPTIONS = [
{ value: 'round-robin', labelKey: 'config.routing.roundRobin' },
{ value: 'weighted-round-robin', labelKey: 'config.routing.weightedRoundRobin' },
{ value: 'fill-first', labelKey: 'config.routing.fillFirst' },
] as const;
+14 -4
View File
@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core';
import { getCurrentLocale, translate } from '../i18n';
import { normalizeProviderModels } from './providerModels';
export type ManagementJson = Record<string, unknown> | unknown[] | string | number | boolean | null;
@@ -58,22 +59,28 @@ export function flattenV8ProviderGroups(provider: string, payload: unknown): Rec
if (provider === 'openai-compatibility') {
const record: Record<string, unknown> = { ...group };
delete record.keys;
delete record['test-model'];
delete record.testModel;
if (keys.length > 0) record['api-key-entries'] = keys.map((key) => ({ ...key }));
return [record];
return [normalizeProviderModels(record)];
}
const shared = Object.fromEntries(
Object.entries(group).filter(([key]) => key === 'base-url' || SHARED_PROVIDER_FIELDS.has(key)),
);
const name = readString(group, 'name');
const generatedName = name.startsWith(`${provider}-`) && /^[0-9]+$/.test(name.slice(provider.length + 1));
if (name && !generatedName) shared.name = name;
return keys.map((key) => {
const overrides = Object.fromEntries(Object.entries(key).filter(([, value]) => value !== null));
return { ...shared, ...overrides };
return normalizeProviderModels({ ...shared, ...overrides });
});
});
}
export function groupLegacyProviderRecords(provider: string, payload: unknown): Record<string, unknown>[] {
if (!Array.isArray(payload)) return [];
return payload.filter(isRecord).map((record, index) => {
return payload.filter(isRecord).map((input, index) => {
const record = normalizeProviderModels(input);
if (provider === 'openai-compatibility') {
const group: Record<string, unknown> = {
...record,
@@ -82,11 +89,14 @@ export function groupLegacyProviderRecords(provider: string, payload: unknown):
: [],
};
delete group['api-key-entries'];
delete group['test-model'];
delete group.testModel;
return group;
}
const group: Record<string, unknown> = { name: `${provider}-${index + 1}` };
const group: Record<string, unknown> = { name: readString(record, 'name') || `${provider}-${index + 1}` };
const key: Record<string, unknown> = {};
Object.entries(record).forEach(([field, value]) => {
if (field === 'name') return;
if (field === 'base-url' || SHARED_PROVIDER_FIELDS.has(field)) group[field] = value;
else key[field] = value;
});
+3 -2
View File
@@ -1,5 +1,6 @@
import { apiCallErrorMessage, isRecord, managementApi, readString } from './managementApi';
import { getCurrentLocale, translate } from '../i18n';
import { normalizeThinkingConfig } from './providerModels';
const modelText = (key: Parameters<typeof translate>[1]) => translate(getCurrentLocale(), key);
@@ -144,13 +145,13 @@ const normalizeModelList = (payload: unknown, preserveExistingAlias = false): Mo
const alias = preserveExistingAlias ? rawAlias : usableModelAlias(rawAlias);
const displayName = record ? readString(record, 'display-name', 'display_name', 'displayName') : '';
const thinking = record && isRecord(record.thinking)
? { ...record.thinking }
? normalizeThinkingConfig(record.thinking, !preserveExistingAlias)
: undefined;
return {
name,
...(alias && alias !== name ? { alias } : {}),
...(displayName && displayName !== name ? { displayName } : {}),
...(thinking ? { thinking } : {}),
...(thinking && (preserveExistingAlias || Object.keys(thinking).length > 0) ? { thinking } : {}),
};
}).filter((item): item is ModelOption => item !== null);
};
+25
View File
@@ -0,0 +1,25 @@
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value);
export function normalizeThinkingConfig(value: Record<string, unknown>, discovered = false) {
const next = { ...value };
for (const [json, yaml] of [['zero_allowed', 'zero-allowed'], ['dynamic_allowed', 'dynamic-allowed']]) {
if (!(yaml in next) && json in next) next[yaml] = next[json];
delete next[json];
}
if (!discovered) return next;
return Object.fromEntries(Object.entries(next).filter(([key, item]) => {
if (key === 'min' || key === 'max') return Number.isSafeInteger(item) && Number(item) >= 0;
if (key === 'zero-allowed' || key === 'dynamic-allowed') return typeof item === 'boolean';
return key === 'levels' && Array.isArray(item) && item.every((level) => typeof level === 'string');
}));
}
export function normalizeProviderModels(record: Record<string, unknown>): Record<string, unknown> {
if (!Array.isArray(record.models)) return { ...record };
return { ...record, models: record.models.map((model) => (
isObject(model) && isObject(model.thinking)
? { ...model, thinking: normalizeThinkingConfig(model.thinking) }
: model
)) };
}
+8 -1
View File
@@ -5324,7 +5324,8 @@ textarea:focus-visible {
background: var(--theme-fffdf8);
}
.provider-cloak-settings select {
.provider-cloak-settings select,
.provider-advanced-toggle > select {
width: 100%;
min-width: 0;
height: 40px;
@@ -5335,6 +5336,12 @@ textarea:focus-visible {
color: var(--theme-2d2a26);
}
.provider-advanced-toggle > select {
width: auto;
max-width: 55%;
flex: 0 1 auto;
}
.thinking-alias-page {
grid-template-rows: auto;
gap: 12px;