Providers: add Opencode Go support (#42313)

* feat(providers): add opencode-go provider support and onboarding

* Onboard: unify OpenCode auth handling openclaw#42313 thanks @ImLukeF

* Docs: merge OpenCode Zen and Go docs openclaw#42313 thanks @ImLukeF

* Update CHANGELOG.md

---------

Co-authored-by: Ubuntu <ubuntu@vps-90352893.vps.ovh.ca>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Luke
2026-03-11 16:31:06 +11:00
committed by GitHub
parent bd33a340fb
commit 7761e7626f
48 changed files with 468 additions and 140 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- iOS/Home canvas: add a bundled welcome screen with a live agent overview that refreshes on connect, reconnect, and foreground return, and move the compact connection pill off the top-left canvas overlay. (#42456) Thanks @ngutman.
- iOS/Home canvas: replace floating controls with a docked toolbar, make the bundled home scaffold adapt to smaller phones, and open chat in the resolved main session instead of a synthetic `ios` session. (#42456) Thanks @ngutman.
- Discord/auto threads: add `autoArchiveDuration` channel config for auto-created threads so Discord thread archiving can stay at 1 hour, 1 day, 3 days, or 1 week instead of always using the 1-hour default. (#35065) Thanks @davidguttman.
- OpenCode/onboarding: add new OpenCode Go provider, treat Zen and Go as one OpenCode setup in the wizard/docs while keeping the runtime providers split, store one shared OpenCode key for both profiles, and stop overriding the built-in `opencode-go` catalog routing. (#42313) Thanks @ImLukeF and @vincentkoc.
- macOS/chat UI: add a chat model picker, persist explicit thinking-level selections across relaunch, and harden provider-aware session model sync for the shared chat composer. (#42314) Thanks @ImLukeF.
### Breaking

View File

@@ -337,7 +337,7 @@ Options:
- `--non-interactive`
- `--mode <local|remote>`
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|mistral-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|custom-api-key|skip>`
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|mistral-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|opencode-go|custom-api-key|skip>`
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
- `--token <token>` (non-interactive; used with `--auth-choice token`)
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
@@ -354,6 +354,7 @@ Options:
- `--zai-api-key <key>`
- `--minimax-api-key <key>`
- `--opencode-zen-api-key <key>`
- `--opencode-go-api-key <key>`
- `--custom-base-url <url>` (non-interactive; used with `--auth-choice custom-api-key`)
- `--custom-model-id <id>` (non-interactive; used with `--auth-choice custom-api-key`)
- `--custom-api-key <key>` (non-interactive; optional; used with `--auth-choice custom-api-key`; falls back to `CUSTOM_API_KEY` when omitted)

View File

@@ -86,12 +86,13 @@ OpenClaw ships with the piai catalog. These providers require **no**
}
```
### OpenCode Zen
### OpenCode
- Provider: `opencode`
- Auth: `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`)
- Example model: `opencode/claude-opus-4-6`
- CLI: `openclaw onboard --auth-choice opencode-zen`
- Zen runtime provider: `opencode`
- Go runtime provider: `opencode-go`
- Example models: `opencode/claude-opus-4-6`, `opencode-go/kimi-k2.5`
- CLI: `openclaw onboard --auth-choice opencode-zen` or `openclaw onboard --auth-choice opencode-go`
```json5
{
@@ -104,8 +105,8 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Provider: `google`
- Auth: `GEMINI_API_KEY`
- Optional rotation: `GEMINI_API_KEYS`, `GEMINI_API_KEY_1`, `GEMINI_API_KEY_2`, `GOOGLE_API_KEY` fallback, and `OPENCLAW_LIVE_GEMINI_KEY` (single override)
- Example models: `google/gemini-3.1-pro-preview`, `google/gemini-3-flash-preview`, `google/gemini-3.1-flash-lite-preview`
- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview`, and bare `google/gemini-3.1-flash-lite` is normalized to `google/gemini-3.1-flash-lite-preview`
- Example models: `google/gemini-3.1-pro-preview`, `google/gemini-3-flash-preview`
- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview`
- CLI: `openclaw onboard --auth-choice gemini-api-key`
### Google Vertex, Antigravity, and Gemini CLI

View File

@@ -55,8 +55,8 @@ subscription** (OAuth) and **Anthropic** (API key or `claude setup-token`).
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
to `zai/*`.
Provider configuration examples (including OpenCode Zen) live in
[/gateway/configuration](/gateway/configuration#opencode-zen-multi-model-proxy).
Provider configuration examples (including OpenCode) live in
[/gateway/configuration](/gateway/configuration#opencode).
## “Model is not allowed” (and why replies stop)

View File

@@ -103,6 +103,10 @@
"source": "/opencode",
"destination": "/providers/opencode"
},
{
"source": "/opencode-go",
"destination": "/providers/opencode-go"
},
{
"source": "/qianfan",
"destination": "/providers/qianfan"
@@ -1013,8 +1017,7 @@
"tools/browser",
"tools/browser-login",
"tools/chrome-extension",
"tools/browser-linux-troubleshooting",
"tools/browser-wsl2-windows-remote-cdp-troubleshooting"
"tools/browser-linux-troubleshooting"
]
},
{
@@ -1112,6 +1115,7 @@
"providers/nvidia",
"providers/ollama",
"providers/openai",
"providers/opencode-go",
"providers/opencode",
"providers/openrouter",
"providers/qianfan",

View File

@@ -2079,7 +2079,7 @@ Use `cerebras/zai-glm-4.7` for Cerebras; `zai/glm-4.7` for Z.AI direct.
</Accordion>
<Accordion title="OpenCode Zen">
<Accordion title="OpenCode">
```json5
{
@@ -2092,7 +2092,7 @@ Use `cerebras/zai-glm-4.7` for Cerebras; `zai/glm-4.7` for Z.AI direct.
}
```
Set `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`). Shortcut: `openclaw onboard --auth-choice opencode-zen`.
Set `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`). Use `opencode/...` refs for the Zen catalog or `opencode-go/...` refs for the Go catalog. Shortcut: `openclaw onboard --auth-choice opencode-zen` or `openclaw onboard --auth-choice opencode-go`.
</Accordion>

View File

@@ -63,7 +63,7 @@ cat ~/.openclaw/openclaw.json
- Health check + restart prompt.
- Skills status summary (eligible/missing/blocked).
- Config normalization for legacy values.
- OpenCode Zen provider override warnings (`models.providers.opencode`).
- OpenCode provider override warnings (`models.providers.opencode` / `models.providers.opencode-go`).
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
- Legacy cron store migration (`jobId`, `schedule.cron`, top-level delivery/payload fields, payload `provider`, simple `notify: true` webhook fallback jobs).
- State integrity and permissions checks (sessions, transcripts, state dir).
@@ -134,12 +134,12 @@ Doctor warnings also include account-default guidance for multi-account channels
- If two or more `channels.<channel>.accounts` entries are configured without `channels.<channel>.defaultAccount` or `accounts.default`, doctor warns that fallback routing can pick an unexpected account.
- If `channels.<channel>.defaultAccount` is set to an unknown account ID, doctor warns and lists configured account IDs.
### 2b) OpenCode Zen provider overrides
### 2b) OpenCode provider overrides
If youve added `models.providers.opencode` (or `opencode-zen`) manually, it
overrides the built-in OpenCode Zen catalog from `@mariozechner/pi-ai`. That can
force every model onto a single API or zero out costs. Doctor warns so you can
remove the override and restore per-model API routing + costs.
If youve added `models.providers.opencode`, `opencode-zen`, or `opencode-go`
manually, it overrides the built-in OpenCode catalog from `@mariozechner/pi-ai`.
That can force models onto the wrong API or zero out costs. Doctor warns so you
can remove the override and restore per-model API routing + costs.
### 3) Legacy state migrations (disk layout)

View File

@@ -311,11 +311,11 @@ Include at least one image-capable model in `OPENCLAW_LIVE_GATEWAY_MODELS` (Clau
If you have keys enabled, we also support testing via:
- OpenRouter: `openrouter/...` (hundreds of models; use `openclaw models scan` to find tool+image capable candidates)
- OpenCode Zen: `opencode/...` (auth via `OPENCODE_API_KEY` / `OPENCODE_ZEN_API_KEY`)
- OpenCode: `opencode/...` for Zen and `opencode-go/...` for Go (auth via `OPENCODE_API_KEY` / `OPENCODE_ZEN_API_KEY`)
More providers you can include in the live matrix (if you have creds/config):
- Built-in: `openai`, `openai-codex`, `anthropic`, `google`, `google-vertex`, `google-antigravity`, `google-gemini-cli`, `zai`, `openrouter`, `opencode`, `xai`, `groq`, `cerebras`, `mistral`, `github-copilot`
- Built-in: `openai`, `openai-codex`, `anthropic`, `google`, `google-vertex`, `google-antigravity`, `google-gemini-cli`, `zai`, `openrouter`, `opencode`, `opencode-go`, `xai`, `groq`, `cerebras`, `mistral`, `github-copilot`
- Via `models.providers` (custom endpoints): `minimax` (cloud/API), plus any OpenAI/Anthropic-compatible proxy (LM Studio, vLLM, LiteLLM, etc.)
Tip: dont try to hardcode “all models” in docs. The authoritative list is whatever `discoverModels(...)` returns on your machine + whatever keys are available.

View File

@@ -39,7 +39,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
- [NVIDIA](/providers/nvidia)
- [Ollama (local models)](/providers/ollama)
- [OpenAI (API + Codex)](/providers/openai)
- [OpenCode Zen](/providers/opencode)
- [OpenCode (Zen + Go)](/providers/opencode)
- [OpenRouter](/providers/openrouter)
- [Qianfan](/providers/qianfan)
- [Qwen (OAuth)](/providers/qwen)

View File

@@ -32,7 +32,7 @@ model as `provider/model`.
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
- [Mistral](/providers/mistral)
- [Synthetic](/providers/synthetic)
- [OpenCode Zen](/providers/opencode)
- [OpenCode (Zen + Go)](/providers/opencode)
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)

View File

@@ -0,0 +1,45 @@
---
summary: "Use the OpenCode Go catalog with the shared OpenCode setup"
read_when:
- You want the OpenCode Go catalog
- You need the runtime model refs for Go-hosted models
title: "OpenCode Go"
---
# OpenCode Go
OpenCode Go is the Go catalog within [OpenCode](/providers/opencode).
It uses the same `OPENCODE_API_KEY` as the Zen catalog, but keeps the runtime
provider id `opencode-go` so upstream per-model routing stays correct.
## Supported models
- `opencode-go/kimi-k2.5`
- `opencode-go/glm-5`
- `opencode-go/minimax-m2.5`
## CLI setup
```bash
openclaw onboard --auth-choice opencode-go
# or non-interactive
openclaw onboard --opencode-go-api-key "$OPENCODE_API_KEY"
```
## Config snippet
```json5
{
env: { OPENCODE_API_KEY: "YOUR_API_KEY_HERE" }, // pragma: allowlist secret
agents: { defaults: { model: { primary: "opencode-go/kimi-k2.5" } } },
}
```
## Routing behavior
OpenClaw handles per-model routing automatically when the model ref uses `opencode-go/...`.
## Notes
- Use [OpenCode](/providers/opencode) for the shared onboarding and catalog overview.
- Runtime refs stay explicit: `opencode/...` for Zen, `opencode-go/...` for Go.

View File

@@ -1,25 +1,38 @@
---
summary: "Use OpenCode Zen (curated models) with OpenClaw"
summary: "Use OpenCode Zen and Go catalogs with OpenClaw"
read_when:
- You want OpenCode Zen for model access
- You want a curated list of coding-friendly models
title: "OpenCode Zen"
- You want OpenCode-hosted model access
- You want to pick between the Zen and Go catalogs
title: "OpenCode"
---
# OpenCode Zen
# OpenCode
OpenCode Zen is a **curated list of models** recommended by the OpenCode team for coding agents.
It is an optional, hosted model access path that uses an API key and the `opencode` provider.
Zen is currently in beta.
OpenCode exposes two hosted catalogs in OpenClaw:
- `opencode/...` for the **Zen** catalog
- `opencode-go/...` for the **Go** catalog
Both catalogs use the same OpenCode 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.
## CLI setup
### Zen catalog
```bash
openclaw onboard --auth-choice opencode-zen
# or non-interactive
openclaw onboard --opencode-zen-api-key "$OPENCODE_API_KEY"
```
### Go catalog
```bash
openclaw onboard --auth-choice opencode-go
openclaw onboard --opencode-go-api-key "$OPENCODE_API_KEY"
```
## Config snippet
```json5
@@ -29,8 +42,23 @@ openclaw onboard --opencode-zen-api-key "$OPENCODE_API_KEY"
}
```
## Catalogs
### Zen
- Runtime provider: `opencode`
- Example models: `opencode/claude-opus-4-6`, `opencode/gpt-5.2`, `opencode/gemini-3-pro`
- Best when you want the curated OpenCode multi-model proxy
### Go
- Runtime provider: `opencode-go`
- Example models: `opencode-go/kimi-k2.5`, `opencode-go/glm-5`, `opencode-go/minimax-m2.5`
- Best when you want the OpenCode-hosted Kimi/GLM/MiniMax lineup
## Notes
- `OPENCODE_ZEN_API_KEY` is also supported.
- You sign in to Zen, add billing details, and copy your API key.
- OpenCode Zen bills per request; check the OpenCode dashboard for details.
- Entering one OpenCode key during onboarding stores credentials for both runtime providers.
- You sign in to OpenCode, add billing details, and copy your API key.
- Billing and catalog availability are managed from the OpenCode dashboard.

View File

@@ -38,7 +38,7 @@ For a high-level overview, see [Onboarding Wizard](/start/wizard).
- Sets `agents.defaults.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`.
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then stores it in auth profiles.
- **xAI (Grok) API key**: prompts for `XAI_API_KEY` and configures xAI as a model provider.
- **OpenCode Zen (multi-model proxy)**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth).
- **OpenCode**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth) and lets you pick the Zen or Go catalog.
- **API key**: stores the key for you.
- **Vercel AI Gateway (multi-model proxy)**: prompts for `AI_GATEWAY_API_KEY`.
- More detail: [Vercel AI Gateway](/providers/vercel-ai-gateway)
@@ -228,7 +228,7 @@ openclaw onboard --non-interactive \
--gateway-bind loopback
```
</Accordion>
<Accordion title="OpenCode Zen example">
<Accordion title="OpenCode example">
```bash
openclaw onboard --non-interactive \
--mode local \
@@ -237,6 +237,7 @@ openclaw onboard --non-interactive \
--gateway-port 18789 \
--gateway-bind loopback
```
Swap to `--auth-choice opencode-go --opencode-go-api-key "$OPENCODE_API_KEY"` for the Go catalog.
</Accordion>
</AccordionGroup>

View File

@@ -123,7 +123,7 @@ openclaw onboard --non-interactive \
--gateway-bind loopback
```
</Accordion>
<Accordion title="OpenCode Zen example">
<Accordion title="OpenCode example">
```bash
openclaw onboard --non-interactive \
--mode local \
@@ -132,6 +132,7 @@ openclaw onboard --non-interactive \
--gateway-port 18789 \
--gateway-bind loopback
```
Swap to `--auth-choice opencode-go --opencode-go-api-key "$OPENCODE_API_KEY"` for the Go catalog.
</Accordion>
<Accordion title="Custom provider example">
```bash

View File

@@ -155,8 +155,8 @@ What you set:
<Accordion title="xAI (Grok) API key">
Prompts for `XAI_API_KEY` and configures xAI as a model provider.
</Accordion>
<Accordion title="OpenCode Zen">
Prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`).
<Accordion title="OpenCode">
Prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`) and lets you choose the Zen or Go catalog.
Setup URL: [opencode.ai/auth](https://opencode.ai/auth).
</Accordion>
<Accordion title="API key (generic)">

View File

@@ -81,7 +81,7 @@ export function isModernModelRef(ref: ModelRef): boolean {
return false;
}
if (provider === "openrouter" || provider === "opencode") {
if (provider === "openrouter" || provider === "opencode" || provider === "opencode-go") {
// OpenRouter/opencode are pass-through proxies; accept any model ID
// rather than restricting to a static prefix list.
return true;

View File

@@ -4,6 +4,7 @@ export const PROVIDER_ENV_API_KEY_CANDIDATES: Record<string, string[]> = {
chutes: ["CHUTES_OAUTH_TOKEN", "CHUTES_API_KEY"],
zai: ["ZAI_API_KEY", "Z_AI_API_KEY"],
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
"qwen-portal": ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"],
volcengine: ["VOLCANO_ENGINE_API_KEY"],
"volcengine-plan": ["VOLCANO_ENGINE_API_KEY"],

View File

@@ -412,4 +412,18 @@ describe("getApiKeyForModel", () => {
},
);
});
it("resolveEnvApiKey('opencode-go') falls back to OPENCODE_ZEN_API_KEY", async () => {
await withEnvAsync(
{
OPENCODE_API_KEY: undefined,
OPENCODE_ZEN_API_KEY: "sk-opencode-zen-fallback", // pragma: allowlist secret
},
async () => {
const resolved = resolveEnvApiKey("opencode-go");
expect(resolved?.apiKey).toBe("sk-opencode-zen-fallback");
expect(resolved?.source).toContain("OPENCODE_ZEN_API_KEY");
},
);
});
});

View File

@@ -313,6 +313,12 @@ describe("isModernModelRef", () => {
expect(isModernModelRef({ provider: "opencode", id: "claude-opus-4-6" })).toBe(true);
expect(isModernModelRef({ provider: "opencode", id: "gemini-3-pro" })).toBe(true);
});
it("accepts all opencode-go models without zen exclusions", () => {
expect(isModernModelRef({ provider: "opencode-go", id: "kimi-k2.5" })).toBe(true);
expect(isModernModelRef({ provider: "opencode-go", id: "glm-5" })).toBe(true);
expect(isModernModelRef({ provider: "opencode-go", id: "minimax-m2.5" })).toBe(true);
});
});
describe("resolveForwardCompatModel", () => {

View File

@@ -46,6 +46,9 @@ export function normalizeProviderId(provider: string): string {
if (normalized === "opencode-zen") {
return "opencode";
}
if (normalized === "opencode-go-auth") {
return "opencode-go";
}
if (normalized === "qwen") {
return "qwen-portal";
}

View File

@@ -9,10 +9,6 @@ import {
isAnthropicBillingError,
isAnthropicRateLimitError,
} from "./live-auth-keys.js";
import {
isMiniMaxModelNotFoundErrorMessage,
isModelNotFoundErrorMessage,
} from "./live-model-errors.js";
import { isModernModelRef } from "./live-model-filter.js";
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
@@ -86,6 +82,35 @@ function isGoogleModelNotFoundError(err: unknown): boolean {
return false;
}
function isModelNotFoundErrorMessage(raw: string): boolean {
const msg = raw.trim();
if (!msg) {
return false;
}
if (/\b404\b/.test(msg) && /not(?:[\s_-]+)?found/i.test(msg)) {
return true;
}
if (/not_found_error/i.test(msg)) {
return true;
}
if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not(?:[\s_-]+)?found/i.test(msg)) {
return true;
}
return false;
}
describe("isModelNotFoundErrorMessage", () => {
it("matches whitespace-separated not found errors", () => {
expect(isModelNotFoundErrorMessage("404 model not found")).toBe(true);
expect(isModelNotFoundErrorMessage("model: minimax-text-01 not found")).toBe(true);
});
it("still matches underscore and hyphen variants", () => {
expect(isModelNotFoundErrorMessage("404 model not_found")).toBe(true);
expect(isModelNotFoundErrorMessage("404 model not-found")).toBe(true);
});
});
function isChatGPTUsageLimitErrorMessage(raw: string): boolean {
const msg = raw.toLowerCase();
return msg.includes("hit your chatgpt usage limit") && msg.includes("try again in");
@@ -475,11 +500,7 @@ describeLive("live models (profile keys)", () => {
if (ok.res.stopReason === "error") {
const msg = ok.res.errorMessage ?? "";
if (
allowNotFoundSkip &&
(isModelNotFoundErrorMessage(msg) ||
(model.provider === "minimax" && isMiniMaxModelNotFoundErrorMessage(msg)))
) {
if (allowNotFoundSkip && isModelNotFoundErrorMessage(msg)) {
skipped.push({ model: id, reason: msg });
logProgress(`${progressLabel}: skip (model not found)`);
break;
@@ -500,7 +521,9 @@ describeLive("live models (profile keys)", () => {
}
if (
ok.text.length === 0 &&
(model.provider === "openrouter" || model.provider === "opencode")
(model.provider === "openrouter" ||
model.provider === "opencode" ||
model.provider === "opencode-go")
) {
skipped.push({
model: id,
@@ -563,15 +586,6 @@ describeLive("live models (profile keys)", () => {
logProgress(`${progressLabel}: skip (google model not found)`);
break;
}
if (
allowNotFoundSkip &&
model.provider === "minimax" &&
isMiniMaxModelNotFoundErrorMessage(message)
) {
skipped.push({ model: id, reason: message });
logProgress(`${progressLabel}: skip (model not found)`);
break;
}
if (
allowNotFoundSkip &&
model.provider === "minimax" &&
@@ -592,7 +606,7 @@ describeLive("live models (profile keys)", () => {
}
if (
allowNotFoundSkip &&
model.provider === "opencode" &&
(model.provider === "opencode" || model.provider === "opencode-go") &&
isRateLimitErrorMessage(message)
) {
skipped.push({ model: id, reason: message });

View File

@@ -47,6 +47,7 @@ describe("resolveProviderCapabilities", () => {
it("flags providers that opt out of OpenAI-compatible turn validation", () => {
expect(supportsOpenAiCompatTurnValidation("openrouter")).toBe(false);
expect(supportsOpenAiCompatTurnValidation("opencode")).toBe(false);
expect(supportsOpenAiCompatTurnValidation("opencode-go")).toBe(false);
expect(supportsOpenAiCompatTurnValidation("moonshot")).toBe(true);
});
@@ -63,6 +64,12 @@ describe("resolveProviderCapabilities", () => {
modelId: "gemini-2.0-flash",
}),
).toBe(true);
expect(
shouldSanitizeGeminiThoughtSignaturesForModel({
provider: "opencode-go",
modelId: "google/gemini-2.5-pro-preview",
}),
).toBe(true);
expect(resolveTranscriptToolCallIdMode("mistral", "mistral-large-latest")).toBe("strict9");
});

View File

@@ -66,6 +66,11 @@ const PROVIDER_CAPABILITIES: Record<string, Partial<ProviderCapabilities>> = {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
},
"opencode-go": {
openAiCompatTurnValidation: false,
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],
},
kilocode: {
geminiThoughtSignatureSanitization: true,
geminiThoughtSignatureModelHints: ["gemini"],

View File

@@ -114,7 +114,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
sandboxed: false,
runtimeFirecrawl: {
active: false,
apiKeySource: "secretRef",
apiKeySource: "secretRef", // pragma: allowlist secret
diagnostics: [],
},
});

View File

@@ -652,7 +652,7 @@ describe("web_search Perplexity lazy resolution", () => {
web: {
search: {
provider: "gemini",
gemini: { apiKey: "gemini-config-test" },
gemini: { apiKey: "gemini-config-test" }, // pragma: allowlist secret
perplexity: perplexityConfig as { apiKey?: string; baseUrl?: string; model?: string },
},
},

View File

@@ -19,6 +19,7 @@ const MODEL_PICK_PROVIDER_PREFERENCE = [
"zai",
"openrouter",
"opencode",
"opencode-go",
"github-copilot",
"groq",
"cerebras",

View File

@@ -168,6 +168,7 @@ export function registerOnboardCommand(program: Command) {
togetherApiKey: opts.togetherApiKey as string | undefined,
huggingfaceApiKey: opts.huggingfaceApiKey as string | undefined,
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
opencodeGoApiKey: opts.opencodeGoApiKey as string | undefined,
xaiApiKey: opts.xaiApiKey as string | undefined,
litellmApiKey: opts.litellmApiKey as string | undefined,
volcengineApiKey: opts.volcengineApiKey as string | undefined,

View File

@@ -41,6 +41,7 @@ describe("buildAuthChoiceOptions", () => {
"volcengine-api-key",
"byteplus-api-key",
"vllm",
"opencode-go",
]) {
expect(options.some((opt) => opt.value === value)).toBe(true);
}
@@ -80,4 +81,16 @@ describe("buildAuthChoiceOptions", () => {
expect(chutesGroup).toBeDefined();
expect(chutesGroup?.options.some((opt) => opt.value === "chutes")).toBe(true);
});
it("groups OpenCode Zen and Go under one OpenCode entry", () => {
const { groups } = buildAuthChoiceGroups({
store: EMPTY_STORE,
includeSkip: false,
});
const openCodeGroup = groups.find((group) => group.value === "opencode");
expect(openCodeGroup).toBeDefined();
expect(openCodeGroup?.options.some((opt) => opt.value === "opencode-zen")).toBe(true);
expect(openCodeGroup?.options.some((opt) => opt.value === "opencode-go")).toBe(true);
});
});

View File

@@ -138,10 +138,10 @@ const AUTH_CHOICE_GROUP_DEFS: {
choices: ["ai-gateway-api-key"],
},
{
value: "opencode-zen",
label: "OpenCode Zen",
hint: "API key",
choices: ["opencode-zen"],
value: "opencode",
label: "OpenCode",
hint: "Shared API key for Zen + Go catalogs",
choices: ["opencode-zen", "opencode-go"],
},
{
value: "xiaomi",
@@ -199,6 +199,8 @@ const PROVIDER_AUTH_CHOICE_OPTION_HINTS: Partial<Record<AuthChoice, string>> = {
"venice-api-key": "Privacy-focused inference (uncensored models)",
"together-api-key": "Access to Llama, DeepSeek, Qwen, and more open models",
"huggingface-api-key": "Inference Providers — OpenAI-compatible chat",
"opencode-zen": "Shared OpenCode key; curated Zen catalog",
"opencode-go": "Shared OpenCode key; Kimi/GLM/MiniMax Go catalog",
};
const PROVIDER_AUTH_CHOICE_OPTION_LABELS: Partial<Record<AuthChoice, string>> = {
@@ -206,6 +208,8 @@ const PROVIDER_AUTH_CHOICE_OPTION_LABELS: Partial<Record<AuthChoice, string>> =
"moonshot-api-key-cn": "Kimi API key (.cn)",
"kimi-code-api-key": "Kimi Code API key (subscription)",
"cloudflare-ai-gateway-api-key": "Cloudflare AI Gateway",
"opencode-zen": "OpenCode Zen catalog",
"opencode-go": "OpenCode Go catalog",
};
function buildProviderAuthChoiceOptions(): AuthChoiceOption[] {
@@ -289,7 +293,7 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
{ value: "apiKey", label: "Anthropic API key" },
{
value: "opencode-zen",
label: "OpenCode Zen (multi-model proxy)",
label: "OpenCode Zen catalog",
hint: "Claude, GPT, Gemini via opencode.ai/zen",
},
{ value: "minimax-api", label: "MiniMax M2.5" },
@@ -301,7 +305,7 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
{
value: "minimax-api-lightning",
label: "MiniMax M2.5 Highspeed",
hint: "Official fast tier",
hint: "Official fast tier (legacy: Lightning)",
},
{ value: "qianfan-api-key", label: "Qianfan API key" },
{

View File

@@ -34,6 +34,8 @@ import {
applyMoonshotConfigCn,
applyMoonshotProviderConfig,
applyMoonshotProviderConfigCn,
applyOpencodeGoConfig,
applyOpencodeGoProviderConfig,
applyOpencodeZenConfig,
applyOpencodeZenProviderConfig,
applySyntheticConfig,
@@ -68,6 +70,7 @@ import {
setKimiCodingApiKey,
setMistralApiKey,
setMoonshotApiKey,
setOpencodeGoApiKey,
setOpencodeZenApiKey,
setSyntheticApiKey,
setTogetherApiKey,
@@ -84,6 +87,7 @@ import {
setModelStudioApiKey,
} from "./onboard-auth.js";
import type { AuthChoice, SecretInputMode } from "./onboard-types.js";
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "./opencode-go-model-default.js";
import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js";
import { detectZaiEndpoint } from "./zai-endpoint-detect.js";
@@ -104,6 +108,7 @@ const API_KEY_TOKEN_PROVIDER_AUTH_CHOICE: Record<string, AuthChoice> = {
huggingface: "huggingface-api-key",
mistral: "mistral-api-key",
opencode: "opencode-zen",
"opencode-go": "opencode-go",
kilocode: "kilocode-api-key",
qianfan: "qianfan-api-key",
};
@@ -240,20 +245,40 @@ const SIMPLE_API_KEY_PROVIDER_FLOWS: Partial<Record<AuthChoice, SimpleApiKeyProv
"opencode-zen": {
provider: "opencode",
profileId: "opencode:default",
expectedProviders: ["opencode"],
expectedProviders: ["opencode", "opencode-go"],
envLabel: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode Zen API key",
promptMessage: "Enter OpenCode API key",
setCredential: setOpencodeZenApiKey,
defaultModel: OPENCODE_ZEN_DEFAULT_MODEL,
applyDefaultConfig: applyOpencodeZenConfig,
applyProviderConfig: applyOpencodeZenProviderConfig,
noteDefault: OPENCODE_ZEN_DEFAULT_MODEL,
noteMessage: [
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
"OpenCode uses one API key across the Zen and Go catalogs.",
"Zen provides access to Claude, GPT, Gemini, and more models.",
"Get your API key at: https://opencode.ai/auth",
"OpenCode Zen bills per request. Check your OpenCode dashboard for details.",
"Choose the Zen catalog when you want the curated multi-model proxy.",
].join("\n"),
noteTitle: "OpenCode Zen",
noteTitle: "OpenCode",
},
"opencode-go": {
provider: "opencode-go",
profileId: "opencode-go:default",
expectedProviders: ["opencode", "opencode-go"],
envLabel: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
setCredential: setOpencodeGoApiKey,
defaultModel: OPENCODE_GO_DEFAULT_MODEL_REF,
applyDefaultConfig: applyOpencodeGoConfig,
applyProviderConfig: applyOpencodeGoProviderConfig,
noteDefault: OPENCODE_GO_DEFAULT_MODEL_REF,
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Go provides access to Kimi, GLM, and MiniMax models through the Go catalog.",
"Get your API key at: https://opencode.ai/auth",
"Choose the Go catalog when you want the OpenCode-hosted Kimi/GLM/MiniMax lineup.",
].join("\n"),
noteTitle: "OpenCode",
},
"together-api-key": {
provider: "together",

View File

@@ -39,6 +39,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"minimax-api-lightning": "minimax",
minimax: "lmstudio",
"opencode-zen": "opencode",
"opencode-go": "opencode-go",
"xai-api-key": "xai",
"litellm-api-key": "litellm",
"qwen-portal": "qwen-portal",

View File

@@ -498,6 +498,15 @@ describe("applyAuthChoice", () => {
profileId: "opencode:default",
provider: "opencode",
modelPrefix: "opencode/",
extraProfiles: ["opencode-go:default"],
},
{
authChoice: "opencode-go",
tokenProvider: "opencode-go",
profileId: "opencode-go:default",
provider: "opencode-go",
modelPrefix: "opencode-go/",
extraProfiles: ["opencode:default"],
},
{
authChoice: "together-api-key",
@@ -522,7 +531,7 @@ describe("applyAuthChoice", () => {
},
] as const)(
"uses opts token for $authChoice without prompting",
async ({ authChoice, tokenProvider, profileId, provider, modelPrefix }) => {
async ({ authChoice, tokenProvider, profileId, provider, modelPrefix, extraProfiles }) => {
await setupTempState();
const text = vi.fn();
@@ -554,6 +563,9 @@ describe("applyAuthChoice", () => {
),
).toBe(true);
expect((await readAuthProfile(profileId))?.key).toBe(token);
for (const extraProfile of extraProfiles ?? []) {
expect((await readAuthProfile(extraProfile))?.key).toBe(token);
}
},
);
@@ -805,14 +817,15 @@ describe("applyAuthChoice", () => {
it("keeps existing default model for explicit provider keys when setDefaultModel=false", async () => {
const scenarios: Array<{
authChoice: "xai-api-key" | "opencode-zen";
authChoice: "xai-api-key" | "opencode-zen" | "opencode-go";
token: string;
promptMessage: string;
existingPrimary: string;
expectedOverride: string;
profileId?: string;
profileProvider?: string;
expectProviderConfigUndefined?: "opencode-zen";
extraProfileId?: string;
expectProviderConfigUndefined?: "opencode" | "opencode-go" | "opencode-zen";
agentId?: string;
}> = [
{
@@ -828,10 +841,24 @@ describe("applyAuthChoice", () => {
{
authChoice: "opencode-zen",
token: "sk-opencode-zen-test",
promptMessage: "Enter OpenCode Zen API key",
promptMessage: "Enter OpenCode API key",
existingPrimary: "anthropic/claude-opus-4-5",
expectedOverride: "opencode/claude-opus-4-6",
expectProviderConfigUndefined: "opencode-zen",
profileId: "opencode:default",
profileProvider: "opencode",
extraProfileId: "opencode-go:default",
expectProviderConfigUndefined: "opencode",
},
{
authChoice: "opencode-go",
token: "sk-opencode-go-test",
promptMessage: "Enter OpenCode API key",
existingPrimary: "anthropic/claude-opus-4-5",
expectedOverride: "opencode-go/kimi-k2.5",
profileId: "opencode-go:default",
profileProvider: "opencode-go",
extraProfileId: "opencode:default",
expectProviderConfigUndefined: "opencode-go",
},
];
for (const scenario of scenarios) {
@@ -863,6 +890,9 @@ describe("applyAuthChoice", () => {
});
expect((await readAuthProfile(scenario.profileId))?.key).toBe(scenario.token);
}
if (scenario.extraProfileId) {
expect((await readAuthProfile(scenario.extraProfileId))?.key).toBe(scenario.token);
}
if (scenario.expectProviderConfigUndefined) {
expect(
result.config.models?.providers?.[scenario.expectProviderConfigUndefined],

View File

@@ -105,18 +105,22 @@ export function noteOpencodeProviderOverrides(cfg: OpenClawConfig): void {
if (providers["opencode-zen"]) {
overrides.push("opencode-zen");
}
if (providers["opencode-go"]) {
overrides.push("opencode-go");
}
if (overrides.length === 0) {
return;
}
const lines = overrides.flatMap((id) => {
const providerLabel = id === "opencode-go" ? "OpenCode Go" : "OpenCode Zen";
const providerEntry = providers[id];
const api =
isRecord(providerEntry) && typeof providerEntry.api === "string"
? providerEntry.api
: undefined;
return [
`- models.providers.${id} is set; this overrides the built-in OpenCode Zen catalog.`,
`- models.providers.${id} is set; this overrides the built-in ${providerLabel} catalog.`,
api ? `- models.providers.${id}.api=${api}` : null,
].filter((line): line is string => Boolean(line));
});
@@ -124,7 +128,7 @@ export function noteOpencodeProviderOverrides(cfg: OpenClawConfig): void {
lines.push(
"- Remove these entries to restore per-model API routing + costs (then re-run onboarding if needed).",
);
note(lines.join("\n"), "OpenCode Zen");
note(lines.join("\n"), "OpenCode");
}
export function noteIncludeConfinementWarning(snapshot: {

View File

@@ -41,6 +41,10 @@ describe("doctor command", () => {
api: "openai-completions",
baseUrl: "https://opencode.ai/zen/v1",
},
"opencode-go": {
api: "openai-completions",
baseUrl: "https://opencode.ai/zen/go/v1",
},
},
},
},
@@ -53,7 +57,9 @@ describe("doctor command", () => {
const warned = note.mock.calls.some(
([message, title]) =>
title === "OpenCode Zen" && String(message).includes("models.providers.opencode"),
title === "OpenCode" &&
String(message).includes("models.providers.opencode") &&
String(message).includes("models.providers.opencode-go"),
);
expect(warned).toBe(true);
});

View File

@@ -0,0 +1,36 @@
import type { OpenClawConfig } from "../config/config.js";
import { applyAgentDefaultModelPrimary } from "./onboard-auth.config-shared.js";
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "./opencode-go-model-default.js";
const OPENCODE_GO_ALIAS_DEFAULTS: Record<string, string> = {
"opencode-go/kimi-k2.5": "Kimi",
"opencode-go/glm-5": "GLM",
"opencode-go/minimax-m2.5": "MiniMax",
};
export function applyOpencodeGoProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
// Use the built-in opencode-go provider from pi-ai; only seed allowlist aliases.
const models = { ...cfg.agents?.defaults?.models };
for (const [modelRef, alias] of Object.entries(OPENCODE_GO_ALIAS_DEFAULTS)) {
models[modelRef] = {
...models[modelRef],
alias: models[modelRef]?.alias ?? alias,
};
}
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
models,
},
},
};
}
export function applyOpencodeGoConfig(cfg: OpenClawConfig): OpenClawConfig {
const next = applyOpencodeGoProviderConfig(cfg);
return applyAgentDefaultModelPrimary(next, OPENCODE_GO_DEFAULT_MODEL_REF);
}

View File

@@ -3,6 +3,7 @@ import {
setByteplusApiKey,
setCloudflareAiGatewayConfig,
setMoonshotApiKey,
setOpencodeZenApiKey,
setOpenaiApiKey,
setVolcengineApiKey,
} from "./onboard-auth.js";
@@ -22,6 +23,7 @@ describe("onboard auth credentials secret refs", () => {
"CLOUDFLARE_AI_GATEWAY_API_KEY",
"VOLCANO_ENGINE_API_KEY",
"BYTEPLUS_API_KEY",
"OPENCODE_API_KEY",
]);
afterEach(async () => {
@@ -207,4 +209,25 @@ describe("onboard auth credentials secret refs", () => {
});
expect(parsed.profiles?.["byteplus:default"]?.key).toBeUndefined();
});
it("stores shared OpenCode credentials for both runtime providers", async () => {
const env = await setupAuthTestEnv("openclaw-onboard-auth-credentials-opencode-");
lifecycle.setStateDir(env.stateDir);
process.env.OPENCODE_API_KEY = "sk-opencode-env"; // pragma: allowlist secret
await setOpencodeZenApiKey("sk-opencode-env", env.agentDir, {
secretInputMode: "ref", // pragma: allowlist secret
});
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(env.agentDir);
expect(parsed.profiles?.["opencode:default"]).toMatchObject({
keyRef: { source: "env", provider: "default", id: "OPENCODE_API_KEY" },
});
expect(parsed.profiles?.["opencode-go:default"]).toMatchObject({
keyRef: { source: "env", provider: "default", id: "OPENCODE_API_KEY" },
});
});
});

View File

@@ -433,11 +433,30 @@ export async function setOpencodeZenApiKey(
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "opencode:default",
credential: buildApiKeyCredential("opencode", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
await setSharedOpencodeApiKey(key, agentDir, options);
}
export async function setOpencodeGoApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
await setSharedOpencodeApiKey(key, agentDir, options);
}
async function setSharedOpencodeApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
const resolvedAgentDir = resolveAuthAgentDir(agentDir);
for (const provider of ["opencode", "opencode-go"] as const) {
upsertAuthProfile({
profileId: `${provider}:default`,
credential: buildApiKeyCredential(provider, key, undefined, options),
agentDir: resolvedAgentDir,
});
}
}
export async function setTogetherApiKey(

View File

@@ -16,6 +16,8 @@ import {
applyMistralProviderConfig,
applyMinimaxApiConfig,
applyMinimaxApiProviderConfig,
applyOpencodeGoConfig,
applyOpencodeGoProviderConfig,
applyOpencodeZenConfig,
applyOpencodeZenProviderConfig,
applyOpenrouterConfig,
@@ -675,6 +677,11 @@ describe("allowlist provider helpers", () => {
modelRef: "opencode/claude-opus-4-6",
alias: "My Opus",
},
{
applyConfig: applyOpencodeGoProviderConfig,
modelRef: "opencode-go/kimi-k2.5",
alias: "Kimi",
},
{
applyConfig: applyOpenrouterProviderConfig,
modelRef: OPENROUTER_DEFAULT_MODEL_REF,
@@ -729,6 +736,10 @@ describe("default-model config helpers", () => {
applyConfig: applyOpencodeZenConfig,
primaryModel: "opencode/claude-opus-4-6",
},
{
applyConfig: applyOpencodeGoConfig,
primaryModel: "opencode-go/kimi-k2.5",
},
{
applyConfig: applyOpenrouterConfig,
primaryModel: OPENROUTER_DEFAULT_MODEL_REF,

View File

@@ -60,6 +60,10 @@ export {
applyOpencodeZenConfig,
applyOpencodeZenProviderConfig,
} from "./onboard-auth.config-opencode.js";
export {
applyOpencodeGoConfig,
applyOpencodeGoProviderConfig,
} from "./onboard-auth.config-opencode-go.js";
export {
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
KILOCODE_DEFAULT_MODEL_REF,
@@ -77,6 +81,7 @@ export {
setMinimaxApiKey,
setMistralApiKey,
setMoonshotApiKey,
setOpencodeGoApiKey,
setOpencodeZenApiKey,
setOpenrouterApiKey,
setSyntheticApiKey,

View File

@@ -42,11 +42,6 @@ let upsertAuthProfile: typeof import("../agents/auth-profiles.js").upsertAuthPro
type ProviderAuthConfigSnapshot = {
auth?: { profiles?: Record<string, { provider?: string; mode?: string }> };
agents?: { defaults?: { model?: { primary?: string } } };
talk?: {
provider?: string;
apiKey?: string | { source?: string; id?: string };
providers?: Record<string, { apiKey?: string | { source?: string; id?: string } }>;
};
models?: {
providers?: Record<
string,
@@ -362,38 +357,6 @@ describe("onboard (non-interactive): provider auth", () => {
});
});
it("does not persist talk fallback secrets when OpenAI ref onboarding starts from an empty config", async () => {
await withOnboardEnv("openclaw-onboard-openai-ref-no-talk-leak-", async (env) => {
await withEnvAsync(
{
OPENAI_API_KEY: "sk-openai-env-key", // pragma: allowlist secret
ELEVENLABS_API_KEY: "elevenlabs-env-key", // pragma: allowlist secret
},
async () => {
const cfg = await runOnboardingAndReadConfig(env, {
authChoice: "openai-api-key",
secretInputMode: "ref", // pragma: allowlist secret
});
expect(cfg.agents?.defaults?.model?.primary).toBe(OPENAI_DEFAULT_MODEL);
expect(cfg.talk).toBeUndefined();
const store = ensureAuthProfileStore();
const profile = store.profiles["openai:default"];
expect(profile?.type).toBe("api_key");
if (profile?.type === "api_key") {
expect(profile.key).toBeUndefined();
expect(profile.keyRef).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
});
}
},
);
});
});
it.each([
{
name: "anthropic",
@@ -479,7 +442,7 @@ describe("onboard (non-interactive): provider auth", () => {
},
);
it("stores the detected env alias as keyRef for opencode ref mode", async () => {
it("stores the detected env alias as keyRef for both OpenCode runtime providers", async () => {
await withOnboardEnv("openclaw-onboard-ref-opencode-alias-", async ({ runtime }) => {
await withEnvAsync(
{
@@ -494,15 +457,17 @@ describe("onboard (non-interactive): provider auth", () => {
});
const store = ensureAuthProfileStore();
const profile = store.profiles["opencode:default"];
expect(profile?.type).toBe("api_key");
if (profile?.type === "api_key") {
expect(profile.key).toBeUndefined();
expect(profile.keyRef).toEqual({
source: "env",
provider: "default",
id: "OPENCODE_ZEN_API_KEY",
});
for (const profileId of ["opencode:default", "opencode-go:default"]) {
const profile = store.profiles[profileId];
expect(profile?.type).toBe("api_key");
if (profile?.type === "api_key") {
expect(profile.key).toBeUndefined();
expect(profile.keyRef).toEqual({
source: "env",
provider: "default",
id: "OPENCODE_ZEN_API_KEY",
});
}
}
},
);

View File

@@ -27,6 +27,7 @@ type AuthChoiceFlagOptions = Pick<
| "xiaomiApiKey"
| "minimaxApiKey"
| "opencodeZenApiKey"
| "opencodeGoApiKey"
| "xaiApiKey"
| "litellmApiKey"
| "qianfanApiKey"

View File

@@ -23,6 +23,7 @@ import {
applyMinimaxConfig,
applyMoonshotConfig,
applyMoonshotConfigCn,
applyOpencodeGoConfig,
applyOpencodeZenConfig,
applyOpenrouterConfig,
applySyntheticConfig,
@@ -48,6 +49,7 @@ import {
setMinimaxApiKey,
setMoonshotApiKey,
setOpenaiApiKey,
setOpencodeGoApiKey,
setOpencodeZenApiKey,
setOpenrouterApiKey,
setSyntheticApiKey,
@@ -926,6 +928,33 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyOpencodeZenConfig(nextConfig);
}
if (authChoice === "opencode-go") {
const resolved = await resolveApiKey({
provider: "opencode-go",
cfg: baseConfig,
flagValue: opts.opencodeGoApiKey,
flagName: "--opencode-go-api-key",
envVar: "OPENCODE_API_KEY",
runtime,
});
if (!resolved) {
return null;
}
if (
!(await maybeSetResolvedApiKey(resolved, (value) =>
setOpencodeGoApiKey(value, undefined, apiKeyStorageOptions),
))
) {
return null;
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "opencode-go:default",
provider: "opencode-go",
mode: "api_key",
});
return applyOpencodeGoConfig(nextConfig);
}
if (authChoice === "together-api-key") {
const resolved = await resolveApiKey({
provider: "together",

View File

@@ -20,6 +20,7 @@ type OnboardProviderAuthOptionKey = keyof Pick<
| "togetherApiKey"
| "huggingfaceApiKey"
| "opencodeZenApiKey"
| "opencodeGoApiKey"
| "xaiApiKey"
| "litellmApiKey"
| "qianfanApiKey"
@@ -163,7 +164,14 @@ export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray<OnboardProviderAuthFlag>
authChoice: "opencode-zen",
cliFlag: "--opencode-zen-api-key",
cliOption: "--opencode-zen-api-key <key>",
description: "OpenCode Zen API key",
description: "OpenCode API key (Zen catalog)",
},
{
optionKey: "opencodeGoApiKey",
authChoice: "opencode-go",
cliFlag: "--opencode-go-api-key",
cliOption: "--opencode-go-api-key <key>",
description: "OpenCode API key (Go catalog)",
},
{
optionKey: "xaiApiKey",

View File

@@ -41,6 +41,7 @@ export type AuthChoice =
| "minimax-api-lightning"
| "minimax-portal"
| "opencode-zen"
| "opencode-go"
| "github-copilot"
| "copilot-proxy"
| "qwen-portal"
@@ -68,7 +69,7 @@ export type AuthChoiceGroupId =
| "moonshot"
| "zai"
| "xiaomi"
| "opencode-zen"
| "opencode"
| "minimax"
| "synthetic"
| "venice"
@@ -134,6 +135,7 @@ export type OnboardOptions = {
togetherApiKey?: string;
huggingfaceApiKey?: string;
opencodeZenApiKey?: string;
opencodeGoApiKey?: string;
xaiApiKey?: string;
volcengineApiKey?: string;
byteplusApiKey?: string;

View File

@@ -0,0 +1,11 @@
import type { OpenClawConfig } from "../config/config.js";
import { applyAgentDefaultPrimaryModel } from "./model-default.js";
export const OPENCODE_GO_DEFAULT_MODEL_REF = "opencode-go/kimi-k2.5";
export function applyOpencodeGoModelDefault(cfg: OpenClawConfig): {
next: OpenClawConfig;
changed: boolean;
} {
return applyAgentDefaultPrimaryModel({ cfg, model: OPENCODE_GO_DEFAULT_MODEL_REF });
}

View File

@@ -15,6 +15,7 @@ export const PROVIDER_ENV_VARS: Record<string, readonly string[]> = {
litellm: ["LITELLM_API_KEY"],
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
together: ["TOGETHER_API_KEY"],
huggingface: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
qianfan: ["QIANFAN_API_KEY"],

View File

@@ -184,7 +184,7 @@ describe("runtime web tools resolution", () => {
},
}),
env: {
BRAVE_API_KEY_REF: "brave-runtime-key",
BRAVE_API_KEY_REF: "brave-runtime-key", // pragma: allowlist secret
},
});
@@ -225,7 +225,7 @@ describe("runtime web tools resolution", () => {
},
}),
env: {
GEMINI_API_KEY_REF: "gemini-runtime-key",
GEMINI_API_KEY_REF: "gemini-runtime-key", // pragma: allowlist secret
},
});
@@ -260,7 +260,7 @@ describe("runtime web tools resolution", () => {
},
}),
env: {
GEMINI_API_KEY_REF: "gemini-runtime-key",
GEMINI_API_KEY_REF: "gemini-runtime-key", // pragma: allowlist secret
},
});
@@ -397,7 +397,7 @@ describe("runtime web tools resolution", () => {
},
}),
env: {
FIRECRAWL_API_KEY: "firecrawl-fallback-key",
FIRECRAWL_API_KEY: "firecrawl-fallback-key", // pragma: allowlist secret
},
});

View File

@@ -18,7 +18,7 @@ const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
type WebSearchProvider = (typeof WEB_SEARCH_PROVIDERS)[number];
type SecretResolutionSource = "config" | "secretRef" | "env" | "missing";
type SecretResolutionSource = "config" | "secretRef" | "env" | "missing"; // pragma: allowlist secret
type RuntimeWebProviderSource = "configured" | "auto-detect" | "none";
export type RuntimeWebDiagnosticCode =