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;