Skip to content

Commit 74d2341

Browse files
authored
fix: Codex auth for low-cost tool calls (#4483)
## Summary - pass worker model config/catalog into fetch_url so spawned workers use the current Codex auth path - fall back to reading the fresh Codex access token for openai-codex low-cost model calls without modelConfig - avoid clearing ELECTRIC_CODEX_ACCESS_TOKEN during desktop Codex token refresh - add regression coverage for Codex low-cost calls without modelConfig ## Tests - pnpm --filter @electric-ax/agents-runtime test model-runner.test.ts -- --run - pnpm --filter @electric-ax/agents test -- worker *(passed in the main worktree before moving changes; in the nested .worktrees checkout it fails to resolve workspace deps like pino/@electric-ax/agents-runtime)* - pnpm --filter @electric-ax/agents-desktop typecheck
1 parent fc52b39 commit 74d2341

5 files changed

Lines changed: 52 additions & 9 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@electric-ax/agents": patch
3+
"@electric-ax/agents-runtime": patch
4+
"@electric-ax/agents-desktop": patch
5+
---
6+
7+
Fix Codex auth for low-cost tool calls by passing fresh access tokens to URL extraction and worker tools.

packages/agents-desktop/src/credentials/codex-auth.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,11 @@ async function clearCodexAuth(deps: CodexAuthDeps): Promise<void> {
292292

293293
export async function syncCodexEnvironment(deps: CodexAuthDeps): Promise<void> {
294294
process.env.ELECTRIC_CODEX_REQUIRE_OPT_IN = `1`
295-
delete process.env.ELECTRIC_CODEX_ACCESS_TOKEN
296295
const codex = deps.settings.codex ?? { enabled: false, source: null }
297-
if (!codex.enabled || !codex.source) return
296+
if (!codex.enabled || !codex.source) {
297+
delete process.env.ELECTRIC_CODEX_ACCESS_TOKEN
298+
return
299+
}
298300

299301
const stored = await loadStoredCodexAuth(deps)
300302
const refreshed =
@@ -310,6 +312,7 @@ export async function syncCodexEnvironment(deps: CodexAuthDeps): Promise<void> {
310312
// produced. Desktop OAuth tokens should refresh through the OAuth token
311313
// endpoint above; stale tokens imported from CLI/opencode auth files should be
312314
// deleted rather than repeatedly trusted as an enabled desktop credential.
315+
delete process.env.ELECTRIC_CODEX_ACCESS_TOKEN
313316
await clearCodexAuth(deps)
314317
}
315318

packages/agents-runtime/src/model-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ async function resolveLowCostApiKey(input: {
180180
if (input.modelConfig?.getApiKey) {
181181
return await input.modelConfig.getApiKey(input.provider)
182182
}
183+
if (input.provider === `openai-codex`) return readCodexAccessToken()
183184
if (input.provider === MOONSHOT_PROVIDER) return getMoonshotApiKey()
184185
return undefined
185186
}

packages/agents-runtime/test/model-runner.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,27 @@ describe(`completeWithLowCostModel`, () => {
4848
)
4949
})
5050

51+
test(`passes fresh ELECTRIC_CODEX_ACCESS_TOKEN for openai-codex low-cost models without modelConfig`, async () => {
52+
process.env.ELECTRIC_CODEX_ACCESS_TOKEN = `codex-token`
53+
54+
await completeWithLowCostModel({
55+
catalog: {
56+
choices: [
57+
{ provider: `openai-codex`, id: `gpt-5.4-mini`, reasoning: false },
58+
],
59+
},
60+
purpose: `URL extraction`,
61+
systemPrompt: `Custom instructions`,
62+
prompt: `Extract the title`,
63+
maxTokens: 128,
64+
})
65+
66+
expect(completeSimple).toHaveBeenCalledOnce()
67+
expect(completeSimple.mock.calls[0][2]).toMatchObject({
68+
apiKey: `codex-token`,
69+
})
70+
})
71+
5172
test(`passes MOONSHOT_API_KEY for moonshot low-cost models`, async () => {
5273
process.env.MOONSHOT_API_KEY = `moonshot-key`
5374

packages/agents/src/agents/worker.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { WORKER_TOOL_NAMES, createSpawnWorkerTool } from '../tools/spawn-worker'
1414
import {
1515
REASONING_EFFORT_VALUES,
1616
resolveBuiltinModelConfig,
17+
type BuiltinAgentModelConfig,
1718
type BuiltinModelCatalog,
1819
} from '../model-catalog'
1920
import type { WorkerToolName } from '../tools/spawn-worker'
@@ -117,7 +118,11 @@ function buildToolsForWorker(
117118
tools: ReadonlyArray<WorkerToolName>,
118119
sandbox: Sandbox,
119120
ctx: HandlerContext,
120-
readSet: Set<string>
121+
readSet: Set<string>,
122+
opts: {
123+
modelCatalog: BuiltinModelCatalog
124+
modelConfig: BuiltinAgentModelConfig
125+
}
121126
): Array<AgentTool> {
122127
const out: Array<AgentTool> = []
123128
for (const name of tools) {
@@ -138,7 +143,12 @@ function buildToolsForWorker(
138143
out.push(braveSearchTool)
139144
break
140145
case `fetch_url`:
141-
out.push(createFetchUrlTool(sandbox))
146+
out.push(
147+
createFetchUrlTool(sandbox, {
148+
catalog: opts.modelCatalog,
149+
modelConfig: opts.modelConfig,
150+
})
151+
)
142152
break
143153
case `spawn_worker`:
144154
out.push(createSpawnWorkerTool(ctx))
@@ -298,15 +308,16 @@ export function registerWorker(
298308
// ctx.sandbox is provisioned and disposed by the runtime sandbox
299309
// pool — subsequent wakes for the same worker reuse the same
300310
// instance until idle-TTL eviction.
311+
const modelConfig = resolveBuiltinModelConfig(
312+
modelCatalog,
313+
args as unknown as Readonly<Record<string, unknown>>
314+
)
301315
const builtinTools = buildToolsForWorker(
302316
args.tools,
303317
ctx.sandbox,
304318
ctx,
305-
readSet
306-
)
307-
const modelConfig = resolveBuiltinModelConfig(
308-
modelCatalog,
309-
args as unknown as Readonly<Record<string, unknown>>
319+
readSet,
320+
{ modelCatalog, modelConfig }
310321
)
311322

312323
const sharedStateTools: Array<AgentTool> = []

0 commit comments

Comments
 (0)