feat(api-access): manage DeepSeek through Codex API
This commit is contained in:
1 parent
2a99db2908
commit
2b41eab8df
11 files changed
+88
-145
No files matched your search
@@ -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、またはカスタムレベル',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`];
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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] },
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -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 兼容接入把选定思考等级写入全部开放模型', () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user