Files
metona-ai-desktop/electron/harness/adapters/shared/openai-format.ts
T

110 lines
4.1 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.
/**
* OpenAI 兼容 API 格式构建工具
*
* 将 MetonaRequest 转换为 OpenAI /chat/completions 兼容的原生请求格式。
* DeepSeek、Agnes AI 和 MiMo 共享此工具,各自 Adapter 只需处理 Provider 特有的差异参数。
*
* @see electron/harness/types/metona-request.ts — MetonaRequest 定义
* @see apis/deepseek-api-docs-20260518.html
* @see apis/agnes-ai-api-docs-20260625.html
* @see apis/mimo-api-docs-20260715.html
*/
import type { MetonaRequest, MetonaToolDef } from '../../types';
/**
* 构建 OpenAI 兼容的 messages 数组
*
* 处理:
* - System Prompt 拼接(静态区 + 动态区 + 安全准则)
* - 工具调用历史保留(reasoning_content + tool_calls
* - 工具结果注入(tool_call_id + content
*
* 注意:图片(多模态)处理不属于此共享函数。
* 各 Provider 对多模态的支持不同(DeepSeek 不支持,Agnes/Ollama 支持但格式各异),
* 应在各自 Adapter 的 toNativeRequest 中处理。
*/
export function buildOpenAICompatibleMessages(
request: MetonaRequest,
): Array<Record<string, unknown>> {
const systemContent = [
request.systemPrompt.roleDefinition,
request.systemPrompt.outputConstraints,
request.systemPrompt.safetyGuidelines,
request.systemPrompt.dynamicReminders,
]
.filter(Boolean)
.join('\n\n');
const nonSystemMessages = request.messages
.filter((m) => m.role !== 'system')
.map((m) => {
// v0.3.0 修复: assistant 消息有 tool_calls 但 content 为空时,content 设为 null
// DeepSeek/OpenAI API 要求有 tool_calls 的 assistant 消息 content 必须为 null 而非空字符串
const msg: Record<string, unknown> = {
role: m.role,
content: m.content,
};
// 注意:图片(多模态)处理不在此共享函数中。
// DeepSeek 不支持多模态,images 被静默丢弃是正确行为。
// Agnes/MiMo 各自的 toNativeRequest 中有独立的 images 处理。
// 审查修复: #27 曾在此添加 images 处理,但 DeepSeek 不支持多模态会导致 API 400,已撤销。
// === Assistant 消息 ===
if (m.role === 'assistant') {
// 工具调用历史
if (m.toolCalls?.length) {
msg.tool_calls = m.toolCalls.map((tc) => ({
id: tc.id,
type: 'function',
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
}));
// 有 tool_calls 时 content 必须为 nullAPI 规范)
if (!m.content) msg.content = null;
}
// 推理内容(无论是否有工具调用,都保留 reasoning_content
if (m.reasoningContent) {
msg.reasoning_content = m.reasoningContent;
}
}
// === 工具执行结果 ===
if (m.role === 'tool' && m.toolResult) {
msg.tool_call_id = m.toolResult.toolCallId;
// CE-2 修复: 工具失败时 result 为 null,优先用 error 字段作为 content
// 否则 LLM 看到 "null" 不知道失败原因,可能重复调用导致死循环
msg.content = m.toolResult.error
? m.toolResult.error
: (typeof m.toolResult.result === 'string'
? m.toolResult.result
: JSON.stringify(m.toolResult.result));
// #26 修复: 确保 tool 消息 content 不为 undefined
// JSON.stringify(undefined) 返回 undefined(非字符串),会导致 content 字段在序列化后消失
// OpenAI/DeepSeek/Agnes API 严格要求 tool 消息必须有 content 字段,缺失会返回 400
if (msg.content === undefined) msg.content = '';
}
return msg;
});
return [{ role: 'system', content: systemContent }, ...nonSystemMessages];
}
/**
* 构建 OpenAI 兼容的 tools 数组
*/
export function buildOpenAICompatibleTools(
tools?: MetonaToolDef[],
): Array<Record<string, unknown>> | undefined {
if (!tools?.length) return undefined;
return tools.map((t) => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
}