/** * DeepSeek Provider Adapter * * 基于 OpenAI 兼容 API 的 DeepSeek 适配器。 * 支持 Tool Calling、Thinking 模式、流式输出、JSON 结构化输出。 * * 完整实现 DeepSeek API 文档中所有参数: * - model, messages, thinking, reasoning_effort * - stream, stream_options * - tools, tool_choice * - response_format (JSON) * - stop, max_tokens, temperature, top_p * - logprobs, top_logprobs * - prompt_cache_hit/miss_tokens, reasoning_tokens * - GET /models, GET /user/balance * * @see apis/deepseek-api-docs-20260518.html */ import { BaseAdapter } from './base-adapter'; import { nanoid } from 'nanoid'; import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types'; import { MetonaFinishReason, MetonaStreamEventType, MetonaErrorCode } from '../types'; export class DeepSeekAdapter extends BaseAdapter { override readonly provider: string = 'deepseek'; readonly supportedModels = ['deepseek-v4-pro', 'deepseek-v4-flash']; readonly supportsToolCalling = true; readonly supportsThinking = true; async chat(request: MetonaRequest): Promise { const nativeRequest = this.toNativeRequest(request); const response = await fetch(`${this.config.baseURL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}`, ...this.config.headers, }, body: JSON.stringify(nativeRequest), signal: AbortSignal.timeout(this.config.timeoutMs ?? 120_000), }); if (!response.ok) { const errorBody = await response.text().catch(() => ''); throw new Error(`DeepSeek API error: ${response.status} ${response.statusText} - ${errorBody}`); } const data = await response.json(); return this.toMetonaResponse(data, request.meta.requestId); } async *chatStream(request: MetonaRequest): AsyncIterable { const nativeRequest = { ...this.toNativeRequest(request), stream: true, stream_options: { include_usage: true } }; const response = await fetch(`${this.config.baseURL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}`, ...this.config.headers, }, body: JSON.stringify(nativeRequest), }); if (!response.ok || !response.body) { throw new Error(`DeepSeek stream error: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let seq = 0; let buffer = ''; // 工具调用缓冲区:index → { name, argsBuffer } const toolCallsBuffer = new Map(); while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith('data: ')) continue; const data = trimmed.slice(6); if (data === '[DONE]') { // 流结束前,将缓冲区中的工具调用转为 TOOL_CALL_COMPLETE for (const [index, buf] of toolCallsBuffer) { try { yield { type: MetonaStreamEventType.TOOL_CALL_COMPLETE, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: seq++, timestamp: Date.now(), toolCall: { id: `tc_${nanoid(8)}`, name: buf.name, args: buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {}, iteration: request.meta.iteration, timestamp: Date.now(), }, }; } catch { // JSON 解析失败,跳过 } } toolCallsBuffer.clear(); yield { type: MetonaStreamEventType.DONE, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: seq++, timestamp: Date.now(), }; return; } try { const chunk = JSON.parse(data); const delta = chunk.choices?.[0]?.delta; // 文本内容增量 if (delta?.content) { yield { type: MetonaStreamEventType.TEXT_DELTA, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: seq++, timestamp: Date.now(), delta: delta.content, }; } // 推理内容增量(Thinking 模式) if (delta?.reasoning_content) { yield { type: MetonaStreamEventType.REASONING_DELTA, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: seq++, timestamp: Date.now(), delta: delta.reasoning_content, }; } // 工具调用增量 — 缓冲拼接 if (delta?.tool_calls) { for (const tc of delta.tool_calls) { const idx = tc.index ?? 0; if (!toolCallsBuffer.has(idx)) { toolCallsBuffer.set(idx, { name: tc.function?.name ?? '', argsBuffer: '' }); } const buf = toolCallsBuffer.get(idx)!; if (tc.function?.name) buf.name = tc.function.name; if (tc.function?.arguments) buf.argsBuffer += tc.function.arguments; yield { type: MetonaStreamEventType.TOOL_CALL_DELTA, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: seq++, timestamp: Date.now(), toolCallDelta: { index: idx, name: tc.function?.name, argsDelta: tc.function?.arguments, }, }; } } // 流结束时的 usage 信息 if (chunk.usage) { yield { type: MetonaStreamEventType.USAGE, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: seq++, timestamp: Date.now(), usage: { inputTokens: chunk.usage.prompt_tokens ?? 0, outputTokens: chunk.usage.completion_tokens ?? 0, totalTokens: chunk.usage.total_tokens ?? 0, reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens, cacheHitTokens: chunk.usage.prompt_cache_hit_tokens, cacheMissTokens: chunk.usage.prompt_cache_miss_tokens, }, }; } } catch { // 跳过解析失败的行 } } } } /** * 列出可用模型 */ 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) return this.supportedModels; const data = await response.json() as { data?: Array<{ id: string }> }; return data.data?.map((m) => m.id) ?? this.supportedModels; } catch { return this.supportedModels; } } /** * 查询账户余额 */ async getBalance(): Promise<{ currency: string; totalBalance: string; grantedBalance: string; toppedUpBalance: string } | null> { try { const response = await fetch(`${this.config.baseURL}/user/balance`, { headers: { Authorization: `Bearer ${this.config.apiKey}` }, signal: AbortSignal.timeout(10_000), }); if (!response.ok) return null; const data = await response.json() as { currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string }; return { currency: data.currency ?? 'CNY', totalBalance: data.total_balance ?? '0', grantedBalance: data.granted_balance ?? '0', toppedUpBalance: data.topped_up_balance ?? '0', }; } catch { return null; } } // ========== 私有转换方法 ========== /** * 将 MetonaRequest 转换为 OpenAI 兼容的原生请求格式 */ protected toNativeRequest(request: MetonaRequest): Record { const messages = [ { role: 'system', content: [ request.systemPrompt.roleDefinition, request.systemPrompt.outputConstraints, request.systemPrompt.safetyGuidelines, request.systemPrompt.dynamicReminders, ].filter(Boolean).join('\n\n'), }, ...request.messages.filter((m) => m.role !== 'system').map((m) => { const msg: Record = { role: m.role, content: m.content }; if (m.role === 'assistant' && m.toolCalls?.length) { msg.tool_calls = m.toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.args) }, })); // 工具调用轮次的 reasoning_content 必须携带回上下文 if (m.reasoningContent) { msg.reasoning_content = m.reasoningContent; } } if (m.role === 'tool' && m.toolResult) { msg.tool_call_id = m.toolResult.toolCallId; msg.content = typeof m.toolResult.result === 'string' ? m.toolResult.result : JSON.stringify(m.toolResult.result); } return msg; }), ]; const body: Record = { model: this.config.defaultModel, messages, temperature: request.params.temperature ?? 0, max_tokens: request.params.maxTokens, stream: request.params.stream ?? false, }; // Tool Calling if (request.tools?.length) { body.tools = request.tools.map((t) => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters, }, })); } // Thinking 模式 if (request.params.thinkingEnabled) { body.thinking = { type: 'enabled' }; // reasoning_effort 映射 const effortMap: Record = { low: 'high', medium: 'high', high: 'high', max: 'max' }; body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high'; } // 停止序列 if (request.params.stopSequences?.length) { body.stop = request.params.stopSequences; } // 安全约束 if (request.constraints?.allowedTools?.length) { body.tool_choice = 'auto'; } return body; } private toMetonaResponse(data: Record, requestId: string): MetonaResponse { const choice = (data.choices as Array>)?.[0]; const message = choice?.message as Record | undefined; const usage = data.usage as Record | undefined; const toolCalls = message?.tool_calls as Array> | undefined; return { meta: { requestId, provider: this.provider, model: (data.model as string) ?? this.config.defaultModel, latencyMs: 0, timestamp: Date.now(), }, content: (message?.content as string) ?? '', reasoningContent: message?.reasoning_content as string | undefined, toolCalls: toolCalls?.map((tc) => { const fn = tc.function as Record; return { id: tc.id as string, name: fn.name as string, args: JSON.parse(fn.arguments as string), iteration: 0, timestamp: Date.now(), }; }), usage: { inputTokens: (usage?.prompt_tokens as number) ?? 0, outputTokens: (usage?.completion_tokens as number) ?? 0, totalTokens: (usage?.total_tokens as number) ?? 0, reasoningTokens: (usage?.completion_tokens_details as Record)?.reasoning_tokens as number | undefined, cacheHitTokens: usage?.prompt_cache_hit_tokens as number | undefined, cacheMissTokens: usage?.prompt_cache_miss_tokens as number | undefined, }, finishReason: this.mapFinishReason(choice?.finish_reason as string), }; } private mapFinishReason(reason: string): MetonaFinishReason { switch (reason) { case 'stop': return MetonaFinishReason.STOP; case 'length': return MetonaFinishReason.LENGTH; case 'tool_calls': return MetonaFinishReason.TOOL_CALLS; case 'content_filter': return MetonaFinishReason.CONTENT_FILTER; default: return MetonaFinishReason.STOP; } } }