fix(ai): send only thinking on/off toggle for Xiaomi MiMo models

MiMo Chat Completions rejects reasoning_effort "minimal" with a 400.
Xiaomi documents only thinking.type enabled/disabled and treats all effort
levels identically, so stop sending reasoning_effort and expose only the
off and high thinking levels.

fixes #10818
This commit is contained in:
David Brailovsky 2026-10-14 13:13:16 +02:00
parent 1692bba634
commit 6374c2baf4
3 changed files with 53 additions and 2 deletions

View file

@ -15,6 +15,7 @@
- Fixed automatic retry not triggering for gateway `Unable to forward upstream stream` errors ([#10920](https://github.com/earendil-works/pi/issues/10920))
- Fixed OpenAI-compatible responses showing thinking after the answer when a proxy such as LiteLLM sends reasoning and content in the same stream chunk ([#10926](https://github.com/earendil-works/pi/issues/10926))
- Fixed Gemini 4 Argon requests on Google Vertex failing with "Thinking budget is not supported", and its thinking level being ignored on the Gemini API, by sending `thinkingLevel` for Gemini 4 and later models ([#10890](https://github.com/earendil-works/pi/issues/10890))
- Fixed Xiaomi MiMo models failing with a 400 at the `minimal` thinking level by sending only the documented thinking on/off toggle and exposing only the `off` and `high` levels ([#10818](https://github.com/earendil-works/pi/issues/10818))
## [1.2.0] - 2026-10-12

View file

@ -2462,10 +2462,23 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
// Built-in `xiaomi` targets the API billing endpoint (single stable URL,
// keys from platform.xiaomimimo.com). The three `xiaomi-token-plan-*`
// providers cover prepaid Token Plan endpoints in cn / ams / sgp.
// MiMo Chat Completions only documents thinking.type enabled/disabled and
// rejects some reasoning_effort values (e.g. "minimal"). Xiaomi documents
// all effort levels as identical, so expose only off and high.
const xiaomiCompat: OpenAICompletionsCompat = {
supportsReasoningEffort: false,
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
};
const xiaomiThinkingLevelMap = {
off: "off",
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: null,
} as const;
const xiaomiVariants = [
{ source: "xiaomi", provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/v1" },
{
@ -2502,12 +2515,12 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
baseUrl,
compat: xiaomiCompat,
reasoning: m.reasoning === true,
...(m.reasoning === true ? { thinkingLevelMap: xiaomiThinkingLevelMap } : {}),
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: getModelsDevCost(m.cost),
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
});
recordModelsDevReasoningOptions(provider, modelId, m);
}
}

View file

@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
import { getModels } from "../src/compat.ts";
import { getModels, streamSimple } from "../src/compat.ts";
import { getSupportedThinkingLevels } from "../src/models.ts";
import type { ThinkingLevel } from "../src/types.ts";
const XIAOMI_PROVIDERS = ["xiaomi", "xiaomi-token-plan-cn", "xiaomi-token-plan-ams", "xiaomi-token-plan-sgp"] as const;
const DEPRECATED_MODEL_IDS = ["mimo-v2-flash", "mimo-v2-omni", "mimo-v2-pro"] as const;
@ -15,4 +17,39 @@ describe("Xiaomi MiMo models", () => {
const modelIds = getModels(provider).map((model) => model.id);
for (const modelId of REPLACEMENT_MODEL_IDS) expect(modelIds).toContain(modelId);
});
// #10818: MiMo Chat Completions only supports a thinking on/off toggle.
it.each(XIAOMI_PROVIDERS)("models %s reasoning as an off/on toggle", async (provider) => {
const models = getModels(provider).filter((model) => model.reasoning);
expect(models.length).toBeGreaterThan(0);
for (const model of models) {
expect(getSupportedThinkingLevels(model)).toEqual(["off", "high"]);
const capturePayload = async (reasoning: ThinkingLevel | undefined) => {
let payload: Record<string, unknown> | undefined;
await streamSimple(
model,
{ messages: [{ role: "user", content: "test", timestamp: 0 }] },
{
apiKey: "test-xiaomi-key",
reasoning,
onPayload: (value) => {
payload = value as Record<string, unknown>;
throw new Error("payload captured");
},
},
).result();
return payload;
};
const enabled = await capturePayload("high");
expect(enabled?.thinking).toEqual({ type: "enabled" });
expect(enabled?.reasoning_effort).toBeUndefined();
const disabled = await capturePayload(undefined);
expect(disabled?.thinking).toEqual({ type: "disabled" });
expect(disabled?.reasoning_effort).toBeUndefined();
}
});
});