Enhance support for the v8 kernel
This commit is contained in:
1 parent
9991aea818
commit
59b05e9281
25 files changed
+1463
-185
No files matched your search
+7
-3
@@ -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 件)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user