From e7492c6fca234050c6ed02cc2ccdd76eb6ed9116 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 9 Jul 2026 08:16:50 +0100 Subject: [PATCH] feat(providers): refresh Qwen, Cohere, and Mistral catalogs (#102489) * feat(providers): refresh popular model catalogs * chore: leave release changelog to release automation * docs: refresh provider docs map --- docs/docs_map.md | 1 + docs/providers/cohere.md | 9 +++ docs/providers/mistral.md | 23 +++---- docs/providers/qwen.md | 38 +++++------ extensions/cohere/README.md | 3 +- extensions/cohere/index.test.ts | 69 +++++++++++++++++--- extensions/cohere/models.ts | 1 + extensions/cohere/onboard.test.ts | 16 +++-- extensions/cohere/openclaw.plugin.json | 32 +++++++++ extensions/mistral/api.test.ts | 40 +++++++++--- extensions/mistral/api.ts | 5 +- extensions/mistral/index.ts | 11 +++- extensions/mistral/model-definitions.test.ts | 15 ++++- extensions/mistral/openclaw.plugin.json | 24 +++++-- extensions/qwen/api.ts | 3 + extensions/qwen/index.test.ts | 24 +++++-- extensions/qwen/index.ts | 8 +-- extensions/qwen/models.ts | 47 +++++++++++-- extensions/qwen/openclaw.plugin.json | 10 +-- extensions/qwen/provider-catalog.test.ts | 43 ++++++++++-- 20 files changed, 333 insertions(+), 89 deletions(-) diff --git a/docs/docs_map.md b/docs/docs_map.md index 8509db6d1a98..660ba40df696 100644 --- a/docs/docs_map.md +++ b/docs/docs_map.md @@ -7382,6 +7382,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - Route: /providers/cohere - Headings: + - H2: Built-in catalog - H2: Get started - H2: Environment-only setup - H2: Related diff --git a/docs/providers/cohere.md b/docs/providers/cohere.md index 08ec9d958654..b3d78e961bed 100644 --- a/docs/providers/cohere.md +++ b/docs/providers/cohere.md @@ -20,6 +20,15 @@ read_when: | Default model | `cohere/command-a-03-2025` | | Context window | 256,000 tokens | +## Built-in catalog + +| Model ref | Input | Context | Max output | Notes | +| ---------------------------- | ----------- | ------- | ---------- | -------------------------------------- | +| `cohere/command-a-03-2025` | text | 256,000 | 8,000 | Default model | +| `cohere/north-mini-code-1-0` | text, image | 256,000 | 64,000 | Agentic coding; reasoning; free limits | + +North Mini Code supports Cohere's two Compatibility API reasoning modes. OpenClaw maps **off** to `none` and every enabled thinking level to `high`. + ## Get started 1. Cohere ships with current OpenClaw packages. If it is missing, install the external package and restart the Gateway: diff --git a/docs/providers/mistral.md b/docs/providers/mistral.md index 1ba96a6d2656..af670e364bfd 100644 --- a/docs/providers/mistral.md +++ b/docs/providers/mistral.md @@ -58,16 +58,17 @@ The bundled `mistral` plugin registers four contracts: chat completions, media u ## Built-in LLM catalog -| Model ref | Input | Context | Max output | Notes | -| -------------------------------- | ----------- | ------- | ---------- | ---------------------------------------------------------------- | -| `mistral/mistral-large-latest` | text, image | 262,144 | 16,384 | Default model | -| `mistral/mistral-medium-2508` | text, image | 262,144 | 8,192 | Mistral Medium 3.1 | -| `mistral/mistral-medium-3-5` | text, image | 262,144 | 8,192 | Mistral Medium 3.5; adjustable reasoning | -| `mistral/mistral-small-latest` | text, image | 128,000 | 16,384 | Mistral Small 4; adjustable reasoning via API `reasoning_effort` | -| `mistral/pixtral-large-latest` | text, image | 128,000 | 32,768 | Pixtral | -| `mistral/codestral-latest` | text | 256,000 | 4,096 | Coding | -| `mistral/devstral-medium-latest` | text | 262,144 | 32,768 | Devstral 2 | -| `mistral/magistral-small` | text | 128,000 | 40,000 | Reasoning-enabled | +| Model ref | Input | Context | Max output | Notes | +| -------------------------------- | ----------- | ------- | ---------- | ----------------------------------------------------- | +| `mistral/mistral-large-latest` | text, image | 262,144 | 16,384 | Default model | +| `mistral/mistral-medium-2508` | text, image | 262,144 | 8,192 | Mistral Medium 3.1 | +| `mistral/mistral-medium-3-5` | text, image | 262,144 | 8,192 | Mistral Medium 3.5; adjustable reasoning | +| `mistral/mistral-small-latest` | text, image | 262,144 | 16,384 | Mistral Small 4 latest; adjustable `reasoning_effort` | +| `mistral/mistral-small-2603` | text, image | 262,144 | 16,384 | Mistral Small 4 pinned; adjustable `reasoning_effort` | +| `mistral/pixtral-large-latest` | text, image | 128,000 | 32,768 | Pixtral | +| `mistral/codestral-latest` | text | 256,000 | 4,096 | Coding | +| `mistral/devstral-medium-latest` | text | 262,144 | 32,768 | Devstral 2 | +| `mistral/magistral-small` | text | 128,000 | 40,000 | Reasoning-enabled | Browse the bundled catalog row before changing config: @@ -148,7 +149,7 @@ OpenClaw defaults Mistral realtime STT to `pcm_mulaw` at 8 kHz so Voice Call can - `mistral/mistral-small-latest` and `mistral/mistral-medium-3-5` support [adjustable reasoning](https://docs.mistral.ai/studio-api/conversations/reasoning/adjustable) on the Chat Completions API via `reasoning_effort` (`none` minimizes extra thinking in the output; `high` surfaces full thinking traces before the final answer). + `mistral/mistral-small-latest`, `mistral/mistral-small-2603`, and `mistral/mistral-medium-3-5` support [adjustable reasoning](https://docs.mistral.ai/studio-api/conversations/reasoning/adjustable) on the Chat Completions API via `reasoning_effort` (`none` minimizes extra thinking in the output; `high` surfaces full thinking traces before the final answer). OpenClaw maps the session **thinking** level to Mistral's API: diff --git a/docs/providers/qwen.md b/docs/providers/qwen.md index f360886d4707..1f35874de92c 100644 --- a/docs/providers/qwen.md +++ b/docs/providers/qwen.md @@ -17,7 +17,7 @@ Qwen Cloud is an official external OpenClaw provider plugin with canonical id `q | API style | OpenAI-compatible | -For `qwen3.6-plus`, use a **Standard (pay-as-you-go)** endpoint. It is not available on the Coding Plan endpoints. +`qwen3.7-plus` and `qwen3.6-plus` work with Coding Plan and Standard endpoints. For `qwen3.7-max`, use a **Standard (pay-as-you-go)** endpoint. ## Install plugin @@ -84,7 +84,7 @@ Choose your plan type and follow the setup steps. - **Best for:** pay-as-you-go access through the Standard Model Studio endpoint, including models like `qwen3.6-plus` that are not available on the Coding Plan. + **Best for:** pay-as-you-go access through the Standard Model Studio endpoint, including `qwen3.7-max`, which is not available on the Coding Plan. @@ -198,7 +198,9 @@ Plan configs omit models that only work on the Standard endpoint. | Model ref | Input | Context | Notes | | --------------------------- | ----------- | --------- | ----------------------- | | `qwen/qwen3.5-plus` | text, image | 1,000,000 | Default model | -| `qwen/qwen3.6-plus` | text, image | 1,000,000 | Standard endpoints only | +| `qwen/qwen3.6-plus` | text, image | 1,000,000 | Coding Plan + Standard | +| `qwen/qwen3.7-max` | text | 1,000,000 | Standard endpoints only | +| `qwen/qwen3.7-plus` | text, image | 1,000,000 | Coding Plan + Standard | | `qwen/qwen3-max-2026-01-23` | text | 262,144 | Qwen Max line | | `qwen/qwen3-coder-next` | text | 262,144 | Coding | | `qwen/qwen3-coder-plus` | text | 1,000,000 | Coding | @@ -215,13 +217,13 @@ present in the static catalog. ## Thinking controls -`qwen/MiniMax-M2.5` is the only reasoning-enabled model in the built-in -catalog. For reasoning models on the `qwen` family, the provider maps -OpenClaw thinking levels to DashScope's top-level `enable_thinking` request -flag: disabled thinking sends `enable_thinking: false`, any other level sends -`enable_thinking: true`. Custom models can opt into an alternate chat-template -thinking payload by setting `compat.thinkingFormat: "qwen-chat-template"` on -the model entry. +`qwen3.7-max`, `qwen3.7-plus`, `qwen3.6-plus`, and `qwen/MiniMax-M2.5` are +reasoning-enabled in the built-in catalog. For reasoning models on the `qwen` +family, the provider maps OpenClaw thinking levels to DashScope's top-level +`enable_thinking` request flag: disabled thinking sends `enable_thinking: false`, +any other level sends `enable_thinking: true`. Custom models can opt into an +alternate chat-template thinking payload by setting +`compat.thinkingFormat: "qwen-chat-template"` on the model entry. ## Multimodal add-ons @@ -261,21 +263,15 @@ See [Video generation](/tools/video-generation) for shared tool parameters, prov ## Advanced configuration - - `qwen3.6-plus` is available on the Standard (pay-as-you-go) endpoints: + + `qwen3.7-plus` is available on Coding Plan and Standard endpoints. `qwen3.7-max` is Standard-only. The Standard (pay-as-you-go) endpoints are: - China: `dashscope.aliyuncs.com/compatible-mode/v1` - Global: `dashscope-intl.aliyuncs.com/compatible-mode/v1` - If the Coding Plan endpoints return an "unsupported model" error for - `qwen3.6-plus`, switch to Standard (pay-as-you-go) instead of the Coding Plan - endpoint/key pair. - - OpenClaw's Qwen static catalog does not advertise `qwen3.6-plus` on Coding - Plan endpoints, but an explicitly configured `qwen/qwen3.6-plus` entry under - `models.providers.qwen.models` is honored on Coding Plan base URLs, so you - can opt that model in if Aliyun enables it on your subscription. The - upstream API still decides whether the call succeeds. + OpenClaw omits `qwen3.7-max` from Coding Plan catalogs. If a Coding Plan + endpoint returns an "unsupported model" error for it, switch to the matching + Standard endpoint and key. diff --git a/extensions/cohere/README.md b/extensions/cohere/README.md index 73d0eab59be2..951077386b17 100644 --- a/extensions/cohere/README.md +++ b/extensions/cohere/README.md @@ -10,4 +10,5 @@ openclaw plugins install @openclaw/cohere-provider openclaw gateway restart ``` -Configure a Cohere API key, then select `cohere/command-a-03-2025`. +Configure a Cohere API key, then select `cohere/command-a-03-2025` or +`cohere/north-mini-code-1-0`. diff --git a/extensions/cohere/index.test.ts b/extensions/cohere/index.test.ts index b985cc9401c0..ca7fab6fc310 100644 --- a/extensions/cohere/index.test.ts +++ b/extensions/cohere/index.test.ts @@ -5,6 +5,7 @@ import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-ru import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime"; import { describe, expect, it } from "vitest"; import plugin from "./index.js"; +import { COHERE_NORTH_MINI_CODE_MODEL_ID } from "./models.js"; import { buildCohereProvider } from "./provider-catalog.js"; import { createCohereCompletionsWrapper } from "./stream.js"; @@ -15,23 +16,32 @@ function readManifest() { }; } -function requireCohereModel(): Model<"openai-completions"> { - const model = buildCohereProvider().models?.[0]; +function requireCohereModel(modelId = "command-a-03-2025"): Model<"openai-completions"> { + const provider = buildCohereProvider(); + const model = provider.models?.find((candidate) => candidate.id === modelId); if (!model) { throw new Error("Cohere catalog did not provide a model"); } - return model as Model<"openai-completions">; + return { + ...model, + provider: "cohere", + api: "openai-completions", + baseUrl: provider.baseUrl, + } as Model<"openai-completions">; } -function captureCoherePayload(context: Context): Record { +function captureCoherePayload( + context: Context, + settings?: { modelId?: string; reasoning?: string }, +): Record { let captured: Record | undefined; - const baseStreamFn: StreamFn = (model, streamContext, options) => { + const baseStreamFn: StreamFn = (model, streamContext, streamOptions) => { const payload = buildOpenAICompletionsParams( model as Model<"openai-completions">, streamContext, - { maxTokens: 2048 } as never, + { maxTokens: 2048, reasoning: settings?.reasoning } as never, ); - options?.onPayload?.(payload, model); + streamOptions?.onPayload?.(payload, model); return {} as ReturnType; }; @@ -39,7 +49,7 @@ function captureCoherePayload(context: Context): Record { if (!wrappedStreamFn) { throw new Error("Cohere wrapper did not return a stream function"); } - void wrappedStreamFn(requireCohereModel(), context, { + void wrappedStreamFn(requireCohereModel(settings?.modelId), context, { onPayload: (payload) => { captured = payload as Record; }, @@ -89,6 +99,32 @@ describe("Cohere provider plugin", () => { maxTokensField: "max_tokens", }, }), + expect.objectContaining({ + id: "north-mini-code-1-0", + reasoning: true, + input: ["text", "image"], + contextWindow: 256000, + maxTokens: 64000, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + compat: { + supportsStore: false, + supportsUsageInStreaming: false, + supportsReasoningEffort: true, + supportedReasoningEfforts: ["none", "high"], + reasoningEffortMap: { + off: "none", + none: "none", + minimal: "high", + low: "high", + medium: "high", + high: "high", + xhigh: "high", + adaptive: "high", + max: "high", + }, + maxTokensField: "max_tokens", + }, + }), ], }); }); @@ -118,4 +154,21 @@ describe("Cohere provider plugin", () => { expect.arrayContaining([expect.objectContaining({ role: "system", content: "system" })]), ); }); + + it("maps North Mini Code thinking levels to Cohere's supported reasoning efforts", () => { + const context = { messages: [] } as Context; + + expect( + captureCoherePayload(context, { + modelId: COHERE_NORTH_MINI_CODE_MODEL_ID, + reasoning: "off", + }).reasoning_effort, + ).toBe("none"); + expect( + captureCoherePayload(context, { + modelId: COHERE_NORTH_MINI_CODE_MODEL_ID, + reasoning: "high", + }).reasoning_effort, + ).toBe("high"); + }); }); diff --git a/extensions/cohere/models.ts b/extensions/cohere/models.ts index 3d4974682195..01cc197138c0 100644 --- a/extensions/cohere/models.ts +++ b/extensions/cohere/models.ts @@ -9,6 +9,7 @@ const COHERE_MANIFEST_CATALOG = manifest.modelCatalog.providers.cohere; export const COHERE_BASE_URL = COHERE_MANIFEST_CATALOG.baseUrl; export const COHERE_MODEL_CATALOG = COHERE_MANIFEST_CATALOG.models; +export const COHERE_NORTH_MINI_CODE_MODEL_ID = "north-mini-code-1-0"; export function buildCohereCatalogModels(): ModelDefinitionConfig[] { return buildManifestModelProviderConfig({ diff --git a/extensions/cohere/onboard.test.ts b/extensions/cohere/onboard.test.ts index 65b693840526..2a1084f2808d 100644 --- a/extensions/cohere/onboard.test.ts +++ b/extensions/cohere/onboard.test.ts @@ -1,12 +1,13 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard"; import { describe, expect, it } from "vitest"; -import { buildCohereCatalogModels, COHERE_BASE_URL, COHERE_MODEL_CATALOG } from "./models.js"; import { - applyCohereConfig, - COHERE_DEFAULT_MODEL_ID, - COHERE_DEFAULT_MODEL_REF, -} from "./onboard.js"; + buildCohereCatalogModels, + COHERE_BASE_URL, + COHERE_MODEL_CATALOG, + COHERE_NORTH_MINI_CODE_MODEL_ID, +} from "./models.js"; +import { applyCohereConfig, COHERE_DEFAULT_MODEL_ID, COHERE_DEFAULT_MODEL_REF } from "./onboard.js"; describe("Cohere onboarding", () => { it("registers the manifest catalog through the onboarding preset", () => { @@ -17,7 +18,10 @@ describe("Cohere onboarding", () => { baseUrl: COHERE_BASE_URL, api: "openai-completions", }); - expect(provider?.models?.map((model) => model.id)).toEqual([COHERE_DEFAULT_MODEL_ID]); + expect(provider?.models?.map((model) => model.id)).toEqual([ + COHERE_DEFAULT_MODEL_ID, + COHERE_NORTH_MINI_CODE_MODEL_ID, + ]); expect(buildCohereCatalogModels()).toHaveLength(COHERE_MODEL_CATALOG.length); }); diff --git a/extensions/cohere/openclaw.plugin.json b/extensions/cohere/openclaw.plugin.json index 24e71f1d9faa..472bca030216 100644 --- a/extensions/cohere/openclaw.plugin.json +++ b/extensions/cohere/openclaw.plugin.json @@ -30,6 +30,38 @@ "supportsUsageInStreaming": false, "maxTokensField": "max_tokens" } + }, + { + "id": "north-mini-code-1-0", + "name": "North Mini Code 1.0", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 256000, + "maxTokens": 64000, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "compat": { + "supportsStore": false, + "supportsUsageInStreaming": false, + "supportsReasoningEffort": true, + "supportedReasoningEfforts": ["none", "high"], + "reasoningEffortMap": { + "off": "none", + "none": "none", + "minimal": "high", + "low": "high", + "medium": "high", + "high": "high", + "xhigh": "high", + "adaptive": "high", + "max": "high" + }, + "maxTokensField": "max_tokens" + } } ] } diff --git a/extensions/mistral/api.test.ts b/extensions/mistral/api.test.ts index 1cde724db9c9..f147f4f5c1b3 100644 --- a/extensions/mistral/api.test.ts +++ b/extensions/mistral/api.test.ts @@ -5,6 +5,7 @@ import { applyMistralModelCompat, MISTRAL_MEDIUM_3_5_ID, MISTRAL_MODEL_TRANSPORT_PATCH, + MISTRAL_SMALL_4_ID, MISTRAL_SMALL_LATEST_ID, resolveMistralCompatPatch, } from "./api.js"; @@ -70,6 +71,17 @@ describe("resolveMistralCompatPatch", () => { }); }); + it("enables reasoning_effort mapping for Mistral Small 4's pinned id", () => { + expect(resolveMistralCompatPatch({ id: MISTRAL_SMALL_4_ID })).toEqual({ + supportsStore: false, + supportsPromptCacheKey: true, + supportsLongCacheRetention: false, + supportsReasoningEffort: true, + maxTokensField: "max_tokens", + reasoningEffortMap: MISTRAL_REASONING_EFFORT_MAP, + }); + }); + it("enables reasoning_effort mapping for mistral-medium-3-5", () => { expect(resolveMistralCompatPatch({ id: MISTRAL_MEDIUM_3_5_ID })).toEqual({ supportsStore: false, @@ -107,6 +119,13 @@ describe("applyMistralModelCompat", () => { expect(reasoningEffortMap(normalized)?.off).toBe("none"); }); + it("applies reasoning compat for Mistral Small 4's pinned id", () => { + const normalized = applyMistralModelCompat({ id: MISTRAL_SMALL_4_ID }); + expect(supportsReasoningEffort(normalized)).toBe(true); + expect(reasoningEffortMap(normalized)?.high).toBe("high"); + expect(reasoningEffortMap(normalized)?.off).toBe("none"); + }); + it("applies reasoning compat for mistral-medium-3-5", () => { const normalized = applyMistralModelCompat({ id: MISTRAL_MEDIUM_3_5_ID }); expect(supportsReasoningEffort(normalized)).toBe(true); @@ -170,14 +189,17 @@ describe("applyMistralModelCompat", () => { expect(applyMistralModelCompat(model)).toBe(model); }); - it("exposes thinking profile levels for mistral-medium-3-5", async () => { - const provider = await registerSingleProviderPlugin(mistralPlugin); + it.each([MISTRAL_SMALL_LATEST_ID, MISTRAL_SMALL_4_ID, MISTRAL_MEDIUM_3_5_ID])( + "exposes binary thinking profile levels for %s", + async (modelId) => { + const provider = await registerSingleProviderPlugin(mistralPlugin); - expect( - provider.resolveThinkingProfile?.({ - provider: "mistral", - modelId: MISTRAL_MEDIUM_3_5_ID, - }), - ).toEqual({ levels: [{ id: "off" }, { id: "high" }], defaultLevel: "off" }); - }); + expect( + provider.resolveThinkingProfile?.({ + provider: "mistral", + modelId, + }), + ).toEqual({ levels: [{ id: "off" }, { id: "high" }], defaultLevel: "off" }); + }, + ); }); diff --git a/extensions/mistral/api.ts b/extensions/mistral/api.ts index e8c56f9ecb01..8b1ac60a2650 100644 --- a/extensions/mistral/api.ts +++ b/extensions/mistral/api.ts @@ -37,6 +37,7 @@ const MISTRAL_SMALL_LATEST_REASONING_EFFORT_MAP: Record = { }; export const MISTRAL_SMALL_LATEST_ID = "mistral-small-latest"; +export const MISTRAL_SMALL_4_ID = "mistral-small-2603"; export const MISTRAL_MEDIUM_3_5_ID = "mistral-medium-3-5"; export function resolveMistralCompatPatch(model: { id?: string }): { @@ -48,7 +49,9 @@ export function resolveMistralCompatPatch(model: { id?: string }): { reasoningEffortMap?: Record; } { const reasoningEnabled = - model.id === MISTRAL_SMALL_LATEST_ID || model.id === MISTRAL_MEDIUM_3_5_ID; + model.id === MISTRAL_SMALL_LATEST_ID || + model.id === MISTRAL_SMALL_4_ID || + model.id === MISTRAL_MEDIUM_3_5_ID; return { ...MISTRAL_MODEL_TRANSPORT_PATCH, supportsReasoningEffort: reasoningEnabled, diff --git a/extensions/mistral/index.ts b/extensions/mistral/index.ts index 52f104e6c19d..395b41f1fe39 100644 --- a/extensions/mistral/index.ts +++ b/extensions/mistral/index.ts @@ -1,6 +1,11 @@ // Mistral plugin entrypoint registers its OpenClaw integration. import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { applyMistralModelCompat, MISTRAL_SMALL_LATEST_ID, MISTRAL_MEDIUM_3_5_ID } from "./api.js"; +import { + applyMistralModelCompat, + MISTRAL_MEDIUM_3_5_ID, + MISTRAL_SMALL_4_ID, + MISTRAL_SMALL_LATEST_ID, +} from "./api.js"; import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { mistralMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "./onboard.js"; @@ -46,7 +51,9 @@ export default defineSingleProviderPluginEntry({ /\bmistral\b.*(?:input.*too long|token limit.*exceeded)/i.test(errorMessage), normalizeResolvedModel: ({ model }) => applyMistralModelCompat(model), resolveThinkingProfile: ({ modelId }) => - modelId === MISTRAL_SMALL_LATEST_ID || modelId === MISTRAL_MEDIUM_3_5_ID + modelId === MISTRAL_SMALL_LATEST_ID || + modelId === MISTRAL_SMALL_4_ID || + modelId === MISTRAL_MEDIUM_3_5_ID ? { levels: [{ id: "off" }, { id: "high" }], defaultLevel: "off" } : undefined, buildReplayPolicy: () => buildMistralReplayPolicy(), diff --git a/extensions/mistral/model-definitions.test.ts b/extensions/mistral/model-definitions.test.ts index 83e4497a8645..d79969d21d6f 100644 --- a/extensions/mistral/model-definitions.test.ts +++ b/extensions/mistral/model-definitions.test.ts @@ -68,8 +68,21 @@ describe("mistral model definitions", () => { const smallLatest = catalogModelById(models, "mistral-small-latest"); expect(smallLatest.reasoning).toBe(true); expect(smallLatest.input).toEqual(["text", "image"]); - expect(smallLatest.contextWindow).toBe(128000); + expect(smallLatest.contextWindow).toBe(262144); expect(smallLatest.maxTokens).toBe(16384); + expect(smallLatest.cost).toEqual({ + input: 0.15, + output: 0.6, + cacheRead: 0.015, + cacheWrite: 0, + }); + + const small4 = catalogModelById(models, "mistral-small-2603"); + expect(small4.reasoning).toBe(true); + expect(small4.input).toEqual(["text", "image"]); + expect(small4.contextWindow).toBe(262144); + expect(small4.maxTokens).toBe(16384); + expect(small4.cost).toEqual(smallLatest.cost); const pixtralLarge = catalogModelById(models, "pixtral-large-latest"); expect(pixtralLarge.input).toEqual(["text", "image"]); diff --git a/extensions/mistral/openclaw.plugin.json b/extensions/mistral/openclaw.plugin.json index 1c1c3260cc95..811af57df926 100644 --- a/extensions/mistral/openclaw.plugin.json +++ b/extensions/mistral/openclaw.plugin.json @@ -107,15 +107,29 @@ }, { "id": "mistral-small-latest", - "name": "Mistral Small (latest)", + "name": "Mistral Small 4 (latest)", "input": ["text", "image"], "reasoning": true, - "contextWindow": 128000, + "contextWindow": 262144, "maxTokens": 16384, "cost": { - "input": 0.1, - "output": 0.3, - "cacheRead": 0.01, + "input": 0.15, + "output": 0.6, + "cacheRead": 0.015, + "cacheWrite": 0 + } + }, + { + "id": "mistral-small-2603", + "name": "Mistral Small 4 (26.03)", + "input": ["text", "image"], + "reasoning": true, + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { + "input": 0.15, + "output": 0.6, + "cacheRead": 0.015, "cacheWrite": 0 } }, diff --git a/extensions/qwen/api.ts b/extensions/qwen/api.ts index 0d0dd8a1c0ee..7fdda179c76c 100644 --- a/extensions/qwen/api.ts +++ b/extensions/qwen/api.ts @@ -7,7 +7,10 @@ export { isNativeQwenBaseUrl, isQwen36PlusSupportedBaseUrl, isQwenCodingPlanBaseUrl, + isQwenStandardOnlyModelId, QWEN_36_PLUS_MODEL_ID, + QWEN_37_MAX_MODEL_ID, + QWEN_37_PLUS_MODEL_ID, QWEN_BASE_URL, QWEN_CN_BASE_URL, QWEN_DEFAULT_COST, diff --git a/extensions/qwen/index.test.ts b/extensions/qwen/index.test.ts index 968f70e7fd8e..f46bfe84533f 100644 --- a/extensions/qwen/index.test.ts +++ b/extensions/qwen/index.test.ts @@ -6,7 +6,12 @@ import { import type { ProviderCatalogResult } from "openclaw/plugin-sdk/provider-catalog-shared"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; import { describe, expect, it } from "vitest"; -import { QWEN_36_PLUS_MODEL_ID, QWEN_BASE_URL } from "./api.js"; +import { + QWEN_36_PLUS_MODEL_ID, + QWEN_37_MAX_MODEL_ID, + QWEN_37_PLUS_MODEL_ID, + QWEN_BASE_URL, +} from "./api.js"; import qwenPlugin from "./index.js"; import { wrapQwenProviderStream } from "./stream.js"; @@ -27,18 +32,27 @@ async function registerQwenProvider() { } describe("qwen provider plugin", () => { - it("keeps qwen3.6-plus out of Coding Plan normalized catalogs", async () => { + it("keeps Standard-only models out of Coding Plan normalized catalogs", async () => { const provider = await registerQwenProvider(); const normalized = provider.normalizeConfig?.({ provider: "qwen", providerConfig: { baseUrl: QWEN_BASE_URL, - models: [{ id: "qwen3.5-plus" }, { id: QWEN_36_PLUS_MODEL_ID }], + models: [ + { id: "qwen3.5-plus" }, + { id: QWEN_36_PLUS_MODEL_ID }, + { id: QWEN_37_MAX_MODEL_ID }, + { id: QWEN_37_PLUS_MODEL_ID }, + ], }, } as never); - expect(normalized?.models?.map((model) => model.id)).toEqual(["qwen3.5-plus"]); + expect(normalized?.models?.map((model) => model.id)).toEqual([ + "qwen3.5-plus", + QWEN_36_PLUS_MODEL_ID, + QWEN_37_PLUS_MODEL_ID, + ]); }); it("does not expose runtime model suppression hooks", async () => { @@ -67,6 +81,8 @@ describe("qwen provider plugin", () => { const catalogProvider = requireCatalogProvider(result); expect(catalogProvider.baseUrl).toBe("https://portal.qwen.ai/v1"); expect(catalogProvider.models?.map((model) => model.id)).toContain("qwen3.5-plus"); + expect(catalogProvider.models?.map((model) => model.id)).not.toContain(QWEN_37_MAX_MODEL_ID); + expect(catalogProvider.models?.map((model) => model.id)).not.toContain(QWEN_37_PLUS_MODEL_ID); }); it("reuses legacy qwen portal auth profiles for qwen-oauth catalog", async () => { diff --git a/extensions/qwen/index.ts b/extensions/qwen/index.ts index e12267fc2101..c6d7c855bc2d 100644 --- a/extensions/qwen/index.ts +++ b/extensions/qwen/index.ts @@ -5,7 +5,7 @@ import { applyQwenNativeStreamingUsageCompat } from "./api.js"; import { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { isQwenCodingPlanBaseUrl, - QWEN_36_PLUS_MODEL_ID, + isQwenStandardOnlyModelId, QWEN_BASE_URL, QWEN_DEFAULT_MODEL_REF, QWEN_OAUTH_DEFAULT_MODEL_REF, @@ -73,7 +73,7 @@ export default defineSingleProviderPluginEntry({ "Manage API keys: https://home.qwencloud.com/api-keys", "Docs: https://docs.qwencloud.com/", "Endpoint: dashscope.aliyuncs.com/compatible-mode/v1", - "Models: qwen3.6-plus, qwen3.5-plus, qwen3-coder-plus, etc.", + "Models: qwen3.7-max, qwen3.7-plus, qwen3.6-plus, qwen3.5-plus, etc.", ].join("\n"), noteTitle: "Qwen Cloud Standard (China)", wizard: { @@ -96,7 +96,7 @@ export default defineSingleProviderPluginEntry({ "Manage API keys: https://home.qwencloud.com/api-keys", "Docs: https://docs.qwencloud.com/", "Endpoint: dashscope-intl.aliyuncs.com/compatible-mode/v1", - "Models: qwen3.6-plus, qwen3.5-plus, qwen3-coder-plus, etc.", + "Models: qwen3.7-max, qwen3.7-plus, qwen3.6-plus, qwen3.5-plus, etc.", ].join("\n"), noteTitle: "Qwen Cloud Standard (Global/Intl)", wizard: { @@ -174,7 +174,7 @@ export default defineSingleProviderPluginEntry({ if (!isQwenCodingPlanBaseUrl(providerConfig.baseUrl)) { return undefined; } - const models = providerConfig.models?.filter((model) => model.id !== QWEN_36_PLUS_MODEL_ID); + const models = providerConfig.models?.filter((model) => !isQwenStandardOnlyModelId(model.id)); return models && models.length !== providerConfig.models?.length ? { ...providerConfig, models } : undefined; diff --git a/extensions/qwen/models.ts b/extensions/qwen/models.ts index fa6cd699074f..3d7bcd3d6b1c 100644 --- a/extensions/qwen/models.ts +++ b/extensions/qwen/models.ts @@ -19,6 +19,8 @@ export const QWEN_OAUTH_BASE_URL = "https://portal.qwen.ai/v1"; export const QWEN_DEFAULT_MODEL_ID = "qwen3.5-plus"; export const QWEN_36_PLUS_MODEL_ID = "qwen3.6-plus"; +export const QWEN_37_MAX_MODEL_ID = "qwen3.7-max"; +export const QWEN_37_PLUS_MODEL_ID = "qwen3.7-plus"; export const QWEN_DEFAULT_COST = { input: 0, output: 0, @@ -41,7 +43,25 @@ export const QWEN_MODEL_CATALOG: ReadonlyArray = [ { id: QWEN_36_PLUS_MODEL_ID, name: QWEN_36_PLUS_MODEL_ID, - reasoning: false, + reasoning: true, + input: ["text", "image"], + cost: QWEN_DEFAULT_COST, + contextWindow: 1_000_000, + maxTokens: 65_536, + }, + { + id: QWEN_37_MAX_MODEL_ID, + name: QWEN_37_MAX_MODEL_ID, + reasoning: true, + input: ["text"], + cost: QWEN_DEFAULT_COST, + contextWindow: 1_000_000, + maxTokens: 65_536, + }, + { + id: QWEN_37_PLUS_MODEL_ID, + name: QWEN_37_PLUS_MODEL_ID, + reasoning: true, input: ["text", "image"], cost: QWEN_DEFAULT_COST, contextWindow: 1_000_000, @@ -128,16 +148,27 @@ export function isQwenCodingPlanBaseUrl(baseUrl: string | undefined): boolean { } } -export function isQwen36PlusSupportedBaseUrl(baseUrl: string | undefined): boolean { - return !isQwenCodingPlanBaseUrl(baseUrl); +export function isQwen36PlusSupportedBaseUrl(_baseUrl: string | undefined): boolean { + return true; +} + +const QWEN_STANDARD_ONLY_MODEL_IDS = new Set([QWEN_37_MAX_MODEL_ID]); + +const QWEN_OAUTH_UNSUPPORTED_MODEL_IDS = new Set([ + QWEN_37_MAX_MODEL_ID, + QWEN_37_PLUS_MODEL_ID, +]); + +export function isQwenStandardOnlyModelId(modelId: string): boolean { + return QWEN_STANDARD_ONLY_MODEL_IDS.has(modelId); } export function buildQwenModelCatalogForBaseUrl( baseUrl: string | undefined, ): ReadonlyArray { - return isQwen36PlusSupportedBaseUrl(baseUrl) - ? QWEN_MODEL_CATALOG - : QWEN_MODEL_CATALOG.filter((model) => model.id !== QWEN_36_PLUS_MODEL_ID); + return isQwenCodingPlanBaseUrl(baseUrl) + ? QWEN_MODEL_CATALOG.filter((model) => !isQwenStandardOnlyModelId(model.id)) + : QWEN_MODEL_CATALOG; } export function isNativeQwenBaseUrl(baseUrl: string | undefined): boolean { @@ -183,7 +214,9 @@ export function buildQwenDefaultModelDefinition(): ModelDefinitionConfig { } export function buildQwenOAuthModelCatalog(): ReadonlyArray { - return QWEN_MODEL_CATALOG.map((model) => ({ ...model, maxTokens: 65_536 })); + return QWEN_MODEL_CATALOG.filter((model) => !QWEN_OAUTH_UNSUPPORTED_MODEL_IDS.has(model.id)).map( + (model) => Object.assign({}, model, { maxTokens: 65_536 }), + ); } /** @deprecated Use QWEN_BASE_URL. */ diff --git a/extensions/qwen/openclaw.plugin.json b/extensions/qwen/openclaw.plugin.json index c3f3708f5493..3a19f56a57a2 100644 --- a/extensions/qwen/openclaw.plugin.json +++ b/extensions/qwen/openclaw.plugin.json @@ -62,8 +62,8 @@ "suppressions": [ { "provider": "qwen", - "model": "qwen3.6-plus", - "reason": "qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.", + "model": "qwen3.7-max", + "reason": "qwen3.7-max is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint.", "when": { "baseUrlHosts": ["coding.dashscope.aliyuncs.com", "coding-intl.dashscope.aliyuncs.com"], "providerConfigApiIn": ["qwen", "modelstudio"] @@ -71,8 +71,8 @@ }, { "provider": "modelstudio", - "model": "qwen3.6-plus", - "reason": "qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.", + "model": "qwen3.7-max", + "reason": "qwen3.7-max is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint.", "when": { "baseUrlHosts": ["coding.dashscope.aliyuncs.com", "coding-intl.dashscope.aliyuncs.com"], "providerConfigApiIn": ["qwen", "modelstudio"] @@ -101,7 +101,7 @@ { "id": "qwen3.6-plus", "name": "qwen3.6-plus", - "reasoning": false, + "reasoning": true, "input": ["text", "image"], "cost": { "input": 0, diff --git a/extensions/qwen/provider-catalog.test.ts b/extensions/qwen/provider-catalog.test.ts index b0ff17a84956..826e19ab8cc4 100644 --- a/extensions/qwen/provider-catalog.test.ts +++ b/extensions/qwen/provider-catalog.test.ts @@ -4,9 +4,13 @@ import { applyQwenNativeStreamingUsageCompat, buildQwenProvider, QWEN_BASE_URL, + QWEN_37_MAX_MODEL_ID, + QWEN_37_PLUS_MODEL_ID, QWEN_STANDARD_GLOBAL_BASE_URL, QWEN_DEFAULT_MODEL_ID, } from "./api.js"; +import manifest from "./openclaw.plugin.json" with { type: "json" }; +import { buildQwenOAuthProvider } from "./provider-catalog.js"; type QwenProvider = ReturnType; @@ -23,19 +27,50 @@ describe("qwen provider catalog", () => { const modelIds = getQwenModelIds(provider); expect(modelIds.length).toBeGreaterThan(0); expect(modelIds).toContain(QWEN_DEFAULT_MODEL_ID); - expect(modelIds).not.toContain("qwen3.6-plus"); + expect(modelIds).toContain("qwen3.6-plus"); + expect(modelIds).not.toContain(QWEN_37_MAX_MODEL_ID); + expect(modelIds).toContain(QWEN_37_PLUS_MODEL_ID); }); - it("only advertises qwen3.6-plus on Standard endpoints", () => { + it("only advertises Standard-only Qwen models on Standard endpoints", () => { const coding = buildQwenProvider({ baseUrl: QWEN_BASE_URL }); const codingTrailingDot = buildQwenProvider({ baseUrl: " https://coding-intl.dashscope.aliyuncs.com./v1 ", }); const standard = buildQwenProvider({ baseUrl: QWEN_STANDARD_GLOBAL_BASE_URL }); - expect(getQwenModelIds(coding)).not.toContain("qwen3.6-plus"); - expect(getQwenModelIds(codingTrailingDot)).not.toContain("qwen3.6-plus"); + expect(getQwenModelIds(coding)).toContain("qwen3.6-plus"); + expect(getQwenModelIds(codingTrailingDot)).toContain("qwen3.6-plus"); expect(getQwenModelIds(standard)).toContain("qwen3.6-plus"); + expect(getQwenModelIds(coding)).not.toContain(QWEN_37_MAX_MODEL_ID); + expect(getQwenModelIds(coding)).toContain(QWEN_37_PLUS_MODEL_ID); + expect(coding.models.find((model) => model.id === "qwen3.6-plus")?.reasoning).toBe(true); + + expect(standard.models.find((model) => model.id === QWEN_37_MAX_MODEL_ID)).toMatchObject({ + reasoning: true, + input: ["text"], + contextWindow: 1_000_000, + maxTokens: 65_536, + }); + expect(standard.models.find((model) => model.id === QWEN_37_PLUS_MODEL_ID)).toMatchObject({ + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + maxTokens: 65_536, + }); + }); + + it("keeps Qwen 3.7 out of the portal catalog", () => { + const portal = buildQwenOAuthProvider(); + const portalQwen36 = portal.models.find((model) => model.id === "qwen3.6-plus"); + const manifestQwen36 = manifest.modelCatalog.providers["qwen-oauth"].models.find( + (model) => model.id === "qwen3.6-plus", + ); + + expect(getQwenModelIds(portal)).not.toContain(QWEN_37_MAX_MODEL_ID); + expect(getQwenModelIds(portal)).not.toContain(QWEN_37_PLUS_MODEL_ID); + expect(portalQwen36?.reasoning).toBe(true); + expect(manifestQwen36?.reasoning).toBe(portalQwen36?.reasoning); }); it("opts native Qwen baseUrls into streaming usage only inside the extension", () => {