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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -86,12 +86,13 @@ OpenClaw ships with the pi‑ai 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 pi‑ai 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 you’ve 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 you’ve 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)
|
||||
|
||||
|
||||
@@ -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: don’t try to hardcode “all models” in docs. The authoritative list is whatever `discoverModels(...)` returns on your machine + whatever keys are available.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
45
docs/providers/opencode-go.md
Normal file
45
docs/providers/opencode-go.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
sandboxed: false,
|
||||
runtimeFirecrawl: {
|
||||
active: false,
|
||||
apiKeySource: "secretRef",
|
||||
apiKeySource: "secretRef", // pragma: allowlist secret
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ const MODEL_PICK_PROVIDER_PREFERENCE = [
|
||||
"zai",
|
||||
"openrouter",
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"github-copilot",
|
||||
"groq",
|
||||
"cerebras",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
36
src/commands/onboard-auth.config-opencode-go.ts
Normal file
36
src/commands/onboard-auth.config-opencode-go.ts
Normal 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);
|
||||
}
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ type AuthChoiceFlagOptions = Pick<
|
||||
| "xiaomiApiKey"
|
||||
| "minimaxApiKey"
|
||||
| "opencodeZenApiKey"
|
||||
| "opencodeGoApiKey"
|
||||
| "xaiApiKey"
|
||||
| "litellmApiKey"
|
||||
| "qianfanApiKey"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
src/commands/opencode-go-model-default.ts
Normal file
11
src/commands/opencode-go-model-default.ts
Normal 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 });
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user