fix: polish opencode-zen onboarding (#623) (thanks @magimetal)
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Models/Auth: add OpenCode Zen (multi-model proxy) onboarding. (#623) — thanks @magimetal
|
||||||
- WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete
|
- WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete
|
||||||
- WhatsApp: include phone numbers when multiple contacts are shared. (#625) — thanks @mahmoudashraf93
|
- WhatsApp: include phone numbers when multiple contacts are shared. (#625) — thanks @mahmoudashraf93
|
||||||
- Agents: warn on small context windows (<32k) and block unusable ones (<16k). — thanks @steipete
|
- Agents: warn on small context windows (<32k) and block unusable ones (<16k). — thanks @steipete
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ Options:
|
|||||||
- `--workspace <dir>`
|
- `--workspace <dir>`
|
||||||
- `--non-interactive`
|
- `--non-interactive`
|
||||||
- `--mode <local|remote>`
|
- `--mode <local|remote>`
|
||||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax-api|minimax|skip>`
|
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax-api|minimax|opencode-zen|skip>`
|
||||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token <token>` (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`)
|
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||||
@@ -186,6 +186,7 @@ Options:
|
|||||||
- `--openai-api-key <key>`
|
- `--openai-api-key <key>`
|
||||||
- `--gemini-api-key <key>`
|
- `--gemini-api-key <key>`
|
||||||
- `--minimax-api-key <key>`
|
- `--minimax-api-key <key>`
|
||||||
|
- `--opencode-zen-api-key <key>`
|
||||||
- `--gateway-port <port>`
|
- `--gateway-port <port>`
|
||||||
- `--gateway-bind <loopback|lan|tailnet|auto>`
|
- `--gateway-bind <loopback|lan|tailnet|auto>`
|
||||||
- `--gateway-auth <off|token|password>`
|
- `--gateway-auth <off|token|password>`
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
|||||||
- **OpenAI Codex OAuth**: browser flow; paste the `code#state`.
|
- **OpenAI Codex OAuth**: browser flow; paste the `code#state`.
|
||||||
- Sets `agents.defaults.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`.
|
- 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 saves it to `~/.clawdbot/.env` so launchd can read it.
|
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.clawdbot/.env` so launchd can read it.
|
||||||
|
- **OpenCode Zen (multi-model proxy)**: prompts for `OPENCODE_ZEN_API_KEY` (get it at https://opencode.ai/auth).
|
||||||
- **API key**: stores the key for you.
|
- **API key**: stores the key for you.
|
||||||
- **MiniMax M2.1 (minimax.io)**: config is auto‑written for the OpenAI-compatible `/v1` endpoint.
|
- **MiniMax M2.1 (minimax.io)**: config is auto‑written for the OpenAI-compatible `/v1` endpoint.
|
||||||
- **MiniMax API (platform.minimax.io)**: config is auto‑written for the Anthropic-compatible `/anthropic` endpoint.
|
- **MiniMax API (platform.minimax.io)**: config is auto‑written for the Anthropic-compatible `/anthropic` endpoint.
|
||||||
@@ -185,6 +186,17 @@ clawdbot onboard --non-interactive \
|
|||||||
--gateway-bind loopback
|
--gateway-bind loopback
|
||||||
```
|
```
|
||||||
|
|
||||||
|
OpenCode Zen example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --non-interactive \
|
||||||
|
--mode local \
|
||||||
|
--auth-choice opencode-zen \
|
||||||
|
--opencode-zen-api-key "$OPENCODE_ZEN_API_KEY" \
|
||||||
|
--gateway-port 18789 \
|
||||||
|
--gateway-bind loopback
|
||||||
|
```
|
||||||
|
|
||||||
Add agent (non‑interactive) example:
|
Add agent (non‑interactive) example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -133,6 +133,29 @@ describe("cli program", () => {
|
|||||||
expect(setupCommand).not.toHaveBeenCalled();
|
expect(setupCommand).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes opencode-zen api key to onboard", async () => {
|
||||||
|
const program = buildProgram();
|
||||||
|
await program.parseAsync(
|
||||||
|
[
|
||||||
|
"onboard",
|
||||||
|
"--non-interactive",
|
||||||
|
"--auth-choice",
|
||||||
|
"opencode-zen",
|
||||||
|
"--opencode-zen-api-key",
|
||||||
|
"sk-opencode-zen-test",
|
||||||
|
],
|
||||||
|
{ from: "user" },
|
||||||
|
);
|
||||||
|
expect(onboardCommand).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
nonInteractive: true,
|
||||||
|
authChoice: "opencode-zen",
|
||||||
|
opencodeZenApiKey: "sk-opencode-zen-test",
|
||||||
|
}),
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("runs providers login", async () => {
|
it("runs providers login", async () => {
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
await program.parseAsync(["providers", "login", "--account", "work"], {
|
await program.parseAsync(["providers", "login", "--account", "work"], {
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export function buildProgram() {
|
|||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--auth-choice <choice>",
|
||||||
"Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
|
"Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax-api|minimax|opencode-zen|skip",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--token-provider <id>",
|
"--token-provider <id>",
|
||||||
@@ -267,6 +267,7 @@ export function buildProgram() {
|
|||||||
.option("--openai-api-key <key>", "OpenAI API key")
|
.option("--openai-api-key <key>", "OpenAI API key")
|
||||||
.option("--gemini-api-key <key>", "Gemini API key")
|
.option("--gemini-api-key <key>", "Gemini API key")
|
||||||
.option("--minimax-api-key <key>", "MiniMax API key")
|
.option("--minimax-api-key <key>", "MiniMax API key")
|
||||||
|
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
|
||||||
.option("--gateway-port <port>", "Gateway port")
|
.option("--gateway-port <port>", "Gateway port")
|
||||||
.option("--gateway-bind <mode>", "Gateway bind: loopback|lan|tailnet|auto")
|
.option("--gateway-bind <mode>", "Gateway bind: loopback|lan|tailnet|auto")
|
||||||
.option("--gateway-auth <mode>", "Gateway auth: off|token|password")
|
.option("--gateway-auth <mode>", "Gateway auth: off|token|password")
|
||||||
@@ -314,7 +315,9 @@ export function buildProgram() {
|
|||||||
| "gemini-api-key"
|
| "gemini-api-key"
|
||||||
| "apiKey"
|
| "apiKey"
|
||||||
| "minimax-cloud"
|
| "minimax-cloud"
|
||||||
|
| "minimax-api"
|
||||||
| "minimax"
|
| "minimax"
|
||||||
|
| "opencode-zen"
|
||||||
| "skip"
|
| "skip"
|
||||||
| undefined,
|
| undefined,
|
||||||
tokenProvider: opts.tokenProvider as string | undefined,
|
tokenProvider: opts.tokenProvider as string | undefined,
|
||||||
@@ -325,6 +328,7 @@ export function buildProgram() {
|
|||||||
openaiApiKey: opts.openaiApiKey as string | undefined,
|
openaiApiKey: opts.openaiApiKey as string | undefined,
|
||||||
geminiApiKey: opts.geminiApiKey as string | undefined,
|
geminiApiKey: opts.geminiApiKey as string | undefined,
|
||||||
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
||||||
|
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
|
||||||
gatewayPort:
|
gatewayPort:
|
||||||
typeof opts.gatewayPort === "string"
|
typeof opts.gatewayPort === "string"
|
||||||
? Number.parseInt(opts.gatewayPort, 10)
|
? Number.parseInt(opts.gatewayPort, 10)
|
||||||
|
|||||||
@@ -97,4 +97,57 @@ describe("applyAuthChoice", () => {
|
|||||||
};
|
};
|
||||||
expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test");
|
expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not override the default model when selecting opencode-zen without setDefaultModel", async () => {
|
||||||
|
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||||
|
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||||
|
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
|
||||||
|
const text = vi.fn().mockResolvedValue("sk-opencode-zen-test");
|
||||||
|
const select: WizardPrompter["select"] = vi.fn(
|
||||||
|
async (params) => params.options[0]?.value as never,
|
||||||
|
);
|
||||||
|
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
||||||
|
const prompter: WizardPrompter = {
|
||||||
|
intro: vi.fn(noopAsync),
|
||||||
|
outro: vi.fn(noopAsync),
|
||||||
|
note: vi.fn(noopAsync),
|
||||||
|
select,
|
||||||
|
multiselect,
|
||||||
|
text,
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||||
|
};
|
||||||
|
const runtime: RuntimeEnv = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn((code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await applyAuthChoice({
|
||||||
|
authChoice: "opencode-zen",
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
setDefaultModel: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ message: "Enter OpenCode Zen API key" }),
|
||||||
|
);
|
||||||
|
expect(result.config.agents?.defaults?.model?.primary).toBe(
|
||||||
|
"anthropic/claude-opus-4-5",
|
||||||
|
);
|
||||||
|
expect(result.config.models?.providers?.["opencode-zen"]).toBeDefined();
|
||||||
|
expect(result.agentModelOverride).toBe("opencode-zen/claude-opus-4-5");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
applyMinimaxHostedProviderConfig,
|
applyMinimaxHostedProviderConfig,
|
||||||
applyMinimaxProviderConfig,
|
applyMinimaxProviderConfig,
|
||||||
applyOpencodeZenConfig,
|
applyOpencodeZenConfig,
|
||||||
|
applyOpencodeZenProviderConfig,
|
||||||
MINIMAX_HOSTED_MODEL_REF,
|
MINIMAX_HOSTED_MODEL_REF,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
@@ -678,7 +679,7 @@ export async function applyAuthChoice(params: {
|
|||||||
"Model configured",
|
"Model configured",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
nextConfig = applyOpencodeZenConfig(nextConfig);
|
nextConfig = applyOpencodeZenProviderConfig(nextConfig);
|
||||||
agentModelOverride = OPENCODE_ZEN_DEFAULT_MODEL;
|
agentModelOverride = OPENCODE_ZEN_DEFAULT_MODEL;
|
||||||
await noteAgentModel(OPENCODE_ZEN_DEFAULT_MODEL);
|
await noteAgentModel(OPENCODE_ZEN_DEFAULT_MODEL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyMinimaxApiConfig,
|
applyMinimaxApiConfig,
|
||||||
applyMinimaxApiProviderConfig,
|
applyMinimaxApiProviderConfig,
|
||||||
|
applyOpencodeZenConfig,
|
||||||
|
applyOpencodeZenProviderConfig,
|
||||||
writeOAuthCredentials,
|
writeOAuthCredentials,
|
||||||
} from "./onboard-auth.js";
|
} from "./onboard-auth.js";
|
||||||
|
|
||||||
@@ -250,3 +252,61 @@ describe("applyMinimaxApiProviderConfig", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("applyOpencodeZenProviderConfig", () => {
|
||||||
|
it("adds opencode-zen provider with correct settings", () => {
|
||||||
|
const cfg = applyOpencodeZenProviderConfig({});
|
||||||
|
expect(cfg.models?.providers?.["opencode-zen"]).toMatchObject({
|
||||||
|
baseUrl: "https://opencode.ai/zen/v1",
|
||||||
|
apiKey: "opencode-zen",
|
||||||
|
api: "openai-completions",
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
cfg.models?.providers?.["opencode-zen"]?.models.length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds allowlist entries for fallback models", () => {
|
||||||
|
const cfg = applyOpencodeZenProviderConfig({});
|
||||||
|
const models = cfg.agents?.defaults?.models ?? {};
|
||||||
|
expect(Object.keys(models)).toContain("opencode-zen/claude-opus-4-5");
|
||||||
|
expect(Object.keys(models)).toContain("opencode-zen/gpt-5.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves existing alias for the default model", () => {
|
||||||
|
const cfg = applyOpencodeZenProviderConfig({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
models: {
|
||||||
|
"opencode-zen/claude-opus-4-5": { alias: "My Opus" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
cfg.agents?.defaults?.models?.["opencode-zen/claude-opus-4-5"]?.alias,
|
||||||
|
).toBe("My Opus");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyOpencodeZenConfig", () => {
|
||||||
|
it("sets correct primary model", () => {
|
||||||
|
const cfg = applyOpencodeZenConfig({});
|
||||||
|
expect(cfg.agents?.defaults?.model?.primary).toBe(
|
||||||
|
"opencode-zen/claude-opus-4-5",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves existing model fallbacks", () => {
|
||||||
|
const cfg = applyOpencodeZenConfig({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { fallbacks: ["anthropic/claude-opus-4-5"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(cfg.agents?.defaults?.model?.fallbacks).toEqual([
|
||||||
|
"anthropic/claude-opus-4-5",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -402,18 +402,24 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) {
|
|||||||
export function applyOpencodeZenProviderConfig(
|
export function applyOpencodeZenProviderConfig(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
): ClawdbotConfig {
|
): ClawdbotConfig {
|
||||||
|
const opencodeModels = getOpencodeZenStaticFallbackModels();
|
||||||
|
|
||||||
const providers = { ...cfg.models?.providers };
|
const providers = { ...cfg.models?.providers };
|
||||||
providers["opencode-zen"] = {
|
providers["opencode-zen"] = {
|
||||||
baseUrl: OPENCODE_ZEN_API_BASE_URL,
|
baseUrl: OPENCODE_ZEN_API_BASE_URL,
|
||||||
apiKey: "opencode-zen",
|
apiKey: "opencode-zen",
|
||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: getOpencodeZenStaticFallbackModels(),
|
models: opencodeModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
const models = { ...cfg.agents?.defaults?.models };
|
const models = { ...cfg.agents?.defaults?.models };
|
||||||
|
for (const model of opencodeModels) {
|
||||||
|
const key = `opencode-zen/${model.id}`;
|
||||||
|
models[key] = models[key] ?? {};
|
||||||
|
}
|
||||||
models[OPENCODE_ZEN_DEFAULT_MODEL_REF] = {
|
models[OPENCODE_ZEN_DEFAULT_MODEL_REF] = {
|
||||||
...models[OPENCODE_ZEN_DEFAULT_MODEL_REF],
|
...models[OPENCODE_ZEN_DEFAULT_MODEL_REF],
|
||||||
alias: "Opus",
|
alias: models[OPENCODE_ZEN_DEFAULT_MODEL_REF]?.alias ?? "Opus",
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user