From 20705d1b3787e56be70fcd7e1de5038c20556646 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 02:48:53 +0100 Subject: [PATCH] fix: set codex oauth model default --- CHANGELOG.md | 2 + docs/onboarding.md | 1 + docs/wizard.md | 4 +- src/agents/model-auth.test.ts | 72 +++++++++++++++++++++++++++ src/agents/model-auth.ts | 10 ++++ src/wizard/onboarding.ts | 91 +++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e954950..6ed9ccd79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixes - Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step. +- Onboarding: when OpenAI Codex OAuth is used, default to `openai-codex/gpt-5.2` and warn if the selected model lacks auth. - CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup). - Auth: prioritize OAuth profiles but fall back to API keys when refresh fails; stored profiles now load without explicit auth order. - Docs: add group chat participation guidance to the AGENTS template. @@ -27,6 +28,7 @@ - Model: `/model list` is an alias for `/model`. - Model: `/model` output now includes auth source location (env/auth.json/models.json). - Model: avoid duplicate `missing (missing)` auth labels in `/model` list output. +- Auth: when `openai` has no API key but Codex OAuth exists, suggest `openai-codex/gpt-5.2` vs `OPENAI_API_KEY`. - Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding. - Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments. - Control UI: show a reading indicator bubble while the assistant is responding. diff --git a/docs/onboarding.md b/docs/onboarding.md index d1b1c1dd1..bc897e5e3 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -50,6 +50,7 @@ The macOS app should: - Auto-capture the callback on `http://127.0.0.1:1455/auth/callback` when possible. - If the callback fails, prompt the user to paste the redirect URL or code. - Store credentials in `~/.clawdbot/credentials/oauth.json` (same OAuth store as Anthropic). +- Set `agent.model` to `openai-codex/gpt-5.2` when the model is unset or `openai/*`. ### Alternative: API key (instructions only) diff --git a/docs/wizard.md b/docs/wizard.md index ccb885f7d..883f4e003 100644 --- a/docs/wizard.md +++ b/docs/wizard.md @@ -49,10 +49,12 @@ It does **not** install or change anything on the remote host. 2) **Model/Auth** - **Anthropic OAuth (recommended)**: browser flow; paste the `code#state`. - **OpenAI Codex OAuth**: browser flow; paste the `code#state`. + - Sets `agent.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`. - **API key**: stores the key for you. - **Minimax M2.1 (LM Studio)**: config is auto‑written for the LM Studio endpoint. - **Skip**: no auth configured yet. - - OAuth credentials live in `~/.clawdbot/credentials/oauth.json`; auth profiles live in `~/.clawdbot/agent/auth-profiles.json` (API keys + OAuth). + - Wizard runs a model check and warns if the configured model is unknown or missing auth. + - OAuth credentials live in `~/.clawdbot/credentials/oauth.json`; auth profiles live in `~/.clawdbot/agent/auth-profiles.json` (API keys + OAuth). 3) **Workspace** - Default `~/clawd` (configurable). diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 578eeec20..ac37fad6a 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -87,4 +87,76 @@ describe("getApiKeyForModel", () => { await fs.rm(tempDir, { recursive: true, force: true }); } }); + + it("suggests openai-codex when only Codex OAuth is configured", async () => { + const previousStateDir = process.env.CLAWDBOT_STATE_DIR; + const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; + const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; + const previousOpenAiKey = process.env.OPENAI_API_KEY; + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-")); + + try { + delete process.env.OPENAI_API_KEY; + process.env.CLAWDBOT_STATE_DIR = tempDir; + process.env.CLAWDBOT_AGENT_DIR = path.join(tempDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; + + const authProfilesPath = path.join( + tempDir, + "agent", + "auth-profiles.json", + ); + await fs.mkdir(path.dirname(authProfilesPath), { + recursive: true, + mode: 0o700, + }); + await fs.writeFile( + authProfilesPath, + `${JSON.stringify( + { + version: 1, + profiles: { + "openai-codex:default": { + type: "oauth", + provider: "openai-codex", + ...oauthFixture, + }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + await expect( + resolveApiKeyForProvider({ provider: "openai" }), + ).rejects.toThrow(/openai-codex\/gpt-5\\.2/); + } finally { + if (previousOpenAiKey === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = previousOpenAiKey; + } + if (previousStateDir === undefined) { + delete process.env.CLAWDBOT_STATE_DIR; + } else { + process.env.CLAWDBOT_STATE_DIR = previousStateDir; + } + if (previousAgentDir === undefined) { + delete process.env.CLAWDBOT_AGENT_DIR; + } else { + process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; + } + if (previousPiAgentDir === undefined) { + delete process.env.PI_CODING_AGENT_DIR; + } else { + process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; + } + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index bf29e165b..0564381e4 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -5,6 +5,7 @@ import { getShellEnvAppliedKeys } from "../infra/shell-env.js"; import { type AuthProfileStore, ensureAuthProfileStore, + listProfilesForProvider, resolveApiKeyForProfile, resolveAuthProfileOrder, } from "./auth-profiles.js"; @@ -83,6 +84,15 @@ export async function resolveApiKeyForProvider(params: { return { apiKey: customKey, source: "models.json" }; } + if (provider === "openai") { + const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0; + if (hasCodex) { + throw new Error( + 'No API key found for provider "openai". You are authenticated with OpenAI Codex OAuth. Use openai-codex/gpt-5.2 (ChatGPT OAuth) or set OPENAI_API_KEY for openai/gpt-5.2.', + ); + } + } + throw new Error(`No API key found for provider "${provider}".`); } diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 883d14614..1578db290 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -6,6 +6,11 @@ import { type OAuthCredentials, type OAuthProvider, } from "@mariozechner/pi-ai"; +import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; +import { getCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js"; +import { loadModelCatalog } from "../agents/model-catalog.js"; +import { resolveConfiguredModelRef } from "../agents/model-selection.js"; import { isRemoteEnvironment, loginAntigravityVpsAware, @@ -58,6 +63,82 @@ import { defaultRuntime } from "../runtime.js"; import { resolveUserPath, sleep } from "../utils.js"; import type { WizardPrompter } from "./prompts.js"; +const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2"; + +function shouldSetOpenAICodexModel(model?: string): boolean { + const trimmed = model?.trim(); + if (!trimmed) return true; + const normalized = trimmed.toLowerCase(); + if (normalized.startsWith("openai-codex/")) return false; + if (normalized.startsWith("openai/")) return true; + return normalized === "gpt" || normalized === "gpt-mini"; +} + +function applyOpenAICodexModelDefault( + cfg: ClawdbotConfig, +): { next: ClawdbotConfig; changed: boolean } { + if (!shouldSetOpenAICodexModel(cfg.agent?.model)) { + return { next: cfg, changed: false }; + } + return { + next: { + ...cfg, + agent: { + ...cfg.agent, + model: OPENAI_CODEX_DEFAULT_MODEL, + }, + }, + changed: true, + }; +} + +async function warnIfModelConfigLooksOff( + config: ClawdbotConfig, + prompter: WizardPrompter, +) { + const ref = resolveConfiguredModelRef({ + cfg: config, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + const warnings: string[] = []; + const catalog = await loadModelCatalog({ config, useCache: false }); + if (catalog.length > 0) { + const known = catalog.some( + (entry) => entry.provider === ref.provider && entry.id === ref.model, + ); + if (!known) { + warnings.push( + `Model not found: ${ref.provider}/${ref.model}. Update agent.model or run /models list.`, + ); + } + } + + const store = ensureAuthProfileStore(); + const hasProfile = listProfilesForProvider(store, ref.provider).length > 0; + const envKey = resolveEnvApiKey(ref.provider); + const customKey = getCustomProviderApiKey(config, ref.provider); + if (!hasProfile && !envKey && !customKey) { + warnings.push( + `No auth configured for provider "${ref.provider}". The agent may fail until credentials are added.`, + ); + } + + if (ref.provider === "openai") { + const hasCodex = + listProfilesForProvider(store, "openai-codex").length > 0; + if (hasCodex) { + warnings.push( + `Detected OpenAI Codex OAuth. Consider setting agent.model to ${OPENAI_CODEX_DEFAULT_MODEL}.`, + ); + } + } + + if (warnings.length > 0) { + await prompter.note(warnings.join("\n"), "Model check"); + } +} + export async function runOnboardingWizard( opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime, @@ -287,6 +368,14 @@ export async function runOnboardingWizard( provider: "openai-codex", mode: "oauth", }); + const applied = applyOpenAICodexModelDefault(nextConfig); + nextConfig = applied.next; + if (applied.changed) { + await prompter.note( + `Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`, + "Model configured", + ); + } } } catch (err) { spin.stop("OpenAI OAuth failed"); @@ -380,6 +469,8 @@ export async function runOnboardingWizard( nextConfig = applyMinimaxConfig(nextConfig); } + await warnIfModelConfigLooksOff(nextConfig, prompter); + const portRaw = await prompter.text({ message: "Gateway port", initialValue: String(localPort),