/** * 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 模型元信息(1M 上下文,384K 最大输出) private static readonly MODEL_INFO: Record = { 'deepseek-v4-pro': { id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', contextWindow: 1_000_000, maxOutputTokens: 384_000, supportsToolCalling: true, supportsThinking: true, description: 'DeepSeek 旗舰模型,1M 上下文,支持深度推理与工具调用', }, 'deepseek-v4-flash': { id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', contextWindow: 1_000_000, maxOutputTokens: 384_000, supportsToolCalling: true, supportsThinking: true, description: 'DeepSeek 快速版,1M 上下文,低延迟推理', }, // 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)', contextWindow: 128_000, maxOutputTokens: 8_192, 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.5.3: max_tokens 按模型上限钳制 — 引擎默认 63488 超过部分模型上限时 API 直接 400 const modelInfo = DeepSeekAdapter.MODEL_INFO[this.config.defaultModel]; const maxOutput = modelInfo?.maxOutputTokens ?? 384_000; const maxTokens = Math.min(request.params.maxTokens ?? maxOutput, maxOutput); const body: Record = { model: this.config.defaultModel, messages, temperature: request.params.temperature, max_tokens: 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 P0-3 根治: 模型能力门控 —— MODEL_INFO.supportsThinking === false 的 // 模型(如 deepseek-v4-flash-vision-exp)一律不发思考参数并显式 disabled, // 防止思考消耗输出预算(生产事故:vision-exp 8192 输出预算被 max 档思考 // 全部烧光 → finish_reason=length 空回复 → 会话静默停止)。 const modelThinkingSupported = DeepSeekAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking !== false; const wantThinking = request.params.thinkingEnabled === true && modelThinkingSupported; // API 默认 thinking.type = "enabled",必须显式发送 disabled 才能关闭 if (!wantThinking) { body.thinking = { type: 'disabled' }; } else { body.thinking = { type: 'enabled' }; const effortMap: Record = { low: 'high', medium: 'high', high: 'high', max: 'max', }; // DeepSeek API 仅支持 high / max 两档,low/medium 映射为 high body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high'; // v0.8.0 P0-3: 思考会占用输出预算 —— 钳制后预算过小时显式告警 //(思考 token 计入 max_tokens,预算过小会出现"思考耗尽正文为零"截断) if (maxTokens < 8192) { log.warn( `[DeepSeek] thinking enabled with small output budget (${maxTokens} tokens after model clamp) — reasoning may consume the entire budget and truncate the answer`, ); } } // 停止序列 if (request.params.stopSequences?.length) { body.stop = request.params.stopSequences; } return body; } }