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
This commit is contained in:
Mario Zechner 2026-01-22 01:33:46 +01:00
parent d327b9c768
commit b712d1ca43
5 changed files with 95 additions and 47 deletions

View file

@ -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 || {})) {

View file

@ -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"),
);
}
}

View file

@ -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<OAuthServerInfo> {
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") {