Files
metona-ai-desktop/electron/harness/adapters/agnes-ai.adapter.ts
T
thzxx aa0dd464ac fix: 对照 API 文档修复 3 个 Adapter 的 8 个实现差异
通过逐接口对比 DeepSeek/AgnesAI/Ollama 官方文档与实现代码,发现并修复:

DeepSeek (4 fix):
- temperature 默认值: 错误发送 0(API默认=1),改为 undefined 让 API 用默认值
- thinking 禁用: API 默认 enabled,不传=开启。改为显式发送 {type:disabled}
- reasoning_effort: 补充 xhigh→max 映射(文档规定)
- stream 请求: 补充 AbortSignal.timeout 超时控制

Agnes AI (2 fix):
- temperature 默认值: 同上
- 图片 image_url: 移除文档未提及的 detail 字段

Ollama (3 fix):
- temperature 默认值: 同上(Ollama默认≈0.8)
- tool_calls arguments: 从对象改为 JSON 字符串(REST API 要求)
- finishReason: 使用 done_reason 字段正确映射(之前始终返回 STOP)
2026-06-27 21:48:15 +08:00

160 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Agnes AI Provider Adapter
*
* OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、多模态(图片)。
*
* 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用
* OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。
*
* 与 DeepSeek 的差异:
* - Thinking 模式使用 chat_template_kwargs(非 thinking 字段)
* - 默认 max_tokens 更大(65536 vs 8192
*
* @see apis/agnes-ai-api-docs-20260625.html
*/
import { BaseAdapter } from './base-adapter';
import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types';
import { MetonaFinishReason } from '../types';
import { buildOpenAICompatibleMessages, buildOpenAICompatibleTools } from './shared/openai-format';
import { parseSSEStream, parseOpenAICompatibleResponse } from './shared/sse-stream';
export class AgnesAdapter extends BaseAdapter {
override readonly provider: string = 'agnes';
readonly supportedModels = ['agnes-2.0-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 ?? 300_000),
});
if (!response.ok) {
const errorBody = await response.text().catch(() => '');
throw new Error(`Agnes AI 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),
});
if (!response.ok || !response.body) {
throw new Error(`Agnes AI stream error: ${response.status}`);
}
yield* parseSSEStream(
response.body,
request.meta.requestId,
request.meta.sessionId,
request.meta.iteration,
);
}
// ========== 私有方法 ==========
/**
* 构建 Agnes AI 原生请求体
*
* Agnes AI 特有参数:
* - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}]
* - chat_template_kwargs: { enable_thinking: true } — 启用思考模式(非 thinking 字段)
* - 默认 max_tokens: 65536512K 上下文)
*/
private toNativeRequest(request: MetonaRequest, stream: boolean): Record<string, unknown> {
const messages = buildOpenAICompatibleMessages(request);
const tools = buildOpenAICompatibleTools(request.tools);
// === Agnes 多模态:将 images 转为 OpenAI content 数组 ===
// buildOpenAICompatibleMessages 不处理图片(各 Provider 自行处理)
const nonSystemMsgs = request.messages.filter((m) => m.role !== 'system');
for (let i = 0; i < messages.length; i++) {
// messages[0] 是 system,非 system 消息从 messages[1] 开始
if (i === 0) continue;
const origMsg = nonSystemMsgs[i - 1];
if (!origMsg?.images?.length) continue;
const contentParts: Array<Record<string, unknown>> = [];
if (origMsg.content) {
contentParts.push({ type: 'text', text: origMsg.content });
}
for (const img of origMsg.images) {
contentParts.push({
type: 'image_url',
image_url: { url: img.url },
});
}
messages[i].content = contentParts;
}
const body: Record<string, unknown> = {
model: this.config.defaultModel,
messages,
temperature: request.params.temperature,
max_tokens: request.params.maxTokens ?? 65536,
stream,
};
if (stream) {
body.stream_options = { include_usage: true };
}
if (tools) {
body.tools = tools;
}
// Thinking 模式:Agnes 使用 chat_template_kwargs 而非 thinking
if (request.params.thinkingEnabled) {
body.chat_template_kwargs = { enable_thinking: true };
}
// 停止序列
if (request.params.stopSequences?.length) {
body.stop = request.params.stopSequences;
}
return body;
}
}