mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user