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
This commit is contained in:
Peter Steinberger
2026-07-09 08:16:50 +01:00
committed by GitHub
parent 3e787f3187
commit e7492c6fca
20 changed files with 333 additions and 89 deletions
+1
View File
@@ -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
+9
View File
@@ -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:
+12 -11
View File
@@ -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
<AccordionGroup>
<Accordion title="Adjustable reasoning">
`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:
+17 -21
View File
@@ -17,7 +17,7 @@ Qwen Cloud is an official external OpenClaw provider plugin with canonical id `q
| API style | OpenAI-compatible |
<Tip>
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.
</Tip>
## Install plugin
@@ -84,7 +84,7 @@ Choose your plan type and follow the setup steps.
</Tab>
<Tab title="Standard (pay-as-you-go)">
**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.
<Steps>
<Step title="Get your API key">
@@ -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
<AccordionGroup>
<Accordion title="Qwen 3.6 Plus availability">
`qwen3.6-plus` is available on the Standard (pay-as-you-go) endpoints:
<Accordion title="Qwen 3.7 availability">
`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.
</Accordion>
+2 -1
View File
@@ -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`.
+61 -8
View File
@@ -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<string, unknown> {
function captureCoherePayload(
context: Context,
settings?: { modelId?: string; reasoning?: string },
): Record<string, unknown> {
let captured: Record<string, unknown> | 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<StreamFn>;
};
@@ -39,7 +49,7 @@ function captureCoherePayload(context: Context): Record<string, unknown> {
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<string, unknown>;
},
@@ -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");
});
});
+1
View File
@@ -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({
+10 -6
View File
@@ -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);
});
+32
View File
@@ -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"
}
}
]
}
+31 -9
View File
@@ -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" });
},
);
});
+4 -1
View File
@@ -37,6 +37,7 @@ const MISTRAL_SMALL_LATEST_REASONING_EFFORT_MAP: Record<string, string> = {
};
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<string, string>;
} {
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,
+9 -2
View File
@@ -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(),
+14 -1
View File
@@ -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"]);
+19 -5
View File
@@ -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
}
},
+3
View File
@@ -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,
+20 -4
View File
@@ -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 () => {
+4 -4
View File
@@ -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;
+40 -7
View File
@@ -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<ModelDefinitionConfig> = [
{
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<string>([QWEN_37_MAX_MODEL_ID]);
const QWEN_OAUTH_UNSUPPORTED_MODEL_IDS = new Set<string>([
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<ModelDefinitionConfig> {
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<ModelDefinitionConfig> {
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. */
+5 -5
View File
@@ -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,
+39 -4
View File
@@ -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<typeof buildQwenProvider>;
@@ -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", () => {