



























@@ -0,0 +1,317 @@
1+import type { Context, Model } from "@earendil-works/pi-ai";
2+import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
3+import {
4+registerSingleProviderPlugin,
5+resolveProviderPluginChoice,
6+} from "openclaw/plugin-sdk/plugin-test-runtime";
7+import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
8+import { describe, expect, it } from "vitest";
9+import { runSingleProviderCatalog } from "../test-support/provider-model-test-helpers.js";
10+import xiaomiPlugin from "./index.js";
11+import { createMiMoThinkingWrapper } from "./stream.js";
12+13+type OpenAICompletionsModel = Model<"openai-completions">;
14+15+type PayloadCapture = {
16+payload?: Record<string, unknown>;
17+};
18+19+type ThinkingPayload = {
20+type?: unknown;
21+};
22+23+type ReplayToolCall = {
24+id?: unknown;
25+type?: unknown;
26+function?: {
27+name?: unknown;
28+arguments?: unknown;
29+};
30+};
31+32+type RegisteredProvider = Awaited<ReturnType<typeof registerSingleProviderPlugin>>;
33+34+const emptyUsage = {
35+input: 0,
36+output: 0,
37+cacheRead: 0,
38+cacheWrite: 0,
39+totalTokens: 0,
40+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
41+};
42+43+function requireThinkingProfileResolver(
44+provider: RegisteredProvider,
45+): NonNullable<RegisteredProvider["resolveThinkingProfile"]> {
46+if (!provider.resolveThinkingProfile) {
47+throw new Error("Xiaomi provider did not register a thinking profile resolver");
48+}
49+return provider.resolveThinkingProfile;
50+}
51+52+const readToolCall = { type: "toolCall", id: "call_1", name: "read", arguments: {} };
53+const readToolResult = {
54+role: "toolResult",
55+toolCallId: "call_1",
56+toolName: "read",
57+content: [{ type: "text", text: "ok" }],
58+isError: false,
59+timestamp: 3,
60+};
61+const readTool = {
62+name: "read",
63+description: "Read data",
64+parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
65+};
66+67+function mimoReasoningModel(
68+id: "mimo-v2-pro" | "mimo-v2-omni" | "mimo-v2.5" | "mimo-v2.5-pro",
69+): OpenAICompletionsModel {
70+return {
71+provider: "xiaomi",
72+ id,
73+name: id,
74+api: "openai-completions",
75+baseUrl: "https://api.xiaomimimo.com/v1",
76+reasoning: true,
77+input: ["text"],
78+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
79+contextWindow: 1_048_576,
80+maxTokens: 32_000,
81+compat: {},
82+} as OpenAICompletionsModel;
83+}
84+85+function replayAssistantMessage(params: {
86+provider: string;
87+model: string;
88+content: Array<Record<string, unknown>>;
89+stopReason: "stop" | "toolUse";
90+}) {
91+return {
92+role: "assistant",
93+api: "openai-completions",
94+provider: params.provider,
95+model: params.model,
96+content: params.content,
97+usage: emptyUsage,
98+stopReason: params.stopReason,
99+timestamp: 2,
100+};
101+}
102+103+function readToolReplayContext(assistantMessage: ReturnType<typeof replayAssistantMessage>) {
104+return {
105+messages: [{ role: "user", content: "hi", timestamp: 1 }, assistantMessage, readToolResult],
106+tools: [readTool],
107+} as Context;
108+}
109+110+function mimoReasoningToolReplayContext() {
111+return readToolReplayContext(
112+replayAssistantMessage({
113+provider: "xiaomi",
114+model: "mimo-v2.5-pro",
115+content: [
116+{
117+type: "thinking",
118+thinking: "call reasoning",
119+thinkingSignature: "reasoning_content",
120+},
121+readToolCall,
122+],
123+stopReason: "toolUse",
124+}),
125+);
126+}
127+128+function createPayloadCapturingStream(capture: PayloadCapture, model: OpenAICompletionsModel) {
129+return (
130+_streamModel: OpenAICompletionsModel,
131+streamContext: Context,
132+options?: { onPayload?: (payload: unknown, m: unknown) => unknown },
133+) => {
134+capture.payload = buildOpenAICompletionsParams(model, streamContext, {
135+reasoning: "high",
136+} as never);
137+options?.onPayload?.(capture.payload, model);
138+const stream = createAssistantMessageEventStream();
139+queueMicrotask(() => stream.end());
140+return stream;
141+};
142+}
143+144+function requireThinkingWrapper(
145+wrapper: ReturnType<typeof createMiMoThinkingWrapper>,
146+label: string,
147+): NonNullable<ReturnType<typeof createMiMoThinkingWrapper>> {
148+if (!wrapper) {
149+throw new Error(`expected MiMo thinking wrapper for ${label}`);
150+}
151+return wrapper;
152+}
153+154+function readThinking(payload: Record<string, unknown> | undefined): ThinkingPayload | undefined {
155+return payload?.thinking as ThinkingPayload | undefined;
156+}
157+158+function readPayloadMessage(
159+capture: PayloadCapture,
160+index: number,
161+): Record<string, unknown> | undefined {
162+return (capture.payload?.messages as Array<Record<string, unknown>> | undefined)?.[index];
163+}
164+165+function readFirstToolCall(
166+message: Record<string, unknown> | undefined,
167+): ReplayToolCall | undefined {
168+return (message?.tool_calls as ReplayToolCall[] | undefined)?.[0];
169+}
170+171+describe("xiaomi provider plugin", () => {
172+it("registers Xiaomi with api-key auth metadata", async () => {
173+const provider = await registerSingleProviderPlugin(xiaomiPlugin);
174+const resolved = resolveProviderPluginChoice({
175+providers: [provider],
176+choice: "xiaomi-api-key",
177+});
178+179+expect(provider.id).toBe("xiaomi");
180+expect(provider.label).toBe("Xiaomi");
181+expect(provider.envVars).toEqual(["XIAOMI_API_KEY"]);
182+expect(provider.auth).toHaveLength(1);
183+if (!resolved) {
184+throw new Error("expected Xiaomi api-key auth choice");
185+}
186+expect(resolved.provider.id).toBe("xiaomi");
187+expect(resolved.method.id).toBe("api-key");
188+});
189+190+it("builds the static Xiaomi model catalog with reasoning flags", async () => {
191+const provider = await registerSingleProviderPlugin(xiaomiPlugin);
192+const catalogProvider = await runSingleProviderCatalog(provider);
193+194+expect(catalogProvider.api).toBe("openai-completions");
195+expect(catalogProvider.baseUrl).toBe("https://api.xiaomimimo.com/v1");
196+197+const modelIds = catalogProvider.models?.map((m) => m.id);
198+expect(modelIds).toContain("mimo-v2-pro");
199+expect(modelIds).toContain("mimo-v2-omni");
200+expect(modelIds).toContain("mimo-v2.5");
201+expect(modelIds).toContain("mimo-v2.5-pro");
202+expect(modelIds).toContain("mimo-v2-flash");
203+204+expect(catalogProvider.models?.find((m) => m.id === "mimo-v2-pro")?.reasoning).toBe(true);
205+expect(catalogProvider.models?.find((m) => m.id === "mimo-v2.5-pro")?.reasoning).toBe(true);
206+expect(catalogProvider.models?.find((m) => m.id === "mimo-v2-flash")?.reasoning).toBeFalsy();
207+});
208+209+it("owns OpenAI-compatible replay policy", async () => {
210+const provider = await registerSingleProviderPlugin(xiaomiPlugin);
211+212+const replayPolicy = provider.buildReplayPolicy?.({ modelApi: "openai-completions" } as never);
213+expect(replayPolicy?.sanitizeToolCallIds).toBe(true);
214+expect(replayPolicy?.toolCallIdMode).toBe("strict");
215+expect(replayPolicy?.validateGeminiTurns).toBe(true);
216+expect(replayPolicy?.validateAnthropicTurns).toBe(true);
217+});
218+219+it("advertises thinking profiles for MiMo reasoning models only", async () => {
220+const provider = await registerSingleProviderPlugin(xiaomiPlugin);
221+const resolveThinkingProfile = requireThinkingProfileResolver(provider);
222+const expectedLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
223+224+for (const modelId of ["mimo-v2-pro", "mimo-v2-omni", "mimo-v2.5", "mimo-v2.5-pro"]) {
225+const profile = resolveThinkingProfile({ provider: "xiaomi", modelId } as never);
226+expect(profile?.levels.map((l) => l.id)).toEqual(expectedLevels);
227+expect(profile?.defaultLevel).toBe("high");
228+}
229+230+expect(resolveThinkingProfile({ provider: "xiaomi", modelId: "mimo-v2-flash" } as never)).toBe(
231+undefined,
232+);
233+});
234+235+it("isModernModelRef returns true only for MiMo reasoning models", async () => {
236+const provider = await registerSingleProviderPlugin(xiaomiPlugin);
237+238+expect(
239+provider.isModernModelRef?.({ provider: "xiaomi", modelId: "mimo-v2.5-pro" } as never),
240+).toBe(true);
241+expect(
242+provider.isModernModelRef?.({ provider: "xiaomi", modelId: "mimo-v2-pro" } as never),
243+).toBe(true);
244+expect(
245+provider.isModernModelRef?.({ provider: "xiaomi", modelId: "mimo-v2-flash" } as never),
246+).toBe(false);
247+});
248+249+it("adds blank reasoning_content for replayed tool calls from non-xiaomi turns", async () => {
250+const capture: PayloadCapture = {};
251+const model = mimoReasoningModel("mimo-v2.5-pro");
252+const context = readToolReplayContext(
253+replayAssistantMessage({
254+provider: "openai",
255+model: "gpt-5.5",
256+content: [readToolCall],
257+stopReason: "toolUse",
258+}),
259+);
260+const baseStreamFn = createPayloadCapturingStream(capture, model);
261+262+const wrapThinkingHigh = requireThinkingWrapper(
263+createMiMoThinkingWrapper(baseStreamFn as never, "high"),
264+"high",
265+);
266+await wrapThinkingHigh(model, context, {});
267+268+const assistantMessage = readPayloadMessage(capture, 1);
269+expect(assistantMessage?.role).toBe("assistant");
270+expect(assistantMessage?.reasoning_content).toBe("");
271+const toolCall = readFirstToolCall(assistantMessage);
272+expect(toolCall?.id).toBe("call_1");
273+expect(toolCall?.type).toBe("function");
274+expect(toolCall?.function?.name).toBe("read");
275+expect(toolCall?.function?.arguments).toBe("{}");
276+});
277+278+it("preserves replayed reasoning_content when MiMo thinking is enabled", async () => {
279+const capture: PayloadCapture = {};
280+const model = mimoReasoningModel("mimo-v2.5-pro");
281+const context = mimoReasoningToolReplayContext();
282+const baseStreamFn = createPayloadCapturingStream(capture, model);
283+284+const wrapThinkingHigh = requireThinkingWrapper(
285+createMiMoThinkingWrapper(baseStreamFn as never, "high"),
286+"high",
287+);
288+await wrapThinkingHigh(model, context, {});
289+290+expect(readThinking(capture.payload)?.type).toBe("enabled");
291+const assistantMessage = readPayloadMessage(capture, 1);
292+expect(assistantMessage?.role).toBe("assistant");
293+expect(assistantMessage?.reasoning_content).toBe("call reasoning");
294+const toolCall = readFirstToolCall(assistantMessage);
295+expect(toolCall?.id).toBe("call_1");
296+expect(toolCall?.type).toBe("function");
297+expect(toolCall?.function?.name).toBe("read");
298+});
299+300+it("strips reasoning_content when MiMo thinking is disabled", async () => {
301+const capture: PayloadCapture = {};
302+const model = mimoReasoningModel("mimo-v2-pro");
303+const context = mimoReasoningToolReplayContext();
304+const baseStreamFn = createPayloadCapturingStream(capture, model);
305+306+const wrapThinkingNone = requireThinkingWrapper(
307+createMiMoThinkingWrapper(baseStreamFn as never, "none" as never),
308+"none",
309+);
310+await wrapThinkingNone(model, context, {});
311+312+expect(readThinking(capture.payload)?.type).toBe("disabled");
313+expect((capture.payload?.messages as Array<Record<string, unknown>>)[1]).not.toHaveProperty(
314+"reasoning_content",
315+);
316+});
317+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。