From c139d6c025460881fa7efbbecf5beac63ff2aa46 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 10 Jun 2026 20:53:33 -0700 Subject: [PATCH] fix(codex-local): omit default model so codex CLI picks per auth mode (#7971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents through pluggable local adapters; codex_local wraps OpenAI's `codex` CLI. > - The codex_local adapter declares a hard-coded `DEFAULT_CODEX_LOCAL_MODEL = "gpt-5.3-codex"` and multiple Paperclip consumers (UI build-config, server route, OnboardingWizard, NewAgent form, AgentConfigForm) fall back to it when the operator doesn't pick a model. > - That model — and every `*-codex` model plus the older `gpt-5/5.1/5.2` lines — is API-key-only. Codex CLI rejects them on ChatGPT subscription auth with "The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account." > - Every codex_local agent created through the default onboarding path inherits this pin and breaks on its first heartbeat for any user authed via `codex login` (ChatGPT). > - claude_local already takes the right shape: its build-config only sets `adapterConfig.model` when the operator actually picked one, and falls through to whatever default `claude` CLI uses. > - Codex CLI's own default is auth-mode-aware. ChatGPT-subscription accounts get `gpt-5.5`; API-key accounts get the codex-tuned default. A Paperclip-side pin masks this and downgrades whichever group it wasn't built for. > - This PR makes codex_local match claude_local's shape: omit `adapterConfig.model` when the user picks "default," and let the CLI choose. Subscription users stop breaking; API-key users stop getting downgraded. > - The benefit is auth-mode-correct defaults with no Paperclip-side hard pin, plus future-proofing: when OpenAI bumps the CLI default we inherit it for free. ## What Changed - `packages/adapters/codex-local/src/ui/build-config.ts` — only set `adapterConfig.model` when the operator picked one (parity with `packages/adapters/claude-local/src/ui/build-config.ts`). - `server/src/routes/agents.ts` — drop the codex_local-specific `next.model = DEFAULT_CODEX_LOCAL_MODEL` fallback in `applyCreateDefaultsByAdapterType`. Bypass-sandbox default is left in place (security posture, not a model choice). - `ui/src/pages/NewAgent.tsx`, `ui/src/components/AgentConfigForm.tsx`, `ui/src/components/OnboardingWizard.tsx` — stop pre-populating the model field with `DEFAULT_CODEX_LOCAL_MODEL` when the user selects the Codex adapter. Other adapters' defaults (gemini_local, cursor, opencode_local) are unchanged. - `DEFAULT_CODEX_LOCAL_MODEL` is preserved as an exported constant for downstream consumers / plugin authors who want to opt in to a pin; we just stop forcing it on operators who didn't ask for one. - Test: assert `buildCodexLocalConfig` omits `model` when input is blank. ## Verification - `pnpm exec vitest run packages/adapters/codex-local/src/ui/build-config.test.ts packages/adapters/codex-local/src/server/codex-args.test.ts server/src/__tests__/adapter-registry.test.ts server/src/__tests__/heartbeat-model-profile.test.ts server/src/__tests__/agent-permissions-routes.test.ts` → 74/74 passing - `pnpm exec vitest run ui/src/lib/duplicate-agent-payload.test.ts ui/src/lib/acpx-model-filter.test.ts` → passing - `pnpm tsc --noEmit -p .` → clean - Live: I separately verified live during initial investigation that on ChatGPT-subscription auth, `gpt-5.3-codex` is rejected and `gpt-5.5` is what Codex CLI picks by default. Omitting model lets the CLI handle that. ## Risks - Telemetry: any sink that reads `adapterConfig.model` for cost attribution will now see the empty/omitted case more often. The CLI emits the actually-used model in its event stream; downstream telemetry should already read from there for accuracy, but worth a check. - Operator UX: "default" now means "whatever the CLI picks" instead of a Paperclip-known model. The selectable catalog still includes `gpt-5.5`, `gpt-5.4`, `gpt-5.3-codex`, etc. for operators who want to pin explicitly. - Existing agents are unaffected — their `adapterConfig.model` is already set; this only changes the *new-agent* default flow. ## Related work - Depends on: an open catalog-add PR adding `gpt-5.5` to the selectable model list and to `CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS`. Operators who want to switch to `gpt-5.5` explicitly need that PR merged first; this PR is the structural change that makes "default" mean "let the CLI choose." - Closes #5371 — codex_local default model selection persists `gpt-5.3-codex` instead of adapter default (this PR is the exact fix #5371 proposes). - Related: #5132 (opencode-local: hire-time default model fails on ChatGPT-OAuth accounts) — same problem shape on a sibling adapter; not fixed here but worth tracking for a parallel. - Related: #5939 (codex_local adapter hardcodes `gpt-5.3-codex-spark` validation, fails on ChatGPT OAuth accounts regardless of configured model) — separate validation-path bug; not fixed here. ## Model Used Claude (Sonnet-class), running inside Paperclip as a claude_local executor. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- packages/adapters/codex-local/src/index.ts | 5 +++++ .../codex-local/src/server/codex-args.test.ts | 19 +++++++++++++++++++ .../codex-local/src/ui/build-config.test.ts | 6 ++++++ .../codex-local/src/ui/build-config.ts | 7 ++----- server/src/routes/agents.ts | 8 +------- ui/src/components/AgentConfigForm.tsx | 12 +++--------- ui/src/components/OnboardingWizard.tsx | 14 +++----------- ui/src/pages/NewAgent.tsx | 6 +----- 8 files changed, 40 insertions(+), 37 deletions(-) diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index 8bd1d699..4c4a2de0 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -27,6 +27,11 @@ export function isCodexLocalManualModel(model: string | null | undefined): boole export function isCodexLocalFastModeSupported(model: string | null | undefined): boolean { if (isCodexLocalManualModel(model)) return true; const normalizedModel = typeof model === "string" ? model.trim() : ""; + // Empty means we're omitting --model so the Codex CLI picks its own default. + // On subscription auth that's gpt-5.5 (fast-mode capable); manual model IDs + // are also treated as supported. Match that policy: pass the fast-mode + // overrides through and let the CLI reject them if the chosen model can't use them. + if (!normalizedModel) return true; return CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS.includes( normalizedModel as (typeof CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS)[number], ); diff --git a/packages/adapters/codex-local/src/server/codex-args.test.ts b/packages/adapters/codex-local/src/server/codex-args.test.ts index feb22574..5ef2608d 100644 --- a/packages/adapters/codex-local/src/server/codex-args.test.ts +++ b/packages/adapters/codex-local/src/server/codex-args.test.ts @@ -70,6 +70,25 @@ describe("buildCodexExecArgs", () => { ]); }); + it("enables Codex fast mode overrides when model is omitted (CLI default)", () => { + const result = buildCodexExecArgs({ + fastMode: true, + }); + + expect(result.fastModeRequested).toBe(true); + expect(result.fastModeApplied).toBe(true); + expect(result.fastModeIgnoredReason).toBeNull(); + expect(result.args).toEqual([ + "exec", + "--json", + "-c", + 'service_tier="fast"', + "-c", + "features.fast_mode=true", + "-", + ]); + }); + it("ignores fast mode for unsupported models", () => { const result = buildCodexExecArgs({ model: "gpt-5.3-codex", diff --git a/packages/adapters/codex-local/src/ui/build-config.test.ts b/packages/adapters/codex-local/src/ui/build-config.test.ts index 734d1687..8126c0fb 100644 --- a/packages/adapters/codex-local/src/ui/build-config.test.ts +++ b/packages/adapters/codex-local/src/ui/build-config.test.ts @@ -51,4 +51,10 @@ describe("buildCodexLocalConfig", () => { dangerouslyBypassApprovalsAndSandbox: true, }); }); + + it("omits model when the operator leaves it blank", () => { + const config = buildCodexLocalConfig(makeValues({ model: "" })); + + expect(config).not.toHaveProperty("model"); + }); }); diff --git a/packages/adapters/codex-local/src/ui/build-config.ts b/packages/adapters/codex-local/src/ui/build-config.ts index 78e5e806..ab67feb6 100644 --- a/packages/adapters/codex-local/src/ui/build-config.ts +++ b/packages/adapters/codex-local/src/ui/build-config.ts @@ -1,8 +1,5 @@ import type { CreateConfigValues } from "@paperclipai/adapter-utils"; -import { - DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, - DEFAULT_CODEX_LOCAL_MODEL, -} from "../index.js"; +import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "../index.js"; function parseCommaArgs(value: string): string[] { return value @@ -70,7 +67,7 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record = {}; if (v.cwd) ac.cwd = v.cwd; if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath; - ac.model = v.model || DEFAULT_CODEX_LOCAL_MODEL; + if (v.model) ac.model = v.model; if (v.thinkingEffort) ac.modelReasoningEffort = v.thinkingEffort; ac.timeoutSec = 0; ac.graceSec = 15; diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index fb706f8e..3182c2bf 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -85,10 +85,7 @@ import { DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS, DEFAULT_ACPX_LOCAL_PERMISSION_MODE, } from "@paperclipai/adapter-acpx-local"; -import { - DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, - DEFAULT_CODEX_LOCAL_MODEL, -} from "@paperclipai/adapter-codex-local"; +import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local"; @@ -1112,9 +1109,6 @@ export function agentRoutes( return ensureGatewayDeviceKey(adapterType, next); } if (adapterType === "codex_local") { - if (!asNonEmptyString(next.model)) { - next.model = DEFAULT_CODEX_LOCAL_MODEL; - } const hasBypassFlag = typeof next.dangerouslyBypassApprovalsAndSandbox === "boolean" || typeof next.dangerouslyBypassSandbox === "boolean"; diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 9532ba5d..264e61e9 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -14,10 +14,7 @@ import { environmentsApi } from "../api/environments"; import { instanceSettingsApi } from "../api/instanceSettings"; import { secretsApi } from "../api/secrets"; import { assetsApi } from "../api/assets"; -import { - DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, - DEFAULT_CODEX_LOCAL_MODEL, -} from "@paperclipai/adapter-codex-local"; +import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local"; @@ -852,7 +849,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) { const { adapterType: _at, ...defaults } = defaultCreateValues; const nextValues: CreateConfigValues = { ...defaults, adapterType: t }; if (t === "codex_local") { - nextValues.model = DEFAULT_CODEX_LOCAL_MODEL; nextValues.dangerouslyBypassSandbox = DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX; } else if (t === "gemini_local") { @@ -872,10 +868,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) { modelProfiles: { cheap: { cleared: true } }, adapterConfig: { model: - t === "codex_local" - ? DEFAULT_CODEX_LOCAL_MODEL - : t === "gemini_local" - ? DEFAULT_GEMINI_LOCAL_MODEL + t === "gemini_local" + ? DEFAULT_GEMINI_LOCAL_MODEL : t === "opencode_local" ? DEFAULT_OPENCODE_LOCAL_MODEL : t === "cursor" diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 0f9c2378..f30168cd 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -37,10 +37,7 @@ import { selectDefaultCompanyGoalId } from "../lib/onboarding-launch"; import { buildNewAgentRuntimeConfig } from "../lib/new-agent-runtime-config"; -import { - DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, - DEFAULT_CODEX_LOCAL_MODEL -} from "@paperclipai/adapter-codex-local"; +import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { DEFAULT_OPENCODE_LOCAL_MODEL, isValidOpenCodeModelId } from "@paperclipai/adapter-opencode-local"; @@ -322,10 +319,8 @@ export function OnboardingWizard() { ...defaultCreateValues, adapterType, model: - adapterType === "codex_local" - ? model || DEFAULT_CODEX_LOCAL_MODEL - : adapterType === "gemini_local" - ? model || DEFAULT_GEMINI_LOCAL_MODEL + adapterType === "gemini_local" + ? model || DEFAULT_GEMINI_LOCAL_MODEL : adapterType === "cursor" ? model || DEFAULT_CURSOR_LOCAL_MODEL : adapterType === "opencode_local" @@ -754,9 +749,6 @@ export function OnboardingWizard() { const nextType = opt.type; setAdapterType(nextType); if (nextType === "codex_local") { - if (!model) { - setModel(DEFAULT_CODEX_LOCAL_MODEL); - } return; } if (nextType === "opencode_local") { diff --git a/ui/src/pages/NewAgent.tsx b/ui/src/pages/NewAgent.tsx index efb9411a..a786defc 100644 --- a/ui/src/pages/NewAgent.tsx +++ b/ui/src/pages/NewAgent.tsx @@ -32,10 +32,7 @@ import { ReportsToPicker } from "../components/ReportsToPicker"; import { buildNewAgentHirePayload } from "../lib/new-agent-hire-payload"; import { TrustPresetSection } from "../components/TrustPresetSection"; import { buildPermissionsForTrustPreset, getTrustPreset } from "../lib/trust-policy-ui"; -import { - DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, - DEFAULT_CODEX_LOCAL_MODEL, -} from "@paperclipai/adapter-codex-local"; +import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { DEFAULT_OPENCODE_LOCAL_MODEL, isValidOpenCodeModelId } from "@paperclipai/adapter-opencode-local"; @@ -46,7 +43,6 @@ function createValuesForAdapterType( const { adapterType: _discard, ...defaults } = defaultCreateValues; const nextValues: CreateConfigValues = { ...defaults, adapterType }; if (adapterType === "codex_local") { - nextValues.model = DEFAULT_CODEX_LOCAL_MODEL; nextValues.dangerouslyBypassSandbox = DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX; } else if (adapterType === "gemini_local") {