/** * DeepSeek Provider Adapter * * 基于 OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、流式输出。 * 模型: deepseek-v4-flash / deepseek-v4-pro(1M 上下文,384K 最大输出) * * v0.6.4 P3-1: 继承 OpenAICompatibleAdapter —— send/sendStream/响应组装/ * 认证头/上下文窗口回退链全部收敛到共享基类,本文件只保留 DeepSeek 差异点: * vision 模型判定、/models 合并、/user/balance、thinking+reasoning_effort 映射。 * * @see apis/deepseek-api-docs-20260518.html */ import log from 'electron-log'; import type { MetonaRequest } from '../types'; import type { MetonaModelInfo } from '../types/metona-adapter'; import { buildOpenAICompatibleMessages, buildOpenAICompatibleTools } from './shared/openai-format'; import { OpenAICompatibleAdapter } from './shared/openai-compatible-base'; export class DeepSeekAdapter extends OpenAICompatibleAdapter { // H-2 修复: providerId(规范要求) override readonly providerId: string = 'deepseek'; readonly supportedModels = [ 'deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', ]; readonly supportsToolCalling = true; readonly supportsThinking = true; // H-2 修复: DeepSeek 模型元信息(v0.8.1: 仅承载展示与能力声明 —— 窗口/输出上限 // 数值已按硬性契约删除,唯一合法来源是设置面板 llm.contextWindow / llm.maxTokens) private static readonly MODEL_INFO: Record = { 'deepseek-v4-pro': { id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', supportsToolCalling: true, supportsThinking: true, description: 'DeepSeek 旗舰模型,支持深度推理与工具调用', }, 'deepseek-v4-flash': { id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', supportsToolCalling: true, supportsThinking: true, description: 'DeepSeek 快速版,低延迟推理', }, // v0.5.4: DeepSeek 多模态实验模型(OpenAI image_url content parts 格式) 'deepseek-v4-flash-vision-exp': { id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek V4 Flash Vision (Exp)', supportsToolCalling: true, supportsThinking: false, description: 'DeepSeek 多模态实验模型,支持图片输入(image_url content parts)', }, }; // ===== 共享基类差异声明 ===== protected override chatCompletionsUrl(): string { return `${this.config.baseURL}/chat/completions`; } protected override sendTimeoutMs(): number { return 120_000; } protected override modelInfoTable(): Record { return DeepSeekAdapter.MODEL_INFO; } protected override providerLabel(): string { return 'DeepSeek'; } /** * v0.5.4: 当前模型是否支持多模态图片输入 * * DeepSeek 仅 vision 系列模型支持图片(命名含 'vision'); * 非 vision 模型收到 images 时静默丢弃(避免 API 400)。 * 前端上传入口由 llm.multimodalEnabled 配置总开关控制,此处是 adapter 侧的模型级防线。 */ private isVisionModel(): boolean { return this.config.defaultModel.includes('vision'); } // ===== GET /models ===== /** * 优先尝试从 API 获取实时模型列表,并合并本地 MODEL_INFO 元数据。 * API 不可用时回退到 supportedModels。 */ override async listModels(): Promise { try { const response = await fetch(`${this.config.baseURL}/models`, { headers: { Authorization: `Bearer ${this.config.apiKey}` }, signal: AbortSignal.timeout(10_000), }); if (response.ok) { const data = (await response.json()) as { data?: Array<{ id: string }> }; if (data.data?.length) { // 合并 API 返回的模型 ID 与本地元数据 return data.data.map((m) => DeepSeekAdapter.MODEL_INFO[m.id] ?? { id: m.id }); } } } catch { // API 不可用时降级 } // 回退到 supportedModels(带本地元数据) return this.supportedModels.map((id) => DeepSeekAdapter.MODEL_INFO[id] ?? { id }); } // ===== GET /user/balance ===== /** * 查询账户余额 * * v0.5.2 修复: 官方 API 返回 balance_infos 数组格式(此前按扁平字段解析恒为 0)。 * URL 规范化: 余额端点为 {root}/user/balance(无 /v1 前缀),需剥离配置中的尾斜杠与 /v1。 */ async getBalance(): Promise<{ currency: string; totalBalance: string; grantedBalance: string; toppedUpBalance: string; } | null> { try { const root = this.config.baseURL.replace(/\/+$/, '').replace(/\/v1$/, ''); const response = await fetch(`${root}/user/balance`, { headers: { Authorization: `Bearer ${this.config.apiKey}` }, signal: AbortSignal.timeout(10_000), }); if (!response.ok) return null; const data = (await response.json()) as { is_available?: boolean; balance_infos?: Array<{ currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string; }>; // 扁平格式字段(网关/代理兼容) currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string; }; const info = data.balance_infos?.[0] ?? data; return { currency: info.currency ?? 'CNY', totalBalance: info.total_balance ?? '0', grantedBalance: info.granted_balance ?? '0', toppedUpBalance: info.topped_up_balance ?? '0', }; } catch { return null; } } // ========== 协议参数映射(DeepSeek 差异点) ========== protected override toNativeRequest( request: MetonaRequest, stream: boolean, ): Record { // v0.6.2: images 处理收敛至共享层(includeImages = vision 模型才转换, // 非 vision 静默丢弃——正确行为,见 openai-format.ts #27 记录) const messages = buildOpenAICompatibleMessages(request, this.isVisionModel()); const tools = buildOpenAICompatibleTools(request.tools); // v0.8.1 硬性契约: max_tokens 原样透传设置面板「最大输出上限」(llm.maxTokens), // 删除了旧的按模型元信息钳制逻辑 —— 代码中不存在任何写死的输出上限。 const body: Record = { model: this.config.defaultModel, messages, temperature: request.params.temperature, max_tokens: request.params.maxTokens, stream, }; // v0.5.4: vision 模型图片数审计(转换在共享层完成) if (this.isVisionModel()) { const imageCount = request.messages.reduce((n, m) => n + (m.images?.length ?? 0), 0); if (imageCount > 0) { log.info(`[DeepSeek] Vision model processing ${imageCount} image(s)`); } } if (stream) { body.stream_options = { include_usage: true }; } if (tools) { body.tools = tools; } // Thinking 模式 // v0.8.0 修订(用户意图优先): 思考参数完全遵循用户配置,不再按 // supportsThinking 元信息硬门控 —— 事故复盘中 vision-exp 元信息标注 // "不支持思考"、实际却产生了推理内容,元信息不可靠。预算耗尽风险由 // 引擎空响应守卫的降级重试链路兜底(关闭思考重试一次 → 仍失败则 // OUTPUT_LENGTH_EXCEEDED 明确报错)。元信息与配置不符时仅告警不拦截。 const wantThinking = request.params.thinkingEnabled === true; // API 默认 thinking.type = "enabled",必须显式发送 disabled 才能关闭 if (!wantThinking) { body.thinking = { type: 'disabled' }; } else { body.thinking = { type: 'enabled' }; // v0.8.3: 新增 xhigh / true 档 —— DeepSeek API 仅 high / max 两档,就近映射 high const effortMap: Record = { low: 'high', medium: 'high', high: 'high', xhigh: 'high', max: 'max', true: 'high', }; // DeepSeek API 仅支持 high / max 两档,low/medium/xhigh/true 映射为 high body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high'; // 元信息标注不支持思考但用户开启 —— 告知降级兜底路径(不拦截) if (DeepSeekAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking === false) { log.warn( `[DeepSeek] model "${this.config.defaultModel}" metadata says thinking unsupported — sending thinking params per user config (degraded retry handles budget exhaustion)`, ); } // v0.8.0 P0-3: 思考会占用输出预算 —— 用户配置的输出预算过小时显式告警 //(思考 token 计入 max_tokens,预算过小会出现"思考耗尽正文为零"截断) if (typeof request.params.maxTokens === 'number' && request.params.maxTokens < 8192) { log.warn( `[DeepSeek] thinking enabled with small output budget (${request.params.maxTokens} tokens per user config) — reasoning may consume the entire budget and truncate the answer`, ); } } // 停止序列 if (request.params.stopSequences?.length) { body.stop = request.params.stopSequences; } return body; } }