feat: 升级至 v0.3.4 — 接入 Xiaomi MiMo Provider + 文档全量校准
新增 MiMo (小米) LLM Provider 适配器,支持 mimo-v2.5-pro 和 mimo-v2.5 两个文本模型,复用 OpenAI 兼容 SSE 流式解析,支持 Thinking 模式和 Function Calling。同步校准全量 docs 文档与 README 使其与实际代码一致。 主要变更: - 新增 mimo.adapter.ts 适配器(SSE + thinking.type + max_completion_tokens) - 修复 thinking 逻辑 bug:禁用思考时未传 temperature/top_p - 补全 sse-stream.ts 的 MiMo 缓存字段映射(prompt_tokens_details.cached_tokens) - 补全 sse-stream.ts 的 finish_reason 映射(repetition_truncation) - 注册 MiMo 适配器到 adapters/index.ts、main.ts 工厂 - handlers.ts 添加 mimo.contextWindow 热重载触发 - database.service.ts seed 添加 mimo 默认配置 - SettingsModal/OnboardingWizard/Header 添加 MiMo Provider UI - constants.ts PROVIDER_LABELS 添加 mimo - .env.example 添加 MIMO_API_KEY/MIMO_BASE_URL - 反向修改 4 个 docs HTML 设计文档(工具数量/版本日期/适配器列表/数据库表) - 反向修改 Agent网络工具通用设计-v2.md 附录 B 文件索引 - 完全重写 README.md(v0.3.4、27 工具、4 适配器、9 表)
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Provider Adapter 导出
|
||||
*
|
||||
* 三种 Provider 各自独立继承 BaseAdapter,无耦合关系:
|
||||
* 四种 Provider 各自独立继承 BaseAdapter,无耦合关系:
|
||||
* - DeepSeekAdapter — OpenAI 兼容 + DeepSeek 特有参数
|
||||
* - AgnesAdapter — OpenAI 兼容 + Agnes 特有参数
|
||||
* - MimoAdapter — OpenAI 兼容 + MiMo 特有参数
|
||||
* - OllamaAdapter — Ollama 原生 API
|
||||
*
|
||||
* 共享工具(仅供 OpenAI 兼容 Adapter 使用):
|
||||
@@ -14,4 +15,5 @@
|
||||
export { BaseAdapter } from './base-adapter';
|
||||
export { DeepSeekAdapter } from './deepseek.adapter';
|
||||
export { AgnesAdapter } from './agnes-ai.adapter';
|
||||
export { MimoAdapter } from './mimo.adapter';
|
||||
export { OllamaAdapter } from './ollama.adapter';
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* MiMo (Xiaomi) Provider Adapter
|
||||
*
|
||||
* 基于 OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、流式输出。
|
||||
* 模型: mimo-v2.5-pro(131072 max_tokens)/ mimo-v2.5(32768 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_tokens;mimo-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) {
|
||||
const errorBody = await response.text().catch(() => '');
|
||||
throw new Error(`MiMo API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new Error(`MiMo stream error: ${response.status}`);
|
||||
}
|
||||
|
||||
yield* parseSSEStream(
|
||||
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_tokens(MiMo 使用新字段名)
|
||||
* - 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;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
* OpenAI 兼容 API 格式构建工具
|
||||
*
|
||||
* 将 MetonaRequest 转换为 OpenAI /chat/completions 兼容的原生请求格式。
|
||||
* DeepSeek 和 Agnes AI 共享此工具,各自 Adapter 只需处理 Provider 特有的差异参数。
|
||||
* 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';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* SSE 流式解析工具
|
||||
*
|
||||
* 解析 OpenAI 兼容的 Server-Sent Events (SSE) 流式响应,
|
||||
* 产出 MetonaStreamEvent。DeepSeek 和 Agnes AI 共享此工具。
|
||||
* 产出 MetonaStreamEvent。DeepSeek、Agnes AI 和 MiMo 共享此工具。
|
||||
*
|
||||
* SSE 格式:data: {json}\n\n
|
||||
* 结束标记:data: [DONE]
|
||||
@@ -174,7 +174,10 @@ export async function* parseSSEStream(
|
||||
outputTokens: chunk.usage.completion_tokens ?? 0,
|
||||
totalTokens: chunk.usage.total_tokens ?? 0,
|
||||
reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
|
||||
cacheHitTokens: chunk.usage.prompt_cache_hit_tokens,
|
||||
// DeepSeek: prompt_cache_hit_tokens / prompt_cache_miss_tokens
|
||||
// MiMo: prompt_tokens_details.cached_tokens
|
||||
cacheHitTokens: chunk.usage.prompt_cache_hit_tokens
|
||||
?? chunk.usage.prompt_tokens_details?.cached_tokens,
|
||||
cacheMissTokens: chunk.usage.prompt_cache_miss_tokens,
|
||||
};
|
||||
|
||||
@@ -252,7 +255,9 @@ export function parseOpenAICompatibleResponse(
|
||||
outputTokens: (usage?.completion_tokens as number) ?? 0,
|
||||
totalTokens: (usage?.total_tokens as number) ?? 0,
|
||||
reasoningTokens: (usage?.completion_tokens_details as Record<string, unknown>)?.reasoning_tokens as number | undefined,
|
||||
cacheHitTokens: usage?.prompt_cache_hit_tokens as number | undefined,
|
||||
// DeepSeek: prompt_cache_hit_tokens / MiMo: prompt_tokens_details.cached_tokens
|
||||
cacheHitTokens: (usage?.prompt_cache_hit_tokens as number | undefined)
|
||||
?? (usage?.prompt_tokens_details as Record<string, unknown> | undefined)?.cached_tokens as number | undefined,
|
||||
cacheMissTokens: usage?.prompt_cache_miss_tokens as number | undefined,
|
||||
},
|
||||
};
|
||||
@@ -264,6 +269,8 @@ function mapOpenAIFinishReason(reason: string): string {
|
||||
case 'length': return 'length';
|
||||
case 'tool_calls': return 'tool_calls';
|
||||
case 'content_filter': return 'content_filter';
|
||||
// MiMo 特有:检测到复读截断
|
||||
case 'repetition_truncation': return 'stop';
|
||||
default: return 'stop';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface MetonaModelInfo {
|
||||
* Provider 适配器配置
|
||||
*/
|
||||
export interface AdapterConfig {
|
||||
/** Provider 标识(如 'deepseek'、'agnes'、'ollama') */
|
||||
/** Provider 标识(如 'deepseek'、'agnes'、'mimo'、'ollama') */
|
||||
provider: string;
|
||||
/** API 基础 URL */
|
||||
baseURL: string;
|
||||
|
||||
@@ -667,9 +667,9 @@ export function registerAllIPCHandlers(
|
||||
});
|
||||
|
||||
// LLM 相关配置变更时热重载 Adapter
|
||||
// v0.3.1: 加入 deepseek.contextWindow / agnes.contextWindow,使上下文窗口配置变化也触发热重载
|
||||
// v0.3.1: 加入 deepseek.contextWindow / agnes.contextWindow / mimo.contextWindow,使上下文窗口配置变化也触发热重载
|
||||
if (['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx',
|
||||
'deepseek.contextWindow', 'agnes.contextWindow'].includes(key)) {
|
||||
'deepseek.contextWindow', 'agnes.contextWindow', 'mimo.contextWindow'].includes(key)) {
|
||||
// C-1 修复: reloadAdapter 返回 false 时表示加载失败,需要通知前端
|
||||
const reloadSuccess = reloadAdapter();
|
||||
if (!reloadSuccess) {
|
||||
@@ -700,8 +700,8 @@ export function registerAllIPCHandlers(
|
||||
agentLoop.updateConfig({ contextLength: (value as number) || undefined });
|
||||
orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined });
|
||||
log.info(`[CONFIG] Agent contextLength updated to ${value}`);
|
||||
} else if (key === 'deepseek.contextWindow' || key === 'agnes.contextWindow') {
|
||||
// v0.3.1: DeepSeek/Agnes contextWindow 变更,同步到 Engine 和 Orchestrator
|
||||
} else if (key === 'deepseek.contextWindow' || key === 'agnes.contextWindow' || key === 'mimo.contextWindow') {
|
||||
// v0.3.1: DeepSeek/Agnes/MiMo contextWindow 变更,同步到 Engine 和 Orchestrator
|
||||
// reloadAdapter 已重建 adapter,此处确保 Engine contextWindow 同步(兜底)
|
||||
const ctxWindow = (value as number) || undefined;
|
||||
agentLoop.updateConfig({ contextWindow: ctxWindow });
|
||||
|
||||
@@ -36,6 +36,7 @@ import { AgentLoopEngine } from './harness/agent-loop';
|
||||
import { ToolRegistry } from './harness/tools/registry';
|
||||
import { DeepSeekAdapter } from './harness/adapters/deepseek.adapter';
|
||||
import { AgnesAdapter } from './harness/adapters/agnes-ai.adapter';
|
||||
import { MimoAdapter } from './harness/adapters/mimo.adapter';
|
||||
import { OllamaAdapter } from './harness/adapters/ollama.adapter';
|
||||
import {
|
||||
ReadFileTool, WriteFileTool, ListDirectoryTool, SearchFilesTool,
|
||||
@@ -161,6 +162,7 @@ async function initialize(): Promise<void> {
|
||||
const adapterConfig = { provider, baseURL, apiKey, defaultModel: model, contextWindow };
|
||||
switch (provider) {
|
||||
case 'agnes': return new AgnesAdapter(adapterConfig);
|
||||
case 'mimo': return new MimoAdapter(adapterConfig);
|
||||
case 'ollama': return new OllamaAdapter(adapterConfig);
|
||||
default: return new DeepSeekAdapter(adapterConfig);
|
||||
}
|
||||
|
||||
@@ -378,6 +378,8 @@ export class DatabaseService {
|
||||
// v0.3.1: DeepSeek/Agnes 改为可配置,不再写死 1M
|
||||
{ key: 'deepseek.contextWindow', value: '1000000', category: 'deepseek' },
|
||||
{ key: 'agnes.contextWindow', value: '1000000', category: 'agnes' },
|
||||
// v0.3.4: MiMo 上下文窗口(官方未公布,保守设为 131072)
|
||||
{ key: 'mimo.contextWindow', value: '131072', category: 'mimo' },
|
||||
|
||||
// Onboarding
|
||||
{ key: 'onboarding.completed', value: 'false', category: 'general' },
|
||||
|
||||
Reference in New Issue
Block a user