Files
metona-ai-desktop/electron/harness/adapters/mimo.adapter.ts
T
thzxx ceb8ee644d feat: 升级至 v0.3.7 — 前后端状态同步与错误处理全量修复
核心引擎修复:
- CE-1: 上下文压缩摘要 role 从 system 改为 user,避免被 adapter 过滤
- CE-2: 工具失败时优先使用 error 字段(engine/openai-format/ollama 三处)
- P0-1: DeadLoopError 终止时正确传 error 参数,前端可见 ERROR 事件
- MT-1: 新增 waitForAbort 方法,abortSession 等待 run 结束再返回
- MT-2: TERMINATED 状态到达时标记步骤完成,避免 Trace Viewer 转圈
- MT-3: 压缩边界检测孤立 tool 消息,避免 API 400 错误
- isRetryableError 与 catch 分支统一 toLowerCase

IPC 与主进程修复:
- P0-2: createAdapter 配置缺失返回 null,FALLBACK_ADAPTER 兜底
- P0-3: reloadAdapter 失败返回 success:false 通知前端
- P1-5: 校验失败发 ERROR+DONE 流事件,防止 isStreaming 卡死
- P1-6: configLoaded 标志,配置加载前禁用发送按钮
- P1-7: MCP initialize 移到 agentLoop 后,完成后同步工具
- P2-11: beforeLoad 在 loadURL 前注册 IPC handler
- P2-12: provider 切换竞态保护

前端状态同步修复:
- clearSessions 后同步清空前端会话与消息状态
- clearMemories 通过 memoryVersion 触发 MemoryViewer 重新加载
- ContextMenu 4 个 session 操作补全 IPC 调用与 try/catch
- useConfig 配置保存失败回滚 UI 并提示
- handleToggle 工具切换失败回滚单个工具状态

错误处理全量补全:
- 所有 await window.metona 调用补全 try/catch 与 toast 反馈
- MCP addServer/toggleServer/removeServer 检查返回值
- showItemInFolder 检查返回值(handleOpen/handleOpenInFolder)
- sse-stream/ollama NDJSON 解析失败改为 log.warn
- adapter throwHttpError 带 status 属性供 isRetryableError 判断
2026-07-16 22:40:32 +08:00

205 lines
7.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.
/**
* MiMo (Xiaomi) Provider Adapter
*
* 基于 OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、流式输出。
* 模型: mimo-v2.5-pro131072 max_tokens/ mimo-v2.532768 max_tokens
*
* 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用
* OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。
*
* 与 DeepSeek 适配器的关键差异:
* - 使用 max_completion_tokens(非 max_tokens
* - thinking 参数结构与 DeepSeek 一致(thinking.type: "enabled"/"disabled"
* - 不提供 /models 端点(listModels 回退到本地元数据)
* - 不提供 /user/balance 端点
* - tool_choice 仅支持 "auto"
*
* @see apis/mimo-api-docs-20260715.html
*/
import { BaseAdapter } from './base-adapter';
import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types';
import { MetonaFinishReason } from '../types';
import type { MetonaModelInfo } from '../types/metona-adapter';
import { buildOpenAICompatibleMessages, buildOpenAICompatibleTools } from './shared/openai-format';
import { parseSSEStream, parseOpenAICompatibleResponse } from './shared/sse-stream';
export class MimoAdapter extends BaseAdapter {
override readonly providerId: string = 'mimo';
readonly supportedModels = ['mimo-v2.5-pro', 'mimo-v2.5'];
readonly supportsToolCalling = true;
readonly supportsThinking = true;
// MiMo 模型元信息
// mimo-v2.5-pro: 131072 max_completion_tokensmimo-v2.5: 32768
// 官方未公布上下文窗口大小,保守设为 131072(与 pro 的 max_output 一致)
private static readonly MODEL_INFO: Record<string, MetonaModelInfo> = {
'mimo-v2.5-pro': {
id: 'mimo-v2.5-pro',
name: 'MiMo V2.5 Pro',
contextWindow: 131_072,
maxOutputTokens: 131_072,
supportsToolCalling: true,
supportsThinking: true,
description: '小米 MiMo 旗舰模型,支持深度思考与工具调用',
},
'mimo-v2.5': {
id: 'mimo-v2.5',
name: 'MiMo V2.5',
contextWindow: 131_072,
maxOutputTokens: 32_768,
supportsToolCalling: true,
supportsThinking: true,
description: '小米 MiMo 标准模型,低延迟推理',
},
};
// ===== POST /chat/completions (非流式) =====
async send(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: this.getFetchSignal(this.config.timeoutMs ?? 120_000),
});
if (!response.ok) {
await this.throwHttpError(response, 'MiMo API error');
}
const data = await response.json() as Record<string, unknown>;
const parsed = parseOpenAICompatibleResponse(data, request.meta.requestId, this.providerId, this.config.defaultModel);
return {
meta: {
requestId: request.meta.requestId,
provider: this.providerId,
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 *sendStream(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: this.getFetchSignal(this.config.timeoutMs ?? 300_000),
});
if (!response.ok || !response.body) {
await this.throwHttpError(response, 'MiMo stream error');
}
yield* parseSSEStream(
// 非空断言:上方 if 已确保 response.body 不为 null
response.body!,
request.meta.requestId,
request.meta.sessionId,
request.meta.iteration,
);
}
// ===== 模型列表 =====
/**
* MiMo 官方未提供 /models 端点,直接返回本地元数据。
*/
override async listModels(): Promise<MetonaModelInfo[]> {
return this.supportedModels.map((id) => MimoAdapter.MODEL_INFO[id] ?? { id });
}
/**
* 获取上下文窗口大小
*
* v0.3.1: 优先使用配置注入的 contextWindow,回退到 MODEL_INFO 默认值。
* MiMo OpenAI 兼容 API 不支持 context_window 参数,此值仅用于
* Engine 压缩判断和前端 UI 显示。
*/
override getContextWindow(): number {
if (typeof this.config.contextWindow === 'number' && this.config.contextWindow > 0) {
return this.config.contextWindow;
}
const modelInfo = MimoAdapter.MODEL_INFO[this.config.defaultModel];
return modelInfo?.contextWindow ?? 131_072;
}
// ========== 私有方法 ==========
/**
* 构建 MiMo 原生请求体
*
* MiMo 特有参数:
* - thinking: { type: "enabled" / "disabled" } — 与 DeepSeek 一致
* - max_completion_tokens — 非 max_tokensMiMo 使用新字段名)
* - stream_options: { include_usage: true } — 流式返回 usage
* - tool_choice: "auto" — MiMo 仅支持 auto
*
* 思考模式下 temperature/top_p 会被 API 强制覆盖,因此不传这两个参数。
*/
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,
// MiMo 使用 max_completion_tokens(非 max_tokens
max_completion_tokens: request.params.maxTokens,
stream,
};
if (stream) {
body.stream_options = { include_usage: true };
}
if (tools) {
body.tools = tools;
// MiMo 仅支持 tool_choice: "auto"
body.tool_choice = 'auto';
}
// Thinking 模式(与 DeepSeek 参数结构一致)
// MiMo API 默认 thinking.type = "enabled",必须显式发送 disabled 才能关闭
if (request.params.thinkingEnabled === false) {
// 显式禁用思考:传 disabled + temperature/top_p(非思考模式下这两个参数有效)
body.thinking = { type: 'disabled' };
body.temperature = request.params.temperature;
body.top_p = request.params.topP;
} else {
// 启用思考(包括 undefined,因为 MiMo 默认 enabled
// 思考模式下 temperature/top_p 被 API 强制覆盖为 1.0/0.95,不传
body.thinking = { type: 'enabled' };
}
// 停止序列
if (request.params.stopSequences?.length) {
body.stop = request.params.stopSequences;
}
return body;
}
}