diff --git a/docs/providers/opencode-go.md b/docs/providers/opencode-go.md
index 57665588f088..534fda33fa6c 100644
--- a/docs/providers/opencode-go.md
+++ b/docs/providers/opencode-go.md
@@ -6,12 +6,12 @@ read_when:
title: "OpenCode Go"
---
-OpenCode Go is the Go catalog inside [OpenCode](/providers/opencode). It shares
-the `OPENCODE_API_KEY` credential with the Zen catalog, but keeps its own
-runtime provider id (`opencode-go`) so upstream per-model routing stays
-correct. OpenCode Go is bundled in the OpenClaw package for this release, so
-onboarding and configuration are sufficient; no separate plugin install is
-required.
+OpenCode Go is a separate paid subscription inside [OpenCode](/providers/opencode).
+It uses the same `OPENCODE_API_KEY` credential infrastructure as Zen, but a Zen
+key does not automatically include Go entitlement. Go keeps its own runtime
+provider id (`opencode-go`) so upstream per-model routing stays correct.
+OpenCode Go is bundled in the OpenClaw package for this release, so onboarding
+and configuration are sufficient; no separate plugin install is required.
| Property | Value |
| ---------------- | -------------------------------------------------- |
@@ -35,7 +35,7 @@ interactive onboarding or pass the shared OpenCode API key directly.
```bash
- openclaw config set agents.defaults.model.primary "opencode-go/kimi-k2.6"
+ openclaw config set agents.defaults.model.primary "opencode-go/kimi-k3"
```
@@ -67,35 +67,46 @@ interactive onboarding or pass the shared OpenCode API key directly.
```json5
{
env: { OPENCODE_API_KEY: "YOUR_API_KEY_HERE" }, // pragma: allowlist secret
- agents: { defaults: { model: { primary: "opencode-go/kimi-k2.6" } } },
+ agents: { defaults: { model: { primary: "opencode-go/kimi-k3" } } },
}
```
## Catalog
Run `openclaw models list --provider opencode-go` for the current model list.
-Current rows:
+Current active rows:
-| Model ref | Name | Context | Max output | Image input |
-| ------------------------------- | ----------------- | --------- | ---------- | ----------- |
-| `opencode-go/deepseek-v4-pro` | DeepSeek V4 Pro | 1M | 384K | No |
-| `opencode-go/deepseek-v4-flash` | DeepSeek V4 Flash | 1M | 384K | No |
-| `opencode-go/glm-5` | GLM-5 | 202,752 | 32,768 | No |
-| `opencode-go/glm-5.1` | GLM-5.1 | 202,752 | 32,768 | No |
-| `opencode-go/glm-5.2` | GLM-5.2 | 1M | 131,072 | No |
-| `opencode-go/hy3-preview` | HY3 Preview | 262,144 | 32,768 | No |
-| `opencode-go/kimi-k2.5` | Kimi K2.5 | 262,144 | 65,536 | Yes |
-| `opencode-go/kimi-k2.6` | Kimi K2.6 | 262,144 | 65,536 | Yes |
-| `opencode-go/kimi-k2.7-code` | Kimi K2.7 Code | 262,144 | 262,144 | Yes |
-| `opencode-go/mimo-v2.5` | MiMo V2.5 | 1M | 128,000 | Yes |
-| `opencode-go/mimo-v2.5-pro` | MiMo V2.5 Pro | 1,048,576 | 128,000 | No |
-| `opencode-go/minimax-m2.5` | MiniMax M2.5 | 204,800 | 65,536 | No |
-| `opencode-go/minimax-m2.7` | MiniMax M2.7 | 204,800 | 131,072 | No |
-| `opencode-go/minimax-m3` | MiniMax M3 | 204,800 | 131,072 | No |
-| `opencode-go/qwen3.5-plus` | Qwen3.5 Plus | 262,144 | 65,536 | Yes |
-| `opencode-go/qwen3.6-plus` | Qwen3.6 Plus | 262,144 | 65,536 | Yes |
-| `opencode-go/qwen3.7-max` | Qwen3.7 Max | 1M | 65,536 | No |
-| `opencode-go/qwen3.7-plus` | Qwen3.7 Plus | 1M | 65,536 | Yes |
+| Model ref | Context | Max output | Inputs | Transport |
+| ------------------------------- | --------- | ---------- | ----------- | --------- |
+| `opencode-go/deepseek-v4-flash` | 1M | 384K | Text | Chat |
+| `opencode-go/deepseek-v4-pro` | 1M | 384K | Text | Chat |
+| `opencode-go/glm-5.1` | 202,752 | 32,768 | Text | Chat |
+| `opencode-go/glm-5.2` | 1M | 131,072 | Text | Chat |
+| `opencode-go/gpt-5.6-luna` | 1.05M | 128,000 | Text, image | Responses |
+| `opencode-go/grok-4.5` | 500,000 | 500,000 | Text, image | Chat |
+| `opencode-go/hy3` | 256,000 | 64,000 | Text | Chat |
+| `opencode-go/kimi-k2.6` | 262,144 | 65,536 | Text, image | Chat |
+| `opencode-go/kimi-k2.7-code` | 262,144 | 262,144 | Text, image | Chat |
+| `opencode-go/kimi-k3` | 1,048,576 | 131,072 | Text, image | Chat |
+| `opencode-go/mimo-v2.5` | 1M | 128,000 | Text, image | Chat |
+| `opencode-go/mimo-v2.5-pro` | 1,048,576 | 128,000 | Text | Chat |
+| `opencode-go/minimax-m2.7` | 204,800 | 131,072 | Text | Messages |
+| `opencode-go/minimax-m3` | 1M | 131,072 | Text, image | Messages |
+| `opencode-go/qwen3.6-plus` | 1M | 65,536 | Text, image | Messages |
+| `opencode-go/qwen3.7-max` | 1M | 65,536 | Text | Messages |
+| `opencode-go/qwen3.7-plus` | 1M | 65,536 | Text, image | Messages |
+| `opencode-go/qwen3.8-max` | 1M | 131,072 | Text, image | Messages |
+
+Deprecated and preview refs remain resolvable only for existing explicit
+configurations. They are not part of static or live recommendations.
+
+## Privacy
+
+OpenCode's current policy says model training is not used for any active Go
+route. Grok 4.5 and GPT-5.6 Luna retain data for up to 30 days; the other active
+Go routes list zero-day retention. Review the current
+[OpenCode Go privacy table](https://opencode.ai/docs/go/#privacy) before using a
+model, because provider policy can change independently of OpenClaw.
## Advanced configuration
@@ -111,8 +122,9 @@ Current rows:
- One `OPENCODE_API_KEY` covers both the Zen and Go catalogs. Entering the
- key during setup stores credentials for both runtime providers.
+ The same `OPENCODE_API_KEY` can authenticate both runtime providers, so
+ setup may store both profiles. Go access still requires a separate paid
+ subscription in the OpenCode console.
diff --git a/docs/providers/opencode.md b/docs/providers/opencode.md
index b5f3f2acf907..e63529582f01 100644
--- a/docs/providers/opencode.md
+++ b/docs/providers/opencode.md
@@ -13,10 +13,10 @@ OpenCode exposes two hosted catalogs in OpenClaw:
| **Zen** | `opencode/...` | `opencode` |
| **Go** | `opencode-go/...` | `opencode-go` |
-Both catalogs share one OpenCode API key (`OPENCODE_API_KEY`, alias
-`OPENCODE_ZEN_API_KEY`). OpenClaw keeps the runtime provider ids split so
-upstream per-model routing stays correct, but onboarding and docs treat them as
-one OpenCode setup.
+Both catalogs use the same OpenCode API key infrastructure (`OPENCODE_API_KEY`,
+alias `OPENCODE_ZEN_API_KEY`). Go still requires its own paid subscription;
+having a Zen key does not by itself grant Go access. OpenClaw keeps the runtime
+provider ids split so upstream per-model routing stays correct.
## Getting started
@@ -52,7 +52,8 @@ one OpenCode setup.
- **Best for:** the OpenCode-hosted Kimi, GLM, MiniMax, Qwen, and DeepSeek lineup.
+ **Best for:** the separately subscribed Go lineup across DeepSeek, GLM, GPT,
+ Grok, Hy3, Kimi, MiMo, MiniMax, and Qwen.
@@ -72,7 +73,7 @@ one OpenCode setup.
```bash
- openclaw config set agents.defaults.model.primary "opencode-go/kimi-k2.6"
+ openclaw config set agents.defaults.model.primary "opencode-go/kimi-k3"
```
@@ -98,23 +99,30 @@ one OpenCode setup.
### Zen
-| Property | Value |
-| ---------------- | ------------------------------------------------------------------------------------------------- |
-| Runtime provider | `opencode` |
-| Example models | `opencode/gpt-5.6-sol`, `opencode/gemini-3.6-flash`, `opencode/minimax-m3`, `opencode/big-pickle` |
+| Property | Value |
+| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
+| Runtime provider | `opencode` |
+| Example models | `opencode/gpt-5.6-sol`, `opencode/kimi-k3`, `opencode/gemini-3.6-flash`, `opencode/minimax-m3`, `opencode/big-pickle` |
-Run `openclaw models list --provider opencode` for the full current list, which
-also includes the currently promoted free-tier rows `opencode/big-pickle`,
+Run `openclaw models list --provider opencode` for the current active list,
+which also includes the promoted free-tier rows `opencode/big-pickle`,
`opencode/deepseek-v4-flash-free`, `opencode/laguna-s-2.1-free`,
-`opencode/ling-3.0-flash-free`, `opencode/mimo-v2.5-free`,
+`opencode/ling-3.0-tiny-free`, `opencode/longcat-2.0-free`,
+`opencode/mimo-v2.5-free`,
`opencode/nemotron-3-ultra-free`, and `opencode/north-mini-code-free`.
+Live discovery safely intersects OpenCode's returned IDs with trusted OpenClaw
+metadata. A key-scoped response can omit models that are unavailable to that
+workspace; that absence does not retire the offline definition. Deprecated
+explicit refs remain resolvable for existing configurations but are not shown
+as current recommendations.
+
### Go
-| Property | Value |
-| ---------------- | ------------------------------------------------------------------------ |
-| Runtime provider | `opencode-go` |
-| Example models | `opencode-go/kimi-k2.6`, `opencode-go/glm-5`, `opencode-go/minimax-m2.5` |
+| Property | Value |
+| ---------------- | ---------------------------------------------------------------------------- |
+| Runtime provider | `opencode-go` |
+| Example models | `opencode-go/kimi-k3`, `opencode-go/gpt-5.6-luna`, `opencode-go/qwen3.8-max` |
See [OpenCode Go](/providers/opencode-go) for the full Go model table.
@@ -126,8 +134,9 @@ See [OpenCode Go](/providers/opencode-go) for the full Go model table.
- Entering one OpenCode key during setup stores credentials for both runtime
- providers. You do not need to onboard each catalog separately.
+ Entering one OpenCode key during setup can store credentials for both
+ runtime providers. It does not create a Go subscription or grant Go
+ entitlement; subscribe to Go in the OpenCode console before using it.
@@ -145,6 +154,11 @@ See [OpenCode Go](/providers/opencode-go) for the full Go model table.
Non-Gemini OpenCode refs keep the minimal OpenAI-compatible replay policy.
+
+ Billing, retention, and training policies are model-specific. Check the
+ current [OpenCode Zen pricing and policy](https://opencode.ai/docs/zen/)
+ before selecting a route. Free models may be temporary feedback programs.
+
## Related
diff --git a/extensions/opencode-go/index.test.ts b/extensions/opencode-go/index.test.ts
index e28e5bff5b48..4232bc1b52f7 100644
--- a/extensions/opencode-go/index.test.ts
+++ b/extensions/opencode-go/index.test.ts
@@ -1,4 +1,3 @@
-import { clampThinkingLevel } from "openclaw/plugin-sdk/llm";
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
import {
registerProviderPlugin,
@@ -7,6 +6,7 @@ import {
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
import { expectPassthroughReplayPolicy } from "openclaw/plugin-sdk/provider-test-contracts";
+import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
// Opencode Go tests cover index plugin behavior.
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -39,36 +39,43 @@ function requireCatalogEntry(entries: readonly unknown[] | null | undefined, id:
return requireRecord(entry, `supplemental catalog entry ${id}`);
}
-const deepSeekV4ThinkingProfileLevelIds = [
- "off",
- "minimal",
- "low",
- "medium",
- "high",
- "xhigh",
- "max",
-] as const;
-const deepSeekV4ThinkingProfile = {
- levels: deepSeekV4ThinkingProfileLevelIds.map((id) => ({ id })),
- defaultLevel: "high",
-};
-const deepSeekV4ThinkingLevelMap = {
- minimal: "high",
- low: "high",
- medium: "high",
- high: "high",
- xhigh: "max",
- max: "max",
-};
-
-function expectDeepSeekV4ThinkingLevels(model: ProviderRuntimeModel) {
- expect(model.thinkingLevelMap).toEqual(deepSeekV4ThinkingLevelMap);
- expect(clampThinkingLevel(model, "off")).toBe("off");
- expect(clampThinkingLevel(model, "high")).toBe("high");
- expect(clampThinkingLevel(model, "xhigh")).toBe("xhigh");
- expect(clampThinkingLevel(model, "max")).toBe("max");
+function runtimeCompatFields(value: unknown): Record | undefined {
+ if (!value) {
+ return undefined;
+ }
+ const { codeMode: _codeMode, ...compat } = requireRecord(value, "model compat");
+ return compat;
}
+const ACTIVE_MODEL_IDS = [
+ "qwen3.7-plus",
+ "glm-5.1",
+ "deepseek-v4-flash",
+ "minimax-m2.7",
+ "glm-5.2",
+ "qwen3.7-max",
+ "kimi-k2.6",
+ "minimax-m3",
+ "hy3",
+ "deepseek-v4-pro",
+ "qwen3.8-max",
+ "mimo-v2.5",
+ "gpt-5.6-luna",
+ "grok-4.5",
+ "kimi-k2.7-code",
+ "kimi-k3",
+ "mimo-v2.5-pro",
+ "qwen3.6-plus",
+] as const;
+const DEPRECATED_MODEL_IDS = [
+ "glm-5",
+ "qwen3.5-plus",
+ "mimo-v2-omni",
+ "kimi-k2.5",
+ "mimo-v2-pro",
+ "minimax-m2.5",
+] as const;
+
describe("opencode-go provider plugin", () => {
beforeEach(() => {
clearLiveCatalogCacheForTests();
@@ -84,7 +91,7 @@ describe("opencode-go provider plugin", () => {
expect(provider.auth[0]?.wizard).toMatchObject({
choiceLabel: "OpenCode Go catalog",
groupId: "opencode",
- groupHint: "Shared API key for Zen + Go catalogs",
+ groupHint: "Shared API key infrastructure for Zen + Go",
});
});
@@ -126,26 +133,8 @@ describe("opencode-go provider plugin", () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(provider.catalog).toBeDefined();
- const expectedModelIds = [
- "deepseek-v4-flash",
- "deepseek-v4-pro",
- "glm-5",
- "glm-5.1",
- "glm-5.2",
- "hy3-preview",
- "kimi-k2.5",
- "kimi-k2.6",
- "kimi-k2.7-code",
- "mimo-v2.5",
- "mimo-v2.5-pro",
- "minimax-m2.5",
- "minimax-m2.7",
- "minimax-m3",
- "qwen3.5-plus",
- "qwen3.6-plus",
- "qwen3.7-max",
- "qwen3.7-plus",
- ];
+ const expectedModelIds = [...ACTIVE_MODEL_IDS, ...DEPRECATED_MODEL_IDS, "hy3-preview"];
+ expect(new Set(expectedModelIds).size).toBe(expectedModelIds.length);
const models = new Map();
for (const modelId of expectedModelIds) {
const model = provider.resolveDynamicModel?.({ modelId } as never);
@@ -154,18 +143,79 @@ describe("opencode-go provider plugin", () => {
}
models.set(model.id, model);
}
- expect([...models.keys()]).toEqual(expectedModelIds);
- expectDeepSeekV4ThinkingLevels(requireMapEntry(models, "deepseek-v4-pro"));
- expectDeepSeekV4ThinkingLevels(requireMapEntry(models, "deepseek-v4-flash"));
+ expect([...models.keys()].toSorted()).toEqual(expectedModelIds.toSorted());
expect(
- provider.resolveThinkingProfile?.({ provider: "opencode-go", modelId: "deepseek-v4-pro" }),
- ).toEqual(deepSeekV4ThinkingProfile);
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "deepseek-v4-pro",
+ api: "openai-completions",
+ reasoning: true,
+ compat: { supportedReasoningEfforts: ["high", "max"] },
+ }),
+ ).toEqual({
+ levels: [{ id: "off" }, { id: "high" }, { id: "max" }],
+ defaultLevel: "high",
+ });
expect(
- provider.resolveThinkingProfile?.({ provider: "opencode-go", modelId: "deepseek-v4-flash" }),
- ).toEqual(deepSeekV4ThinkingProfile);
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "deepseek-v4-flash",
+ api: "openai-completions",
+ reasoning: true,
+ compat: { supportedReasoningEfforts: ["low", "high", "max"] },
+ }),
+ ).toEqual({
+ levels: [{ id: "off" }, { id: "low" }, { id: "high" }, { id: "max" }],
+ defaultLevel: "high",
+ });
expect(
- provider.resolveThinkingProfile?.({ provider: "opencode-go", modelId: "glm-5" }),
- ).toBeUndefined();
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "kimi-k3",
+ api: "openai-completions",
+ reasoning: true,
+ compat: { supportedReasoningEfforts: ["max"] },
+ }),
+ ).toEqual({ levels: [{ id: "off" }, { id: "max" }], defaultLevel: "off" });
+ expect(
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "glm-5",
+ api: "openai-completions",
+ reasoning: true,
+ }),
+ ).toEqual({ levels: [{ id: "off", label: "always on" }], defaultLevel: "off" });
+ expect(
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "grok-4.5",
+ api: "openai-completions",
+ reasoning: true,
+ compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
+ }),
+ ).toEqual({
+ levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }],
+ defaultLevel: "medium",
+ });
+ expect(
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "minimax-m2.7",
+ api: "anthropic-messages",
+ reasoning: true,
+ }),
+ ).toEqual({ levels: [{ id: "high", label: "always on" }], defaultLevel: "high" });
+ expect(
+ provider.resolveThinkingProfile?.({
+ provider: "opencode-go",
+ modelId: "minimax-m3",
+ api: "anthropic-messages",
+ reasoning: true,
+ }),
+ ).toEqual({
+ levels: [{ id: "off" }, { id: "high", label: "on" }],
+ defaultLevel: "high",
+ });
const supplemental = await provider.augmentModelCatalog?.({
entries: [...models.values()].map((model) => ({
provider: model.provider,
@@ -173,12 +223,23 @@ describe("opencode-go provider plugin", () => {
name: model.name,
})),
} as never);
+ const supplementalIds = (supplemental ?? []).map((entry) => entry.id);
+ expect(new Set(supplementalIds).size).toBe(supplementalIds.length);
+ expect(supplementalIds.toSorted()).toEqual(expectedModelIds.toSorted());
const deepSeekPro = requireCatalogEntry(supplemental, "deepseek-v4-pro");
expect(deepSeekPro.provider).toBe("opencode-go");
expect(deepSeekPro.name).toBe("DeepSeek V4 Pro");
const deepSeekFlash = requireCatalogEntry(supplemental, "deepseek-v4-flash");
expect(deepSeekFlash.provider).toBe("opencode-go");
expect(deepSeekFlash.name).toBe("DeepSeek V4 Flash");
+ for (const modelId of DEPRECATED_MODEL_IDS) {
+ expect(requireCatalogEntry(supplemental, modelId).status).toBe("deprecated");
+ expect(requireCatalogEntry(supplemental, modelId).replacedBy).toBeUndefined();
+ }
+ for (const modelId of ACTIVE_MODEL_IDS) {
+ expect(requireCatalogEntry(supplemental, modelId).status).toBeUndefined();
+ }
+ expect(requireCatalogEntry(supplemental, "hy3-preview").status).toBe("preview");
const glm52 = requireMapEntry(models, "glm-5.2");
expect(glm52.api).toBe("openai-completions");
@@ -194,6 +255,15 @@ describe("opencode-go provider plugin", () => {
cacheWrite: 0,
});
+ expect(requireMapEntry(models, "kimi-k3")).toMatchObject({
+ api: "openai-completions",
+ input: ["text", "image"],
+ contextWindow: 1_048_576,
+ maxTokens: 131_072,
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
+ compat: { supportsReasoningEffort: true, supportedReasoningEfforts: ["max"] },
+ });
+
const kimi = requireMapEntry(models, "kimi-k2.6");
expect(kimi.api).toBe("openai-completions");
expect(kimi.baseUrl).toBe("https://opencode.ai/zen/go/v1");
@@ -226,7 +296,8 @@ describe("opencode-go provider plugin", () => {
expect(minimaxM3.api).toBe("anthropic-messages");
expect(minimaxM3.baseUrl).toBe("https://opencode.ai/zen/go");
expect(minimaxM3.reasoning).toBe(true);
- expect(minimaxM3.contextWindow).toBe(204_800);
+ expect(minimaxM3.input).toEqual(["text", "image"]);
+ expect(minimaxM3.contextWindow).toBe(1_000_000);
expect(minimaxM3.maxTokens).toBe(131_072);
const mimoPro = requireMapEntry(models, "mimo-v2.5-pro");
@@ -291,7 +362,7 @@ describe("opencode-go provider plugin", () => {
expect(compat.maxTokensField).toBe("max_tokens");
});
- it("loads OpenCode Go model discovery through the provider runtime", () => {
+ it("loads model discovery and keeps every promoted row identical to runtime", async () => {
expect(manifest.providerCatalogEntry).toBe("./provider-discovery.ts");
expect(manifest.modelCatalog.discovery["opencode-go"]).toBe("runtime");
const manifestProvider = requireRecord(
@@ -301,12 +372,42 @@ describe("opencode-go provider plugin", () => {
if (!Array.isArray(manifestProvider.models)) {
throw new Error("expected manifest models");
}
- expect(
- requireCatalogEntry(manifestProvider.models, "deepseek-v4-pro").thinkingLevelMap,
- ).toEqual(deepSeekV4ThinkingLevelMap);
- expect(
- requireCatalogEntry(manifestProvider.models, "deepseek-v4-flash").thinkingLevelMap,
- ).toEqual(deepSeekV4ThinkingLevelMap);
+ const manifestIds = manifestProvider.models.map((model) =>
+ String(requireRecord(model, "manifest model").id),
+ );
+ expect(new Set(manifestIds).size).toBe(manifestIds.length);
+ const provider = await registerSingleProviderPlugin(plugin);
+ for (const manifestModel of manifestProvider.models) {
+ const model = requireRecord(manifestModel, "manifest model");
+ const modelId = String(model.id);
+ const runtime = requireRecord(
+ provider.resolveDynamicModel?.({ modelId } as never),
+ `runtime model ${modelId}`,
+ );
+ expect({
+ api: model.api ?? manifestProvider.api,
+ baseUrl: model.baseUrl ?? manifestProvider.baseUrl,
+ reasoning: model.reasoning,
+ input: model.input,
+ contextWindow: model.contextWindow,
+ contextTokens: model.contextTokens,
+ maxTokens: model.maxTokens,
+ thinkingLevelMap: model.thinkingLevelMap,
+ cost: model.cost,
+ compat: runtimeCompatFields(model.compat),
+ }).toEqual({
+ api: runtime.api,
+ baseUrl: runtime.baseUrl,
+ reasoning: runtime.reasoning,
+ input: runtime.input,
+ contextWindow: runtime.contextWindow,
+ contextTokens: runtime.contextTokens,
+ maxTokens: runtime.maxTokens,
+ thinkingLevelMap: runtime.thinkingLevelMap,
+ cost: runtime.cost,
+ compat: runtimeCompatFields(runtime.compat),
+ });
+ }
});
it("exposes the complete offline catalog through provider discovery", async () => {
@@ -318,18 +419,20 @@ describe("opencode-go provider plugin", () => {
const deepSeekFlash = result.provider.models.find((model) => model.id === "deepseek-v4-flash");
const glm52 = result.provider.models.find((model) => model.id === "glm-5.2");
- expect(result.provider.models).toHaveLength(18);
+ const modelIds = result.provider.models.map((model) => model.id);
+ expect(new Set(modelIds).size).toBe(modelIds.length);
+ expect(modelIds.toSorted()).toEqual(ACTIVE_MODEL_IDS.toSorted());
expect(deepSeekPro).toMatchObject({
provider: "opencode-go",
contextWindow: 1_000_000,
maxTokens: 384_000,
- thinkingLevelMap: deepSeekV4ThinkingLevelMap,
+ compat: { supportedReasoningEfforts: ["high", "max"] },
});
expect(deepSeekFlash).toMatchObject({
provider: "opencode-go",
contextWindow: 1_000_000,
maxTokens: 384_000,
- thinkingLevelMap: deepSeekV4ThinkingLevelMap,
+ compat: { supportedReasoningEfforts: ["low", "high", "max"] },
});
expect(glm52).toMatchObject({
provider: "opencode-go",
@@ -351,18 +454,26 @@ describe("opencode-go provider plugin", () => {
).resolves.toBeNull();
});
- it("keeps deprecated upstream MiMo aliases out of static and live catalogs", async () => {
- const deprecatedModelIds = ["mimo-v2-omni", "mimo-v2-pro"];
+ it("keeps compatibility rows explicit-resolvable but out of static and live catalogs", async () => {
+ const provider = await registerSingleProviderPlugin(plugin);
+ const compatibilityModelIds = [...DEPRECATED_MODEL_IDS, "hy3-preview"];
const activeModelIds = ["mimo-v2.5", "mimo-v2.5-pro"];
const staticModelIds = buildStaticOpencodeGoProviderConfig().models.map((model) => model.id);
- expect(staticModelIds).toEqual(expect.arrayContaining(activeModelIds));
- expect(staticModelIds).toEqual(expect.not.arrayContaining(deprecatedModelIds));
+ expect(new Set(staticModelIds).size).toBe(staticModelIds.length);
+ expect(staticModelIds.toSorted()).toEqual(ACTIVE_MODEL_IDS.toSorted());
+ expect(staticModelIds).toEqual(expect.not.arrayContaining(compatibilityModelIds));
+ for (const modelId of compatibilityModelIds) {
+ expect(provider.resolveDynamicModel?.({ modelId } as never)).toMatchObject({ id: modelId });
+ }
const fetchGuard = vi.fn(async () => ({
response: new Response(
JSON.stringify({
- data: [...deprecatedModelIds, ...activeModelIds].map((id) => ({ id, object: "model" })),
+ data: [...compatibilityModelIds, ...activeModelIds].map((id) => ({
+ id,
+ object: "model",
+ })),
}),
),
finalUrl: "https://opencode.ai/zen/go/v1/models",
@@ -436,16 +547,9 @@ describe("opencode-go provider plugin", () => {
expect(fetchGuard).toHaveBeenCalledTimes(1);
expect(first.apiKey).toBe("OPENCODE_API_KEY");
- expect(first.models.map((model) => model.id)).toEqual([
- "minimax-m3",
- "qwen3.7-max",
- "qwen3.7-plus",
- ]);
- expect(second.models.map((model) => model.id)).toEqual([
- "minimax-m3",
- "qwen3.7-max",
- "qwen3.7-plus",
- ]);
+ const liveIds = ["minimax-m3", "qwen3.7-max", "qwen3.7-plus"];
+ expect(first.models.map((model) => model.id).toSorted()).toEqual(liveIds);
+ expect(second.models.map((model) => model.id).toSorted()).toEqual(liveIds);
clearLiveCatalogCacheForTests();
fetchGuard.mockRejectedValueOnce(new Error("network unavailable"));
@@ -455,8 +559,9 @@ describe("opencode-go provider plugin", () => {
fetchGuard,
});
expect(fallback.apiKey).toBe("OPENCODE_API_KEY");
- expect(fallback.models.map((model) => model.id)).toContain("deepseek-v4-pro");
- expect(fallback.models.map((model) => model.id)).toContain("minimax-m3");
+ expect(fallback.models.map((model) => model.id).toSorted()).toEqual(
+ ACTIVE_MODEL_IDS.toSorted(),
+ );
});
it.each(["deepseek-v4-pro", "deepseek-v4-flash"] as const)(
@@ -497,11 +602,41 @@ describe("opencode-go provider plugin", () => {
);
it.each([
- ["minimal", "high"],
- ["low", "high"],
- ["medium", "high"],
+ ["glm-5.2", "max", undefined],
+ ["grok-4.5", "high", undefined],
+ ["hy3", "low", "none"],
+ ] as const)(
+ "maps %s only to supported wire efforts",
+ async (modelId, enabledEffort, offEffort) => {
+ const provider = await registerSingleProviderPlugin(plugin);
+ const model = provider.resolveDynamicModel?.({ modelId } as never);
+ if (!model) {
+ throw new Error(`expected ${modelId}`);
+ }
+ const context = {
+ systemPrompt: "",
+ messages: [{ role: "user", content: "test", timestamp: 1 }],
+ } as never;
+
+ const offPayload = buildOpenAICompletionsParams(model as never, context, {
+ reasoning: "off",
+ } as never);
+ if (offEffort === undefined) {
+ expect(offPayload).not.toHaveProperty("reasoning_effort");
+ } else {
+ expect(offPayload).toHaveProperty("reasoning_effort", offEffort);
+ }
+ expect(
+ buildOpenAICompletionsParams(model as never, context, {
+ reasoning: enabledEffort,
+ } as never),
+ ).toHaveProperty("reasoning_effort", enabledEffort);
+ },
+ );
+
+ it.each([
+ ["low", "low"],
["high", "high"],
- ["xhigh", "max"],
["max", "max"],
] as const)(
"maps OpenCode Go DeepSeek V4 %s thinking to %s reasoning effort",
@@ -600,6 +735,73 @@ describe("opencode-go provider plugin", () => {
]);
});
+ it.each(["minimax-m2.5", "minimax-m2.7"])(
+ "keeps fixed-reasoning %s on the provider default wire path",
+ async (modelId) => {
+ const provider = await registerSingleProviderPlugin(plugin);
+ const capturedPayloads: Record[] = [];
+ const baseStreamFn = (_model: unknown, _context: unknown, options: unknown) => {
+ const payload = {
+ model: modelId,
+ thinking: { type: "enabled", budget_tokens: 8192 },
+ output_config: { effort: "high" },
+ };
+ (options as { onPayload?: (payload: Record) => void })?.onPayload?.(
+ payload,
+ );
+ capturedPayloads.push(payload);
+ return {} as never;
+ };
+ const streamFn = provider.wrapStreamFn?.({
+ streamFn: baseStreamFn as never,
+ providerId: "opencode-go",
+ modelId,
+ thinkingLevel: "high",
+ } as never);
+
+ await streamFn?.(
+ { provider: "opencode-go", id: modelId, api: "anthropic-messages" } as never,
+ {} as never,
+ {},
+ );
+ expect(capturedPayloads).toEqual([{ model: modelId }]);
+ },
+ );
+
+ it.each([
+ ["off", undefined],
+ ["max", "max"],
+ ] as const)("keeps Kimi K3 reasoning %s exact", async (thinkingLevel, expectedEffort) => {
+ const provider = await registerSingleProviderPlugin(plugin);
+ const capturedPayloads: Record[] = [];
+ const baseStreamFn = (_model: unknown, _context: unknown, options: unknown) => {
+ const payload: Record = {
+ model: "kimi-k3",
+ reasoning_effort: "max",
+ };
+ (options as { onPayload?: (payload: Record) => void })?.onPayload?.(payload);
+ capturedPayloads.push(payload);
+ return {} as never;
+ };
+ const streamFn = provider.wrapStreamFn?.({
+ streamFn: baseStreamFn as never,
+ providerId: "opencode-go",
+ modelId: "kimi-k3",
+ thinkingLevel,
+ } as never);
+
+ await streamFn?.(
+ { provider: "opencode-go", id: "kimi-k3", api: "openai-completions" } as never,
+ {} as never,
+ {},
+ );
+ expect(capturedPayloads).toEqual([
+ expectedEffort === undefined
+ ? { model: "kimi-k3" }
+ : { model: "kimi-k3", reasoning_effort: expectedEffort },
+ ]);
+ });
+
it("canonicalizes stale OpenCode Go base URLs", async () => {
const provider = await registerSingleProviderPlugin(plugin);
diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts
index 994a8c16952a..2f69995b9cb4 100644
--- a/extensions/opencode-go/index.ts
+++ b/extensions/opencode-go/index.ts
@@ -17,7 +17,7 @@ import { createOpencodeGoWrapper } from "./stream.js";
const PROVIDER_ID = "opencode-go";
const OPENCODE_SHARED_PROFILE_IDS = ["opencode:default", "opencode-go:default"] as const;
-const OPENCODE_SHARED_HINT = "Shared API key for Zen + Go catalogs";
+const OPENCODE_SHARED_HINT = "Shared API key infrastructure for Zen + Go";
type OpencodeGoCatalogAuth = {
apiKey?: string;
discoveryApiKey?: string;
@@ -55,7 +55,7 @@ export default defineSingleProviderPluginEntry({
applyConfig: applyOpencodeGoConfig,
expectedProviders: ["opencode", "opencode-go"],
noteMessage: [
- "OpenCode uses one API key across the Zen and Go catalogs.",
+ "OpenCode Go is a separate paid subscription that uses the shared OpenCode API key.",
"Go focuses on Kimi, GLM, and MiniMax coding models.",
"Get your API key at: https://opencode.ai/auth",
].join("\n"),
diff --git a/extensions/opencode-go/openclaw.plugin.json b/extensions/opencode-go/openclaw.plugin.json
index 86aa330edbf4..72ba9395d038 100644
--- a/extensions/opencode-go/openclaw.plugin.json
+++ b/extensions/opencode-go/openclaw.plugin.json
@@ -30,14 +30,6 @@
"id": "deepseek-v4-pro",
"name": "DeepSeek V4 Pro",
"reasoning": true,
- "thinkingLevelMap": {
- "minimal": "high",
- "low": "high",
- "medium": "high",
- "high": "high",
- "xhigh": "max",
- "max": "max"
- },
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 384000,
@@ -50,7 +42,10 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["high", "max"],
"maxTokensField": "max_tokens",
+ "supportsDeveloperRole": false,
+ "supportsStrictMode": false,
"codeMode": "capable"
}
},
@@ -58,14 +53,6 @@
"id": "deepseek-v4-flash",
"name": "DeepSeek V4 Flash",
"reasoning": true,
- "thinkingLevelMap": {
- "minimal": "high",
- "low": "high",
- "medium": "high",
- "high": "high",
- "xhigh": "max",
- "max": "max"
- },
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 384000,
@@ -78,7 +65,70 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "high", "max"],
"maxTokensField": "max_tokens",
+ "supportsDeveloperRole": false,
+ "supportsStrictMode": false,
+ "codeMode": "capable"
+ }
+ },
+ {
+ "id": "kimi-k3",
+ "name": "Kimi K3",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "contextWindow": 1048576,
+ "maxTokens": 131072,
+ "cost": { "input": 3, "output": 15, "cacheRead": 0.3, "cacheWrite": 0 },
+ "compat": {
+ "supportsUsageInStreaming": true,
+ "supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["max"],
+ "maxTokensField": "max_tokens",
+ "supportsDeveloperRole": false,
+ "supportsStrictMode": false,
+ "codeMode": "capable"
+ }
+ },
+ {
+ "id": "gpt-5.6-luna",
+ "name": "GPT-5.6 Luna",
+ "api": "openai-responses",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "contextWindow": 1050000,
+ "contextTokens": 922000,
+ "maxTokens": 128000,
+ "cost": {
+ "input": 0.2,
+ "output": 1.2,
+ "cacheRead": 0.02,
+ "cacheWrite": 0.25,
+ "tieredPricing": [
+ { "input": 0.2, "output": 1.2, "cacheRead": 0.02, "cacheWrite": 0.25, "range": [0, 272000] },
+ { "input": 0.4, "output": 1.8, "cacheRead": 0.04, "cacheWrite": 0.5, "range": [272000] }
+ ]
+ },
+ "compat": {
+ "supportsUsageInStreaming": true,
+ "supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"],
+ "maxTokensField": "max_tokens",
+ "codeMode": "capable"
+ }
+ },
+ {
+ "id": "qwen3.8-max",
+ "name": "Qwen3.8 Max",
+ "api": "anthropic-messages",
+ "baseUrl": "https://opencode.ai/zen/go",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "contextWindow": 1000000,
+ "maxTokens": 131072,
+ "cost": { "input": 2, "output": 6, "cacheRead": 0.25, "cacheWrite": 2.5 },
+ "compat": {
+ "thinkingFormat": "qwen",
"codeMode": "capable"
}
}
@@ -106,7 +156,7 @@
"choiceLabel": "OpenCode Go catalog",
"groupId": "opencode",
"groupLabel": "OpenCode",
- "groupHint": "Shared API key for Zen + Go catalogs",
+ "groupHint": "Shared API key infrastructure for Zen + Go",
"optionKey": "opencodeGoApiKey",
"cliFlag": "--opencode-go-api-key",
"cliOption": "--opencode-go-api-key ",
diff --git a/extensions/opencode-go/opencode-go.live.test.ts b/extensions/opencode-go/opencode-go.live.test.ts
new file mode 100644
index 000000000000..fc9607e1a8a5
--- /dev/null
+++ b/extensions/opencode-go/opencode-go.live.test.ts
@@ -0,0 +1,74 @@
+import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-live";
+import { describe, expect, it } from "vitest";
+import {
+ buildStaticOpencodeGoProviderConfig,
+ listOpencodeGoModelCatalogEntries,
+} from "./provider-catalog.js";
+
+const OPENCODE_GO_MODELS_URL = "https://opencode.ai/zen/go/v1/models";
+const OPENCODE_API_KEY =
+ process.env.OPENCODE_API_KEY?.trim() || process.env.OPENCODE_ZEN_API_KEY?.trim() || "";
+const LIVE = isLiveTestEnabled(["OPENCODE_GO_LIVE_TEST"]) && OPENCODE_API_KEY.length > 0;
+const describeLive = LIVE ? describe : describe.skip;
+
+type ModelsResponse = { data?: Array<{ id?: unknown; object?: unknown }> };
+
+describeLive("OpenCode Go live catalog drift", () => {
+ it("classifies every live id as active, deprecated, or preview", async () => {
+ const response = await fetch(OPENCODE_GO_MODELS_URL, {
+ headers: {
+ accept: "application/json",
+ authorization: `Bearer ${OPENCODE_API_KEY}`,
+ "accept-encoding": "identity",
+ },
+ });
+ expect(response.ok).toBe(true);
+ const body = (await response.json()) as ModelsResponse;
+ const liveIds = (body.data ?? [])
+ .filter((row) => row.object === undefined || row.object === "model")
+ .map((row) => row.id)
+ .filter((id): id is string => typeof id === "string" && id.trim().length > 0)
+ .map((id) => id.trim().toLowerCase())
+ .toSorted();
+ const trustedRows = listOpencodeGoModelCatalogEntries();
+ const trustedIds = new Set(trustedRows.map((row) => row.id));
+ const activeIds = buildStaticOpencodeGoProviderConfig().models.map((model) => model.id);
+
+ expect(liveIds.filter((id) => !trustedIds.has(id))).toEqual([]);
+ expect(new Set(activeIds).size).toBe(activeIds.length);
+ expect(activeIds.toSorted()).toEqual([
+ "deepseek-v4-flash",
+ "deepseek-v4-pro",
+ "glm-5.1",
+ "glm-5.2",
+ "gpt-5.6-luna",
+ "grok-4.5",
+ "hy3",
+ "kimi-k2.6",
+ "kimi-k2.7-code",
+ "kimi-k3",
+ "mimo-v2.5",
+ "mimo-v2.5-pro",
+ "minimax-m2.7",
+ "minimax-m3",
+ "qwen3.6-plus",
+ "qwen3.7-max",
+ "qwen3.7-plus",
+ "qwen3.8-max",
+ ]);
+ expect(
+ trustedRows
+ .filter((row) => row.status === "deprecated")
+ .map((row) => row.id)
+ .toSorted(),
+ ).toEqual([
+ "glm-5",
+ "kimi-k2.5",
+ "mimo-v2-omni",
+ "mimo-v2-pro",
+ "minimax-m2.5",
+ "qwen3.5-plus",
+ ]);
+ expect(trustedRows.find((row) => row.id === "hy3-preview")?.status).toBe("preview");
+ }, 30_000);
+});
diff --git a/extensions/opencode-go/provider-catalog.ts b/extensions/opencode-go/provider-catalog.ts
index c705d664ea22..cecb2f4604c9 100644
--- a/extensions/opencode-go/provider-catalog.ts
+++ b/extensions/opencode-go/provider-catalog.ts
@@ -15,6 +15,21 @@ const PROVIDER_ID = "opencode-go";
const OPENCODE_GO_OPENAI_BASE_URL = "https://opencode.ai/zen/go/v1";
const OPENCODE_GO_ANTHROPIC_BASE_URL = "https://opencode.ai/zen/go";
+const OPENAI_COMPLETIONS_MODEL = {
+ api: "openai-completions",
+ provider: PROVIDER_ID,
+ baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+} as const;
+const ANTHROPIC_MESSAGES_MODEL = {
+ api: "anthropic-messages",
+ provider: PROVIDER_ID,
+ baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+} as const;
+const OPENAI_RESPONSES_MODEL = {
+ api: "openai-responses",
+ provider: PROVIDER_ID,
+ baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+} as const;
const OPENCODE_GO_KIMI_NO_REASONING_MODEL_IDS = new Set([
"kimi-k2.5",
"kimi-k2.6",
@@ -23,17 +38,6 @@ const OPENCODE_GO_KIMI_NO_REASONING_MODEL_IDS = new Set([
const OPENCODE_GO_MODELS_ENDPOINT = "https://opencode.ai/zen/go/v1/models";
const OPENCODE_GO_MODELS_TIMEOUT_MS = 5_000;
const OPENCODE_GO_MODELS_CACHE_TTL_MS = 60_000;
-// OpenCode Go exposes only high/max provider effort for DeepSeek V4. Lower
-// OpenClaw levels retain their existing high-effort behavior.
-const OPENCODE_GO_DEEPSEEK_V4_THINKING_LEVEL_MAP = {
- minimal: "high",
- low: "high",
- medium: "high",
- high: "high",
- xhigh: "max",
- max: "max",
-} as const;
-
type OpencodeGoModelDefinition = ModelDefinitionConfig & {
provider: typeof PROVIDER_ID;
api: NonNullable;
@@ -41,21 +45,18 @@ type OpencodeGoModelDefinition = ModelDefinitionConfig & {
input: Array<"text" | "image">;
};
-const OPENCODE_GO_MODELS = (
+const OPENCODE_GO_RESOLVABLE_MODELS = (
[
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
- thinkingLevelMap: OPENCODE_GO_DEEPSEEK_V4_THINKING_LEVEL_MAP,
input: ["text"],
cost: {
- input: 1.74,
- output: 3.48,
- cacheRead: 0.145,
+ input: 0.435,
+ output: 0.87,
+ cacheRead: 0.003625,
cacheWrite: 0,
},
contextWindow: 1_000_000,
@@ -63,22 +64,20 @@ const OPENCODE_GO_MODELS = (
compat: {
supportsUsageInStreaming: true,
supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["high", "max"],
maxTokensField: "max_tokens",
},
},
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
- thinkingLevelMap: OPENCODE_GO_DEEPSEEK_V4_THINKING_LEVEL_MAP,
input: ["text"],
cost: {
input: 0.14,
output: 0.28,
- cacheRead: 0.028,
+ cacheRead: 0.0028,
cacheWrite: 0,
},
contextWindow: 1_000_000,
@@ -86,15 +85,14 @@ const OPENCODE_GO_MODELS = (
compat: {
supportsUsageInStreaming: true,
supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["low", "high", "max"],
maxTokensField: "max_tokens",
},
},
{
id: "glm-5",
name: "GLM-5",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text"],
cost: {
@@ -109,9 +107,7 @@ const OPENCODE_GO_MODELS = (
{
id: "glm-5.1",
name: "GLM-5.1",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text"],
cost: {
@@ -126,9 +122,7 @@ const OPENCODE_GO_MODELS = (
{
id: "glm-5.2",
name: "GLM-5.2",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text"],
cost: {
@@ -139,13 +133,75 @@ const OPENCODE_GO_MODELS = (
},
contextWindow: 1_000_000,
maxTokens: 131_072,
+ compat: {
+ supportsUsageInStreaming: true,
+ supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["high", "max"],
+ maxTokensField: "max_tokens",
+ },
+ },
+ {
+ id: "gpt-5.6-luna",
+ name: "GPT-5.6 Luna",
+ ...OPENAI_RESPONSES_MODEL,
+ reasoning: true,
+ input: ["text", "image"],
+ cost: {
+ input: 0.2,
+ output: 1.2,
+ cacheRead: 0.02,
+ cacheWrite: 0.25,
+ tieredPricing: [
+ { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25, range: [0, 272_000] },
+ { input: 0.4, output: 1.8, cacheRead: 0.04, cacheWrite: 0.5, range: [272_000] },
+ ],
+ },
+ contextWindow: 1_050_000,
+ contextTokens: 922_000,
+ maxTokens: 128_000,
+ compat: {
+ supportsUsageInStreaming: true,
+ supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
+ maxTokensField: "max_tokens",
+ },
+ },
+ {
+ id: "grok-4.5",
+ name: "Grok 4.5",
+ ...OPENAI_COMPLETIONS_MODEL,
+ reasoning: true,
+ input: ["text", "image"],
+ cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
+ contextWindow: 500_000,
+ maxTokens: 500_000,
+ compat: {
+ supportsUsageInStreaming: true,
+ supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["low", "medium", "high"],
+ maxTokensField: "max_tokens",
+ },
+ },
+ {
+ id: "hy3",
+ name: "Hy3",
+ ...OPENAI_COMPLETIONS_MODEL,
+ reasoning: true,
+ input: ["text"],
+ cost: { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
+ contextWindow: 256_000,
+ maxTokens: 64_000,
+ compat: {
+ supportsUsageInStreaming: true,
+ supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["none", "low", "high"],
+ maxTokensField: "max_tokens",
+ },
},
{
id: "hy3-preview",
name: "HY3 Preview",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text"],
cost: {
@@ -160,9 +216,7 @@ const OPENCODE_GO_MODELS = (
{
id: "kimi-k2.5",
name: "Kimi K2.5",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text", "image"],
cost: {
@@ -177,9 +231,7 @@ const OPENCODE_GO_MODELS = (
{
id: "kimi-k2.6",
name: "Kimi K2.6",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text", "image"],
cost: {
@@ -194,9 +246,7 @@ const OPENCODE_GO_MODELS = (
{
id: "kimi-k2.7-code",
name: "Kimi K2.7 Code",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text", "image"],
cost: {
@@ -208,18 +258,61 @@ const OPENCODE_GO_MODELS = (
contextWindow: 262_144,
maxTokens: 262_144,
},
+ {
+ id: "kimi-k3",
+ name: "Kimi K3",
+ ...OPENAI_COMPLETIONS_MODEL,
+ reasoning: true,
+ input: ["text", "image"],
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
+ contextWindow: 1_048_576,
+ maxTokens: 131_072,
+ compat: {
+ supportsUsageInStreaming: true,
+ supportsReasoningEffort: true,
+ supportedReasoningEfforts: ["max"],
+ maxTokensField: "max_tokens",
+ },
+ },
+ {
+ id: "mimo-v2-omni",
+ name: "MiMo V2 Omni",
+ ...OPENAI_COMPLETIONS_MODEL,
+ reasoning: true,
+ input: ["text", "image"],
+ cost: { input: 0.4, output: 2, cacheRead: 0.08, cacheWrite: 0 },
+ contextWindow: 262_144,
+ maxTokens: 128_000,
+ },
+ {
+ id: "mimo-v2-pro",
+ name: "MiMo V2 Pro",
+ ...OPENAI_COMPLETIONS_MODEL,
+ reasoning: true,
+ input: ["text"],
+ cost: {
+ input: 1,
+ output: 3,
+ cacheRead: 0.2,
+ cacheWrite: 0,
+ tieredPricing: [
+ { input: 1, output: 3, cacheRead: 0.2, cacheWrite: 0, range: [0, 256_000] },
+ { input: 2, output: 6, cacheRead: 0.4, cacheWrite: 0, range: [256_000] },
+ ],
+ },
+ contextWindow: 1_048_576,
+ maxTokens: 128_000,
+ },
{
id: "mimo-v2.5",
name: "MiMo V2.5",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text", "image"],
cost: {
- input: 0.4,
- output: 2,
- cacheRead: 0.08,
+ input: 0.14,
+ output: 0.28,
+ cacheRead: 0.0028,
cacheWrite: 0,
},
contextWindow: 1_000_000,
@@ -228,15 +321,13 @@ const OPENCODE_GO_MODELS = (
{
id: "mimo-v2.5-pro",
name: "MiMo V2.5 Pro",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...OPENAI_COMPLETIONS_MODEL,
reasoning: true,
input: ["text"],
cost: {
- input: 1,
- output: 3,
- cacheRead: 0.2,
+ input: 0.435,
+ output: 0.87,
+ cacheRead: 0.003625,
cacheWrite: 0,
},
contextWindow: 1_048_576,
@@ -245,15 +336,13 @@ const OPENCODE_GO_MODELS = (
{
id: "minimax-m2.5",
name: "MiniMax M2.5",
- api: "anthropic-messages",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
reasoning: true,
input: ["text"],
cost: {
input: 0.3,
output: 1.2,
- cacheRead: 0.03,
+ cacheRead: 0.06,
cacheWrite: 0.375,
},
contextWindow: 204_800,
@@ -262,9 +351,7 @@ const OPENCODE_GO_MODELS = (
{
id: "minimax-m2.7",
name: "MiniMax M2.7",
- api: "anthropic-messages",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
reasoning: true,
input: ["text"],
cost: {
@@ -279,26 +366,26 @@ const OPENCODE_GO_MODELS = (
{
id: "minimax-m3",
name: "MiniMax M3",
- api: "anthropic-messages",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
reasoning: true,
- input: ["text"],
+ input: ["text", "image"],
cost: {
- input: 0.6,
- output: 2.4,
- cacheRead: 0.12,
- cacheWrite: 0.75,
+ input: 0.3,
+ output: 1.2,
+ cacheRead: 0.06,
+ cacheWrite: 0,
+ tieredPricing: [
+ { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0, range: [0, 512_000] },
+ { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0, range: [512_000] },
+ ],
},
- contextWindow: 204_800,
+ contextWindow: 1_000_000,
maxTokens: 131_072,
},
{
id: "qwen3.5-plus",
name: "Qwen3.5 Plus",
- api: "openai-completions",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
compat: { thinkingFormat: "qwen" },
reasoning: true,
input: ["text", "image"],
@@ -314,9 +401,7 @@ const OPENCODE_GO_MODELS = (
{
id: "qwen3.7-max",
name: "Qwen3.7 Max",
- api: "anthropic-messages",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
compat: { thinkingFormat: "qwen" },
reasoning: true,
input: ["text"],
@@ -332,9 +417,7 @@ const OPENCODE_GO_MODELS = (
{
id: "qwen3.7-plus",
name: "Qwen3.7 Plus",
- api: "anthropic-messages",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
compat: { thinkingFormat: "qwen" },
reasoning: true,
input: ["text", "image"],
@@ -343,16 +426,34 @@ const OPENCODE_GO_MODELS = (
output: 1.6,
cacheRead: 0.04,
cacheWrite: 0.5,
+ tieredPricing: [
+ { input: 0.4, output: 1.6, cacheRead: 0.04, cacheWrite: 0.5, range: [0, 256_000] },
+ { input: 1.2, output: 4.8, cacheRead: 0.12, cacheWrite: 1.5, range: [256_000] },
+ ],
},
contextWindow: 1_000_000,
maxTokens: 65_536,
},
+ {
+ id: "qwen3.8-max",
+ name: "Qwen3.8 Max",
+ ...ANTHROPIC_MESSAGES_MODEL,
+ compat: { thinkingFormat: "qwen" },
+ reasoning: true,
+ input: ["text", "image"],
+ cost: {
+ input: 2,
+ output: 6,
+ cacheRead: 0.25,
+ cacheWrite: 2.5,
+ },
+ contextWindow: 1_000_000,
+ maxTokens: 131_072,
+ },
{
id: "qwen3.6-plus",
name: "Qwen3.6 Plus",
- api: "anthropic-messages",
- provider: PROVIDER_ID,
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
+ ...ANTHROPIC_MESSAGES_MODEL,
compat: { thinkingFormat: "qwen" },
reasoning: true,
input: ["text", "image"],
@@ -361,13 +462,34 @@ const OPENCODE_GO_MODELS = (
output: 3,
cacheRead: 0.05,
cacheWrite: 0.625,
+ tieredPricing: [
+ { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.625, range: [0, 256_000] },
+ { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 2.5, range: [256_000] },
+ ],
},
- contextWindow: 262_144,
+ contextWindow: 1_000_000,
maxTokens: 65_536,
},
] satisfies OpencodeGoModelDefinition[]
).map((model) => normalizeModelCompat(model) as OpencodeGoModelDefinition);
+const OPENCODE_GO_MODEL_STATUS = new Map([
+ ["glm-5", "deprecated"],
+ ["qwen3.5-plus", "deprecated"],
+ ["mimo-v2-omni", "deprecated"],
+ ["kimi-k2.5", "deprecated"],
+ ["mimo-v2-pro", "deprecated"],
+ ["minimax-m2.5", "deprecated"],
+ ["hy3-preview", "preview"],
+]);
+
+const OPENCODE_GO_MODEL_BY_ID = new Map(
+ OPENCODE_GO_RESOLVABLE_MODELS.map((model) => [model.id, model]),
+);
+const OPENCODE_GO_MODELS = OPENCODE_GO_RESOLVABLE_MODELS.filter(
+ (model) => !OPENCODE_GO_MODEL_STATUS.has(model.id),
+);
+
type FetchOpencodeGoLiveModelIdsParams = {
apiKey?: string;
discoveryApiKey?: string;
@@ -375,22 +497,15 @@ type FetchOpencodeGoLiveModelIdsParams = {
signal?: AbortSignal;
};
-function buildOpencodeGoProviderConfig(
- models: OpencodeGoModelDefinition[],
- apiKey?: string,
-): ModelProviderConfig {
+export function buildStaticOpencodeGoProviderConfig(apiKey?: string): ModelProviderConfig {
return {
api: "openai-completions",
baseUrl: OPENCODE_GO_OPENAI_BASE_URL,
...(apiKey ? { apiKey } : {}),
- models,
+ models: OPENCODE_GO_MODELS,
};
}
-export function buildStaticOpencodeGoProviderConfig(apiKey?: string): ModelProviderConfig {
- return buildOpencodeGoProviderConfig(OPENCODE_GO_MODELS, apiKey);
-}
-
export async function buildOpencodeGoLiveProviderConfig(
params: FetchOpencodeGoLiveModelIdsParams = {},
): Promise {
@@ -413,19 +528,30 @@ export async function buildOpencodeGoLiveProviderConfig(
}
export function listOpencodeGoModelCatalogEntries(): ModelCatalogEntry[] {
- return OPENCODE_GO_MODELS.map((model) => ({
- provider: model.provider,
- id: model.id,
- name: model.name,
- reasoning: model.reasoning,
- input: model.input,
- contextWindow: model.contextWindow,
- }));
+ return OPENCODE_GO_RESOLVABLE_MODELS.map((model) => {
+ const entry: ModelCatalogEntry = {
+ provider: model.provider,
+ id: model.id,
+ name: model.name,
+ api: model.api,
+ baseUrl: model.baseUrl,
+ reasoning: model.reasoning,
+ input: model.input,
+ contextWindow: model.contextWindow,
+ contextTokens: model.contextTokens,
+ compat: model.compat,
+ };
+ const status = OPENCODE_GO_MODEL_STATUS.get(model.id);
+ if (status) {
+ entry.status = status;
+ }
+ return entry;
+ });
}
export function resolveOpencodeGoModel(modelId: string): ProviderRuntimeModel | undefined {
const normalizedModelId = modelId.trim().toLowerCase();
- return OPENCODE_GO_MODELS.find((model) => model.id === normalizedModelId);
+ return OPENCODE_GO_MODEL_BY_ID.get(normalizedModelId);
}
export function isOpencodeGoKimiNoReasoningModelId(modelId: unknown): boolean {
diff --git a/extensions/opencode-go/provider-policy-api.ts b/extensions/opencode-go/provider-policy-api.ts
index 7306f704854e..fff6f01d4498 100644
--- a/extensions/opencode-go/provider-policy-api.ts
+++ b/extensions/opencode-go/provider-policy-api.ts
@@ -4,40 +4,105 @@ import type {
ProviderThinkingProfile,
} from "openclaw/plugin-sdk/plugin-entry";
-const OPENCODE_GO_DEEPSEEK_V4_MODEL_IDS = new Set(["deepseek-v4-flash", "deepseek-v4-pro"]);
-const OPENCODE_GO_DEEPSEEK_V4_THINKING_LEVEL_IDS = [
- "off",
- "minimal",
- "low",
- "medium",
- "high",
- "xhigh",
- "max",
-] as const;
-const OPENCODE_GO_DEEPSEEK_V4_THINKING_PROFILE = {
- levels: OPENCODE_GO_DEEPSEEK_V4_THINKING_LEVEL_IDS.map((id) => ({ id })),
+const KIMI_K2_THINKING_PROFILE = {
+ levels: [{ id: "off" }],
+ defaultLevel: "off",
+} as const satisfies ProviderThinkingProfile;
+const BINARY_REASONING_PROFILE = {
+ levels: [{ id: "off" }, { id: "high", label: "on" }],
defaultLevel: "high",
-} satisfies ProviderThinkingProfile;
+} as const satisfies ProviderThinkingProfile;
+const FIXED_REASONING_PROFILE = {
+ levels: [{ id: "off", label: "always on" }],
+ defaultLevel: "off",
+} as const satisfies ProviderThinkingProfile;
+const FIXED_ANTHROPIC_REASONING_PROFILE = {
+ levels: [{ id: "high", label: "always on" }],
+ defaultLevel: "high",
+} as const satisfies ProviderThinkingProfile;
+const KIMI_K2_MODEL_IDS = new Set(["kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code"]);
+const FIXED_ANTHROPIC_REASONING_MODEL_IDS = new Set(["minimax-m2.5", "minimax-m2.7"]);
+const BINARY_REASONING_MODEL_IDS = new Set(["minimax-m3"]);
+const THINKING_LEVEL_IDS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
+
+function resolveEffortThinkingProfile(
+ efforts: readonly string[] | null | undefined,
+): ProviderThinkingProfile | undefined {
+ if (!efforts || efforts.length === 0) {
+ return undefined;
+ }
+ const acceptedLevelIds = ["off", ...efforts.map((effort) => (effort === "none" ? "off" : effort))]
+ .filter((id) => THINKING_LEVEL_IDS.has(id))
+ .filter((id, index, values) => values.indexOf(id) === index) as Array<
+ "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
+ >;
+ const levels = acceptedLevelIds.map((id) => ({ id }));
+ const levelIdSet = new Set(acceptedLevelIds);
+ const defaultLevel = levelIdSet.has("medium")
+ ? "medium"
+ : levelIdSet.has("high")
+ ? "high"
+ : levelIdSet.has("low")
+ ? "low"
+ : "off";
+ return { levels, defaultLevel };
+}
export function isOpencodeGoDeepSeekV4ModelId(modelId: unknown): boolean {
+ const normalized = typeof modelId === "string" ? modelId.trim().toLowerCase() : "";
+ return normalized === "deepseek-v4-flash" || normalized === "deepseek-v4-pro";
+}
+
+export function isOpencodeGoFixedAnthropicReasoningModelId(modelId: unknown): boolean {
return (
typeof modelId === "string" &&
- OPENCODE_GO_DEEPSEEK_V4_MODEL_IDS.has(modelId.trim().toLowerCase())
+ FIXED_ANTHROPIC_REASONING_MODEL_IDS.has(modelId.trim().toLowerCase())
);
}
export function resolveOpencodeGoThinkingProfile(
modelId: string,
+ context?: Pick,
): ProviderThinkingProfile | undefined {
- return isOpencodeGoDeepSeekV4ModelId(modelId)
- ? OPENCODE_GO_DEEPSEEK_V4_THINKING_PROFILE
- : undefined;
+ const normalized = modelId.trim().toLowerCase();
+ if (normalized === "deepseek-v4-flash") {
+ return {
+ levels: [{ id: "off" }, { id: "low" }, { id: "high" }, { id: "max" }],
+ defaultLevel: "high",
+ };
+ }
+ if (normalized === "deepseek-v4-pro") {
+ return {
+ levels: [{ id: "off" }, { id: "high" }, { id: "max" }],
+ defaultLevel: "high",
+ };
+ }
+ if (normalized === "kimi-k3") {
+ return { levels: [{ id: "off" }, { id: "max" }], defaultLevel: "off" };
+ }
+ if (KIMI_K2_MODEL_IDS.has(normalized)) {
+ return KIMI_K2_THINKING_PROFILE;
+ }
+ const effortProfile = resolveEffortThinkingProfile(context?.compat?.supportedReasoningEfforts);
+ if (effortProfile) {
+ return effortProfile;
+ }
+ if (BINARY_REASONING_MODEL_IDS.has(normalized)) {
+ return BINARY_REASONING_PROFILE;
+ }
+ if (FIXED_ANTHROPIC_REASONING_MODEL_IDS.has(normalized)) {
+ return FIXED_ANTHROPIC_REASONING_PROFILE;
+ }
+ if (context?.reasoning === true && context.api === "openai-completions") {
+ return FIXED_REASONING_PROFILE;
+ }
+ return undefined;
}
export function resolveThinkingProfile(
context: ProviderDefaultThinkingPolicyContext,
): ProviderThinkingProfile | undefined {
return context.provider.trim().toLowerCase() === "opencode-go"
- ? resolveOpencodeGoThinkingProfile(context.modelId)
+ ? resolveOpencodeGoThinkingProfile(context.modelId, context)
: undefined;
}
diff --git a/extensions/opencode-go/stream-termination.test.ts b/extensions/opencode-go/stream-termination.test.ts
index 425943f7fdac..e1af71020e03 100644
--- a/extensions/opencode-go/stream-termination.test.ts
+++ b/extensions/opencode-go/stream-termination.test.ts
@@ -130,7 +130,12 @@ async function createStreamHarness(options: StreamHarnessOptions = {}) {
});
const downstream = await Promise.resolve(
wrapper(
- options.model ?? ({ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel),
+ options.model ??
+ ({
+ api: "openai-completions",
+ provider: "opencode-go",
+ id: "deepseek-v4-flash",
+ } as ProviderModel),
{} as ProviderContext,
options.callOptions ?? ({} as ProviderCallOptions),
),
@@ -400,7 +405,13 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
});
it("aborts and releases the underlying stream when no first event arrives", async () => {
- const { downstream, getReturnCalls, capturedSignals, wasAborted } = await createStreamHarness();
+ const { downstream, getReturnCalls, capturedSignals, wasAborted } = await createStreamHarness({
+ model: asProviderModel({
+ api: "openai-responses",
+ provider: "opencode-go",
+ id: "gpt-5.6-luna",
+ }),
+ });
expect(downstream).toBeDefined();
if (!downstream) {
return;
@@ -414,11 +425,42 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
expect(capturedSignals).toHaveLength(1);
expect(wasAborted()).toBe(true);
expect(getReturnCalls()).toBe(1);
- expect(received.some((event) => event.type === "error" && event.reason === "error")).toBe(true);
+ const error = received.find((event): event is ErrorEvent => event.type === "error");
+ expect(error?.error).toMatchObject({
+ api: "openai-responses",
+ provider: "opencode-go",
+ model: "gpt-5.6-luna",
+ });
await consumer;
});
+ it("preserves Anthropic model identity when a stream ends before its first event", async () => {
+ const { controller, downstream } = await createStreamHarness({
+ model: asProviderModel({
+ api: "anthropic-messages",
+ provider: "opencode-go",
+ id: "qwen3.8-max",
+ }),
+ });
+ expect(downstream).toBeDefined();
+ if (!downstream) {
+ return;
+ }
+
+ const received: AnyEvent[] = [];
+ const consumer = consumeStream(downstream, received);
+ controller.end();
+ await consumer;
+
+ const error = received.find((event): event is ErrorEvent => event.type === "error");
+ expect(error?.error).toMatchObject({
+ api: "anthropic-messages",
+ provider: "opencode-go",
+ model: "qwen3.8-max",
+ });
+ });
+
it("aborts stream creation when the upstream stream promise never resolves", async () => {
const { downstream, wasAborted } = await createStreamHarness({
source: new Promise(() => {
diff --git a/extensions/opencode-go/stream-termination.ts b/extensions/opencode-go/stream-termination.ts
index 80667a587ecf..76af4d49cff1 100644
--- a/extensions/opencode-go/stream-termination.ts
+++ b/extensions/opencode-go/stream-termination.ts
@@ -66,7 +66,10 @@ function isProviderProgressEvent(event: AssistantMessageEvent): boolean {
const STALLED_STREAM_ERROR_MESSAGE =
"opencode-go stream timed out after provider-owned SSE boundary stalled";
-function buildStalledErrorEvent(partial: AssistantMessage | undefined): AssistantMessageEvent {
+function buildStalledErrorEvent(
+ partial: AssistantMessage | undefined,
+ model: Parameters[0],
+): AssistantMessageEvent {
if (partial) {
return {
type: "error",
@@ -81,11 +84,14 @@ function buildStalledErrorEvent(partial: AssistantMessage | undefined): Assistan
return {
type: "error",
reason: "error",
- error: synthesizeMinimalAssistantMessage(STALLED_STREAM_ERROR_MESSAGE, "error"),
+ error: synthesizeMinimalAssistantMessage(STALLED_STREAM_ERROR_MESSAGE, "error", model),
};
}
-function buildUnterminatedErrorEvent(partial: AssistantMessage | undefined): AssistantMessageEvent {
+function buildUnterminatedErrorEvent(
+ partial: AssistantMessage | undefined,
+ model: Parameters[0],
+): AssistantMessageEvent {
if (partial) {
return {
type: "error",
@@ -103,6 +109,7 @@ function buildUnterminatedErrorEvent(partial: AssistantMessage | undefined): Ass
error: synthesizeMinimalAssistantMessage(
"opencode-go stream ended without a terminal event",
"error",
+ model,
),
};
}
@@ -110,6 +117,7 @@ function buildUnterminatedErrorEvent(partial: AssistantMessage | undefined): Ass
function buildCaughtErrorEvent(
partial: AssistantMessage | undefined,
error: unknown,
+ model: Parameters[0],
): AssistantMessageEvent {
const message = error instanceof Error ? error.message : String(error);
if (partial) {
@@ -126,20 +134,21 @@ function buildCaughtErrorEvent(
return {
type: "error",
reason: "error",
- error: synthesizeMinimalAssistantMessage(message, "error"),
+ error: synthesizeMinimalAssistantMessage(message, "error", model),
};
}
function synthesizeMinimalAssistantMessage(
errorMessage: string,
stopReason: AssistantMessage["stopReason"],
+ model: Parameters[0],
): AssistantMessage {
return {
role: "assistant",
content: [],
- api: "openai-completions",
- provider: "opencode-go",
- model: "",
+ api: model.api,
+ provider: model.provider,
+ model: model.id,
usage: {
input: 0,
output: 0,
@@ -251,7 +260,7 @@ export function createOpencodeGoStalledStreamWrapper(
clearIdleTimer();
controller.abort(new Error("opencode-go stream stalled"));
releaseBaseStream();
- output.push(buildStalledErrorEvent(lastSeenPartial));
+ output.push(buildStalledErrorEvent(lastSeenPartial, model));
output.end();
};
@@ -306,7 +315,7 @@ export function createOpencodeGoStalledStreamWrapper(
return;
}
if (result.done) {
- finishWith(buildUnterminatedErrorEvent(lastSeenPartial));
+ finishWith(buildUnterminatedErrorEvent(lastSeenPartial, model));
return;
}
const event = result.value;
@@ -323,7 +332,7 @@ export function createOpencodeGoStalledStreamWrapper(
}
} catch (error) {
if (!settled) {
- finishWith(buildCaughtErrorEvent(lastSeenPartial, error));
+ finishWith(buildCaughtErrorEvent(lastSeenPartial, error, model));
}
} finally {
cleanup();
diff --git a/extensions/opencode-go/stream.ts b/extensions/opencode-go/stream.ts
index 12fdb1dfceae..19033fcbbfc6 100644
--- a/extensions/opencode-go/stream.ts
+++ b/extensions/opencode-go/stream.ts
@@ -2,10 +2,11 @@
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import {
createDeepSeekV4OpenAICompatibleThinkingWrapper,
+ createOpenAICompatibleCompletionsThinkingOffWrapper,
createPayloadPatchStreamWrapper,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { isOpencodeGoKimiNoReasoningModelId } from "./provider-catalog.js";
-import { isOpencodeGoDeepSeekV4ModelId } from "./provider-policy-api.js";
+import { isOpencodeGoFixedAnthropicReasoningModelId } from "./provider-policy-api.js";
import { stripOpencodeGoKimiReasoningPayload } from "./reasoning-sanitizer.js";
import {
createOpencodeGoStalledStreamWrapper,
@@ -17,16 +18,18 @@ function createOpencodeGoDeepSeekV4Wrapper(
baseStreamFn: ProviderWrapStreamFnContext["streamFn"],
thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"],
): ProviderWrapStreamFnContext["streamFn"] {
- return createDeepSeekV4OpenAICompatibleThinkingWrapper({
+ const flashWrapped = createDeepSeekV4OpenAICompatibleThinkingWrapper({
baseStreamFn,
thinkingLevel,
shouldPatchModel: (model) =>
- model.provider === "opencode-go" && isOpencodeGoDeepSeekV4ModelId(model.id),
+ model.provider === "opencode-go" && model.id === "deepseek-v4-flash",
+ resolveReasoningEffort: (level) => (level === "low" ? "low" : level === "max" ? "max" : "high"),
+ });
+ return createDeepSeekV4OpenAICompatibleThinkingWrapper({
+ baseStreamFn: flashWrapped,
+ thinkingLevel,
+ shouldPatchModel: (model) => model.provider === "opencode-go" && model.id === "deepseek-v4-pro",
});
-}
-
-function stripReasoningParams(payloadObj: Record): void {
- stripOpencodeGoKimiReasoningPayload(payloadObj);
}
function createOpencodeGoKimiNoReasoningWrapper(
@@ -37,7 +40,7 @@ function createOpencodeGoKimiNoReasoningWrapper(
}
return createPayloadPatchStreamWrapper(
baseStreamFn,
- ({ payload }) => stripReasoningParams(payload),
+ ({ payload }) => stripOpencodeGoKimiReasoningPayload(payload),
{
shouldPatch: ({ model }) =>
model.provider === "opencode-go" && isOpencodeGoKimiNoReasoningModelId(model.id),
@@ -45,6 +48,42 @@ function createOpencodeGoKimiNoReasoningWrapper(
);
}
+function createOpencodeGoFixedAnthropicReasoningWrapper(
+ baseStreamFn: ProviderWrapStreamFnContext["streamFn"],
+): ProviderWrapStreamFnContext["streamFn"] {
+ if (!baseStreamFn) {
+ return undefined;
+ }
+ return createPayloadPatchStreamWrapper(
+ baseStreamFn,
+ ({ payload }) => {
+ delete payload.thinking;
+ delete payload.output_config;
+ },
+ {
+ shouldPatch: ({ model }) =>
+ model.provider === "opencode-go" && isOpencodeGoFixedAnthropicReasoningModelId(model.id),
+ },
+ );
+}
+
+function createOpencodeGoKimiK3ThinkingOffWrapper(
+ baseStreamFn: ProviderWrapStreamFnContext["streamFn"],
+ thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"],
+): ProviderWrapStreamFnContext["streamFn"] {
+ if (!baseStreamFn) {
+ return undefined;
+ }
+ const thinkingOff = createOpenAICompatibleCompletionsThinkingOffWrapper(
+ baseStreamFn,
+ thinkingLevel,
+ );
+ return (model, context, options) =>
+ model.provider === "opencode-go" && model.id === "kimi-k3"
+ ? thinkingOff(model, context, options)
+ : baseStreamFn(model, context, options);
+}
+
export function createOpencodeGoWrapper(
baseStreamFn: ProviderWrapStreamFnContext["streamFn"],
thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"],
@@ -53,8 +92,13 @@ export function createOpencodeGoWrapper(
return undefined;
}
const kimiWrapped = createOpencodeGoKimiNoReasoningWrapper(baseStreamFn) ?? baseStreamFn;
+ const kimiK3Wrapped =
+ createOpencodeGoKimiK3ThinkingOffWrapper(kimiWrapped, thinkingLevel) ?? kimiWrapped;
+ const fixedAnthropicWrapped =
+ createOpencodeGoFixedAnthropicReasoningWrapper(kimiK3Wrapped) ?? kimiK3Wrapped;
const deepSeekWrapped =
- createOpencodeGoDeepSeekV4Wrapper(kimiWrapped, thinkingLevel) ?? kimiWrapped;
+ createOpencodeGoDeepSeekV4Wrapper(fixedAnthropicWrapped, thinkingLevel) ??
+ fixedAnthropicWrapped;
// Outermost layer: provider-owned stalled SSE termination so the underlying
// OpenAI SDK request is aborted at the raw opencode-go boundary instead of
// waiting for the shared runtime stuck-session recovery.
diff --git a/extensions/opencode/index.test.ts b/extensions/opencode/index.test.ts
index d8bb73f09ef2..3d8695e8282e 100644
--- a/extensions/opencode/index.test.ts
+++ b/extensions/opencode/index.test.ts
@@ -36,6 +36,88 @@ function requireCatalogEntry(entries: readonly unknown[] | null | undefined, id:
return requireRecord(entry, `supplemental catalog entry ${id}`);
}
+function runtimeCompatFields(value: unknown): Record | undefined {
+ if (!value) {
+ return undefined;
+ }
+ const { codeMode: _codeMode, ...compat } = requireRecord(value, "model compat");
+ return compat;
+}
+
+const ACTIVE_MODEL_IDS = [
+ "claude-fable-5",
+ "claude-opus-5",
+ "claude-opus-4-7",
+ "claude-opus-4-6",
+ "claude-opus-4-5",
+ "claude-sonnet-5",
+ "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
+ "claude-haiku-4-5",
+ "gemini-3.6-flash",
+ "gemini-3.5-flash-lite",
+ "gemini-3.5-flash",
+ "gemini-3.1-pro",
+ "gemini-3-flash",
+ "gpt-5.6-sol",
+ "gpt-5.6-terra",
+ "gpt-5.6-luna",
+ "gpt-5.5-pro",
+ "gpt-5.4",
+ "gpt-5.4-pro",
+ "gpt-5.4-mini",
+ "gpt-5.4-nano",
+ "gpt-5.3-codex-spark",
+ "gpt-5.3-codex",
+ "gpt-5.2",
+ "gpt-5.1",
+ "gpt-5",
+ "gpt-5-nano",
+ "grok-build-0.1",
+ "grok-4.5",
+ "deepseek-v4-pro",
+ "deepseek-v4-flash",
+ "glm-5.2",
+ "glm-5.1",
+ "minimax-m3",
+ "kimi-k3",
+ "kimi-k2.7-code",
+ "kimi-k2.6",
+ "qwen3.6-plus",
+ "qwen3.5-plus",
+ "big-pickle",
+ "deepseek-v4-flash-free",
+ "mimo-v2.5-free",
+ "ling-3.0-tiny-free",
+ "nemotron-3-ultra-free",
+ "north-mini-code-free",
+ "laguna-s-2.1-free",
+ "longcat-2.0-free",
+] as const;
+
+const DEPRECATED_MODEL_IDS = [
+ "claude-opus-4-1",
+ "claude-opus-4-8",
+ "claude-sonnet-4",
+ "glm-5",
+ "gpt-5-codex",
+ "gpt-5.1-codex",
+ "gpt-5.1-codex-max",
+ "gpt-5.1-codex-mini",
+ "gpt-5.2-codex",
+ "gpt-5.5",
+ "kimi-k2.5",
+ "ling-3.0-flash-free",
+ "minimax-m2.5",
+ "minimax-m2.7",
+] as const;
+
+const REPLACED_BY = new Map([
+ ["claude-opus-4-8", "claude-opus-5"],
+ ["gpt-5.5", "gpt-5.6-sol"],
+ ["minimax-m2.7", "minimax-m3"],
+]);
+
describe("opencode provider plugin", () => {
beforeEach(() => {
clearLiveCatalogCacheForTests();
@@ -51,7 +133,7 @@ describe("opencode provider plugin", () => {
expect(provider.auth[0]?.wizard).toMatchObject({
choiceLabel: "OpenCode Zen catalog",
groupId: "opencode",
- groupHint: "Shared API key for Zen + Go catalogs",
+ groupHint: "Shared API key infrastructure for Zen + Go",
});
});
@@ -93,67 +175,8 @@ describe("opencode provider plugin", () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(provider.catalog).toBeDefined();
- const expectedModelIds = [
- "claude-fable-5",
- "claude-opus-5",
- "claude-opus-4-8",
- "claude-opus-4-7",
- "claude-opus-4-6",
- "claude-opus-4-5",
- "claude-opus-4-1",
- "claude-sonnet-5",
- "claude-sonnet-4-6",
- "claude-sonnet-4-5",
- "claude-sonnet-4",
- "claude-haiku-4-5",
- "gemini-3.6-flash",
- "gemini-3.5-flash",
- "gemini-3.5-flash-lite",
- "gemini-3.1-pro",
- "gemini-3-flash",
- "gpt-5.6-sol",
- "gpt-5.6-terra",
- "gpt-5.6-luna",
- "gpt-5.5",
- "gpt-5.5-pro",
- "gpt-5.4",
- "gpt-5.4-pro",
- "gpt-5.4-mini",
- "gpt-5.4-nano",
- "gpt-5.3-codex-spark",
- "gpt-5.3-codex",
- "gpt-5.2",
- "gpt-5.2-codex",
- "gpt-5.1",
- "gpt-5.1-codex-max",
- "gpt-5.1-codex",
- "gpt-5.1-codex-mini",
- "gpt-5",
- "gpt-5-codex",
- "gpt-5-nano",
- "grok-build-0.1",
- "grok-4.5",
- "deepseek-v4-pro",
- "deepseek-v4-flash",
- "glm-5.2",
- "glm-5.1",
- "glm-5",
- "minimax-m3",
- "minimax-m2.7",
- "minimax-m2.5",
- "kimi-k2.7-code",
- "kimi-k2.6",
- "kimi-k2.5",
- "qwen3.6-plus",
- "qwen3.5-plus",
- "big-pickle",
- "deepseek-v4-flash-free",
- "mimo-v2.5-free",
- "laguna-s-2.1-free",
- "ling-3.0-flash-free",
- "nemotron-3-ultra-free",
- "north-mini-code-free",
- ];
+ const expectedModelIds = [...ACTIVE_MODEL_IDS, ...DEPRECATED_MODEL_IDS];
+ expect(new Set(expectedModelIds).size).toBe(expectedModelIds.length);
const models = new Map();
for (const modelId of expectedModelIds) {
const model = provider.resolveDynamicModel?.({ modelId } as never);
@@ -162,7 +185,7 @@ describe("opencode provider plugin", () => {
}
models.set(model.id, model);
}
- expect([...models.keys()]).toEqual(expectedModelIds);
+ expect([...models.keys()].toSorted()).toEqual(expectedModelIds.toSorted());
const supplemental = await provider.augmentModelCatalog?.({
entries: [...models.values()].map((model) => ({
@@ -171,17 +194,28 @@ describe("opencode provider plugin", () => {
name: model.name,
})),
} as never);
+ const supplementalIds = (supplemental ?? []).map((entry) => entry.id);
+ expect(new Set(supplementalIds).size).toBe(supplementalIds.length);
+ expect(supplementalIds.toSorted()).toEqual(expectedModelIds.toSorted());
const opus48 = requireCatalogEntry(supplemental, "claude-opus-4-8");
expect(opus48.provider).toBe("opencode");
expect(opus48.name).toBe("Claude Opus 4.8");
+ for (const modelId of ACTIVE_MODEL_IDS) {
+ expect(requireCatalogEntry(supplemental, modelId).status).toBeUndefined();
+ expect(requireCatalogEntry(supplemental, modelId).replacedBy).toBeUndefined();
+ }
+ for (const modelId of DEPRECATED_MODEL_IDS) {
+ expect(requireCatalogEntry(supplemental, modelId).status).toBe("deprecated");
+ expect(requireCatalogEntry(supplemental, modelId).replacedBy).toBe(REPLACED_BY.get(modelId));
+ }
const opus46 = requireMapEntry(models, "claude-opus-4-6");
expect(opus46.api).toBe("anthropic-messages");
expect(opus46.baseUrl).toBe("https://opencode.ai/zen");
expect(opus46.input).toEqual(["text", "image"]);
expect(opus46.reasoning).toBe(true);
- expect(opus46.contextWindow).toBe(200_000);
- expect(opus46.maxTokens).toBe(65_536);
+ expect(opus46.contextWindow).toBe(1_000_000);
+ expect(opus46.maxTokens).toBe(128_000);
expect(requireMapEntry(models, "gpt-5.5")).toMatchObject({
api: "openai-responses",
@@ -195,13 +229,13 @@ describe("opencode provider plugin", () => {
contextWindow: 1_050_000,
maxTokens: 128_000,
cost: {
- input: 1,
- output: 6,
- cacheRead: 0.1,
- cacheWrite: 1.25,
+ input: 0.2,
+ output: 1.2,
+ cacheRead: 0.02,
+ cacheWrite: 0.25,
tieredPricing: [
- { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25, range: [0, 272_000] },
- { input: 2, output: 9, cacheRead: 0.2, cacheWrite: 2.5, range: [272_000] },
+ { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25, range: [0, 272_000] },
+ { input: 0.4, output: 1.8, cacheRead: 0.04, cacheWrite: 0.5, range: [272_000] },
],
},
compat: {
@@ -212,7 +246,7 @@ describe("opencode provider plugin", () => {
name: "GPT-5.6 Terra",
contextWindow: 1_050_000,
maxTokens: 128_000,
- cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 },
+ cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
});
expect(requireMapEntry(models, "gpt-5.6-sol")).toMatchObject({
name: "GPT-5.6 Sol",
@@ -231,7 +265,7 @@ describe("opencode provider plugin", () => {
cost: { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
});
expect(requireMapEntry(models, "gemini-3.5-flash-lite")).toMatchObject({
- name: "Gemini 3.5 Flash-Lite",
+ name: "Gemini 3.5 Flash Lite",
contextWindow: 1_048_576,
maxTokens: 65_536,
cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 },
@@ -263,7 +297,7 @@ describe("opencode provider plugin", () => {
});
expect(requireMapEntry(models, "grok-4.5")).toMatchObject({
name: "Grok 4.5",
- api: "openai-completions",
+ api: "openai-responses",
baseUrl: "https://opencode.ai/zen/v1",
input: ["text", "image"],
contextWindow: 500_000,
@@ -271,11 +305,11 @@ describe("opencode provider plugin", () => {
cost: {
input: 2,
output: 6,
- cacheRead: 0.5,
+ cacheRead: 0.3,
cacheWrite: 0,
tieredPricing: [
- { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0, range: [0, 200_000] },
- { input: 4, output: 12, cacheRead: 1, cacheWrite: 0, range: [200_000] },
+ { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0, range: [0, 200_000] },
+ { input: 4, output: 12, cacheRead: 0.6, cacheWrite: 0, range: [200_000] },
],
},
});
@@ -288,6 +322,16 @@ describe("opencode provider plugin", () => {
maxTokens: 262_144,
cost: { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
});
+ expect(requireMapEntry(models, "kimi-k3")).toMatchObject({
+ name: "Kimi K3",
+ api: "openai-completions",
+ baseUrl: "https://opencode.ai/zen/v1",
+ reasoning: true,
+ input: ["text", "image"],
+ contextWindow: 1_048_576,
+ maxTokens: 131_072,
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
+ });
expect(requireMapEntry(models, "minimax-m3")).toMatchObject({
name: "MiniMax M3",
api: "openai-completions",
@@ -321,15 +365,19 @@ describe("opencode provider plugin", () => {
if (!Array.isArray(manifestModels)) {
throw new Error("expected manifest opencode models");
}
- expect(manifestModels.map((model) => requireRecord(model, "manifest model").id)).toEqual([
+ const manifestIds = manifestModels.map((model) => requireRecord(model, "manifest model").id);
+ expect(new Set(manifestIds).size).toBe(manifestIds.length);
+ expect(manifestIds).toEqual([
"claude-opus-5",
"claude-opus-4-8",
+ "claude-sonnet-4",
"gpt-5.6-sol",
"gpt-5.5",
"gemini-3.6-flash",
"gemini-3.1-pro",
"minimax-m3",
"minimax-m2.7",
+ "kimi-k3",
"big-pickle",
"deepseek-v4-flash-free",
"mimo-v2.5-free",
@@ -337,6 +385,8 @@ describe("opencode provider plugin", () => {
"ling-3.0-flash-free",
"nemotron-3-ultra-free",
"north-mini-code-free",
+ "ling-3.0-tiny-free",
+ "longcat-2.0-free",
]);
const manifestClaude48 = requireRecord(
manifestModels.find(
@@ -345,19 +395,19 @@ describe("opencode provider plugin", () => {
"manifest claude-opus-4-8",
);
expect(manifestClaude48).toMatchObject({
- status: "deprecated",
- replacedBy: "claude-opus-5",
contextWindow: 1_000_000,
maxTokens: 128_000,
+ status: "deprecated",
+ replacedBy: "claude-opus-5",
});
const manifestGpt55 = requireRecord(
manifestModels.find((model) => requireRecord(model, "manifest model").id === "gpt-5.5"),
"manifest gpt-5.5",
);
expect(manifestGpt55).toMatchObject({
+ contextWindow: 1_050_000,
status: "deprecated",
replacedBy: "gpt-5.6-sol",
- contextWindow: 1_050_000,
});
const manifestMiniMax = requireRecord(
manifestModels.find((model) => requireRecord(model, "manifest model").id === "minimax-m2.7"),
@@ -366,9 +416,24 @@ describe("opencode provider plugin", () => {
expect(manifestMiniMax.api).toBe("openai-completions");
expect(manifestMiniMax.baseUrl).toBe("https://opencode.ai/zen/v1");
expect(manifestMiniMax).toMatchObject({
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
status: "deprecated",
replacedBy: "minimax-m3",
- cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
+ });
+ const manifestKimiK3 = requireRecord(
+ manifestModels.find((model) => requireRecord(model, "manifest model").id === "kimi-k3"),
+ "manifest kimi-k3",
+ );
+ expect(manifestKimiK3).toMatchObject({
+ name: "Kimi K3",
+ api: "openai-completions",
+ provider: "opencode",
+ baseUrl: "https://opencode.ai/zen/v1",
+ reasoning: true,
+ input: ["text", "image"],
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
+ contextWindow: 1_048_576,
+ maxTokens: 131_072,
});
});
@@ -400,6 +465,7 @@ describe("opencode provider plugin", () => {
if (!Array.isArray(manifestModels)) {
throw new Error("expected manifest opencode models");
}
+ const supplemental = await provider.augmentModelCatalog?.({ entries: [] } as never);
for (const manifestModel of manifestModels) {
const manifestModelRecord = requireRecord(manifestModel, "manifest model");
@@ -425,36 +491,36 @@ describe("opencode provider plugin", () => {
[
"gpt-5.6-luna",
{
- input: 1,
- output: 6,
- cacheRead: 0.1,
- cacheWrite: 1.25,
+ input: 0.2,
+ output: 1.2,
+ cacheRead: 0.02,
+ cacheWrite: 0.25,
tieredPricing: [
- { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25, range: [0, 272_000] },
- { input: 2, output: 9, cacheRead: 0.2, cacheWrite: 2.5, range: [272_000] },
+ { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25, range: [0, 272_000] },
+ { input: 0.4, output: 1.8, cacheRead: 0.04, cacheWrite: 0.5, range: [272_000] },
],
},
],
[
"gpt-5.6-terra",
{
- input: 2.5,
- output: 15,
- cacheRead: 0.25,
- cacheWrite: 3.125,
+ input: 2,
+ output: 12,
+ cacheRead: 0.2,
+ cacheWrite: 2.5,
tieredPricing: [
{
- input: 2.5,
- output: 15,
- cacheRead: 0.25,
- cacheWrite: 3.125,
+ input: 2,
+ output: 12,
+ cacheRead: 0.2,
+ cacheWrite: 2.5,
range: [0, 272_000],
},
{
- input: 5,
- output: 22.5,
- cacheRead: 0.5,
- cacheWrite: 6.25,
+ input: 4,
+ output: 18,
+ cacheRead: 0.4,
+ cacheWrite: 5,
range: [272_000],
},
],
@@ -476,6 +542,7 @@ describe("opencode provider plugin", () => {
["gpt-5.4-mini", { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }],
["glm-5.2", { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 }],
["kimi-k2.7-code", { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 }],
+ ["kimi-k3", { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 }],
["laguna-s-2.1-free", { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }],
["ling-3.0-flash-free", { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }],
["minimax-m2.5", { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }],
@@ -501,7 +568,34 @@ describe("opencode provider plugin", () => {
provider.resolveDynamicModel?.({ modelId } as never),
`runtime manifest anchor ${modelId}`,
);
- expect(manifestModelRecord.cost).toEqual(runtimeModel.cost);
+ const lifecycleEntry = requireCatalogEntry(supplemental, modelId);
+ expect({
+ api: manifestModelRecord.api ?? manifestProvider.api,
+ baseUrl: manifestModelRecord.baseUrl ?? manifestProvider.baseUrl,
+ reasoning: manifestModelRecord.reasoning,
+ input: manifestModelRecord.input,
+ cost: manifestModelRecord.cost,
+ contextWindow: manifestModelRecord.contextWindow,
+ contextTokens: manifestModelRecord.contextTokens,
+ maxTokens: manifestModelRecord.maxTokens,
+ thinkingLevelMap: manifestModelRecord.thinkingLevelMap,
+ compat: runtimeCompatFields(manifestModelRecord.compat),
+ status: manifestModelRecord.status,
+ replacedBy: manifestModelRecord.replacedBy,
+ }).toEqual({
+ api: runtimeModel.api,
+ baseUrl: runtimeModel.baseUrl,
+ reasoning: runtimeModel.reasoning,
+ input: runtimeModel.input,
+ cost: runtimeModel.cost,
+ contextWindow: runtimeModel.contextWindow,
+ contextTokens: runtimeModel.contextTokens,
+ maxTokens: runtimeModel.maxTokens,
+ thinkingLevelMap: runtimeModel.thinkingLevelMap,
+ compat: runtimeCompatFields(runtimeModel.compat),
+ status: lifecycleEntry.status,
+ replacedBy: lifecycleEntry.replacedBy,
+ });
}
});
@@ -517,21 +611,9 @@ describe("opencode provider plugin", () => {
throw new Error("expected OpenCode Zen static provider");
}
- expect(result.provider.models).toHaveLength(59);
- expect(result.provider.models.map((model) => model.id)).toContain("claude-opus-5");
- expect(result.provider.models.map((model) => model.id)).toContain("claude-opus-4-8");
- expect(result.provider.models.map((model) => model.id)).toContain("claude-sonnet-5");
- expect(result.provider.models.map((model) => model.id)).toContain("glm-5.2");
- expect(result.provider.models.map((model) => model.id)).toContain("grok-4.5");
- expect(result.provider.models.map((model) => model.id)).toContain("kimi-k2.7-code");
- expect(result.provider.models.map((model) => model.id)).toContain("minimax-m2.7");
- expect(result.provider.models.map((model) => model.id)).toContain("minimax-m3");
- expect(result.provider.models.map((model) => model.id)).toContain("gpt-5.6-luna");
- expect(result.provider.models.find((model) => model.id === "minimax-m2.7")).toMatchObject({
- api: "openai-completions",
- baseUrl: "https://opencode.ai/zen/v1",
- provider: "opencode",
- });
+ const modelIds = result.provider.models.map((model) => model.id);
+ expect(new Set(modelIds).size).toBe(modelIds.length);
+ expect(modelIds).toEqual(ACTIVE_MODEL_IDS);
});
it("exposes the offline catalog fallback through the full provider registration", async () => {
@@ -541,13 +623,9 @@ describe("opencode provider plugin", () => {
throw new Error("expected registered OpenCode Zen static provider");
}
- expect(result.provider.models).toHaveLength(59);
- expect(result.provider.models.map((model) => model.id)).toContain("claude-opus-5");
- expect(result.provider.models.map((model) => model.id)).toContain("claude-sonnet-5");
- expect(result.provider.models.map((model) => model.id)).toContain("gpt-5.6-sol");
- expect(result.provider.models.map((model) => model.id)).toContain("minimax-m3");
+ expect(result.provider.models.map((model) => model.id)).toEqual(ACTIVE_MODEL_IDS);
expect(result.provider.models.find((model) => model.id === "grok-4.5")).toMatchObject({
- api: "openai-completions",
+ api: "openai-responses",
baseUrl: "https://opencode.ai/zen/v1",
provider: "opencode",
});
@@ -591,7 +669,7 @@ describe("opencode provider plugin", () => {
throw new Error("expected OpenCode Zen provider result");
}
expect(result.provider.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
- expect(result.provider.models.map((model) => model.id)).toContain("claude-opus-4-8");
+ expect(result.provider.models.map((model) => model.id)).toContain("claude-opus-4-7");
expect(fetchMock).not.toHaveBeenCalled();
} finally {
fetchMock.mockRestore();
@@ -603,7 +681,13 @@ describe("opencode provider plugin", () => {
response: new Response(
JSON.stringify({
data: [
+ { id: "kimi-k3", object: "model" },
+ { id: "claude-opus-4-7", object: "model" },
{ id: "claude-opus-4-8", object: "model" },
+ { id: "claude-sonnet-4", object: "model" },
+ { id: "gpt-5.5", object: "model" },
+ { id: "minimax-m2.7", object: "model" },
+ { id: "ling-3.0-flash-free", object: "model" },
{ id: "gpt-6-experimental", object: "model" },
],
}),
@@ -625,9 +709,15 @@ describe("opencode provider plugin", () => {
expect(fetchGuard).toHaveBeenCalledTimes(1);
expect(first.apiKey).toBe("OPENCODE_API_KEY");
- expect(first.models.map((model) => model.id)).toEqual(["claude-opus-4-8"]);
- expect(second.models.map((model) => model.id)).toEqual(["claude-opus-4-8"]);
- const claudeModel = first.models.find((model) => model.id === "claude-opus-4-8");
+ expect(first.models.map((model) => model.id)).toEqual(["kimi-k3", "claude-opus-4-7"]);
+ expect(second.models.map((model) => model.id)).toEqual(["kimi-k3", "claude-opus-4-7"]);
+ expect(first.models.find((model) => model.id === "kimi-k3")).toMatchObject({
+ api: "openai-completions",
+ baseUrl: "https://opencode.ai/zen/v1",
+ contextWindow: 1_048_576,
+ maxTokens: 131_072,
+ });
+ const claudeModel = first.models.find((model) => model.id === "claude-opus-4-7");
expect(claudeModel).toMatchObject({
api: "anthropic-messages",
baseUrl: "https://opencode.ai/zen",
@@ -651,7 +741,7 @@ describe("opencode provider plugin", () => {
discoveryApiKey: "resolved-opencode-key",
fetchGuard,
});
- expect(unknownOnly.models.map((model) => model.id)).toContain("claude-opus-4-8");
+ expect(unknownOnly.models.map((model) => model.id)).toEqual(ACTIVE_MODEL_IDS);
clearLiveCatalogCacheForTests();
fetchGuard.mockRejectedValueOnce(new Error("network unavailable"));
@@ -661,8 +751,7 @@ describe("opencode provider plugin", () => {
fetchGuard,
});
expect(fallback.apiKey).toBe("OPENCODE_API_KEY");
- expect(fallback.models.map((model) => model.id)).toContain("claude-opus-4-8");
- expect(fallback.models.map((model) => model.id)).toContain("claude-opus-4-6");
+ expect(fallback.models.map((model) => model.id)).toEqual(ACTIVE_MODEL_IDS);
});
it("keeps live OpenCode Zen discovery caches scoped to discovery credentials", async () => {
@@ -670,13 +759,13 @@ describe("opencode provider plugin", () => {
.fn()
.mockResolvedValueOnce({
response: new Response(
- JSON.stringify({ data: [{ id: "claude-opus-4-8", object: "model" }] }),
+ JSON.stringify({ data: [{ id: "claude-opus-4-7", object: "model" }] }),
),
finalUrl: "https://opencode.ai/zen/v1/models",
release: vi.fn(async () => undefined),
})
.mockResolvedValueOnce({
- response: new Response(JSON.stringify({ data: [{ id: "gpt-5.5", object: "model" }] })),
+ response: new Response(JSON.stringify({ data: [{ id: "gpt-5.6-luna", object: "model" }] })),
finalUrl: "https://opencode.ai/zen/v1/models",
release: vi.fn(async () => undefined),
});
@@ -699,11 +788,45 @@ describe("opencode provider plugin", () => {
expect(fetchGuard).toHaveBeenCalledTimes(2);
expect(first.apiKey).toBe("runtime-a");
- expect(first.models.map((model) => model.id)).toEqual(["claude-opus-4-8"]);
+ expect(first.models.map((model) => model.id)).toEqual(["claude-opus-4-7"]);
expect(second.apiKey).toBe("runtime-b");
- expect(second.models.map((model) => model.id)).toEqual(["gpt-5.5"]);
+ expect(second.models.map((model) => model.id)).toEqual(["gpt-5.6-luna"]);
expect(secondCached.apiKey).toBe("runtime-c");
- expect(secondCached.models.map((model) => model.id)).toEqual(["gpt-5.5"]);
+ expect(secondCached.models.map((model) => model.id)).toEqual(["gpt-5.6-luna"]);
+ });
+
+ it.each([
+ ["off", undefined],
+ ["max", "max"],
+ ] as const)("keeps Kimi K3 reasoning %s exact", async (thinkingLevel, expectedEffort) => {
+ const provider = await registerSingleProviderPlugin(plugin);
+ const capturedPayloads: Record[] = [];
+ const baseStreamFn = (_model: unknown, _context: unknown, options: unknown) => {
+ const payload: Record = {
+ model: "kimi-k3",
+ reasoning_effort: "max",
+ };
+ (options as { onPayload?: (payload: Record) => void })?.onPayload?.(payload);
+ capturedPayloads.push(payload);
+ return {} as never;
+ };
+ const streamFn = provider.wrapStreamFn?.({
+ streamFn: baseStreamFn as never,
+ providerId: "opencode",
+ modelId: "kimi-k3",
+ thinkingLevel,
+ } as never);
+
+ await streamFn?.(
+ { provider: "opencode", id: "kimi-k3", api: "openai-completions" } as never,
+ {} as never,
+ {},
+ );
+ expect(capturedPayloads).toEqual([
+ expectedEffort === undefined
+ ? { model: "kimi-k3" }
+ : { model: "kimi-k3", reasoning_effort: expectedEffort },
+ ]);
});
it("canonicalizes stale OpenCode Zen base URLs", async () => {
@@ -818,5 +941,35 @@ describe("opencode provider plugin", () => {
expect(gpt56LevelIds).not.toContain("minimal");
expect(gpt56LevelIds).toContain("xhigh");
expect(gpt56LevelIds).toContain("max");
+
+ expect(
+ resolveThinkingProfile({
+ provider: "opencode",
+ modelId: "kimi-k3",
+ api: "openai-completions",
+ reasoning: true,
+ compat: { supportedReasoningEfforts: ["max"] },
+ }),
+ ).toEqual({ levels: [{ id: "off" }, { id: "max" }], defaultLevel: "off" });
+ expect(
+ resolveThinkingProfile({
+ provider: "opencode",
+ modelId: "grok-4.5",
+ api: "openai-responses",
+ reasoning: true,
+ compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
+ }),
+ ).toEqual({
+ levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }],
+ defaultLevel: "medium",
+ });
+ expect(
+ resolveThinkingProfile({
+ provider: "opencode",
+ modelId: "big-pickle",
+ api: "openai-completions",
+ reasoning: true,
+ }),
+ ).toEqual({ levels: [{ id: "off", label: "always on" }], defaultLevel: "off" });
});
});
diff --git a/extensions/opencode/index.ts b/extensions/opencode/index.ts
index 1f566b56dc38..3df16c0deca2 100644
--- a/extensions/opencode/index.ts
+++ b/extensions/opencode/index.ts
@@ -4,6 +4,7 @@ import {
buildProviderReplayFamilyHooks,
matchesExactOrPrefix,
} from "openclaw/plugin-sdk/provider-model-shared";
+import { createOpenAICompatibleCompletionsThinkingOffWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { applyOpencodeZenConfig, OPENCODE_ZEN_DEFAULT_MODEL } from "./api.js";
import { opencodeMediaUnderstandingProvider } from "./media-understanding-provider.js";
@@ -21,7 +22,7 @@ import { registerOpenCodeSessionCatalog } from "./session-catalog-plugin.js";
const PROVIDER_ID = "opencode";
const MINIMAX_MODERN_MODEL_MATCHERS = ["minimax-m2.7"] as const;
const OPENCODE_SHARED_PROFILE_IDS = ["opencode:default", "opencode-go:default"] as const;
-const OPENCODE_SHARED_HINT = "Shared API key for Zen + Go catalogs";
+const OPENCODE_SHARED_HINT = "Shared API key infrastructure for Zen + Go";
type OpencodeZenCatalogAuth = {
apiKey?: string;
discoveryApiKey?: string;
@@ -67,7 +68,7 @@ export default defineSingleProviderPluginEntry({
applyConfig: applyOpencodeZenConfig,
expectedProviders: ["opencode", "opencode-go"],
noteMessage: [
- "OpenCode uses one API key across the Zen and Go catalogs.",
+ "One OpenCode API key can authenticate Zen and a separately subscribed Go catalog.",
"Zen provides access to Claude, GPT, Gemini, and more models.",
"Get your API key at: https://opencode.ai/auth",
"Choose the Zen catalog when you want the curated multi-model proxy.",
@@ -127,6 +128,20 @@ export default defineSingleProviderPluginEntry({
...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }),
isModernModelRef: ({ modelId }) => isModernOpencodeModel(modelId),
resolveThinkingProfile: resolveOpencodeThinkingProfile,
+ wrapStreamFn: (ctx) => {
+ if (!ctx.streamFn) {
+ return undefined;
+ }
+ const baseStreamFn = ctx.streamFn;
+ const thinkingOff = createOpenAICompatibleCompletionsThinkingOffWrapper(
+ baseStreamFn,
+ ctx.thinkingLevel,
+ );
+ return (model, context, options) =>
+ model.provider === PROVIDER_ID && model.id === "kimi-k3"
+ ? thinkingOff(model, context, options)
+ : baseStreamFn(model, context, options);
+ },
},
register(api) {
api.registerMediaUnderstandingProvider(opencodeMediaUnderstandingProvider);
diff --git a/extensions/opencode/openclaw.plugin.json b/extensions/opencode/openclaw.plugin.json
index fa5d5a317b83..accaa1c729c2 100644
--- a/extensions/opencode/openclaw.plugin.json
+++ b/extensions/opencode/openclaw.plugin.json
@@ -52,6 +52,7 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"],
"maxTokensField": "max_tokens",
"codeMode": "capable"
}
@@ -78,12 +79,51 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"],
"maxTokensField": "max_tokens",
"codeMode": "capable"
},
"status": "deprecated",
"replacedBy": "claude-opus-5"
},
+ {
+ "id": "claude-sonnet-4",
+ "name": "Claude Sonnet 4",
+ "api": "anthropic-messages",
+ "provider": "opencode",
+ "baseUrl": "https://opencode.ai/zen",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cacheRead": 0.3,
+ "cacheWrite": 3.75,
+ "tieredPricing": [
+ {
+ "input": 3,
+ "output": 15,
+ "cacheRead": 0.3,
+ "cacheWrite": 3.75,
+ "range": [0, 200000]
+ },
+ {
+ "input": 6,
+ "output": 22.5,
+ "cacheRead": 0.6,
+ "cacheWrite": 7.5,
+ "range": [200000]
+ }
+ ]
+ },
+ "contextWindow": 1000000,
+ "maxTokens": 64000,
+ "compat": {
+ "supportsUsageInStreaming": true,
+ "maxTokensField": "max_tokens"
+ },
+ "status": "deprecated"
+ },
{
"id": "gpt-5.6-sol",
"name": "GPT-5.6 Sol",
@@ -123,6 +163,7 @@
]
},
"contextWindow": 1050000,
+ "contextTokens": 922000,
"maxTokens": 128000,
"compat": {
"supportsUsageInStreaming": true,
@@ -178,10 +219,12 @@
]
},
"contextWindow": 1050000,
+ "contextTokens": 922000,
"maxTokens": 128000,
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh"],
"maxTokensField": "max_tokens",
"codeMode": "capable"
},
@@ -210,12 +253,13 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["minimal", "low", "medium", "high"],
"maxTokensField": "max_tokens"
}
},
{
"id": "gemini-3.1-pro",
- "name": "Gemini 3.1 Pro",
+ "name": "Gemini 3.1 Pro Preview",
"api": "google-generative-ai",
"provider": "opencode",
"baseUrl": "https://opencode.ai/zen/v1",
@@ -256,6 +300,7 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "medium", "high"],
"maxTokensField": "max_tokens"
}
},
@@ -280,7 +325,6 @@
"maxTokens": 128000,
"compat": {
"supportsUsageInStreaming": true,
- "supportsReasoningEffort": true,
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -306,7 +350,6 @@
"maxTokens": 131072,
"compat": {
"supportsUsageInStreaming": true,
- "supportsReasoningEffort": true,
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -314,6 +357,35 @@
"status": "deprecated",
"replacedBy": "minimax-m3"
},
+ {
+ "id": "kimi-k3",
+ "name": "Kimi K3",
+ "api": "openai-completions",
+ "provider": "opencode",
+ "baseUrl": "https://opencode.ai/zen/v1",
+ "reasoning": true,
+ "input": [
+ "text",
+ "image"
+ ],
+ "cost": {
+ "input": 3,
+ "output": 15,
+ "cacheRead": 0.3,
+ "cacheWrite": 0
+ },
+ "contextWindow": 1048576,
+ "maxTokens": 131072,
+ "compat": {
+ "supportsUsageInStreaming": true,
+ "supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["max"],
+ "maxTokensField": "max_tokens",
+ "supportsDeveloperRole": false,
+ "supportsStrictMode": false,
+ "codeMode": "capable"
+ }
+ },
{
"id": "big-pickle",
"name": "Big Pickle",
@@ -331,10 +403,10 @@
"cacheWrite": 0
},
"contextWindow": 200000,
+ "contextTokens": 160000,
"maxTokens": 32000,
"compat": {
"supportsUsageInStreaming": true,
- "supportsReasoningEffort": true,
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -361,6 +433,7 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "high", "max"],
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -387,7 +460,6 @@
"maxTokens": 32000,
"compat": {
"supportsUsageInStreaming": true,
- "supportsReasoningEffort": true,
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -414,6 +486,7 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "medium", "high"],
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -440,10 +513,12 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["low", "medium", "high"],
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
- }
+ },
+ "status": "deprecated"
},
{
"id": "nemotron-3-ultra-free",
@@ -465,7 +540,6 @@
"maxTokens": 128000,
"compat": {
"supportsUsageInStreaming": true,
- "supportsReasoningEffort": true,
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -492,6 +566,43 @@
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
+ "supportedReasoningEfforts": ["none", "high"],
+ "maxTokensField": "max_tokens",
+ "supportsDeveloperRole": false,
+ "supportsStrictMode": false
+ }
+ },
+ {
+ "id": "ling-3.0-tiny-free",
+ "name": "Ling-3.0-tiny Free",
+ "api": "openai-completions",
+ "provider": "opencode",
+ "baseUrl": "https://opencode.ai/zen/v1",
+ "reasoning": true,
+ "input": ["text"],
+ "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
+ "contextWindow": 262144,
+ "maxTokens": 32768,
+ "compat": {
+ "supportsUsageInStreaming": true,
+ "maxTokensField": "max_tokens",
+ "supportsDeveloperRole": false,
+ "supportsStrictMode": false
+ }
+ },
+ {
+ "id": "longcat-2.0-free",
+ "name": "LongCat-2.0 Free",
+ "api": "openai-completions",
+ "provider": "opencode",
+ "baseUrl": "https://opencode.ai/zen/v1",
+ "reasoning": true,
+ "input": ["text"],
+ "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
+ "contextWindow": 1000000,
+ "maxTokens": 131072,
+ "compat": {
+ "supportsUsageInStreaming": true,
"maxTokensField": "max_tokens",
"supportsDeveloperRole": false,
"supportsStrictMode": false
@@ -524,7 +635,7 @@
"choiceLabel": "OpenCode Zen catalog",
"groupId": "opencode",
"groupLabel": "OpenCode",
- "groupHint": "Shared API key for Zen + Go catalogs",
+ "groupHint": "Shared API key infrastructure for Zen + Go",
"optionKey": "opencodeZenApiKey",
"cliFlag": "--opencode-zen-api-key",
"cliOption": "--opencode-zen-api-key ",
diff --git a/extensions/opencode/opencode.live.test.ts b/extensions/opencode/opencode.live.test.ts
index 08f95872779c..3cfede769f0e 100644
--- a/extensions/opencode/opencode.live.test.ts
+++ b/extensions/opencode/opencode.live.test.ts
@@ -8,7 +8,10 @@ import {
import { extractNonEmptyAssistantText, isLiveTestEnabled } from "openclaw/plugin-sdk/test-live";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
-import { buildStaticOpencodeZenProviderConfig } from "./provider-catalog.js";
+import {
+ buildStaticOpencodeZenProviderConfig,
+ listOpencodeZenModelCatalogEntries,
+} from "./provider-catalog.js";
const OPENCODE_ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models";
const OPENCODE_API_KEY =
@@ -89,24 +92,46 @@ function listStaticOpencodeZenModelIds(): string[] {
}
describeCatalogLive("opencode Zen live catalog drift", () => {
- it("keeps the provider-owned static seed aligned with the live model ids", async () => {
+ it("covers every global live id with trusted metadata and filters deprecated rows", async () => {
const liveIds = await fetchOpencodeZenModelIds();
const staticIds = listStaticOpencodeZenModelIds();
expect(new Set(staticIds).size).toBe(staticIds.length);
- const staticIdSet = new Set(staticIds);
- const liveIdSet = new Set(liveIds);
- const missingStaticMetadata = liveIds.filter((id) => !staticIdSet.has(id));
- const staleStaticRows = staticIds.filter((id) => !liveIdSet.has(id));
+ const trustedRows = listOpencodeZenModelCatalogEntries();
+ const trustedIdSet = new Set(trustedRows.map((row) => row.id));
+ const missingTrustedMetadata = liveIds.filter((id) => !trustedIdSet.has(id));
+ const deprecatedLiveIds = trustedRows
+ .filter((row) => row.status === "deprecated" && liveIds.includes(row.id))
+ .map((row) => row.id)
+ .toSorted();
+ const expectedActiveIds = liveIds.filter((id) => !deprecatedLiveIds.includes(id));
expect(
- { missingStaticMetadata, staleStaticRows },
+ { missingTrustedMetadata, deprecatedLiveIds, staticIds },
[
- "OpenCode Zen live catalog drifted from the provider-owned static seed.",
- "Add routing/baseUrl/cost/context/capability metadata for missing live ids,",
- "or remove stale static rows if OpenCode retired them.",
+ "OpenCode Zen global catalog has ids without trusted provider metadata,",
+ "or active discovery no longer matches global availability after lifecycle filtering.",
+ "Key-scoped absence is not retirement evidence.",
].join(" "),
- ).toEqual({ missingStaticMetadata: [], staleStaticRows: [] });
+ ).toEqual({
+ missingTrustedMetadata: [],
+ deprecatedLiveIds: [
+ "claude-opus-4-8",
+ "claude-sonnet-4",
+ "glm-5",
+ "gpt-5-codex",
+ "gpt-5.1-codex",
+ "gpt-5.1-codex-max",
+ "gpt-5.1-codex-mini",
+ "gpt-5.2-codex",
+ "gpt-5.5",
+ "kimi-k2.5",
+ "ling-3.0-flash-free",
+ "minimax-m2.5",
+ "minimax-m2.7",
+ ],
+ staticIds: expectedActiveIds,
+ });
}, 30_000);
});
diff --git a/extensions/opencode/provider-catalog.ts b/extensions/opencode/provider-catalog.ts
index 7b81ca31cc65..b43cc3c47183 100644
--- a/extensions/opencode/provider-catalog.ts
+++ b/extensions/opencode/provider-catalog.ts
@@ -27,29 +27,146 @@ const FREE_COST: ModelDefinitionConfig["cost"] = {
cacheWrite: 0,
};
-// Zen publishes route-specific limits that differ from the family defaults below.
-const MODEL_LIMITS: Record = {
- "claude-opus-5": { contextWindow: 1_000_000, maxTokens: 128_000 },
- "claude-sonnet-5": { contextWindow: 1_000_000, maxTokens: 128_000 },
- "gpt-5.6-luna": { contextWindow: 1_050_000, maxTokens: 128_000 },
- "gpt-5.6-sol": { contextWindow: 1_050_000, maxTokens: 128_000 },
- "gpt-5.6-terra": { contextWindow: 1_050_000, maxTokens: 128_000 },
- "glm-5.2": { contextWindow: 1_000_000, maxTokens: 131_072 },
- "grok-4.5": { contextWindow: 500_000, maxTokens: 500_000 },
- "kimi-k2.7-code": { contextWindow: 262_144, maxTokens: 262_144 },
- "laguna-s-2.1-free": { contextWindow: 256_000, maxTokens: 32_000 },
- "ling-3.0-flash-free": { contextWindow: 262_144, maxTokens: 32_768 },
- "minimax-m3": { contextWindow: 512_000, maxTokens: 128_000 },
+type ZenModelCapabilities = {
+ contextWindow: number;
+ contextTokens?: number;
+ maxTokens: number;
+ input: ReadonlyArray<"text" | "image">;
+ reasoningEfforts?: readonly string[];
+ status?: "deprecated";
+ replacedBy?: string;
};
-// These rows are the inverse of their family's usual image-input capability.
-const MODEL_IMAGE_INPUT_OVERRIDES = new Map([
- ["laguna-s-2.1-free", false],
- ["ling-3.0-flash-free", false],
- ["minimax-m3", true],
-]);
+const T = ["text"] as const;
+const TI = ["text", "image"] as const;
-const MODEL_COSTS: Record = {
+// The official machine catalog owns limits, representable modalities, and the
+// reasoning boolean. Pinned provider metadata/source owns exact effort enums.
+const E_LMHXM = ["low", "medium", "high", "xhigh", "max"] as const;
+const E_LMHM = ["low", "medium", "high", "max"] as const;
+const E_LMH = ["low", "medium", "high"] as const;
+const E_MIN_LMH = ["minimal", "low", "medium", "high"] as const;
+const E_NONE_LMHX = ["none", "low", "medium", "high", "xhigh"] as const;
+const E_MHX = ["medium", "high", "xhigh"] as const;
+const E_NONE_LMHXM = ["none", "low", "medium", "high", "xhigh", "max"] as const;
+const E_LMHX = ["low", "medium", "high", "xhigh"] as const;
+const E_NONE_LMH = ["none", "low", "medium", "high"] as const;
+const E_LOW_HIGH_MAX = ["low", "high", "max"] as const;
+const E_HIGH_MAX = ["high", "max"] as const;
+const E_MAX = ["max"] as const;
+const E_NONE_HIGH = ["none", "high"] as const;
+
+type ZenModelMetadata = Pick;
+
+const INPUT_128 = { contextTokens: 128_000 } as const;
+const INPUT_160 = { contextTokens: 160_000 } as const;
+const INPUT_272 = { contextTokens: 272_000 } as const;
+const INPUT_922 = { contextTokens: 922_000 } as const;
+const DEPRECATED = { status: "deprecated" } as const;
+const INPUT_272_DEPRECATED = { contextTokens: 272_000, status: "deprecated" } as const;
+const DEPRECATED_BY_OPUS_5 = { status: "deprecated", replacedBy: "claude-opus-5" } as const;
+const DEPRECATED_BY_GPT_56_SOL = {
+ status: "deprecated",
+ replacedBy: "gpt-5.6-sol",
+} as const;
+const INPUT_922_DEPRECATED_BY_GPT_56_SOL = {
+ contextTokens: 922_000,
+ ...DEPRECATED_BY_GPT_56_SOL,
+} as const;
+const DEPRECATED_BY_MINIMAX_M3 = {
+ status: "deprecated",
+ replacedBy: "minimax-m3",
+} as const;
+
+type ZenModelCapabilityRow = readonly [
+ id: string,
+ contextWindow: number,
+ maxTokens: number,
+ input: ReadonlyArray<"text" | "image">,
+ reasoningEfforts?: readonly string[],
+ metadata?: ZenModelMetadata,
+];
+
+const MODEL_CAPABILITY_ROWS = [
+ ["claude-fable-5", 1000000, 128000, TI, E_LMHXM],
+ ["claude-opus-5", 1000000, 128000, TI, E_LMHXM],
+ ["claude-opus-4-8", 1000000, 128000, TI, E_LMHXM, DEPRECATED_BY_OPUS_5],
+ ["claude-opus-4-7", 1000000, 128000, TI, E_LMHXM],
+ ["claude-opus-4-6", 1000000, 128000, TI, E_LMHM],
+ ["claude-opus-4-5", 200000, 64000, TI, E_LMH],
+ ["claude-sonnet-5", 1000000, 128000, TI, E_LMHXM],
+ ["claude-sonnet-4-6", 1000000, 64000, TI, E_LMHM],
+ ["claude-sonnet-4-5", 1000000, 64000, TI],
+ ["claude-sonnet-4", 1000000, 64000, TI, undefined, DEPRECATED],
+ ["claude-haiku-4-5", 200000, 64000, TI],
+ ["gemini-3.6-flash", 1048576, 65536, TI, E_MIN_LMH],
+ ["gemini-3.5-flash-lite", 1048576, 65536, TI, E_MIN_LMH],
+ ["gemini-3.5-flash", 1048576, 65536, TI, E_MIN_LMH],
+ ["gemini-3.1-pro", 1048576, 65536, TI, E_LMH],
+ ["gemini-3-flash", 1048576, 65536, TI, E_MIN_LMH],
+ ["gpt-5.6-sol", 1050000, 128000, TI, E_NONE_LMHXM, INPUT_922],
+ ["gpt-5.6-terra", 1050000, 128000, TI, E_NONE_LMHXM, INPUT_922],
+ ["gpt-5.6-luna", 1050000, 128000, TI, E_NONE_LMHXM, INPUT_922],
+ ["gpt-5.5", 1050000, 128000, TI, E_NONE_LMHX, INPUT_922_DEPRECATED_BY_GPT_56_SOL],
+ ["gpt-5.5-pro", 1050000, 128000, TI, E_MHX, INPUT_922],
+ ["gpt-5.4", 1050000, 128000, TI, E_NONE_LMHX, INPUT_922],
+ ["gpt-5.4-pro", 1050000, 128000, TI, E_MHX, INPUT_922],
+ ["gpt-5.4-mini", 400000, 128000, TI, E_NONE_LMHX, INPUT_272],
+ ["gpt-5.4-nano", 400000, 128000, TI, E_NONE_LMHX, INPUT_272],
+ ["gpt-5.3-codex-spark", 128000, 128000, T, E_LMHX, INPUT_128],
+ ["gpt-5.3-codex", 400000, 128000, TI, E_NONE_LMHX, INPUT_272],
+ ["gpt-5.2", 400000, 128000, TI, E_NONE_LMHX, INPUT_272],
+ ["gpt-5.2-codex", 400000, 128000, TI, E_LMHX, INPUT_272_DEPRECATED],
+ ["gpt-5.1", 400000, 128000, TI, E_NONE_LMH, INPUT_272],
+ ["gpt-5.1-codex-max", 400000, 128000, TI, E_LMHX, INPUT_272_DEPRECATED],
+ ["gpt-5.1-codex", 400000, 128000, TI, E_LMH, INPUT_272_DEPRECATED],
+ ["gpt-5.1-codex-mini", 400000, 128000, TI, E_LMH, INPUT_272_DEPRECATED],
+ ["gpt-5", 400000, 128000, TI, E_MIN_LMH, INPUT_272],
+ ["gpt-5-codex", 400000, 128000, TI, E_LMH, INPUT_272_DEPRECATED],
+ ["gpt-5-nano", 400000, 128000, TI, E_MIN_LMH, INPUT_272],
+ ["grok-build-0.1", 256000, 256000, TI],
+ ["grok-4.5", 500000, 500000, TI, E_LMH],
+ ["deepseek-v4-pro", 1000000, 384000, T, E_HIGH_MAX],
+ ["deepseek-v4-flash", 1000000, 384000, T, E_LOW_HIGH_MAX],
+ ["glm-5.2", 1000000, 131072, T, E_HIGH_MAX],
+ ["glm-5.1", 204800, 131072, T],
+ ["glm-5", 204800, 131072, T, undefined, DEPRECATED],
+ ["minimax-m3", 512000, 128000, TI],
+ ["minimax-m2.7", 204800, 131072, T, undefined, DEPRECATED_BY_MINIMAX_M3],
+ ["minimax-m2.5", 204800, 131072, T, undefined, DEPRECATED],
+ ["kimi-k3", 1048576, 131072, TI, E_MAX],
+ ["kimi-k2.7-code", 262144, 262144, TI],
+ ["kimi-k2.6", 262144, 65536, TI],
+ ["kimi-k2.5", 262144, 65536, TI, undefined, DEPRECATED],
+ ["qwen3.6-plus", 262144, 65536, TI],
+ ["qwen3.5-plus", 262144, 65536, TI],
+ ["big-pickle", 200000, 32000, T, undefined, INPUT_160],
+ ["deepseek-v4-flash-free", 200000, 128000, T, E_LOW_HIGH_MAX],
+ ["mimo-v2.5-free", 200000, 32000, TI],
+ ["ling-3.0-flash-free", 262144, 32768, T, E_LMH, DEPRECATED],
+ ["ling-3.0-tiny-free", 262144, 32768, T],
+ ["nemotron-3-ultra-free", 1000000, 128000, T],
+ ["north-mini-code-free", 256000, 64000, T, E_NONE_HIGH],
+ ["laguna-s-2.1-free", 256000, 32000, T, E_LMH],
+ ["longcat-2.0-free", 1000000, 131072, T],
+ ["claude-opus-4-1", 200000, 32000, TI, undefined, DEPRECATED],
+] as const satisfies readonly ZenModelCapabilityRow[];
+type ZenModelId = (typeof MODEL_CAPABILITY_ROWS)[number][0];
+
+const MODEL_CAPABILITIES = Object.fromEntries(
+ MODEL_CAPABILITY_ROWS.map(([id, contextWindow, maxTokens, input, reasoningEfforts, metadata]) => [
+ id,
+ {
+ contextWindow,
+ maxTokens,
+ input,
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
+ ...metadata,
+ },
+ ]),
+) as Record;
+
+const MODEL_COSTS: Record = {
"big-pickle": FREE_COST,
"claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
@@ -99,13 +216,13 @@ const MODEL_COSTS: Record = {
"gemini-3.5-flash-lite": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 },
"gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
"gpt-5.6-luna": {
- input: 1,
- output: 6,
- cacheRead: 0.1,
- cacheWrite: 1.25,
+ input: 0.2,
+ output: 1.2,
+ cacheRead: 0.02,
+ cacheWrite: 0.25,
tieredPricing: [
- { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25, range: [0, 272_000] },
- { input: 2, output: 9, cacheRead: 0.2, cacheWrite: 2.5, range: [272_000] },
+ { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25, range: [0, 272_000] },
+ { input: 0.4, output: 1.8, cacheRead: 0.04, cacheWrite: 0.5, range: [272_000] },
],
},
"gpt-5.6-sol": {
@@ -119,13 +236,13 @@ const MODEL_COSTS: Record = {
],
},
"gpt-5.6-terra": {
- input: 2.5,
- output: 15,
- cacheRead: 0.25,
- cacheWrite: 3.125,
+ input: 2,
+ output: 12,
+ cacheRead: 0.2,
+ cacheWrite: 2.5,
tieredPricing: [
- { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125, range: [0, 272_000] },
- { input: 5, output: 22.5, cacheRead: 0.5, cacheWrite: 6.25, range: [272_000] },
+ { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5, range: [0, 272_000] },
+ { input: 4, output: 18, cacheRead: 0.4, cacheWrite: 5, range: [272_000] },
],
},
"glm-5": { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 },
@@ -170,18 +287,21 @@ const MODEL_COSTS: Record = {
"grok-4.5": {
input: 2,
output: 6,
- cacheRead: 0.5,
+ cacheRead: 0.3,
cacheWrite: 0,
tieredPricing: [
- { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0, range: [0, 200_000] },
- { input: 4, output: 12, cacheRead: 1, cacheWrite: 0, range: [200_000] },
+ { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0, range: [0, 200_000] },
+ { input: 4, output: 12, cacheRead: 0.6, cacheWrite: 0, range: [200_000] },
],
},
"kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
"kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
"kimi-k2.7-code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
+ "kimi-k3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
"laguna-s-2.1-free": FREE_COST,
"ling-3.0-flash-free": FREE_COST,
+ "ling-3.0-tiny-free": FREE_COST,
+ "longcat-2.0-free": FREE_COST,
"mimo-v2.5-free": FREE_COST,
"minimax-m2.5": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"minimax-m2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
@@ -192,7 +312,7 @@ const MODEL_COSTS: Record = {
"qwen3.6-plus": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.625 },
};
-const MODEL_NAMES: Record = {
+const MODEL_NAMES: Record = {
"big-pickle": "Big Pickle",
"claude-fable-5": "Claude Fable 5",
"claude-haiku-4-5": "Claude Haiku 4.5",
@@ -210,9 +330,9 @@ const MODEL_NAMES: Record = {
"deepseek-v4-flash-free": "DeepSeek V4 Flash Free",
"deepseek-v4-pro": "DeepSeek V4 Pro",
"gemini-3-flash": "Gemini 3 Flash",
- "gemini-3.1-pro": "Gemini 3.1 Pro",
+ "gemini-3.1-pro": "Gemini 3.1 Pro Preview",
"gemini-3.5-flash": "Gemini 3.5 Flash",
- "gemini-3.5-flash-lite": "Gemini 3.5 Flash-Lite",
+ "gemini-3.5-flash-lite": "Gemini 3.5 Flash Lite",
"gemini-3.6-flash": "Gemini 3.6 Flash",
"gpt-5.6-luna": "GPT-5.6 Luna",
"gpt-5.6-sol": "GPT-5.6 Sol",
@@ -242,8 +362,11 @@ const MODEL_NAMES: Record = {
"kimi-k2.5": "Kimi K2.5",
"kimi-k2.6": "Kimi K2.6",
"kimi-k2.7-code": "Kimi K2.7 Code",
+ "kimi-k3": "Kimi K3",
"laguna-s-2.1-free": "Laguna S 2.1 Free",
"ling-3.0-flash-free": "Ling-3.0-flash Free",
+ "ling-3.0-tiny-free": "Ling-3.0-tiny Free",
+ "longcat-2.0-free": "LongCat-2.0 Free",
"mimo-v2.5-free": "MiMo V2.5 Free",
"minimax-m2.5": "MiniMax M2.5",
"minimax-m2.7": "MiniMax M2.7",
@@ -254,9 +377,6 @@ const MODEL_NAMES: Record = {
"qwen3.6-plus": "Qwen3.6 Plus",
};
-const GPT_56_MODEL_IDS = new Set(["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
-const GPT_56_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"];
-
type OpencodeZenModelDefinition = ModelDefinitionConfig & {
provider: typeof PROVIDER_ID;
api: NonNullable;
@@ -271,84 +391,6 @@ type FetchOpencodeZenLiveModelIdsParams = {
signal?: AbortSignal;
};
-function formatModelName(modelId: string): string {
- const exact = MODEL_NAMES[modelId];
- if (exact) {
- return exact;
- }
- return modelId
- .split("-")
- .map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
- .join(" ");
-}
-
-function supportsImageInput(modelId: string): boolean {
- const lower = modelId.toLowerCase();
- const override = MODEL_IMAGE_INPUT_OVERRIDES.get(lower);
- if (override !== undefined) {
- return override;
- }
- return !(
- lower.includes("deepseek") ||
- lower.includes("glm") ||
- lower.includes("minimax") ||
- lower.includes("qwen")
- );
-}
-
-function resolveContextWindow(modelId: string): number {
- const lower = modelId.toLowerCase();
- const limits = MODEL_LIMITS[lower];
- if (limits) {
- return limits.contextWindow;
- }
- if (lower.includes("gemini")) {
- return 1_048_576;
- }
- if (lower.includes("gpt") || lower.includes("codex")) {
- return 400_000;
- }
- if (lower.includes("deepseek")) {
- return 1_000_000;
- }
- if (lower.includes("claude")) {
- return 200_000;
- }
- if (lower.includes("glm") || lower.includes("minimax")) {
- return 204_800;
- }
- if (lower.includes("kimi") || lower.includes("mimo") || lower.includes("qwen")) {
- return 262_144;
- }
- return 128_000;
-}
-
-function resolveMaxTokens(modelId: string): number {
- const lower = modelId.toLowerCase();
- const limits = MODEL_LIMITS[lower];
- if (limits) {
- return limits.maxTokens;
- }
- if (lower.includes("deepseek")) {
- return 384_000;
- }
- if (lower.includes("glm") || lower.includes("minimax")) {
- return 131_072;
- }
- if (lower.includes("gpt") || lower.includes("codex")) {
- return 128_000;
- }
- if (
- lower.includes("claude") ||
- lower.includes("gemini") ||
- lower.includes("kimi") ||
- lower.includes("qwen")
- ) {
- return 65_536;
- }
- return 8_192;
-}
-
type OpencodeZenTransport = {
api: ModelApi;
baseUrl: string;
@@ -356,7 +398,7 @@ type OpencodeZenTransport = {
function resolveOpencodeZenTransport(modelId: string): OpencodeZenTransport {
const lower = modelId.toLowerCase();
- if (lower.startsWith("gpt-")) {
+ if (lower.startsWith("gpt-") || lower.startsWith("grok-")) {
return { api: "openai-responses", baseUrl: OPENCODE_ZEN_OPENAI_BASE_URL };
}
if (lower.startsWith("claude-") || lower.startsWith("qwen")) {
@@ -368,100 +410,52 @@ function resolveOpencodeZenTransport(modelId: string): OpencodeZenTransport {
return { api: "openai-completions", baseUrl: OPENCODE_ZEN_OPENAI_BASE_URL };
}
-function resolveModelCost(modelId: string): ModelDefinitionConfig["cost"] {
- const cost = MODEL_COSTS[modelId];
- if (!cost) {
- throw new Error(`missing OpenCode Zen cost metadata for ${modelId}`);
+function buildOpencodeZenModel(modelId: ZenModelId): OpencodeZenModelDefinition {
+ const capabilities = MODEL_CAPABILITIES[modelId];
+ if (!capabilities) {
+ throw new Error(`missing OpenCode Zen capability metadata for ${modelId}`);
}
- return cost;
-}
-
-function buildOpencodeZenModel(modelId: string): OpencodeZenModelDefinition {
- const normalizedModelId = modelId.trim().toLowerCase();
- const transport = resolveOpencodeZenTransport(normalizedModelId);
+ const transport = resolveOpencodeZenTransport(modelId);
return normalizeModelCompat({
- id: normalizedModelId,
- name: formatModelName(normalizedModelId),
+ id: modelId,
+ name: MODEL_NAMES[modelId],
api: transport.api,
provider: PROVIDER_ID,
baseUrl: transport.baseUrl,
reasoning: true,
- input: supportsImageInput(normalizedModelId) ? ["text", "image"] : ["text"],
- cost: resolveModelCost(normalizedModelId),
- contextWindow: resolveContextWindow(normalizedModelId),
- maxTokens: resolveMaxTokens(normalizedModelId),
+ input: [...capabilities.input],
+ cost: MODEL_COSTS[modelId],
+ contextWindow: capabilities.contextWindow,
+ ...(capabilities.contextTokens ? { contextTokens: capabilities.contextTokens } : {}),
+ maxTokens: capabilities.maxTokens,
+ ...(transport.api === "openai-responses" && !capabilities.reasoningEfforts?.includes("none")
+ ? { thinkingLevelMap: { off: null } }
+ : {}),
compat: {
supportsUsageInStreaming: true,
- supportsReasoningEffort: true,
- ...(GPT_56_MODEL_IDS.has(normalizedModelId)
- ? { supportedReasoningEfforts: GPT_56_REASONING_EFFORTS }
+ ...(capabilities.reasoningEfforts
+ ? {
+ supportsReasoningEffort: true,
+ supportedReasoningEfforts: [...capabilities.reasoningEfforts],
+ }
: {}),
maxTokensField: "max_tokens",
+ ...(transport.api === "openai-completions"
+ ? { supportsDeveloperRole: false, supportsStrictMode: false }
+ : {}),
},
}) as OpencodeZenModelDefinition;
}
-const OPENCODE_ZEN_MODELS = [
- "claude-fable-5",
- "claude-opus-5",
- "claude-opus-4-8",
- "claude-opus-4-7",
- "claude-opus-4-6",
- "claude-opus-4-5",
- "claude-opus-4-1",
- "claude-sonnet-5",
- "claude-sonnet-4-6",
- "claude-sonnet-4-5",
- "claude-sonnet-4",
- "claude-haiku-4-5",
- "gemini-3.6-flash",
- "gemini-3.5-flash",
- "gemini-3.5-flash-lite",
- "gemini-3.1-pro",
- "gemini-3-flash",
- "gpt-5.6-sol",
- "gpt-5.6-terra",
- "gpt-5.6-luna",
- "gpt-5.5",
- "gpt-5.5-pro",
- "gpt-5.4",
- "gpt-5.4-pro",
- "gpt-5.4-mini",
- "gpt-5.4-nano",
- "gpt-5.3-codex-spark",
- "gpt-5.3-codex",
- "gpt-5.2",
- "gpt-5.2-codex",
- "gpt-5.1",
- "gpt-5.1-codex-max",
- "gpt-5.1-codex",
- "gpt-5.1-codex-mini",
- "gpt-5",
- "gpt-5-codex",
- "gpt-5-nano",
- "grok-build-0.1",
- "grok-4.5",
- "deepseek-v4-pro",
- "deepseek-v4-flash",
- "glm-5.2",
- "glm-5.1",
- "glm-5",
- "minimax-m3",
- "minimax-m2.7",
- "minimax-m2.5",
- "kimi-k2.7-code",
- "kimi-k2.6",
- "kimi-k2.5",
- "qwen3.6-plus",
- "qwen3.5-plus",
- "big-pickle",
- "deepseek-v4-flash-free",
- "mimo-v2.5-free",
- "laguna-s-2.1-free",
- "ling-3.0-flash-free",
- "nemotron-3-ultra-free",
- "north-mini-code-free",
-].map(buildOpencodeZenModel);
+const OPENCODE_ZEN_RESOLVABLE_MODELS = MODEL_CAPABILITY_ROWS.map(([modelId]) =>
+ buildOpencodeZenModel(modelId),
+);
+const OPENCODE_ZEN_MODELS = OPENCODE_ZEN_RESOLVABLE_MODELS.filter(
+ (model) => MODEL_CAPABILITIES[model.id]?.status !== "deprecated",
+);
+const OPENCODE_ZEN_MODEL_BY_ID = new Map(
+ OPENCODE_ZEN_RESOLVABLE_MODELS.map((model) => [model.id, model]),
+);
export function buildStaticOpencodeZenProviderConfig(apiKey?: string): ModelProviderConfig {
return {
@@ -528,19 +522,33 @@ export async function buildOpencodeZenLiveProviderConfig(
}
export function listOpencodeZenModelCatalogEntries(): ModelCatalogEntry[] {
- return OPENCODE_ZEN_MODELS.map((model) => ({
- provider: model.provider,
- id: model.id,
- name: model.name,
- reasoning: model.reasoning,
- input: model.input,
- contextWindow: model.contextWindow,
- }));
+ return OPENCODE_ZEN_RESOLVABLE_MODELS.map((model) => {
+ const lifecycle = MODEL_CAPABILITIES[model.id];
+ const entry: ModelCatalogEntry = {
+ provider: model.provider,
+ id: model.id,
+ name: model.name,
+ api: model.api,
+ baseUrl: model.baseUrl,
+ reasoning: model.reasoning,
+ input: model.input,
+ contextWindow: model.contextWindow,
+ contextTokens: model.contextTokens,
+ compat: model.compat,
+ };
+ if (lifecycle?.status) {
+ entry.status = lifecycle.status;
+ }
+ if (lifecycle?.replacedBy) {
+ entry.replacedBy = lifecycle.replacedBy;
+ }
+ return entry;
+ });
}
export function resolveOpencodeZenModel(modelId: string): ProviderRuntimeModel | undefined {
const normalizedModelId = modelId.trim().toLowerCase();
- return OPENCODE_ZEN_MODELS.find((model) => model.id === normalizedModelId);
+ return OPENCODE_ZEN_MODEL_BY_ID.get(normalizedModelId);
}
function normalizeBaseUrl(baseUrl: string | undefined): string {
diff --git a/extensions/opencode/provider-policy-api.test.ts b/extensions/opencode/provider-policy-api.test.ts
index c45dbcd18267..519d0e41a073 100644
--- a/extensions/opencode/provider-policy-api.test.ts
+++ b/extensions/opencode/provider-policy-api.test.ts
@@ -48,6 +48,9 @@ describe("opencode provider policy public artifact", () => {
resolveThinkingProfile({
provider: "opencode",
modelId: "gpt-5.6-luna",
+ compat: {
+ supportedReasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
+ },
}),
).toEqual({
levels: [
@@ -61,4 +64,32 @@ describe("opencode provider policy public artifact", () => {
defaultLevel: "medium",
});
});
+
+ it("derives non-Claude profiles only from exact provider effort metadata", () => {
+ expect(
+ resolveThinkingProfile({
+ provider: "opencode",
+ modelId: "grok-4.5",
+ compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
+ }),
+ ).toEqual({
+ levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }],
+ defaultLevel: "medium",
+ });
+ expect(
+ resolveThinkingProfile({
+ provider: "opencode",
+ modelId: "kimi-k3",
+ compat: { supportedReasoningEfforts: ["max"] },
+ }),
+ ).toEqual({ levels: [{ id: "off" }, { id: "max" }], defaultLevel: "off" });
+ expect(
+ resolveThinkingProfile({
+ provider: "opencode",
+ modelId: "big-pickle",
+ api: "openai-completions",
+ reasoning: true,
+ }),
+ ).toEqual({ levels: [{ id: "off", label: "always on" }], defaultLevel: "off" });
+ });
});
diff --git a/extensions/opencode/provider-policy-api.ts b/extensions/opencode/provider-policy-api.ts
index 4262e4e61bfb..20d468f3573b 100644
--- a/extensions/opencode/provider-policy-api.ts
+++ b/extensions/opencode/provider-policy-api.ts
@@ -5,25 +5,46 @@ import type {
} from "openclaw/plugin-sdk/plugin-entry";
import { resolveClaudeThinkingProfile } from "openclaw/plugin-sdk/provider-model-shared";
-const GPT_56_THINKING_PROFILE = {
- levels: [
- { id: "off" },
- { id: "low" },
- { id: "medium" },
- { id: "high" },
- { id: "xhigh" },
- { id: "max" },
- ],
- defaultLevel: "medium",
+const FIXED_REASONING_PROFILE = {
+ levels: [{ id: "off", label: "always on" }],
+ defaultLevel: "off",
} as const satisfies ProviderThinkingProfile;
-function isGpt56Model(modelId: string): boolean {
- return /^gpt-5\.6(?:-|$)/u.test(modelId.trim().toLowerCase());
+const THINKING_LEVEL_IDS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
+
+function resolveEffortThinkingProfile(
+ efforts: readonly string[] | null | undefined,
+): ProviderThinkingProfile | undefined {
+ if (!efforts || efforts.length === 0) {
+ return undefined;
+ }
+ const acceptedLevelIds = ["off", ...efforts.map((effort) => (effort === "none" ? "off" : effort))]
+ .filter((id) => THINKING_LEVEL_IDS.has(id))
+ .filter((id, index, values) => values.indexOf(id) === index) as Array<
+ "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
+ >;
+ const levels = acceptedLevelIds.map((id) => ({ id }));
+ const levelIdSet = new Set(acceptedLevelIds);
+ const defaultLevel = levelIdSet.has("medium")
+ ? "medium"
+ : levelIdSet.has("high")
+ ? "high"
+ : levelIdSet.has("low")
+ ? "low"
+ : "off";
+ return { levels, defaultLevel };
}
export function resolveThinkingProfile(params: ProviderDefaultThinkingPolicyContext) {
- if (isGpt56Model(params.modelId)) {
- return GPT_56_THINKING_PROFILE;
+ const modelId = params.modelId.trim().toLowerCase();
+ if (modelId.startsWith("claude-")) {
+ return resolveClaudeThinkingProfile(modelId);
}
- return resolveClaudeThinkingProfile(params.modelId);
+ const effortProfile = resolveEffortThinkingProfile(params.compat?.supportedReasoningEfforts);
+ if (effortProfile) {
+ return effortProfile;
+ }
+ return params.reasoning === true && params.api !== "anthropic-messages"
+ ? FIXED_REASONING_PROFILE
+ : undefined;
}
diff --git a/src/plugins/provider-public-artifacts.test.ts b/src/plugins/provider-public-artifacts.test.ts
index 075711ac43e6..67c9c00cda03 100644
--- a/src/plugins/provider-public-artifacts.test.ts
+++ b/src/plugins/provider-public-artifacts.test.ts
@@ -177,15 +177,7 @@ describe("provider public artifacts", () => {
modelId: "deepseek-v4-pro",
}),
).toEqual({
- levels: [
- { id: "off" },
- { id: "minimal" },
- { id: "low" },
- { id: "medium" },
- { id: "high" },
- { id: "xhigh" },
- { id: "max" },
- ],
+ levels: [{ id: "off" }, { id: "high" }, { id: "max" }],
defaultLevel: "high",
});
expect(