From a3cc169d9715f01b93abdbeb1c4c9e2ea864d8c0 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Tue, 30 Jun 2026 15:21:56 +0200 Subject: [PATCH] fix(ai): avoid codex user-agent race The Codex provider previously loaded node:os asynchronously during module evaluation. A fresh process that imports the provider and immediately starts an SSE request can build headers before that promise callback runs, so the first request reports User-Agent: pi (browser) in Node/Bun. Reproduced against parent fd6659dd with a stubbed-fetch harness: 50/50 immediate first requests reported pi (browser) in both Node and Bun. The same harness on this fix reports the OS-specific user agent 50/50 in both runtimes. Use process.getBuiltinModule("node:os") behind the existing Node/Bun runtime guard so OS metadata is available synchronously while still avoiding top-level runtime Node builtin imports that break browser/Vite builds. --- packages/ai/CHANGELOG.md | 4 ++++ packages/ai/src/api/openai-codex-responses.ts | 22 +++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a32cb7ef..5647eb74 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI Codex user-agent construction to synchronously load Node OS metadata, avoiding a startup race that could report `pi (browser)` in Node/Bun. + ## [0.80.3] - 2026-06-30 ### Added diff --git a/packages/ai/src/api/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts index 62886e0c..67db7a93 100644 --- a/packages/ai/src/api/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -6,20 +6,20 @@ import type { ResponseStreamEvent, } from "openai/resources/responses/responses.js"; -// NEVER convert to top-level runtime imports - breaks browser/Vite builds -let _os: typeof NodeOs | null = null; +type ProcessWithOsBuiltinModule = typeof process & { + getBuiltinModule?: (id: "node:os") => typeof NodeOs; +}; -type DynamicImport = (specifier: string) => Promise; - -const dynamicImport: DynamicImport = (specifier) => import(specifier); -const NODE_OS_SPECIFIER = "node:" + "os"; - -if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - dynamicImport(NODE_OS_SPECIFIER).then((m) => { - _os = m as typeof NodeOs; - }); +function loadNodeOs(): typeof NodeOs | null { + if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) { + return null; + } + return (process as ProcessWithOsBuiltinModule).getBuiltinModule?.("node:os") ?? null; } +// NEVER convert to top-level runtime imports - breaks browser/Vite builds +const _os: typeof NodeOs | null = loadNodeOs(); + import { clampThinkingLevel } from "../models.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts"; import type {