后端修复: Ollama adapter 移除死代码/修复超时硬编码/iteration硬编码/pullModel无超时/流异常断开补发DONE; SSE解析器支持非字符串arguments; Sandbox fail-closed安全加固; IPC移除未使用变量 新增功能: TaskOrchestrator子任务编排器; DelegateTaskTool委派工具; Agent Loop并行工具执行; 上下文自动压缩; 记忆检索注入System Prompt 前端修复: useAgentStream text_delta thought累积bug; 工具调用状态正确流转; 修复重复stateChange事件; TraceStep.thought正确填充
189 lines
6.1 KiB
TypeScript
189 lines
6.1 KiB
TypeScript
/**
|
||
* DeepSeek Provider Adapter
|
||
*
|
||
* 基于 OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、流式输出。
|
||
* 模型: deepseek-v4-flash / deepseek-v4-pro(1M 上下文,384K 最大输出)
|
||
*
|
||
* 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用
|
||
* OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。
|
||
*
|
||
* @see apis/deepseek-api-docs-20260518.html
|
||
*/
|
||
|
||
import { BaseAdapter } from './base-adapter';
|
||
import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types';
|
||
import { MetonaFinishReason, MetonaErrorCode } from '../types';
|
||
import { buildOpenAICompatibleMessages, buildOpenAICompatibleTools } from './shared/openai-format';
|
||
import { parseSSEStream, parseOpenAICompatibleResponse } from './shared/sse-stream';
|
||
|
||
export class DeepSeekAdapter extends BaseAdapter {
|
||
override readonly provider: string = 'deepseek';
|
||
readonly supportedModels = ['deepseek-v4-pro', 'deepseek-v4-flash'];
|
||
readonly supportsToolCalling = true;
|
||
readonly supportsThinking = true;
|
||
|
||
// ===== POST /chat/completions (非流式) =====
|
||
|
||
async chat(request: MetonaRequest): Promise<MetonaResponse> {
|
||
const body = this.toNativeRequest(request, false);
|
||
|
||
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(body),
|
||
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() as Record<string, unknown>;
|
||
const parsed = parseOpenAICompatibleResponse(data, request.meta.requestId, this.provider, this.config.defaultModel);
|
||
|
||
return {
|
||
meta: {
|
||
requestId: request.meta.requestId,
|
||
provider: this.provider,
|
||
model: (data.model as string) ?? this.config.defaultModel,
|
||
latencyMs: 0,
|
||
timestamp: Date.now(),
|
||
},
|
||
content: parsed.content,
|
||
reasoningContent: parsed.reasoningContent,
|
||
toolCalls: parsed.toolCalls,
|
||
usage: parsed.usage,
|
||
finishReason: parsed.finishReason as MetonaFinishReason,
|
||
};
|
||
}
|
||
|
||
// ===== POST /chat/completions (流式) =====
|
||
|
||
async *chatStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
|
||
const body = this.toNativeRequest(request, 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(body),
|
||
signal: AbortSignal.timeout(this.config.timeoutMs ?? 300_000),
|
||
});
|
||
|
||
if (!response.ok || !response.body) {
|
||
throw new Error(`DeepSeek stream error: ${response.status}`);
|
||
}
|
||
|
||
yield* parseSSEStream(
|
||
response.body,
|
||
request.meta.requestId,
|
||
request.meta.sessionId,
|
||
request.meta.iteration,
|
||
);
|
||
}
|
||
|
||
// ===== GET /models =====
|
||
|
||
async listModels(): Promise<string[]> {
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ===== GET /user/balance =====
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ========== 私有方法 ==========
|
||
|
||
/**
|
||
* 构建 DeepSeek 原生请求体
|
||
*
|
||
* DeepSeek 特有参数:
|
||
* - thinking: { type: "enabled" } — 启用思考模式
|
||
* - reasoning_effort — 思考强度映射
|
||
* - stream_options: { include_usage: true } — 流式返回 usage
|
||
*/
|
||
private toNativeRequest(request: MetonaRequest, stream: boolean): Record<string, unknown> {
|
||
const messages = buildOpenAICompatibleMessages(request);
|
||
const tools = buildOpenAICompatibleTools(request.tools);
|
||
|
||
const body: Record<string, unknown> = {
|
||
model: this.config.defaultModel,
|
||
messages,
|
||
temperature: request.params.temperature,
|
||
max_tokens: request.params.maxTokens,
|
||
stream,
|
||
};
|
||
|
||
if (stream) {
|
||
body.stream_options = { include_usage: true };
|
||
}
|
||
|
||
if (tools) {
|
||
body.tools = tools;
|
||
}
|
||
|
||
// Thinking 模式
|
||
// API 默认 thinking.type = "enabled",必须显式发送 disabled 才能关闭
|
||
if (request.params.thinkingEnabled === false) {
|
||
body.thinking = { type: 'disabled' };
|
||
} else if (request.params.thinkingEnabled) {
|
||
body.thinking = { type: 'enabled' };
|
||
const effortMap: Record<string, string> = {
|
||
low: 'high', medium: 'high', high: 'high', max: 'max',
|
||
};
|
||
// DeepSeek API 仅支持 high / max 两档,low/medium 映射为 high
|
||
body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high';
|
||
}
|
||
|
||
// 停止序列
|
||
if (request.params.stopSequences?.length) {
|
||
body.stop = request.params.stopSequences;
|
||
}
|
||
|
||
return body;
|
||
}
|
||
}
|