feat(api-access): manage DeepSeek through Codex API

This commit is contained in:
lzt404 committed 2026-09-15 02:39:03 +08:00
1 parent 2a99db2908
commit 2b41eab8df
11 files changed
+88 -145

No files matched your search

-4
View File
@@ -1205,10 +1205,6 @@ export const jaOverrides = {
'apiAccess.remarkPlaceholder': '例:本番環境、チームのメインアカウント',
'apiAccess.baseRequiredPlaceholder': '必須、例:https://api.example.com',
'apiAccess.baseOptionalPlaceholder': '任意。空欄の場合は既定のアドレスを使用',
'apiAccess.preset.title': 'OpenAI 互換プリセット',
'apiAccess.preset.description': 'DeepSeek 公式アドレスを入力済みです。モデル一覧を取得し、公開するモデルを選択してください',
'apiAccess.thinking.builtIn': '組み込み推論レベル',
'apiAccess.thinking.builtInDescription': '現在公開中のすべてのモデルに自動適用します',
'apiAccess.thinking.title': '推論レベル',
'apiAccess.thinking.description': '上流の対応状況に合わせて追加してください',
'apiAccess.thinking.placeholder': '例:low、medium、またはカスタムレベル',
-4
View File
@@ -1205,10 +1205,6 @@ export const en: Record<MessageKey, string> = {
'apiAccess.remarkPlaceholder': 'For example, Production or Team primary account',
'apiAccess.baseRequiredPlaceholder': 'Required, for example https://api.example.com',
'apiAccess.baseOptionalPlaceholder': 'Optional; leave blank to use the default address',
'apiAccess.preset.title': 'OpenAI-Compatible Preset',
'apiAccess.preset.description': 'The official DeepSeek address is prefilled; fetch the list and select the models to open',
'apiAccess.thinking.builtIn': 'Built-in Reasoning Levels',
'apiAccess.thinking.builtInDescription': 'Applied automatically to all currently open models',
'apiAccess.thinking.title': 'Reasoning Levels',
'apiAccess.thinking.description': 'Add levels supported by the upstream provider',
'apiAccess.thinking.placeholder': 'For example: low, medium, or a custom level',
-4
View File
@@ -1204,10 +1204,6 @@ export const zhCN = {
'apiAccess.remarkPlaceholder': '例如:生产环境、团队主账号',
'apiAccess.baseRequiredPlaceholder': '必填,例如 https://api.example.com',
'apiAccess.baseOptionalPlaceholder': '可选,留空使用默认地址',
'apiAccess.preset.title': 'OpenAI 兼容预设',
'apiAccess.preset.description': '已预填 DeepSeek 官方地址;请获取模型列表并选择需要开放的模型',
'apiAccess.thinking.builtIn': '内置思考等级',
'apiAccess.thinking.builtInDescription': '自动应用到当前开放的全部模型',
'apiAccess.thinking.title': '思考等级',
'apiAccess.thinking.description': '按上游支持情况自行添加',
'apiAccess.thinking.placeholder': '例如 low、medium 或自定义等级',
+42 -53
View File
@@ -54,9 +54,7 @@ import {
responseList,
} from '../services/managementApi';
import {
applyDeepSeekModelPreset,
DEEPSEEK_BASE_URL,
DEEPSEEK_THINKING_LEVELS,
fetchModels,
mergeModelOptions,
modelsFromRecord,
@@ -87,7 +85,7 @@ export type ProviderSection =
export type ProviderCategory = ProviderSection | 'deepseek';
export const OPENAI_THINKING_LEVELS = ['low', 'medium', 'high', 'xhigh'] as const;
export { DEEPSEEK_BASE_URL, DEEPSEEK_THINKING_LEVELS };
export { DEEPSEEK_BASE_URL };
type ProviderDefinition = {
id: ProviderCategory;
@@ -229,11 +227,11 @@ const providerDefinitions: ProviderDefinition[] = [
},
{
id: 'deepseek',
section: 'openai-compatibility',
responseKey: 'openai-compatibility',
section: 'codex-api-key',
responseKey: 'codex-api-key',
labelKey: 'apiAccess.provider.deepseek',
icon: deepseekIcon,
openAi: true,
openAi: false,
},
{ id: 'claude-api-key', section: 'claude-api-key', responseKey: 'claude-api-key', labelKey: 'apiAccess.provider.claude', icon: claudeIcon, openAi: false },
{ id: 'gemini-api-key', section: 'gemini-api-key', responseKey: 'gemini-api-key', labelKey: 'apiAccess.provider.gemini', icon: geminiIcon, openAi: false },
@@ -265,9 +263,17 @@ const isDeepSeekRecord = (record: Record<string, unknown>) => {
export const providerCategoryMatchesRecord = (
category: ProviderCategory,
record: Record<string, unknown>,
section: ProviderSection = definitionFor(category).section,
) => {
if (category === 'deepseek') return isDeepSeekRecord(record);
if (category === 'openai-compatibility') return !isDeepSeekRecord(record);
if (category === 'deepseek') {
return section === 'codex-api-key' && isDeepSeekRecord(record);
}
if (category === 'codex-api-key') {
return section === 'codex-api-key' && !isDeepSeekRecord(record);
}
if (category === 'openai-compatibility') {
return section === 'openai-compatibility';
}
return true;
};
@@ -328,9 +334,13 @@ const providerHealthIdentity = (row: ProviderRow) => [
row.models.map((model) => model.name).join('\u0000'),
].join('\u0001');
const providerModelType = (section: ProviderSection): ModelProvider => {
const providerModelType = (
section: ProviderSection,
record?: Record<string, unknown>,
): ModelProvider => {
if (section === 'gemini-api-key') return 'gemini';
if (section === 'claude-api-key') return 'claude';
if (section === 'codex-api-key' && record && isDeepSeekRecord(record)) return 'deepseek';
if (section === 'codex-api-key') return 'codex';
return 'openai';
};
@@ -474,9 +484,9 @@ const thinkingLevelsFromModels = (models: ModelOption[]): string[] => {
const draftFromRow = (row: ProviderRow): ProviderDraft => {
const definition = definitionFor(row.section);
const isDeepSeek = row.section === 'openai-compatibility' && isDeepSeekRecord(row.record);
const isDeepSeek = row.section === 'codex-api-key' && isDeepSeekRecord(row.record);
return {
name: row.name,
name: isDeepSeek ? 'DeepSeek' : row.name,
apiKey: definition.openAi ? row.apiKeys.join('\n') : row.apiKey,
remark: row.remark || (definition.openAi && !isDeepSeek ? row.name : ''),
baseUrl: row.baseUrl,
@@ -541,7 +551,6 @@ export const createProviderDraft = (category: ProviderCategory): ProviderDraft =
name: 'DeepSeek',
remark: '',
baseUrl: DEEPSEEK_BASE_URL,
thinkingLevels: [...DEEPSEEK_THINKING_LEVELS],
};
};
@@ -558,13 +567,6 @@ export const applyProviderPreset = (
category: ProviderCategory,
draft: ProviderDraft,
): ProviderDraft => {
if (category === 'deepseek') {
return {
...draft,
thinkingLevels: [...DEEPSEEK_THINKING_LEVELS],
models: applyDeepSeekModelPreset(draft.models),
};
}
if (!definitionFor(category).openAi || draft.thinkingLevels === undefined) return draft;
const levels = draft.thinkingLevels;
return {
@@ -702,6 +704,9 @@ export const buildProviderRecord = (
'api-key': draft.apiKey.trim(),
models,
};
if (section === 'codex-api-key' && draft.name.trim().toLowerCase() === 'deepseek') {
next.name = 'DeepSeek';
}
if (draft.baseUrl.trim()) next['base-url'] = draft.baseUrl.trim();
else delete next['base-url'];
if (priority !== null && Number.isFinite(priority)) next.priority = priority;
@@ -898,9 +903,12 @@ export function ApiAccessPage() {
.map((record, index) => rowFromRecord(activeSection, record, index))
.map((row) => ({
...row,
name: activeCategory === 'deepseek'
? t('apiAccess.provider.deepseek')
: row.name,
remark: apiAccessRemarks[providerRemarkIdentity(row.section, row.apiKeys)] ?? '',
}))
.filter((row) => providerCategoryMatchesRecord(activeCategory, row.record))
.filter((row) => providerCategoryMatchesRecord(activeCategory, row.record, activeSection))
.filter((row) => {
const query = filter.trim().toLowerCase();
if (!query) return true;
@@ -909,7 +917,7 @@ export function ApiAccessPage() {
.toLowerCase()
.includes(query);
}),
[activeCategory, activeSection, apiAccessRemarks, filter, records],
[activeCategory, activeSection, apiAccessRemarks, filter, records, t],
);
const openCreate = () => {
@@ -925,9 +933,7 @@ export function ApiAccessPage() {
setError('');
setEditingRow(row);
const draft = draftFromRow(row);
setDialogDraft(activeCategory === 'deepseek'
? { ...draft, thinkingLevels: [...DEEPSEEK_THINKING_LEVELS] }
: draft);
setDialogDraft(draft);
setDialogOpen(true);
};
@@ -1189,7 +1195,7 @@ export function ApiAccessPage() {
const countForDefinition = (definition: ProviderDefinition) =>
records[definition.section].filter((record) =>
providerCategoryMatchesRecord(definition.id, record)
providerCategoryMatchesRecord(definition.id, record, definition.section)
).length;
return (
@@ -1390,7 +1396,7 @@ function ProviderHealthDialog({ row, onClose }: ProviderHealthDialogProps) {
}, []);
const healthOptions = useMemo<ProviderHealthCheckOptions>(() => ({
provider: providerModelType(row.section),
provider: providerModelType(row.section, row.record),
baseUrl: row.baseUrl,
apiKeys: row.apiKeys,
authIndex: row.authIndex,
@@ -1713,13 +1719,15 @@ function ApiProviderDialog({
setModelLoading(true);
setModelError('');
try {
const provider: ModelProvider = definition.section === 'gemini-api-key'
? 'gemini'
: definition.section === 'claude-api-key'
? 'claude'
: definition.section === 'codex-api-key'
? 'codex'
: 'openai';
const provider: ModelProvider = activeCategory === 'deepseek'
? 'deepseek'
: definition.section === 'gemini-api-key'
? 'gemini'
: definition.section === 'claude-api-key'
? 'claude'
: definition.section === 'codex-api-key'
? 'codex'
: 'openai';
const modelApiKey = draft.apiKey.split(/\r?\n/).map((value) => value.trim()).find(Boolean) ?? '';
const fetchedModels = await fetchModels(
provider,
@@ -1867,26 +1875,7 @@ function ApiProviderDialog({
/>
</label>
<label><span>{t('apiAccess.field.baseUrl')}</span><input value={draft.baseUrl} onChange={(event) => updateTextField('baseUrl', event.currentTarget.value)} placeholder={activeSection === 'codex-api-key' || activeSection === 'openai-compatibility' ? t('apiAccess.baseRequiredPlaceholder') : t('apiAccess.baseOptionalPlaceholder')} /></label>
{activeCategory === 'deepseek' ? (
<div className="provider-preset-summary">
<img src={deepseekIcon} alt="" className="provider-logo" />
<div>
<strong>{t('apiAccess.preset.title')}</strong>
<span>{t('apiAccess.preset.description')}</span>
</div>
</div>
) : null}
{activeCategory === 'deepseek' ? (
<div className="thinking-level-config">
<div className="thinking-level-heading">
<strong>{t('apiAccess.thinking.builtIn')}</strong>
<span>{t('apiAccess.thinking.builtInDescription')}</span>
</div>
<div className="thinking-level-tags readonly">
{DEEPSEEK_THINKING_LEVELS.map((level) => <span key={level}>{level}</span>)}
</div>
</div>
) : activeCategory === 'openai-compatibility' ? (
{activeCategory === 'openai-compatibility' ? (
<div className="thinking-level-config">
<div className="thinking-level-heading">
<strong>{t('apiAccess.thinking.title')}</strong>
+4 -6
View File
@@ -31,7 +31,6 @@ import {
responseList,
} from "../services/managementApi";
import {
applyDeepSeekModelPreset,
DEEPSEEK_BASE_URL,
fetchModels,
mergeModelOptions,
@@ -89,7 +88,7 @@ const apiSectionOptions: ApiSectionOption[] = [
{ id: "claude", managementSection: "claude-api-key", nameKey: "easyMode.api.platformName.claude", provider: "claude", defaultBaseUrl: "", icon: claudeIcon },
{ id: "codex", managementSection: "codex-api-key", nameKey: "easyMode.api.platformName.codex", provider: "codex", defaultBaseUrl: "", icon: codexIcon },
{ id: "gemini", managementSection: "gemini-api-key", nameKey: "easyMode.api.platformName.gemini", provider: "gemini", defaultBaseUrl: "", icon: geminiIcon },
{ id: "deepseek", managementSection: "openai-compatibility", nameKey: "easyMode.api.platformName.deepseek", provider: "openai", defaultBaseUrl: DEEPSEEK_BASE_URL, icon: deepseekIcon },
{ id: "deepseek", managementSection: "codex-api-key", nameKey: "easyMode.api.platformName.deepseek", provider: "deepseek", defaultBaseUrl: DEEPSEEK_BASE_URL, icon: deepseekIcon },
];
const isDeepSeekRecord = (record: Record<string, unknown>) => {
@@ -196,7 +195,7 @@ export function EasyModePage({
const sourceList = recordsBySection[section.managementSection];
const list = section.id === "deepseek"
? sourceList.filter(isDeepSeekRecord)
: section.id === "openai-compatibility"
: section.id === "codex"
? sourceList.filter((record) => !isDeepSeekRecord(record))
: sourceList;
counts[section.id] = list.length;
@@ -427,9 +426,7 @@ export function EasyModePage({
const configPayload = await managementApi.get("/config");
const list = responseList(configPayload, managementSection);
const selectedModels = apiSelectedModels.map((model) => ({ name: model.name.trim() }));
const models = selectedApiSection === "deepseek"
? applyDeepSeekModelPreset(selectedModels)
: selectedModels;
const models = selectedModels;
const newEntry = managementSection === "openai-compatibility"
? {
name: apiRemark.trim() || `${selectedApiSection} (${list.length + 1})`,
@@ -440,6 +437,7 @@ export function EasyModePage({
models,
}
: {
...(selectedApiSection === "deepseek" ? { name: "DeepSeek" } : {}),
"api-key": apiKey.trim(),
"base-url": normalizeBaseUrl(apiBaseUrl.trim()),
models,
+4 -12
View File
@@ -11,14 +11,13 @@ export type ModelOption = {
inputModalities?: Array<'text' | 'image'>;
thinking?: Record<string, unknown>;
};
export type ModelProvider = 'gemini' | 'codex' | 'claude' | 'openai';
export type ModelProvider = 'gemini' | 'codex' | 'deepseek' | 'claude' | 'openai';
export type ModelSelectionMode = 'initial' | 'refresh';
const DEFAULT_GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com';
const DEFAULT_CLAUDE_BASE_URL = 'https://api.anthropic.com';
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com';
export const DEEPSEEK_THINKING_LEVELS = ['low', 'high', 'max'] as const;
const modelKey = (name: string) => name.trim().toLowerCase();
@@ -56,16 +55,6 @@ export function reconcileModelSelection(
return new Set(Array.from(requestedSelection).filter((name) => availableNames.has(name)));
}
export function applyDeepSeekModelPreset(models: ModelOption[]): ModelOption[] {
return models.map((model) => ({
...model,
thinking: {
...model.thinking,
levels: [...DEEPSEEK_THINKING_LEVELS],
},
}));
}
export function normalizeBaseUrl(value: string): string {
let raw = value.trim();
if (!raw) return '';
@@ -97,6 +86,8 @@ export const modelEndpointCandidates = (provider: ModelProvider, baseUrl: string
? DEFAULT_GEMINI_BASE_URL
: provider === 'claude'
? DEFAULT_CLAUDE_BASE_URL
: provider === 'deepseek'
? DEEPSEEK_BASE_URL
: '');
const normalized = normalizeBaseUrl(resolvedBaseUrl);
if (!normalized) return [];
@@ -107,6 +98,7 @@ export const modelEndpointCandidates = (provider: ModelProvider, baseUrl: string
const withoutVersion = base.replace(/\/(?:v1beta|v1)$/i, '');
if (provider === 'gemini') return [`${withoutVersion}/v1beta/models`];
if (provider === 'claude') return [`${withoutVersion}/v1/models`];
if (provider === 'deepseek') return [`${base}/models`];
return [/\/v1$/i.test(base) ? `${base}/models` : `${base}/v1/models`];
};
+5 -2
View File
@@ -40,6 +40,7 @@ export type ProviderHealthCheckOptions = {
const defaultBaseUrl = (provider: ModelProvider) => {
if (provider === 'claude') return 'https://api.anthropic.com';
if (provider === 'gemini') return 'https://generativelanguage.googleapis.com';
if (provider === 'deepseek') return 'https://api.deepseek.com';
return '';
};
@@ -153,10 +154,12 @@ export function buildProviderHealthProbe(
if (key) setHeaderIfMissing(headers, 'Authorization', `Bearer ${key}`);
else if (authIndex) setHeaderIfMissing(headers, 'Authorization', 'Bearer $TOKEN$');
if (provider === 'codex') {
if (provider === 'codex' || provider === 'deepseek') {
return {
...metadata,
url: `${root}/v1/responses`,
url: provider === 'deepseek'
? `${normalizeBaseUrl(baseUrl.trim() || defaultBaseUrl(provider))}/responses`
: `${root}/v1/responses`,
header: headers,
protocol: 'openai-responses',
data: JSON.stringify({
-34
View File
@@ -4557,40 +4557,6 @@ textarea:focus-visible {
min-height: 42px;
}
.provider-preset-summary {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
align-items: center;
gap: 10px;
min-height: 52px;
padding: 9px 11px;
border: 1px solid var(--theme-dce2fb);
border-radius: 7px;
background: var(--theme-f5f7ff);
}
.provider-preset-summary .provider-logo {
width: 30px;
height: 30px;
}
.provider-preset-summary > div {
display: grid;
min-width: 0;
gap: 3px;
}
.provider-preset-summary strong {
color: var(--theme-344ca1);
font-size: var(--font-size-label);
}
.provider-preset-summary span {
color: var(--theme-66709a);
font-size: var(--font-size-meta);
overflow-wrap: anywhere;
}
.thinking-level-config {
display: grid;
gap: 9px;
-14
View File
@@ -1,7 +1,5 @@
import { describe, expect, it } from 'bun:test';
import {
applyDeepSeekModelPreset,
DEEPSEEK_THINKING_LEVELS,
mergeModelOptions,
reconcileModelSelection,
} from '../src/services/modelService';
@@ -46,15 +44,3 @@ describe('model discovery selection', () => {
)).toEqual([{ name: 'DEEPSEEK-CHAT', alias: 'Chat' }]);
});
});
describe('DeepSeek model preset', () => {
it('adds built-in thinking levels while preserving existing metadata', () => {
expect(applyDeepSeekModelPreset([{
name: 'deepseek-reasoner',
thinking: { custom: true, levels: ['legacy'] },
}])).toEqual([{
name: 'deepseek-reasoner',
thinking: { custom: true, levels: [...DEEPSEEK_THINKING_LEVELS] },
}]);
});
});
+13 -12
View File
@@ -5,7 +5,6 @@ import {
buildProviderRecord,
createProviderDraft,
DEEPSEEK_BASE_URL,
DEEPSEEK_THINKING_LEVELS,
exclusionsForModelSelection,
modelSelectionForDiscovery,
parseProviderHeaders,
@@ -50,8 +49,7 @@ describe('API 接入配置合并', () => {
]);
});
it('DeepSeek 对选定模型应用内置思考等级', () => {
expect(DEEPSEEK_THINKING_LEVELS).toEqual(['low', 'high', 'max']);
it('DeepSeek 使用 Codex API 记录且不写入内置思考等级', () => {
const draft = createProviderDraft('deepseek');
const discovered = [
{ name: 'deepseek-chat' },
@@ -64,7 +62,7 @@ describe('API 接入配置合并', () => {
models: discovered,
});
const identified = applyProviderRemarkIdentity('deepseek', prepared);
const result = buildProviderRecord('openai-compatibility', identified);
const result = buildProviderRecord('codex-api-key', identified);
expect(draft.name).toBe('DeepSeek');
expect(draft.remark).toBe('');
@@ -74,19 +72,16 @@ describe('API 接入配置合并', () => {
expect(result).toMatchObject({
name: 'DeepSeek',
'base-url': 'https://api.deepseek.com',
'api-key-entries': [{ 'api-key': 'deepseek-key' }],
'api-key': 'deepseek-key',
models: [
{
name: 'deepseek-chat',
thinking: { levels: [...DEEPSEEK_THINKING_LEVELS] },
},
{
name: 'deepseek-reasoner',
thinking: { levels: [...DEEPSEEK_THINKING_LEVELS] },
},
{
name: 'deepseek-new-model',
thinking: { levels: [...DEEPSEEK_THINKING_LEVELS] },
},
],
});
@@ -103,14 +98,20 @@ describe('API 接入配置合并', () => {
expect(draft.name).toBe('生产环境');
});
it('DeepSeek 接入单独归类,不在 OpenAI 兼容列表重复显示', () => {
const record = {
it('DeepSeek 只从 Codex API 分类识别,旧 OpenAI 兼容记录保持原样', () => {
const codexRecord = {
name: 'custom-deepseek',
'base-url': 'https://api.deepseek.com/v1',
};
const legacyRecord = {
name: 'custom-deepseek',
'base-url': 'https://api.deepseek.com/v1',
};
expect(providerCategoryMatchesRecord('deepseek', record)).toBe(true);
expect(providerCategoryMatchesRecord('openai-compatibility', record)).toBe(false);
expect(providerCategoryMatchesRecord('deepseek', codexRecord, 'codex-api-key')).toBe(true);
expect(providerCategoryMatchesRecord('codex-api-key', codexRecord, 'codex-api-key')).toBe(false);
expect(providerCategoryMatchesRecord('deepseek', legacyRecord, 'openai-compatibility')).toBe(false);
expect(providerCategoryMatchesRecord('openai-compatibility', legacyRecord, 'openai-compatibility')).toBe(true);
});
it('OpenAI 兼容接入把选定思考等级写入全部开放模型', () => {
+20
View File
@@ -178,6 +178,26 @@ describe('API 接入健康检测', () => {
expect(probe.protocol).toBe('openai-responses');
});
it('为 DeepSeek 使用 Codex Responses 协议并保留自定义路径前缀', () => {
const probe = buildProviderHealthProbe(
'deepseek',
'https://api.deepseek.com',
'deepseek-chat',
'deepseek-key',
);
expect(probe.url).toBe('https://api.deepseek.com/responses');
expect(probe.protocol).toBe('openai-responses');
expect(JSON.parse(probe.data)).toEqual({
model: 'deepseek-chat',
input: 'hi',
stream: true,
});
expect(modelEndpointCandidates('deepseek', 'https://api.deepseek.com')).toEqual([
'https://api.deepseek.com/models',
]);
});
it('为 Gemini 使用默认地址并移除 models/ 前缀', () => {
const probe = buildProviderHealthProbe(
'gemini',