Files
metona-ai-desktop/electron/harness/adapters/agnes-ai.adapter.ts
T
thzxx e4c0259afc feat: Agnes上下文512K→1M + Ollama支持配置num_ctx
Agnes AI:
- adapter注释和API文档更新: 上下文512K→1M (官方已升级)

Ollama num_ctx 支持:
- MetonaGenerationParams 新增 contextLength 字段
- OllamaAdapter.toNativeRequest 发送 options.num_ctx
- AgentLoopEngine 从 config 读取并传递到请求参数
- database.service 新增 ollama.numCtx 默认配置(null=由模型决定)
- SettingsModal LLM设置: Ollama 选中时显示上下文长度输入框
- main.ts 启动时从 configService 读取 ollama.numCtx

数据流:
Settings → configService → main.ts → AgentLoopEngine.config
→ MetonaRequest.params → OllamaAdapter → options.num_ctx
2026-06-27 21:57:05 +08:00

160 lines
5.3 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: 655361M 上下文,65.5K 最大输出)
*/
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;
}
}