From 7958eb0fc45c482e5b21cec8d77f36ec0cccf455 Mon Sep 17 00:00:00 2001 From: Jim Dawdy Date: Wed, 13 May 2026 19:10:20 -0500 Subject: [PATCH] test(xiaomi): add MiMo thinking profile, stream wrapper, and reasoning_content injection tests --- extensions/xiaomi/index.test.ts | 317 ++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 extensions/xiaomi/index.test.ts diff --git a/extensions/xiaomi/index.test.ts b/extensions/xiaomi/index.test.ts new file mode 100644 index 00000000000..e50f6c1b478 --- /dev/null +++ b/extensions/xiaomi/index.test.ts @@ -0,0 +1,317 @@ +import type { Context, Model } from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import { + registerSingleProviderPlugin, + resolveProviderPluginChoice, +} from "openclaw/plugin-sdk/plugin-test-runtime"; +import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime"; +import { describe, expect, it } from "vitest"; +import { runSingleProviderCatalog } from "../test-support/provider-model-test-helpers.js"; +import xiaomiPlugin from "./index.js"; +import { createMiMoThinkingWrapper } from "./stream.js"; + +type OpenAICompletionsModel = Model<"openai-completions">; + +type PayloadCapture = { + payload?: Record; +}; + +type ThinkingPayload = { + type?: unknown; +}; + +type ReplayToolCall = { + id?: unknown; + type?: unknown; + function?: { + name?: unknown; + arguments?: unknown; + }; +}; + +type RegisteredProvider = Awaited>; + +const emptyUsage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, +}; + +function requireThinkingProfileResolver( + provider: RegisteredProvider, +): NonNullable { + if (!provider.resolveThinkingProfile) { + throw new Error("Xiaomi provider did not register a thinking profile resolver"); + } + return provider.resolveThinkingProfile; +} + +const readToolCall = { type: "toolCall", id: "call_1", name: "read", arguments: {} }; +const readToolResult = { + role: "toolResult", + toolCallId: "call_1", + toolName: "read", + content: [{ type: "text", text: "ok" }], + isError: false, + timestamp: 3, +}; +const readTool = { + name: "read", + description: "Read data", + parameters: { type: "object", properties: {}, required: [], additionalProperties: false }, +}; + +function mimoReasoningModel( + id: "mimo-v2-pro" | "mimo-v2-omni" | "mimo-v2.5" | "mimo-v2.5-pro", +): OpenAICompletionsModel { + return { + provider: "xiaomi", + id, + name: id, + api: "openai-completions", + baseUrl: "https://api.xiaomimimo.com/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_048_576, + maxTokens: 32_000, + compat: {}, + } as OpenAICompletionsModel; +} + +function replayAssistantMessage(params: { + provider: string; + model: string; + content: Array>; + stopReason: "stop" | "toolUse"; +}) { + return { + role: "assistant", + api: "openai-completions", + provider: params.provider, + model: params.model, + content: params.content, + usage: emptyUsage, + stopReason: params.stopReason, + timestamp: 2, + }; +} + +function readToolReplayContext(assistantMessage: ReturnType) { + return { + messages: [{ role: "user", content: "hi", timestamp: 1 }, assistantMessage, readToolResult], + tools: [readTool], + } as Context; +} + +function mimoReasoningToolReplayContext() { + return readToolReplayContext( + replayAssistantMessage({ + provider: "xiaomi", + model: "mimo-v2.5-pro", + content: [ + { + type: "thinking", + thinking: "call reasoning", + thinkingSignature: "reasoning_content", + }, + readToolCall, + ], + stopReason: "toolUse", + }), + ); +} + +function createPayloadCapturingStream(capture: PayloadCapture, model: OpenAICompletionsModel) { + return ( + _streamModel: OpenAICompletionsModel, + streamContext: Context, + options?: { onPayload?: (payload: unknown, m: unknown) => unknown }, + ) => { + capture.payload = buildOpenAICompletionsParams(model, streamContext, { + reasoning: "high", + } as never); + options?.onPayload?.(capture.payload, model); + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => stream.end()); + return stream; + }; +} + +function requireThinkingWrapper( + wrapper: ReturnType, + label: string, +): NonNullable> { + if (!wrapper) { + throw new Error(`expected MiMo thinking wrapper for ${label}`); + } + return wrapper; +} + +function readThinking(payload: Record | undefined): ThinkingPayload | undefined { + return payload?.thinking as ThinkingPayload | undefined; +} + +function readPayloadMessage( + capture: PayloadCapture, + index: number, +): Record | undefined { + return (capture.payload?.messages as Array> | undefined)?.[index]; +} + +function readFirstToolCall( + message: Record | undefined, +): ReplayToolCall | undefined { + return (message?.tool_calls as ReplayToolCall[] | undefined)?.[0]; +} + +describe("xiaomi provider plugin", () => { + it("registers Xiaomi with api-key auth metadata", async () => { + const provider = await registerSingleProviderPlugin(xiaomiPlugin); + const resolved = resolveProviderPluginChoice({ + providers: [provider], + choice: "xiaomi-api-key", + }); + + expect(provider.id).toBe("xiaomi"); + expect(provider.label).toBe("Xiaomi"); + expect(provider.envVars).toEqual(["XIAOMI_API_KEY"]); + expect(provider.auth).toHaveLength(1); + if (!resolved) { + throw new Error("expected Xiaomi api-key auth choice"); + } + expect(resolved.provider.id).toBe("xiaomi"); + expect(resolved.method.id).toBe("api-key"); + }); + + it("builds the static Xiaomi model catalog with reasoning flags", async () => { + const provider = await registerSingleProviderPlugin(xiaomiPlugin); + const catalogProvider = await runSingleProviderCatalog(provider); + + expect(catalogProvider.api).toBe("openai-completions"); + expect(catalogProvider.baseUrl).toBe("https://api.xiaomimimo.com/v1"); + + const modelIds = catalogProvider.models?.map((m) => m.id); + expect(modelIds).toContain("mimo-v2-pro"); + expect(modelIds).toContain("mimo-v2-omni"); + expect(modelIds).toContain("mimo-v2.5"); + expect(modelIds).toContain("mimo-v2.5-pro"); + expect(modelIds).toContain("mimo-v2-flash"); + + expect(catalogProvider.models?.find((m) => m.id === "mimo-v2-pro")?.reasoning).toBe(true); + expect(catalogProvider.models?.find((m) => m.id === "mimo-v2.5-pro")?.reasoning).toBe(true); + expect(catalogProvider.models?.find((m) => m.id === "mimo-v2-flash")?.reasoning).toBeFalsy(); + }); + + it("owns OpenAI-compatible replay policy", async () => { + const provider = await registerSingleProviderPlugin(xiaomiPlugin); + + const replayPolicy = provider.buildReplayPolicy?.({ modelApi: "openai-completions" } as never); + expect(replayPolicy?.sanitizeToolCallIds).toBe(true); + expect(replayPolicy?.toolCallIdMode).toBe("strict"); + expect(replayPolicy?.validateGeminiTurns).toBe(true); + expect(replayPolicy?.validateAnthropicTurns).toBe(true); + }); + + it("advertises thinking profiles for MiMo reasoning models only", async () => { + const provider = await registerSingleProviderPlugin(xiaomiPlugin); + const resolveThinkingProfile = requireThinkingProfileResolver(provider); + const expectedLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; + + for (const modelId of ["mimo-v2-pro", "mimo-v2-omni", "mimo-v2.5", "mimo-v2.5-pro"]) { + const profile = resolveThinkingProfile({ provider: "xiaomi", modelId } as never); + expect(profile?.levels.map((l) => l.id)).toEqual(expectedLevels); + expect(profile?.defaultLevel).toBe("high"); + } + + expect(resolveThinkingProfile({ provider: "xiaomi", modelId: "mimo-v2-flash" } as never)).toBe( + undefined, + ); + }); + + it("isModernModelRef returns true only for MiMo reasoning models", async () => { + const provider = await registerSingleProviderPlugin(xiaomiPlugin); + + expect( + provider.isModernModelRef?.({ provider: "xiaomi", modelId: "mimo-v2.5-pro" } as never), + ).toBe(true); + expect( + provider.isModernModelRef?.({ provider: "xiaomi", modelId: "mimo-v2-pro" } as never), + ).toBe(true); + expect( + provider.isModernModelRef?.({ provider: "xiaomi", modelId: "mimo-v2-flash" } as never), + ).toBe(false); + }); + + it("adds blank reasoning_content for replayed tool calls from non-xiaomi turns", async () => { + const capture: PayloadCapture = {}; + const model = mimoReasoningModel("mimo-v2.5-pro"); + const context = readToolReplayContext( + replayAssistantMessage({ + provider: "openai", + model: "gpt-5.5", + content: [readToolCall], + stopReason: "toolUse", + }), + ); + const baseStreamFn = createPayloadCapturingStream(capture, model); + + const wrapThinkingHigh = requireThinkingWrapper( + createMiMoThinkingWrapper(baseStreamFn as never, "high"), + "high", + ); + await wrapThinkingHigh(model, context, {}); + + const assistantMessage = readPayloadMessage(capture, 1); + expect(assistantMessage?.role).toBe("assistant"); + expect(assistantMessage?.reasoning_content).toBe(""); + const toolCall = readFirstToolCall(assistantMessage); + expect(toolCall?.id).toBe("call_1"); + expect(toolCall?.type).toBe("function"); + expect(toolCall?.function?.name).toBe("read"); + expect(toolCall?.function?.arguments).toBe("{}"); + }); + + it("preserves replayed reasoning_content when MiMo thinking is enabled", async () => { + const capture: PayloadCapture = {}; + const model = mimoReasoningModel("mimo-v2.5-pro"); + const context = mimoReasoningToolReplayContext(); + const baseStreamFn = createPayloadCapturingStream(capture, model); + + const wrapThinkingHigh = requireThinkingWrapper( + createMiMoThinkingWrapper(baseStreamFn as never, "high"), + "high", + ); + await wrapThinkingHigh(model, context, {}); + + expect(readThinking(capture.payload)?.type).toBe("enabled"); + const assistantMessage = readPayloadMessage(capture, 1); + expect(assistantMessage?.role).toBe("assistant"); + expect(assistantMessage?.reasoning_content).toBe("call reasoning"); + const toolCall = readFirstToolCall(assistantMessage); + expect(toolCall?.id).toBe("call_1"); + expect(toolCall?.type).toBe("function"); + expect(toolCall?.function?.name).toBe("read"); + }); + + it("strips reasoning_content when MiMo thinking is disabled", async () => { + const capture: PayloadCapture = {}; + const model = mimoReasoningModel("mimo-v2-pro"); + const context = mimoReasoningToolReplayContext(); + const baseStreamFn = createPayloadCapturingStream(capture, model); + + const wrapThinkingNone = requireThinkingWrapper( + createMiMoThinkingWrapper(baseStreamFn as never, "none" as never), + "none", + ); + await wrapThinkingNone(model, context, {}); + + expect(readThinking(capture.payload)?.type).toBe("disabled"); + expect((capture.payload?.messages as Array>)[1]).not.toHaveProperty( + "reasoning_content", + ); + }); +});