@oh-my-pi/pi-ai 15.9.0 → 15.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.9.1] - 2026-06-04
6
+
7
+ ### Added
8
+
9
+ - Added regional Xiaomi Token Plan login/provider entries (`xiaomi-token-plan-sgp`, `xiaomi-token-plan-ams`, `xiaomi-token-plan-cn`) so `omp login` can store token-plan keys against the selected region. ([#1846](https://github.com/can1357/oh-my-pi/issues/1846))
10
+
11
+ ### Fixed
12
+
13
+ - Removed the `context-1m-2025-08-07` (1M long-context) beta from the Anthropic agent request headers, the OAuth model-discovery header, and the Claude usage-API header. Sending it caused subscription/OAuth requests without long-context credits to fail with `429 Usage credits are required for long context requests`, breaking Sonnet. The remaining betas are unchanged.
14
+ - Fixed Kimi K2.x `maxTokens` on Fireworks and Fire Pass (`fireworks/kimi-k2.5`, `fireworks/kimi-k2.6`, `firepass/kimi-k2.6-turbo`) being inherited from Fireworks `/v1/models` discovery (`max_completion_tokens: 65536`) rather than the published Kimi-on-Fireworks output budget, which let callers (and the openai-completions default-injection safety net) ship a budget the router cannot honor and made runaway reasoning traces more likely. The Fireworks resolver now clamps every Kimi K2.x id (public catalog ids and the canonical `accounts/fireworks/{models,routers}/kimi-k2…` wire form) to 32,768 output tokens, and the generator applies the same cap as a post-processing safety net so the `firepass` static fallback and the bundled `fireworks` entries stay in sync across regens. ([#1849](https://github.com/can1357/oh-my-pi/issues/1849))
15
+ - Fixed Xiaomi Token Plan MiMo OpenAI-compatible tool-call continuations omitting required `reasoning_content` replay. ([#1846](https://github.com/can1357/oh-my-pi/issues/1846))
16
+ - Fixed Anthropic prompt caching for OpenAI-compatible Claude proxies by honoring `compat.cacheControlFormat: "anthropic"` outside OpenRouter. ([#1845](https://github.com/can1357/oh-my-pi/issues/1845))
17
+ - Fixed Moonshot Kimi K2.6 silently pausing for many seconds between tool calls because the server discarded the `reasoning_content` that omp was already sending with every assistant tool-call replay. The K2.6 `thinking` parameter takes an extra `keep` field whose default (`null`) ignores historical reasoning, so K2.6 had to re-derive its full chain-of-thought from the user prompt on every iteration of the agent loop. The Moonshot direct (`api.moonshot.ai`) and Kimi Code (`api.kimi.com`) wire bodies now send `thinking: { type: "enabled", keep: "all" }` for `kimi-k2.6` requests with reasoning enabled, matching Moonshot's documented best practice for multi-step tool-calling agents. The flag is gated on the K2.6 id and the two native hosts because earlier Moonshot models (K2.5 and below) 400 on the unknown field and every Kimi gateway (OpenRouter, OpenCode, Kilo, Fireworks, …) speaks its own thinking shape. ([#1838](https://github.com/can1357/oh-my-pi/issues/1838))
18
+ - Fixed Alibaba DashScope (Bailian) compatible-mode endpoint `400 InternalError.Algo.InvalidParameter: The provided messages input is invalid. The error info is [Unexpected item type in content.]` when a screenshot or other image-producing tool result was folded into a known text-only Qwen turn (e.g. `qwen3.7-max`, `qwen-max`, `qwen3-coder-*`) hosted at `dashscope.aliyuncs.com/compatible-mode/v1`. `convertMessages` in `openai-completions` no longer forwards `image_url` content parts for those text-only id families even when a misconfigured custom provider claims `input: ["text", "image"]`; multimodal compatible-mode ids such as `qwen3.7-plus` and `qwen-vl-max` still rely on the catalog `input` field. The tool-result branch and the user-content branch both fall back to the standard `[image omitted: model does not support vision]` placeholder for text-only ids so the model still sees the attachment intent. ([#1859](https://github.com/can1357/oh-my-pi/issues/1859))
19
+
5
20
  ## [15.9.0] - 2026-06-04
6
21
 
7
22
  ### Fixed
@@ -18,6 +33,7 @@
18
33
  - Fixed MiniMax Coding Plan China login opening the international `platform.minimax.io` subscription page instead of the China `platform.minimaxi.com` page.
19
34
 
20
35
  ## [15.8.0] - 2026-06-02
36
+
21
37
  ### Added
22
38
 
23
39
  - Added `AnthropicMessagesClient` and related Anthropic wire types/errors via `anthropic-client` export so callers can build a standalone Anthropic Messages client without depending on `@anthropic-ai/sdk`
@@ -1,5 +1,5 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
- import type { Api, Model } from "../types";
2
+ import type { Api, Model, Provider } from "../types";
3
3
  export interface ModelsDevModel {
4
4
  id?: string;
5
5
  name?: string;
@@ -110,6 +110,27 @@ export interface ZhipuCodingPlanModelManagerConfig {
110
110
  baseUrl?: string;
111
111
  }
112
112
  export declare function zhipuCodingPlanModelManagerOptions(config?: ZhipuCodingPlanModelManagerConfig): ModelManagerOptions<"openai-completions">;
113
+ /**
114
+ * Fireworks-published cap for the Kimi K2 family. Fireworks' `/v1/models`
115
+ * envelope generically reports `max_completion_tokens: 65536` for every Kimi
116
+ * deployment, but Kimi K2 (instruct / thinking / turbo) on Fireworks is
117
+ * documented to ship long reasoning traces that should be bounded — capping
118
+ * at 32,768 prevents handing callers a budget the router cannot honor.
119
+ * See https://github.com/can1357/oh-my-pi/issues/1849.
120
+ */
121
+ export declare const FIREWORKS_KIMI_MAX_TOKENS = 32768;
122
+ /**
123
+ * Returns true for any Kimi K2.x public model id served by Fireworks-backed
124
+ * providers (`fireworks` direct, `firepass` router). Matches both the public
125
+ * catalog id (`kimi-k2.5`, `kimi-k2.6`, `kimi-k2.6-turbo`) and the canonical
126
+ * Fireworks wire id (`accounts/fireworks/{models,routers}/kimi-k2…`).
127
+ */
128
+ export declare function isFireworksKimiK2ModelId(modelId: string): boolean;
129
+ /**
130
+ * Clamp the Kimi K2 family's `maxTokens` to {@link FIREWORKS_KIMI_MAX_TOKENS}
131
+ * on Fireworks-backed providers, leaving every other model untouched.
132
+ */
133
+ export declare function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;
113
134
  export interface FireworksModelManagerConfig {
114
135
  apiKey?: string;
115
136
  baseUrl?: string;
@@ -219,10 +240,16 @@ export interface CloudflareAiGatewayModelManagerConfig {
219
240
  baseUrl?: string;
220
241
  }
221
242
  export declare function cloudflareAiGatewayModelManagerOptions(config?: CloudflareAiGatewayModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
243
+ /** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */
244
+ export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
245
+ /** Configures Xiaomi standard or regional Token Plan OpenAI-compatible model discovery. */
222
246
  export interface XiaomiModelManagerConfig {
223
247
  apiKey?: string;
224
248
  baseUrl?: string;
249
+ providerId?: Provider;
250
+ tokenPlanRegion?: XiaomiTokenPlanRegion;
225
251
  }
252
+ /** Builds a Xiaomi model manager, preserving Token Plan region provider ids during discovery. */
226
253
  export declare function xiaomiModelManagerOptions(config?: XiaomiModelManagerConfig): ModelManagerOptions<"openai-completions">;
227
254
  export interface LiteLLMModelManagerConfig {
228
255
  apiKey?: string;
@@ -1,9 +1,11 @@
1
1
  import type { Model, OpenAICompat } from "../types";
2
2
  type ResolvedToolStrictMode = NonNullable<OpenAICompat["toolStrictMode"]> | "mixed";
3
- export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode">> & {
3
+ export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "cacheControlFormat" | "thinkingKeep">> & {
4
4
  openRouterRouting?: OpenAICompat["openRouterRouting"];
5
5
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
6
6
  extraBody?: OpenAICompat["extraBody"];
7
+ cacheControlFormat?: OpenAICompat["cacheControlFormat"];
8
+ thinkingKeep?: OpenAICompat["thinkingKeep"];
7
9
  toolStrictMode: ResolvedToolStrictMode;
8
10
  };
9
11
  /**
@@ -1,4 +1,4 @@
1
- import type { ImageContent, TextContent } from "../types";
1
+ import type { ImageContent, Model, TextContent } from "../types";
2
2
  export declare const NON_VISION_IMAGE_PLACEHOLDER = "[image omitted: model does not support vision]";
3
3
  export declare function partitionVisionContent(content: ReadonlyArray<TextContent | ImageContent>, supportsImages: boolean): {
4
4
  textBlocks: TextContent[];
@@ -6,3 +6,15 @@ export declare function partitionVisionContent(content: ReadonlyArray<TextConten
6
6
  omittedImages: boolean;
7
7
  };
8
8
  export declare function joinTextWithImagePlaceholder(text: string, omittedImages: boolean): string;
9
+ /**
10
+ * Detect known text-only Qwen models served via Alibaba DashScope's consumer
11
+ * `compatible-mode` endpoint that the upstream chat-completions API rejects
12
+ * multimodal content arrays for. The compatible-mode endpoint also serves
13
+ * multimodal Qwen SKUs without `vl` in the id (e.g. `qwen3.7-plus`), so this
14
+ * guard only covers families verified to be text-only for issue #1859:
15
+ * `qwen*-max` and `qwen*-coder*`.
16
+ *
17
+ * Used as a defensive override in `convertMessages` so a misconfigured custom
18
+ * provider (issue #1859) can't drive the request into an unrecoverable 400.
19
+ */
20
+ export declare function isDashscopeCompatibleModeTextOnlyQwen(model: Model<"openai-completions">): boolean;
@@ -48,7 +48,7 @@ export interface ThinkingConfig {
48
48
  /** Provider-specific transport used to encode the selected effort. */
49
49
  mode: ThinkingControlMode;
50
50
  }
51
- export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "xai-oauth" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "wafer-pass" | "wafer-serverless" | "zenmux" | "lm-studio";
51
+ export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "xai-oauth" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "wafer-pass" | "wafer-serverless" | "zenmux" | "lm-studio";
52
52
  export type Provider = KnownProvider | string;
53
53
  import type { Effort } from "./model-thinking";
54
54
  /** Token budgets for each thinking level (token-based providers only) */
@@ -421,6 +421,8 @@ export interface UserMessage {
421
421
  content: string | (TextContent | ImageContent)[];
422
422
  /** True if the message was injected by the system (e.g., auto-continue). */
423
423
  synthetic?: boolean;
424
+ /** True when injected mid-turn as a steer; consumed by the agent's pre-LLM transform to wrap it for emphasis. Never rendered. */
425
+ steering?: boolean;
424
426
  /** Who initiated this message for billing/attribution semantics. */
425
427
  attribution?: MessageAttribution;
426
428
  /** Provider-specific opaque payload used to reconstruct transport-native history. */
@@ -650,6 +652,8 @@ export interface OpenAICompat {
650
652
  requiresMistralToolIds?: boolean;
651
653
  /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "zai" uses thinking: { type: "enabled" | "disabled" } (also used by Moonshot Kimi), "qwen" uses top-level enable_thinking, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
652
654
  thinkingFormat?: "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
655
+ /** Optional `thinking.keep` value for Z.ai/Moonshot-style thinking params. Set false to suppress auto-detected keep. Default: auto-detected. */
656
+ thinkingKeep?: "all" | false;
653
657
  /** Which reasoning content field to emit on assistant messages. Default: auto-detected. */
654
658
  reasoningContentField?: "reasoning_content" | "reasoning" | "reasoning_text";
655
659
  /** Whether assistant tool-call messages must include reasoning content. Default: false. */
@@ -681,6 +685,8 @@ export interface OpenAICompat {
681
685
  vercelGatewayRouting?: VercelGatewayRouting;
682
686
  /** Extra fields to include in request body (e.g. gateway routing hints for OpenClaw-style proxies). */
683
687
  extraBody?: Record<string, unknown>;
688
+ /** Whether chat-completions payloads should include provider-specific prompt-cache markers. */
689
+ cacheControlFormat?: "anthropic" | undefined;
684
690
  /** Whether the provider supports the `strict` field in tool definitions. Default: auto-detected per provider/baseUrl (conservative for unknown providers). */
685
691
  supportsStrictMode?: boolean;
686
692
  /** Whether tool schemas must be sent either all strict or all non-strict. Undefined keeps the existing per-tool mixed behavior. */
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  };
10
- export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "openrouter" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "wafer-pass" | "wafer-serverless" | "vllm" | "xai-oauth" | "xiaomi" | "zenmux" | "zai" | "zhipu-coding-plan";
10
+ export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "openrouter" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "wafer-pass" | "wafer-serverless" | "vllm" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "zai" | "zhipu-coding-plan";
11
11
  export type OAuthProviderId = OAuthProvider | (string & {});
12
12
  export type OAuthPrompt = {
13
13
  message: string;
@@ -4,12 +4,12 @@
4
4
  * Xiaomi MiMo provides OpenAI-compatible models via
5
5
  * https://api.xiaomimimo.com/v1.
6
6
  *
7
- * This is not OAuth - it's a simple API key flow:
8
- * 1. Open browser to Xiaomi MiMo API key console
9
- * 2. User copies their API key
10
- * 3. User pastes the API key into the CLI
7
+ * Standard Xiaomi login opens the pay-as-you-go API key console. Token Plan
8
+ * login opens plan management so users copy the regional `tp-...` key.
11
9
  */
12
10
  import type { OAuthController } from "./types";
11
+ /** Region codes accepted by the Xiaomi Token Plan login flow. */
12
+ export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
13
13
  /**
14
14
  * Login to Xiaomi MiMo.
15
15
  *
@@ -17,3 +17,9 @@ import type { OAuthController } from "./types";
17
17
  * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
18
18
  */
19
19
  export declare function loginXiaomi(options: OAuthController): Promise<string>;
20
+ /**
21
+ * Login to a regional Xiaomi Token Plan endpoint.
22
+ *
23
+ * Prompts for a token-plan API key and validates it against the selected region.
24
+ */
25
+ export declare function loginXiaomiTokenPlan(options: OAuthController, region: XiaomiTokenPlanRegion): Promise<string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.9.0",
4
+ "version": "15.9.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@bufbuild/protobuf": "^2.12.0",
42
- "@oh-my-pi/pi-utils": "15.9.0",
42
+ "@oh-my-pi/pi-utils": "15.9.1",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
@@ -34,6 +34,7 @@ import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./utils/oau
34
34
  import { loginDeepSeek } from "./utils/oauth/deepseek";
35
35
  import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
36
36
  import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types";
37
+ import { loginXiaomi, loginXiaomiTokenPlan } from "./utils/oauth/xiaomi";
37
38
 
38
39
  // ─────────────────────────────────────────────────────────────────────────────
39
40
  // Credential Types
@@ -1758,11 +1759,25 @@ export class AuthStorage {
1758
1759
  return;
1759
1760
  }
1760
1761
  case "xiaomi": {
1761
- const { loginXiaomi } = await import("./utils/oauth/xiaomi");
1762
1762
  const apiKey = await loginXiaomi(ctrl);
1763
1763
  await saveApiKeyCredential(apiKey);
1764
1764
  return;
1765
1765
  }
1766
+ case "xiaomi-token-plan-sgp": {
1767
+ const apiKey = await loginXiaomiTokenPlan(ctrl, "sgp");
1768
+ await saveApiKeyCredential(apiKey);
1769
+ return;
1770
+ }
1771
+ case "xiaomi-token-plan-ams": {
1772
+ const apiKey = await loginXiaomiTokenPlan(ctrl, "ams");
1773
+ await saveApiKeyCredential(apiKey);
1774
+ return;
1775
+ }
1776
+ case "xiaomi-token-plan-cn": {
1777
+ const apiKey = await loginXiaomiTokenPlan(ctrl, "cn");
1778
+ await saveApiKeyCredential(apiKey);
1779
+ return;
1780
+ }
1766
1781
  case "zenmux": {
1767
1782
  const { loginZenMux } = await import("./utils/oauth/zenmux");
1768
1783
  const apiKey = await loginZenMux(ctrl);
package/src/models.json CHANGED
@@ -2649,7 +2649,7 @@
2649
2649
  },
2650
2650
  "us.meta.llama4-scout-17b-instruct-v1:0": {
2651
2651
  "id": "us.meta.llama4-scout-17b-instruct-v1:0",
2652
- "name": "Llama 4 Scout 17B Instruct",
2652
+ "name": "Llama 4 Scout 17B Instruct (US)",
2653
2653
  "api": "bedrock-converse-stream",
2654
2654
  "provider": "amazon-bedrock",
2655
2655
  "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
@@ -5325,7 +5325,7 @@
5325
5325
  "cacheWrite": 0
5326
5326
  },
5327
5327
  "contextWindow": 262144,
5328
- "maxTokens": 65536,
5328
+ "maxTokens": 32768,
5329
5329
  "thinking": {
5330
5330
  "mode": "effort",
5331
5331
  "minLevel": "minimal",
@@ -5469,7 +5469,7 @@
5469
5469
  "cacheWrite": 0
5470
5470
  },
5471
5471
  "contextWindow": 262144,
5472
- "maxTokens": 65536,
5472
+ "maxTokens": 32768,
5473
5473
  "thinking": {
5474
5474
  "mode": "effort",
5475
5475
  "minLevel": "minimal",
@@ -5494,7 +5494,7 @@
5494
5494
  "cacheWrite": 0
5495
5495
  },
5496
5496
  "contextWindow": 262144,
5497
- "maxTokens": 65536,
5497
+ "maxTokens": 32768,
5498
5498
  "thinking": {
5499
5499
  "mode": "effort",
5500
5500
  "minLevel": "minimal",
@@ -10438,7 +10438,7 @@
10438
10438
  },
10439
10439
  "anthropic/claude-opus-4.8": {
10440
10440
  "id": "anthropic/claude-opus-4.8",
10441
- "name": "Anthropic: Claude Opus 4.8 (new)",
10441
+ "name": "Anthropic: Claude Opus 4.8",
10442
10442
  "api": "openai-completions",
10443
10443
  "provider": "kilo",
10444
10444
  "baseUrl": "https://api.kilo.ai/api/gateway",
@@ -13187,7 +13187,7 @@
13187
13187
  },
13188
13188
  "minimax/minimax-m3": {
13189
13189
  "id": "minimax/minimax-m3",
13190
- "name": "MiniMax: MiniMax M3 (new)",
13190
+ "name": "MiniMax: MiniMax M3 (50% off through 2026-06-07)",
13191
13191
  "api": "openai-completions",
13192
13192
  "provider": "kilo",
13193
13193
  "baseUrl": "https://api.kilo.ai/api/gateway",
@@ -17142,6 +17142,25 @@
17142
17142
  "contextWindow": 222222,
17143
17143
  "maxTokens": 8888
17144
17144
  },
17145
+ "qwen/qwen3.7-plus:free": {
17146
+ "id": "qwen/qwen3.7-plus:free",
17147
+ "name": "Qwen: Qwen3.7 Plus (free)",
17148
+ "api": "openai-completions",
17149
+ "provider": "kilo",
17150
+ "baseUrl": "https://api.kilo.ai/api/gateway",
17151
+ "reasoning": false,
17152
+ "input": [
17153
+ "text"
17154
+ ],
17155
+ "cost": {
17156
+ "input": 0,
17157
+ "output": 0,
17158
+ "cacheRead": 0,
17159
+ "cacheWrite": 0
17160
+ },
17161
+ "contextWindow": 222222,
17162
+ "maxTokens": 8888
17163
+ },
17145
17164
  "qwen/qwq-32b": {
17146
17165
  "id": "qwen/qwq-32b",
17147
17166
  "name": "Qwen: QwQ 32B",
@@ -28051,6 +28070,25 @@
28051
28070
  "contextWindow": 222222,
28052
28071
  "maxTokens": 8888
28053
28072
  },
28073
+ "nex-agi/nex-n2-pro": {
28074
+ "id": "nex-agi/nex-n2-pro",
28075
+ "name": "nex-agi/nex-n2-pro",
28076
+ "api": "openai-completions",
28077
+ "provider": "nanogpt",
28078
+ "baseUrl": "https://nano-gpt.com/api/v1",
28079
+ "reasoning": false,
28080
+ "input": [
28081
+ "text"
28082
+ ],
28083
+ "cost": {
28084
+ "input": 0,
28085
+ "output": 0,
28086
+ "cacheRead": 0,
28087
+ "cacheWrite": 0
28088
+ },
28089
+ "contextWindow": 222222,
28090
+ "maxTokens": 8888
28091
+ },
28054
28092
  "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16": {
28055
28093
  "id": "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16",
28056
28094
  "name": "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16",
@@ -31231,13 +31269,14 @@
31231
31269
  },
31232
31270
  "qwen3.7-plus": {
31233
31271
  "id": "qwen3.7-plus",
31234
- "name": "qwen3.7-plus",
31272
+ "name": "Qwen3.7 Plus",
31235
31273
  "api": "openai-completions",
31236
31274
  "provider": "nanogpt",
31237
31275
  "baseUrl": "https://nano-gpt.com/api/v1",
31238
31276
  "reasoning": true,
31239
31277
  "input": [
31240
- "text"
31278
+ "text",
31279
+ "image"
31241
31280
  ],
31242
31281
  "cost": {
31243
31282
  "input": 0,
@@ -31245,8 +31284,8 @@
31245
31284
  "cacheRead": 0,
31246
31285
  "cacheWrite": 0
31247
31286
  },
31248
- "contextWindow": 222222,
31249
- "maxTokens": 8888,
31287
+ "contextWindow": 262144,
31288
+ "maxTokens": 65536,
31250
31289
  "thinking": {
31251
31290
  "mode": "effort",
31252
31291
  "minLevel": "minimal",
@@ -32579,6 +32618,25 @@
32579
32618
  "contextWindow": 222222,
32580
32619
  "maxTokens": 8888
32581
32620
  },
32621
+ "TEE/qwen3.6-27b": {
32622
+ "id": "TEE/qwen3.6-27b",
32623
+ "name": "TEE/qwen3.6-27b",
32624
+ "api": "openai-completions",
32625
+ "provider": "nanogpt",
32626
+ "baseUrl": "https://nano-gpt.com/api/v1",
32627
+ "reasoning": false,
32628
+ "input": [
32629
+ "text"
32630
+ ],
32631
+ "cost": {
32632
+ "input": 0,
32633
+ "output": 0,
32634
+ "cacheRead": 0,
32635
+ "cacheWrite": 0
32636
+ },
32637
+ "contextWindow": 222222,
32638
+ "maxTokens": 8888
32639
+ },
32582
32640
  "TEE/qwen3.6-35b-a3b-uncensored": {
32583
32641
  "id": "TEE/qwen3.6-35b-a3b-uncensored",
32584
32642
  "name": "TEE/qwen3.6-35b-a3b-uncensored",
@@ -38187,6 +38245,31 @@
38187
38245
  "minLevel": "minimal",
38188
38246
  "maxLevel": "xhigh"
38189
38247
  }
38248
+ },
38249
+ "qwen3.7-plus": {
38250
+ "id": "qwen3.7-plus",
38251
+ "name": "Qwen3.7 Plus",
38252
+ "api": "anthropic-messages",
38253
+ "provider": "opencode-go",
38254
+ "baseUrl": "https://opencode.ai/zen/go",
38255
+ "reasoning": true,
38256
+ "input": [
38257
+ "text",
38258
+ "image"
38259
+ ],
38260
+ "cost": {
38261
+ "input": 0.4,
38262
+ "output": 1.6,
38263
+ "cacheRead": 0.04,
38264
+ "cacheWrite": 0.5
38265
+ },
38266
+ "contextWindow": 262144,
38267
+ "maxTokens": 65536,
38268
+ "thinking": {
38269
+ "mode": "budget",
38270
+ "minLevel": "minimal",
38271
+ "maxLevel": "xhigh"
38272
+ }
38190
38273
  }
38191
38274
  },
38192
38275
  "opencode-zen": {
@@ -45179,8 +45262,8 @@
45179
45262
  "text"
45180
45263
  ],
45181
45264
  "cost": {
45182
- "input": 0.0428,
45183
- "output": 0.1716,
45265
+ "input": 0.04815,
45266
+ "output": 0.19305,
45184
45267
  "cacheRead": 0,
45185
45268
  "cacheWrite": 0
45186
45269
  },
@@ -49913,7 +49996,7 @@
49913
49996
  "cacheRead": 0,
49914
49997
  "cacheWrite": 0
49915
49998
  },
49916
- "contextWindow": 222222,
49999
+ "contextWindow": 1000000,
49917
50000
  "maxTokens": 8888,
49918
50001
  "compat": {
49919
50002
  "supportsUsageInStreaming": false
@@ -312,6 +312,15 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
312
312
  config => xiaomiModelManagerOptions(config),
313
313
  catalog("Xiaomi", ["XIAOMI_API_KEY"]),
314
314
  ),
315
+ descriptor("xiaomi-token-plan-sgp", "mimo-v2.5", config =>
316
+ xiaomiModelManagerOptions({ ...config, providerId: "xiaomi-token-plan-sgp", tokenPlanRegion: "sgp" }),
317
+ ),
318
+ descriptor("xiaomi-token-plan-ams", "mimo-v2.5", config =>
319
+ xiaomiModelManagerOptions({ ...config, providerId: "xiaomi-token-plan-ams", tokenPlanRegion: "ams" }),
320
+ ),
321
+ descriptor("xiaomi-token-plan-cn", "mimo-v2.5", config =>
322
+ xiaomiModelManagerOptions({ ...config, providerId: "xiaomi-token-plan-cn", tokenPlanRegion: "cn" }),
323
+ ),
315
324
  catalogDescriptor(
316
325
  "zenmux",
317
326
  "anthropic/claude-opus-4.6",
@@ -1,7 +1,7 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
2
  import { Effort } from "../model-thinking";
3
3
  import { getBundledModels } from "../models";
4
- import type { Api, Model, ThinkingConfig } from "../types";
4
+ import type { Api, Model, Provider, ThinkingConfig } from "../types";
5
5
  import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
6
6
  import {
7
7
  fetchOpenAICompatibleModels,
@@ -15,7 +15,7 @@ import { createBundledReferenceMap, createReferenceResolver } from "./bundled-re
15
15
  const MODELS_DEV_URL = "https://models.dev/api.json";
16
16
  const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
17
17
  const ANTHROPIC_OAUTH_BETA =
18
- "claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11";
18
+ "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11";
19
19
 
20
20
  export interface ModelsDevModel {
21
21
  id?: string;
@@ -925,6 +925,36 @@ const ZHIPU_VISION_PATTERN = /^glm-[45](?:\.\d+)?v(?:-|$)/;
925
925
  // 7.5 Fireworks
926
926
  // ---------------------------------------------------------------------------
927
927
 
928
+ /**
929
+ * Fireworks-published cap for the Kimi K2 family. Fireworks' `/v1/models`
930
+ * envelope generically reports `max_completion_tokens: 65536` for every Kimi
931
+ * deployment, but Kimi K2 (instruct / thinking / turbo) on Fireworks is
932
+ * documented to ship long reasoning traces that should be bounded — capping
933
+ * at 32,768 prevents handing callers a budget the router cannot honor.
934
+ * See https://github.com/can1357/oh-my-pi/issues/1849.
935
+ */
936
+ export const FIREWORKS_KIMI_MAX_TOKENS = 32_768;
937
+
938
+ /**
939
+ * Returns true for any Kimi K2.x public model id served by Fireworks-backed
940
+ * providers (`fireworks` direct, `firepass` router). Matches both the public
941
+ * catalog id (`kimi-k2.5`, `kimi-k2.6`, `kimi-k2.6-turbo`) and the canonical
942
+ * Fireworks wire id (`accounts/fireworks/{models,routers}/kimi-k2…`).
943
+ */
944
+ export function isFireworksKimiK2ModelId(modelId: string): boolean {
945
+ const trimmed = modelId.toLowerCase();
946
+ if (trimmed.startsWith("kimi-k2")) return true;
947
+ return /\/kimi-k2(?:p\d+)?(?:[._-]|$)/.test(trimmed);
948
+ }
949
+
950
+ /**
951
+ * Clamp the Kimi K2 family's `maxTokens` to {@link FIREWORKS_KIMI_MAX_TOKENS}
952
+ * on Fireworks-backed providers, leaving every other model untouched.
953
+ */
954
+ export function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number {
955
+ return isFireworksKimiK2ModelId(modelId) ? Math.min(candidate, FIREWORKS_KIMI_MAX_TOKENS) : candidate;
956
+ }
957
+
928
958
  export interface FireworksModelManagerConfig {
929
959
  apiKey?: string;
930
960
  baseUrl?: string;
@@ -1004,7 +1034,10 @@ export function fireworksModelManagerOptions(
1004
1034
  name: toFireworksModelName(entry, model.name),
1005
1035
  input: toBoolean(entry.supports_image_input) === true ? ["text", "image"] : ["text"],
1006
1036
  contextWindow: toPositiveNumber(entry.context_length, model.contextWindow),
1007
- maxTokens: toPositiveNumber(entry.max_completion_tokens, model.maxTokens),
1037
+ maxTokens: clampFireworksKimiMaxTokens(
1038
+ publicModelId,
1039
+ toPositiveNumber(entry.max_completion_tokens, model.maxTokens),
1040
+ ),
1008
1041
  };
1009
1042
  },
1010
1043
  });
@@ -1905,33 +1938,49 @@ export function cloudflareAiGatewayModelManagerOptions(
1905
1938
  // 20. Xiaomi
1906
1939
  // ---------------------------------------------------------------------------
1907
1940
 
1941
+ /** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */
1942
+ export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
1943
+
1944
+ /** Configures Xiaomi standard or regional Token Plan OpenAI-compatible model discovery. */
1908
1945
  export interface XiaomiModelManagerConfig {
1909
1946
  apiKey?: string;
1910
1947
  baseUrl?: string;
1948
+ providerId?: Provider;
1949
+ tokenPlanRegion?: XiaomiTokenPlanRegion;
1911
1950
  }
1912
1951
 
1952
+ const XIAOMI_TOKEN_PLAN_BASE_URLS: Record<XiaomiTokenPlanRegion, string> = {
1953
+ sgp: "https://token-plan-sgp.xiaomimimo.com/v1",
1954
+ ams: "https://token-plan-ams.xiaomimimo.com/v1",
1955
+ cn: "https://token-plan-cn.xiaomimimo.com/v1",
1956
+ };
1957
+
1958
+ const XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS = [
1959
+ XIAOMI_TOKEN_PLAN_BASE_URLS.sgp,
1960
+ XIAOMI_TOKEN_PLAN_BASE_URLS.ams,
1961
+ XIAOMI_TOKEN_PLAN_BASE_URLS.cn,
1962
+ ];
1963
+
1964
+ /** Builds a Xiaomi model manager, preserving Token Plan region provider ids during discovery. */
1913
1965
  export function xiaomiModelManagerOptions(
1914
1966
  config?: XiaomiModelManagerConfig,
1915
1967
  ): ModelManagerOptions<"openai-completions"> {
1916
1968
  const apiKey = config?.apiKey;
1917
- // Xiaomi splits API keys across two backends: standard `sk-` keys hit
1918
- // api.xiaomimimo.com; "token plan" `tp-` keys are scoped to a regional
1919
- // cluster and are tried in order until discovery succeeds.
1920
- const TOKEN_PLAN_BASE_URLS = [
1921
- "https://token-plan-sgp.xiaomimimo.com/v1",
1922
- "https://token-plan-ams.xiaomimimo.com/v1",
1923
- "https://token-plan-cn.xiaomimimo.com/v1",
1924
- ] as const;
1925
- const STANDARD_BASE_URL = "https://api.xiaomimimo.com/v1";
1926
- const isTokenPlanKey = apiKey?.startsWith("tp-");
1969
+ const providerId = config?.providerId ?? "xiaomi";
1970
+ const tokenPlanBaseUrls = config?.tokenPlanRegion
1971
+ ? [XIAOMI_TOKEN_PLAN_BASE_URLS[config.tokenPlanRegion]]
1972
+ : XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS;
1973
+ const XIAOMI_STANDARD_BASE_URL = "https://api.xiaomimimo.com/v1";
1974
+ const isTokenPlanProvider = config?.tokenPlanRegion !== undefined || providerId.startsWith("xiaomi-token-plan-");
1975
+ const isTokenPlanKey = isTokenPlanProvider || apiKey?.startsWith("tp-");
1927
1976
  // Token-plan keys always use a TP cluster; config?.baseUrl (from catalog)
1928
1977
  // would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
1929
- const baseUrl = isTokenPlanKey ? TOKEN_PLAN_BASE_URLS[0] : (config?.baseUrl ?? STANDARD_BASE_URL);
1978
+ const baseUrl = isTokenPlanKey ? tokenPlanBaseUrls[0] : (config?.baseUrl ?? XIAOMI_STANDARD_BASE_URL);
1930
1979
  const references = createBundledReferenceMap<"openai-completions">("xiaomi");
1931
1980
  const fetchModels = (url: string) =>
1932
1981
  fetchOpenAICompatibleModels({
1933
1982
  api: "openai-completions",
1934
- provider: "xiaomi",
1983
+ provider: providerId,
1935
1984
  baseUrl: url,
1936
1985
  apiKey,
1937
1986
  filterModel: (_entry, model) => !model.id.includes("-tts"),
@@ -1940,18 +1989,21 @@ export function xiaomiModelManagerOptions(
1940
1989
  const model = mapWithBundledReference(entry, defaults, reference);
1941
1990
  return {
1942
1991
  ...model,
1992
+ api: "openai-completions",
1993
+ provider: providerId,
1994
+ baseUrl: defaults.baseUrl,
1943
1995
  name: toModelName(entry.display_name, model.name),
1944
1996
  };
1945
1997
  },
1946
1998
  });
1947
1999
  return {
1948
- providerId: "xiaomi",
2000
+ providerId,
1949
2001
  ...(apiKey && {
1950
2002
  fetchDynamicModels: async () => {
1951
2003
  if (!isTokenPlanKey) {
1952
2004
  return fetchModels(baseUrl);
1953
2005
  }
1954
- for (const url of TOKEN_PLAN_BASE_URLS) {
2006
+ for (const url of tokenPlanBaseUrls) {
1955
2007
  const result = await fetchModels(url);
1956
2008
  if (result) return result;
1957
2009
  }
@@ -132,7 +132,6 @@ const claudeCodeUtilityBetaDefaults = [
132
132
  const claudeCodeAgentBetaDefaults = [
133
133
  "claude-code-20250219",
134
134
  "oauth-2025-04-20",
135
- "context-1m-2025-08-07",
136
135
  "interleaved-thinking-2025-05-14",
137
136
  "context-management-2025-06-27",
138
137
  "prompt-caching-scope-2026-01-05",
@@ -4,11 +4,21 @@ type OpenAIReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
4
4
  type ResolvedToolStrictMode = NonNullable<OpenAICompat["toolStrictMode"]> | "mixed";
5
5
 
6
6
  export type ResolvedOpenAICompat = Required<
7
- Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode">
7
+ Omit<
8
+ OpenAICompat,
9
+ | "openRouterRouting"
10
+ | "vercelGatewayRouting"
11
+ | "extraBody"
12
+ | "toolStrictMode"
13
+ | "cacheControlFormat"
14
+ | "thinkingKeep"
15
+ >
8
16
  > & {
9
17
  openRouterRouting?: OpenAICompat["openRouterRouting"];
10
18
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
11
19
  extraBody?: OpenAICompat["extraBody"];
20
+ cacheControlFormat?: OpenAICompat["cacheControlFormat"];
21
+ thinkingKeep?: OpenAICompat["thinkingKeep"];
12
22
  toolStrictMode: ResolvedToolStrictMode;
13
23
  };
14
24
 
@@ -54,12 +64,10 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
54
64
  const isZhipu = provider === "zhipu-coding-plan" || baseUrl.includes("open.bigmodel.cn");
55
65
  const isKilo = provider === "kilo" || baseUrl.includes("api.kilo.ai");
56
66
  const isKimiModel = model.id.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(model.id);
57
- const isMoonshotKimi =
58
- isKimiModel &&
59
- (provider === "moonshot" ||
60
- provider === "kimi-code" ||
61
- baseUrl.includes("api.moonshot.ai") ||
62
- baseUrl.includes("api.kimi.com"));
67
+ const isMoonshotNativeHost =
68
+ provider === "moonshot" || provider === "kimi-code" || /api\.moonshot\.ai|api\.kimi\.com/i.test(baseUrl);
69
+ const isMoonshotKimi = isKimiModel && isMoonshotNativeHost;
70
+ const usesMoonshotKimiPreservedThinking = isMoonshotKimi && /(^|\/)kimi-k2\.6(?:[-:]|$)/i.test(model.id);
63
71
  const isAnthropicModel =
64
72
  provider === "anthropic" ||
65
73
  baseUrl.includes("api.anthropic.com") ||
@@ -75,6 +83,10 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
75
83
  // applies when thinking mode is actually engaged.
76
84
  const lowerId = model.id.toLowerCase();
77
85
  const lowerName = (model.name ?? "").toLowerCase();
86
+ const isXiaomiHost =
87
+ provider === "xiaomi" || provider.startsWith("xiaomi-token-plan-") || baseUrl.includes("xiaomimimo.com");
88
+ const isMimoModel = lowerId.includes("mimo") || lowerName.includes("mimo");
89
+ const isXiaomiMimo = isXiaomiHost && isMimoModel;
78
90
  // OpenCode Zen's `big-pickle` is a DeepSeek reasoning alias; the upstream
79
91
  // 400s come from DeepSeek and require exact reasoning_content replay.
80
92
  const isOpenCodeDeepseekAlias =
@@ -101,6 +113,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
101
113
  isZhipu ||
102
114
  isKilo ||
103
115
  isQwen ||
116
+ isXiaomiHost ||
104
117
  provider === "opencode-zen" ||
105
118
  provider === "opencode-go" ||
106
119
  baseUrl.includes("opencode.ai");
@@ -197,7 +210,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
197
210
  // OpenAI's reasoning-API surface.
198
211
  supportsDeveloperRole: isOpenAIHost || isAzureHost,
199
212
  supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
200
- supportsReasoningEffort: !isGrok && !isZai && !isZhipu,
213
+ supportsReasoningEffort: !isGrok && !isZai && !isZhipu && !isXiaomiMimo,
201
214
  reasoningEffortMap,
202
215
  supportsUsageInStreaming: !isCerebras,
203
216
  disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,
@@ -214,18 +227,21 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
214
227
  // etc. — drives reasoning via OpenAI-style `reasoning_effort`
215
228
  // (low|medium|high|xhigh|max|none), so those stay on the "openai" path.
216
229
  thinkingFormat:
217
- isZai || isZhipu || isMoonshotKimi
230
+ isZai || isZhipu || isMoonshotKimi || isXiaomiMimo
218
231
  ? "zai"
219
232
  : provider === "openrouter" || baseUrl.includes("openrouter.ai")
220
233
  ? "openrouter"
221
234
  : isAlibaba || isQwen
222
235
  ? "qwen"
223
236
  : "openai",
237
+ thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
224
238
  reasoningContentField: "reasoning_content",
225
239
  // Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:
226
240
  // - Kimi: documented invariant on its native API.
227
241
  // - DeepSeek-family reasoning models, including aliased OpenCode Zen models
228
242
  // like `big-pickle`, validate exact thinking-mode replay.
243
+ // - Xiaomi MiMo models require exact `reasoning_content` replay on
244
+ // thinking-mode tool-call continuations across standard and Token Plan hosts.
229
245
  // - Any reasoning-capable model reached through OpenRouter can enforce this
230
246
  // server-side whenever the request is in thinking mode. We can't translate
231
247
  // Anthropic's redacted/encrypted reasoning into provider-native plaintext,
@@ -235,11 +251,13 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
235
251
  requiresReasoningContentForToolCalls:
236
252
  (isKimiModel && !isOpenCodeProvider) ||
237
253
  (isDeepseekFamily && Boolean(model.reasoning)) ||
254
+ isXiaomiMimo ||
238
255
  ((provider === "openrouter" || baseUrl.includes("openrouter.ai")) && Boolean(model.reasoning)),
239
- // DeepSeek V4 rejects synthetic reasoning_content placeholders (".") on tool-call turns.
256
+ // DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (".") on tool-call turns.
240
257
  // Kimi and OpenRouter accept them when actual reasoning is unavailable.
241
- allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !model.reasoning,
258
+ allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !model.reasoning) && !isXiaomiMimo,
242
259
  requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
260
+ cacheControlFormat: isOpenRouter && model.id.startsWith("anthropic/") ? "anthropic" : undefined,
243
261
  openRouterRouting: undefined,
244
262
  vercelGatewayRouting: undefined,
245
263
  supportsStrictMode: detectStrictModeSupport(provider, baseUrl),
@@ -280,6 +298,7 @@ export function resolveOpenAICompat(
280
298
  requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
281
299
  requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds,
282
300
  thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
301
+ thinkingKeep: model.compat.thinkingKeep ?? detected.thinkingKeep,
283
302
  reasoningContentField: model.compat.reasoningContentField ?? detected.reasoningContentField,
284
303
  requiresReasoningContentForToolCalls:
285
304
  model.compat.requiresReasoningContentForToolCalls ?? detected.requiresReasoningContentForToolCalls,
@@ -288,6 +307,7 @@ export function resolveOpenAICompat(
288
307
  detected.allowsSyntheticReasoningContentForToolCalls,
289
308
  requiresAssistantContentForToolCalls:
290
309
  model.compat.requiresAssistantContentForToolCalls ?? detected.requiresAssistantContentForToolCalls,
310
+ cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
291
311
  disableReasoningOnForcedToolChoice:
292
312
  model.compat.disableReasoningOnForcedToolChoice ?? detected.disableReasoningOnForcedToolChoice,
293
313
  disableReasoningOnToolChoice: model.compat.disableReasoningOnToolChoice ?? detected.disableReasoningOnToolChoice,
@@ -72,7 +72,11 @@ import {
72
72
  import { detectOpenAICompat, type ResolvedOpenAICompat, resolveOpenAICompat } from "./openai-completions-compat";
73
73
  import { createInitialResponsesAssistantMessage } from "./openai-responses-shared";
74
74
  import { transformMessages } from "./transform-messages";
75
- import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
75
+ import {
76
+ isDashscopeCompatibleModeTextOnlyQwen,
77
+ joinTextWithImagePlaceholder,
78
+ NON_VISION_IMAGE_PLACEHOLDER,
79
+ } from "./vision-guard";
76
80
 
77
81
  /**
78
82
  * Normalize tool call ID for Mistral.
@@ -255,7 +259,7 @@ type OpenAICompletionsParams = OpenAI.Chat.Completions.ChatCompletionCreateParam
255
259
  top_k?: number;
256
260
  min_p?: number;
257
261
  repetition_penalty?: number;
258
- thinking?: { type: "enabled" | "disabled" };
262
+ thinking?: { type: "enabled" | "disabled"; keep?: "all" };
259
263
  enable_thinking?: boolean;
260
264
  chat_template_kwargs?: { enable_thinking: boolean };
261
265
  reasoning?: { effort?: string } | { enabled: false };
@@ -1147,7 +1151,7 @@ function buildParams(
1147
1151
  }
1148
1152
  const isKimiModelId = model.id.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(model.id);
1149
1153
  const messages = convertMessages(model, context, compat);
1150
- maybeAddOpenRouterAnthropicCacheControl(model, messages);
1154
+ maybeAddAnthropicCacheControl(compat, messages);
1151
1155
  const supportsReasoningParams = model.provider !== "github-copilot";
1152
1156
 
1153
1157
  // Kimi (including via OpenRouter and Fireworks router-form IDs such as
@@ -1252,6 +1256,9 @@ function buildParams(
1252
1256
  // Must explicitly disable since z.ai defaults to thinking enabled.
1253
1257
  const enabled = options?.reasoning && !options?.disableReasoning;
1254
1258
  params.thinking = { type: enabled ? "enabled" : "disabled" };
1259
+ if (enabled && compat.thinkingKeep) {
1260
+ params.thinking.keep = compat.thinkingKeep;
1261
+ }
1255
1262
  } else if (supportsReasoningParams && compat.thinkingFormat === "qwen" && model.reasoning) {
1256
1263
  // Qwen uses top-level enable_thinking: boolean
1257
1264
  params.enable_thinking = !!options?.reasoning && !options?.disableReasoning;
@@ -1430,12 +1437,8 @@ function mapReasoningEffort(
1430
1437
  return reasoningEffortMap[effort] ?? effort;
1431
1438
  }
1432
1439
 
1433
- function maybeAddOpenRouterAnthropicCacheControl(
1434
- model: Model<"openai-completions">,
1435
- messages: ChatCompletionMessageParam[],
1436
- ): void {
1437
- if (model.provider !== "openrouter" || !model.id.startsWith("anthropic/")) return;
1438
-
1440
+ function maybeAddAnthropicCacheControl(compat: ResolvedOpenAICompat, messages: ChatCompletionMessageParam[]): void {
1441
+ if (compat.cacheControlFormat !== "anthropic") return;
1439
1442
  // Anthropic-style caching requires cache_control on a text part. Add a breakpoint
1440
1443
  // on the last user/assistant message (walking backwards until we find text content).
1441
1444
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -1567,7 +1570,7 @@ export function convertMessages(
1567
1570
  content: text,
1568
1571
  });
1569
1572
  } else {
1570
- const supportsImages = model.input.includes("image");
1573
+ const supportsImages = model.input.includes("image") && !isDashscopeCompatibleModeTextOnlyQwen(model);
1571
1574
  const content: ChatCompletionContentPart[] = [];
1572
1575
  let omittedImages = false;
1573
1576
  for (const item of msg.content) {
@@ -1810,7 +1813,7 @@ export function convertMessages(
1810
1813
  .filter(c => c.type === "text")
1811
1814
  .map(c => (c as TextContent).text)
1812
1815
  .join("\n");
1813
- const supportsImages = model.input.includes("image");
1816
+ const supportsImages = model.input.includes("image") && !isDashscopeCompatibleModeTextOnlyQwen(model);
1814
1817
  const hasImages = toolMsg.content.some(c => c.type === "image");
1815
1818
  const omittedImages = hasImages && !supportsImages;
1816
1819
 
@@ -1,4 +1,4 @@
1
- import type { ImageContent, TextContent } from "../types";
1
+ import type { ImageContent, Model, TextContent } from "../types";
2
2
 
3
3
  export const NON_VISION_IMAGE_PLACEHOLDER = "[image omitted: model does not support vision]";
4
4
 
@@ -29,3 +29,24 @@ export function joinTextWithImagePlaceholder(text: string, omittedImages: boolea
29
29
  }
30
30
  return parts.join("\n");
31
31
  }
32
+
33
+ /**
34
+ * Detect known text-only Qwen models served via Alibaba DashScope's consumer
35
+ * `compatible-mode` endpoint that the upstream chat-completions API rejects
36
+ * multimodal content arrays for. The compatible-mode endpoint also serves
37
+ * multimodal Qwen SKUs without `vl` in the id (e.g. `qwen3.7-plus`), so this
38
+ * guard only covers families verified to be text-only for issue #1859:
39
+ * `qwen*-max` and `qwen*-coder*`.
40
+ *
41
+ * Used as a defensive override in `convertMessages` so a misconfigured custom
42
+ * provider (issue #1859) can't drive the request into an unrecoverable 400.
43
+ */
44
+ export function isDashscopeCompatibleModeTextOnlyQwen(model: Model<"openai-completions">): boolean {
45
+ const baseUrl = model.baseUrl.toLowerCase();
46
+ if (!baseUrl.includes("dashscope") || !baseUrl.includes("aliyuncs.com") || !baseUrl.includes("/compatible-mode")) {
47
+ return false;
48
+ }
49
+ const id = model.id.toLowerCase();
50
+ if (!id.includes("qwen")) return false;
51
+ return /\bqwen(?:[\d.]+)?-max\b/.test(id) || /\bqwen(?:[\d.]+)?-coder\b/.test(id);
52
+ }
package/src/stream.ts CHANGED
@@ -266,6 +266,9 @@ const serviceProviderMap: Record<string, KeyResolver> = {
266
266
  venice: "VENICE_API_KEY",
267
267
  vllm: "VLLM_API_KEY",
268
268
  xiaomi: "XIAOMI_API_KEY",
269
+ "xiaomi-token-plan-sgp": "XIAOMI_TOKEN_PLAN_SGP_API_KEY",
270
+ "xiaomi-token-plan-ams": "XIAOMI_TOKEN_PLAN_AMS_API_KEY",
271
+ "xiaomi-token-plan-cn": "XIAOMI_TOKEN_PLAN_CN_API_KEY",
269
272
  };
270
273
 
271
274
  /**
package/src/types.ts CHANGED
@@ -142,6 +142,9 @@ export type KnownProvider =
142
142
  | "venice"
143
143
  | "vllm"
144
144
  | "xiaomi"
145
+ | "xiaomi-token-plan-sgp"
146
+ | "xiaomi-token-plan-ams"
147
+ | "xiaomi-token-plan-cn"
145
148
  | "wafer-pass"
146
149
  | "wafer-serverless"
147
150
  | "zenmux"
@@ -576,6 +579,8 @@ export interface UserMessage {
576
579
  content: string | (TextContent | ImageContent)[];
577
580
  /** True if the message was injected by the system (e.g., auto-continue). */
578
581
  synthetic?: boolean;
582
+ /** True when injected mid-turn as a steer; consumed by the agent's pre-LLM transform to wrap it for emphasis. Never rendered. */
583
+ steering?: boolean;
579
584
  /** Who initiated this message for billing/attribution semantics. */
580
585
  attribution?: MessageAttribution;
581
586
  /** Provider-specific opaque payload used to reconstruct transport-native history. */
@@ -784,6 +789,8 @@ export interface OpenAICompat {
784
789
  requiresMistralToolIds?: boolean;
785
790
  /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "zai" uses thinking: { type: "enabled" | "disabled" } (also used by Moonshot Kimi), "qwen" uses top-level enable_thinking, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
786
791
  thinkingFormat?: "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
792
+ /** Optional `thinking.keep` value for Z.ai/Moonshot-style thinking params. Set false to suppress auto-detected keep. Default: auto-detected. */
793
+ thinkingKeep?: "all" | false;
787
794
  /** Which reasoning content field to emit on assistant messages. Default: auto-detected. */
788
795
  reasoningContentField?: "reasoning_content" | "reasoning" | "reasoning_text";
789
796
  /** Whether assistant tool-call messages must include reasoning content. Default: false. */
@@ -815,6 +822,8 @@ export interface OpenAICompat {
815
822
  vercelGatewayRouting?: VercelGatewayRouting;
816
823
  /** Extra fields to include in request body (e.g. gateway routing hints for OpenClaw-style proxies). */
817
824
  extraBody?: Record<string, unknown>;
825
+ /** Whether chat-completions payloads should include provider-specific prompt-cache markers. */
826
+ cacheControlFormat?: "anthropic" | undefined;
818
827
  /** Whether the provider supports the `strict` field in tool definitions. Default: auto-detected per provider/baseUrl (conservative for unknown providers). */
819
828
  supportsStrictMode?: boolean;
820
829
  /** Whether tool schemas must be sent either all strict or all non-strict. Undefined keeps the existing per-tool mixed behavior. */
@@ -22,7 +22,7 @@ const CLAUDE_HEADERS = {
22
22
  accept: "application/json, text/plain, */*",
23
23
  "accept-encoding": "gzip, compress, deflate, br",
24
24
  "anthropic-beta":
25
- "claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11",
25
+ "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11",
26
26
  "content-type": "application/json",
27
27
  "user-agent": "claude-cli/2.1.160 (external, cli)",
28
28
  connection: "keep-alive",
@@ -102,6 +102,21 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
102
102
  name: "Xiaomi MiMo",
103
103
  available: true,
104
104
  },
105
+ {
106
+ id: "xiaomi-token-plan-sgp",
107
+ name: "Xiaomi Token Plan (Singapore)",
108
+ available: true,
109
+ },
110
+ {
111
+ id: "xiaomi-token-plan-ams",
112
+ name: "Xiaomi Token Plan (Europe)",
113
+ available: true,
114
+ },
115
+ {
116
+ id: "xiaomi-token-plan-cn",
117
+ name: "Xiaomi Token Plan (China)",
118
+ available: true,
119
+ },
105
120
  {
106
121
  id: "firepass",
107
122
  name: "Fire Pass (Fireworks Kimi K2.6 Turbo subscription)",
@@ -364,6 +379,9 @@ export async function refreshOAuthToken(
364
379
  case "ollama":
365
380
  case "ollama-cloud":
366
381
  case "xiaomi":
382
+ case "xiaomi-token-plan-sgp":
383
+ case "xiaomi-token-plan-ams":
384
+ case "xiaomi-token-plan-cn":
367
385
  case "zai":
368
386
  case "zhipu-coding-plan":
369
387
  case "qianfan":
@@ -53,6 +53,9 @@ export type OAuthProvider =
53
53
  | "vllm"
54
54
  | "xai-oauth"
55
55
  | "xiaomi"
56
+ | "xiaomi-token-plan-sgp"
57
+ | "xiaomi-token-plan-ams"
58
+ | "xiaomi-token-plan-cn"
56
59
  | "zenmux"
57
60
  | "zai"
58
61
  | "zhipu-coding-plan";
@@ -4,10 +4,8 @@
4
4
  * Xiaomi MiMo provides OpenAI-compatible models via
5
5
  * https://api.xiaomimimo.com/v1.
6
6
  *
7
- * This is not OAuth - it's a simple API key flow:
8
- * 1. Open browser to Xiaomi MiMo API key console
9
- * 2. User copies their API key
10
- * 3. User pastes the API key into the CLI
7
+ * Standard Xiaomi login opens the pay-as-you-go API key console. Token Plan
8
+ * login opens plan management so users copy the regional `tp-...` key.
11
9
  */
12
10
 
13
11
  import type { OAuthController } from "./types";
@@ -15,13 +13,34 @@ import type { OAuthController } from "./types";
15
13
  const PROVIDER_ID = "xiaomi";
16
14
  const PROVIDER_NAME = "Xiaomi MiMo";
17
15
  const STANDARD_AUTH_URL = "https://platform.xiaomimimo.com/#/console/api-keys";
16
+ const TOKEN_PLAN_AUTH_URL = "https://platform.xiaomimimo.com/console/plan-manage";
18
17
  const STANDARD_API_BASE_URL = "https://api.xiaomimimo.com/v1";
19
- const TOKEN_PLAN_SGP_API_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
20
- const TOKEN_PLAN_AMS_API_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
21
- const TOKEN_PLAN_CN_API_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1";
22
18
  const TOKEN_PLAN_KEY_PREFIX = "tp-";
23
19
  const STANDARD_VALIDATION_MODEL = "mimo-v2-flash";
24
20
  const TOKEN_PLAN_VALIDATION_MODEL = "mimo-v2.5";
21
+ const TOKEN_PLAN_SGP_API_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
22
+ const TOKEN_PLAN_AMS_API_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
23
+ const TOKEN_PLAN_CN_API_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1";
24
+
25
+ /** Region codes accepted by the Xiaomi Token Plan login flow. */
26
+ export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
27
+
28
+ type XiaomiValidationEndpoint = {
29
+ baseUrl: string;
30
+ model: string;
31
+ };
32
+
33
+ const TOKEN_PLAN_VALIDATION_ENDPOINTS: Record<XiaomiTokenPlanRegion, XiaomiValidationEndpoint> = {
34
+ sgp: { baseUrl: TOKEN_PLAN_SGP_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
35
+ ams: { baseUrl: TOKEN_PLAN_AMS_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
36
+ cn: { baseUrl: TOKEN_PLAN_CN_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
37
+ };
38
+
39
+ const TOKEN_PLAN_REGION_NAMES: Record<XiaomiTokenPlanRegion, string> = {
40
+ sgp: "Singapore",
41
+ ams: "Europe",
42
+ cn: "China",
43
+ };
25
44
 
26
45
  function isTokenPlanKey(apiKey: string): boolean {
27
46
  return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
@@ -29,16 +48,22 @@ function isTokenPlanKey(apiKey: string): boolean {
29
48
 
30
49
  const VALIDATION_TIMEOUT_MS = 15_000;
31
50
 
32
- async function validateXiaomiApiKey(apiKey: string, signal?: AbortSignal): Promise<void> {
33
- // For token-plan keys try SGP → AMS → CN in order until one succeeds.
34
- // Standard sk- keys only hit the one endpoint.
35
- const endpoints = isTokenPlanKey(apiKey)
36
- ? [
37
- { baseUrl: TOKEN_PLAN_SGP_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
38
- { baseUrl: TOKEN_PLAN_AMS_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
39
- { baseUrl: TOKEN_PLAN_CN_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
40
- ]
41
- : [{ baseUrl: STANDARD_API_BASE_URL, model: STANDARD_VALIDATION_MODEL }];
51
+ async function validateXiaomiApiKey(
52
+ apiKey: string,
53
+ signal?: AbortSignal,
54
+ tokenPlanRegion?: XiaomiTokenPlanRegion,
55
+ ): Promise<void> {
56
+ // Region-specific Token Plan logins must validate against the selected
57
+ // cluster. Generic Xiaomi login keeps the historical SGP → AMS → CN fallback.
58
+ const endpoints = tokenPlanRegion
59
+ ? [TOKEN_PLAN_VALIDATION_ENDPOINTS[tokenPlanRegion]]
60
+ : isTokenPlanKey(apiKey)
61
+ ? [
62
+ TOKEN_PLAN_VALIDATION_ENDPOINTS.sgp,
63
+ TOKEN_PLAN_VALIDATION_ENDPOINTS.ams,
64
+ TOKEN_PLAN_VALIDATION_ENDPOINTS.cn,
65
+ ]
66
+ : [{ baseUrl: STANDARD_API_BASE_URL, model: STANDARD_VALIDATION_MODEL }];
42
67
 
43
68
  let lastError: Error | null = null;
44
69
 
@@ -137,3 +162,33 @@ export async function loginXiaomi(options: OAuthController): Promise<string> {
137
162
  await validateXiaomiApiKey(trimmed, options.signal);
138
163
  return trimmed;
139
164
  }
165
+
166
+ /**
167
+ * Login to a regional Xiaomi Token Plan endpoint.
168
+ *
169
+ * Prompts for a token-plan API key and validates it against the selected region.
170
+ */
171
+ export async function loginXiaomiTokenPlan(options: OAuthController, region: XiaomiTokenPlanRegion): Promise<string> {
172
+ if (!options.onPrompt) {
173
+ throw new Error(`Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]}) login requires onPrompt callback`);
174
+ }
175
+ options.onAuth?.({
176
+ url: TOKEN_PLAN_AUTH_URL,
177
+ instructions: `Copy your token-plan API key for the ${TOKEN_PLAN_REGION_NAMES[region]} region`,
178
+ });
179
+ const apiKey = await options.onPrompt({
180
+ message: `Paste your Xiaomi Token Plan ${TOKEN_PLAN_REGION_NAMES[region]} API key (tp-...)`,
181
+ placeholder: "tp-...",
182
+ });
183
+ if (options.signal?.aborted) {
184
+ throw new Error("Login cancelled");
185
+ }
186
+ const trimmed = apiKey.trim();
187
+ if (!trimmed) {
188
+ throw new Error("API key is required");
189
+ }
190
+
191
+ options.onProgress?.(`Validating Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]}) API key...`);
192
+ await validateXiaomiApiKey(trimmed, options.signal, region);
193
+ return trimmed;
194
+ }