From b712d1ca43db87d9f64c5d610ab952b07e5a5bfb Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 22 Jan 2026 01:33:46 +0100 Subject: [PATCH] fix(ai, web-ui): browser compatibility for pi-ai, update tsgo for decorator support - Update @typescript/native-preview to 7.0.0-dev.20260120.1 (supports experimentalDecorators) - Replace top-level node:fs, node:os, node:path imports with dynamic imports in stream.ts - Replace top-level node:os import with dynamic import in openai-codex-responses.ts - Replace top-level node:crypto, node:http imports with dynamic imports in openai-codex.ts - Replace Buffer.from with atob for browser-compatible base64 decoding fixes #873 --- package-lock.json | 64 +++++++++---------- package.json | 2 +- .../src/providers/openai-codex-responses.ts | 14 +++- packages/ai/src/stream.ts | 33 ++++++++-- packages/ai/src/utils/oauth/openai-codex.ts | 29 +++++++-- 5 files changed, 95 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19027453..1f65fbb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "devDependencies": { "@biomejs/biome": "2.3.5", "@types/node": "^22.10.5", - "@typescript/native-preview": "7.0.0-dev.20251212.1", + "@typescript/native-preview": "7.0.0-dev.20260120.1", "concurrently": "^9.2.1", "husky": "^9.1.7", "tsx": "^4.20.3", @@ -3959,28 +3959,28 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-uNPMu5+ElTN7AZRFJXsTPtSAQ2b7FIXMvpQbU/L0VD5PoBp5nMiQbgO1QFSvbFiIoTTma3I2TX3WSO5olIMTLQ==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-nnEf37C9ue7OBRnF2zmV/OCBmV5Y7T/K4mCHa+nxgiXcF/1w8sA0cgdFl+gHQ0mysqUJ+Bu5btAMeWgpLyjrgg==", "dev": true, "license": "Apache-2.0", "bin": { "tsgo": "bin/tsgo.js" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251212.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251212.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20251212.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251212.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20251212.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251212.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20251212.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260120.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260120.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260120.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260120.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260120.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260120.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260120.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-5tof0OT01yPQ0mcoKPeSrGMxQ9Dl//gTjSKCMKwbLr5urrIPxX5bNRWUH0hxWaB4A3mXQvDvxSSrWR5TMOl2aQ==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-r3pWFuR2H7mn6ScwpH5jJljKQqKto0npVuJSk6pRwFwexpTyxOGmJTZJ1V0AWiisaNxU2+CNAqWFJSJYIE/QTg==", "cpu": [ "arm64" ], @@ -3992,9 +3992,9 @@ ] }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-zUgcCXmDfO2yo5fNZZ3wUCv8hdqc/Qbc1WZUEDYYo3ItnBUL9qp0lUtTwsLtNreL2WmHOCeTQuKWa/JQzdw89g==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-cuC1+wLbUP+Ip2UT94G134fqRdp5w3b3dhcCO6/FQ4yXxvRNyv/WK+upHBUFDaeSOeHgDTyO9/QFYUWwC4If1A==", "cpu": [ "x64" ], @@ -4006,9 +4006,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-peQCeG2+XqMqs6/Sg6nbQPI3Kae91Esi5Qh1VyDETO4wjMbKeAzVjw8t3Qz5X6RDbWNrCpDmbk6chjukfGeWgQ==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-vN6OYVySol/kQZjJGmAzd6L30SyVlCgmCXS8WjUYtE5clN0YrzQHop16RK29fYZHMxpkOniVBtRPxUYQANZBlQ==", "cpu": [ "arm" ], @@ -4020,9 +4020,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-0P59bGDFLppvkdpqQ8/kG+kU6R0iCdQiSLFRNrbrLnaflACBfIi40D3Ono3EmeSxqKsHqh/pNRu3BUJvoNGphw==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-zZGvEGY7wcHYefMZ87KNmvjN3NLIhsCMHEpHZiGCS3khKf+8z6ZsanrzCjOTodvL01VPyBzHxV1EtkSxAcLiQg==", "cpu": [ "arm64" ], @@ -4034,9 +4034,9 @@ ] }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-7QFyqcPe/Sz+IakvzCqh0d5WhQg7A7bKyQil38K7rKSTaPI42LrVwLA6mVtTRfQyS5Sy2XYVinyLNXnWM8ImQQ==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-JBfNhWd/asd5MDeS3VgRvE24pGKBkmvLub6tsux6ypr+Yhy+o0WaAEzVpmlRYZUqss2ai5tvOu4dzPBXzZAtFw==", "cpu": [ "x64" ], @@ -4048,9 +4048,9 @@ ] }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-Y8mh0dPXAcYYNtSZVZYaNcqAOlxOlbJQopJBVATn+ItCxrY4RqBwygzrBWqg+gUo9xLmFI9XLuDVqm1ZAkAfwg==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-tTndRtYCq2xwgE0VkTi9ACNiJaV43+PqvBqCxk8ceYi3X36Ve+CCnwlZfZJ4k9NxZthtrAwF/kUmpC9iIYbq1w==", "cpu": [ "arm64" ], @@ -4062,9 +4062,9 @@ ] }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20251212.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251212.1.tgz", - "integrity": "sha512-bUPWJgGhPdsoL3OR+I8nFF81P/+hwfqyMKaAWFxTg1zeRdEl61lVdrEfgNDBI7Px5Gr+uFGELlkCsDzy/7dAyw==", + "version": "7.0.0-dev.20260120.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260120.1.tgz", + "integrity": "sha512-oZia7hFL6k9pVepfonuPI86Jmyz6WlJKR57tWCDwRNmpA7odxuTq1PbvcYgy1z4+wHF1nnKKJY0PMAiq6ac18w==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 33ec59ce..fcb08556 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "devDependencies": { "@biomejs/biome": "2.3.5", "@types/node": "^22.10.5", - "@typescript/native-preview": "7.0.0-dev.20251212.1", + "@typescript/native-preview": "7.0.0-dev.20260120.1", "concurrently": "^9.2.1", "husky": "^9.1.7", "tsx": "^4.20.3", diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 14f07b42..abf21a5f 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1,4 +1,11 @@ -import os from "node:os"; +// NEVER convert to top-level import - breaks browser/Vite builds (web-ui) +let _os: typeof import("node:os") | null = null; +if (typeof process !== "undefined" && process.versions?.node) { + import("node:os").then((m) => { + _os = m; + }); +} + import type { ResponseFunctionToolCall, ResponseOutputMessage, @@ -686,7 +693,7 @@ function extractAccountId(token: string): string { try { const parts = token.split("."); if (parts.length !== 3) throw new Error("Invalid token"); - const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8")); + const payload = JSON.parse(atob(parts[1])); const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id; if (!accountId) throw new Error("No account ID in token"); return accountId; @@ -707,7 +714,8 @@ function buildHeaders( headers.set("chatgpt-account-id", accountId); headers.set("OpenAI-Beta", "responses=experimental"); headers.set("originator", "pi"); - headers.set("User-Agent", `pi (${os.platform()} ${os.release()}; ${os.arch()})`); + const userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)"; + headers.set("User-Agent", userAgent); headers.set("accept", "text/event-stream"); headers.set("content-type", "application/json"); for (const [key, value] of Object.entries(additionalHeaders || {})) { diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index a390567d..c3804617 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,6 +1,21 @@ -import { existsSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; +// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui) +let _existsSync: typeof import("node:fs").existsSync | null = null; +let _homedir: typeof import("node:os").homedir | null = null; +let _join: typeof import("node:path").join | null = null; + +// Eagerly load in Node.js environment only +if (typeof process !== "undefined" && process.versions?.node) { + import("node:fs").then((m) => { + _existsSync = m.existsSync; + }); + import("node:os").then((m) => { + _homedir = m.homedir; + }); + import("node:path").then((m) => { + _join = m.join; + }); +} + import { supportsXhigh } from "./models.js"; import { type BedrockOptions, streamBedrock } from "./providers/amazon-bedrock.js"; import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic.js"; @@ -31,14 +46,20 @@ let cachedVertexAdcCredentialsExists: boolean | null = null; function hasVertexAdcCredentials(): boolean { if (cachedVertexAdcCredentialsExists === null) { + // In browser or if node modules not loaded yet, return false + if (!_existsSync || !_homedir || !_join) { + cachedVertexAdcCredentialsExists = false; + return false; + } + // Check GOOGLE_APPLICATION_CREDENTIALS env var first (standard way) const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; if (gacPath) { - cachedVertexAdcCredentialsExists = existsSync(gacPath); + cachedVertexAdcCredentialsExists = _existsSync(gacPath); } else { // Fall back to default ADC path (lazy evaluation) - cachedVertexAdcCredentialsExists = existsSync( - join(homedir(), ".config", "gcloud", "application_default_credentials.json"), + cachedVertexAdcCredentialsExists = _existsSync( + _join(_homedir(), ".config", "gcloud", "application_default_credentials.json"), ); } } diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index f8d62a21..1d967640 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -1,9 +1,22 @@ /** * OpenAI Codex (ChatGPT OAuth) flow + * + * NOTE: This module uses Node.js crypto and http for the OAuth callback. + * It is only intended for CLI use, not browser environments. */ -import { randomBytes } from "node:crypto"; -import http from "node:http"; +// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui) +let _randomBytes: typeof import("node:crypto").randomBytes | null = null; +let _http: typeof import("node:http") | null = null; +if (typeof process !== "undefined" && process.versions?.node) { + import("node:crypto").then((m) => { + _randomBytes = m.randomBytes; + }); + import("node:http").then((m) => { + _http = m; + }); +} + import { generatePKCE } from "./pkce.js"; import type { OAuthCredentials, OAuthPrompt } from "./types.js"; @@ -38,7 +51,10 @@ type JwtPayload = { }; function createState(): string { - return randomBytes(16).toString("hex"); + if (!_randomBytes) { + throw new Error("OpenAI Codex OAuth is only available in Node.js environments"); + } + return _randomBytes(16).toString("hex"); } function parseAuthorizationInput(input: string): { code?: string; state?: string } { @@ -76,7 +92,7 @@ function decodeJwt(token: string): JwtPayload | null { const parts = token.split("."); if (parts.length !== 3) return null; const payload = parts[1] ?? ""; - const decoded = Buffer.from(payload, "base64").toString("utf-8"); + const decoded = atob(payload); return JSON.parse(decoded) as JwtPayload; } catch { return null; @@ -194,9 +210,12 @@ type OAuthServerInfo = { }; function startLocalOAuthServer(state: string): Promise { + if (!_http) { + throw new Error("OpenAI Codex OAuth is only available in Node.js environments"); + } let lastCode: string | null = null; let cancelled = false; - const server = http.createServer((req, res) => { + const server = _http.createServer((req, res) => { try { const url = new URL(req.url || "", "http://localhost"); if (url.pathname !== "/auth/callback") {