fix(ai,coding-agent): make pi-ai browser-safe and move OAuth runtime exports

- add browser smoke bundling check to root check + pre-commit

- lazy-load Bedrock provider registration to avoid browser graph traversal

- remove top-level OAuth runtime exports from @mariozechner/pi-ai

- add @mariozechner/pi-ai/oauth subpath export and update coding-agent imports

- move proxy dispatcher init to coding-agent CLI (Node-only)

- document Bedrock/OAuth browser limitations

closes #1814
This commit is contained in:
Mario Zechner 2026-03-04 20:20:54 +01:00
parent 2af0c98b5f
commit e0754fdbb3
26 changed files with 216 additions and 59 deletions

View file

@ -3,16 +3,20 @@ 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;
type DynamicImport = (specifier: string) => Promise<unknown>;
const dynamicImport = new Function("specifier", "return import(specifier);") as DynamicImport;
// Eagerly load in Node.js/Bun environment only
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
import("node:fs").then((m) => {
_existsSync = m.existsSync;
dynamicImport("node:fs").then((m) => {
_existsSync = (m as typeof import("node:fs")).existsSync;
});
import("node:os").then((m) => {
_homedir = m.homedir;
dynamicImport("node:os").then((m) => {
_homedir = (m as typeof import("node:os")).homedir;
});
import("node:path").then((m) => {
_join = m.join;
dynamicImport("node:path").then((m) => {
_join = (m as typeof import("node:path")).join;
});
}

View file

@ -16,7 +16,16 @@ export * from "./stream.js";
export * from "./types.js";
export * from "./utils/event-stream.js";
export * from "./utils/json-parse.js";
export * from "./utils/oauth/index.js";
export type {
OAuthAuthInfo,
OAuthCredentials,
OAuthLoginCallbacks,
OAuthPrompt,
OAuthProvider,
OAuthProviderId,
OAuthProviderInfo,
OAuthProviderInterface,
} from "./utils/oauth/types.js";
export * from "./utils/overflow.js";
export * from "./utils/typebox-helpers.js";
export * from "./utils/validation.js";

1
packages/ai/src/oauth.ts Normal file
View file

@ -0,0 +1 @@
export * from "./utils/oauth/index.js";

View file

@ -1,12 +1,19 @@
// NEVER convert to top-level import - breaks browser/Vite builds (web-ui)
let _os: typeof import("node:os") | null = null;
import type * as NodeOs from "node:os";
import type { Tool as OpenAITool, ResponseInput, ResponseStreamEvent } from "openai/resources/responses/responses.js";
// NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)
let _os: typeof NodeOs | null = null;
type DynamicImport = (specifier: string) => Promise<unknown>;
const dynamicImport = new Function("specifier", "return import(specifier);") as DynamicImport;
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
import("node:os").then((m) => {
_os = m;
dynamicImport("node:os").then((m) => {
_os = m as typeof NodeOs;
});
}
import type { Tool as OpenAITool, ResponseInput, ResponseStreamEvent } from "openai/resources/responses/responses.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { supportsXhigh } from "../models.js";
import type {

View file

@ -1,5 +1,13 @@
import { clearApiProviders, registerApiProvider } from "../api-registry.js";
import { streamBedrock, streamSimpleBedrock } from "./amazon-bedrock.js";
import type {
AssistantMessage,
AssistantMessageEvent,
Context,
Model,
SimpleStreamOptions,
StreamOptions,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { streamAnthropic, streamSimpleAnthropic } from "./anthropic.js";
import { streamAzureOpenAIResponses, streamSimpleAzureOpenAIResponses } from "./azure-openai-responses.js";
import { streamGoogle, streamSimpleGoogle } from "./google.js";
@ -9,6 +17,100 @@ import { streamOpenAICodexResponses, streamSimpleOpenAICodexResponses } from "./
import { streamOpenAICompletions, streamSimpleOpenAICompletions } from "./openai-completions.js";
import { streamOpenAIResponses, streamSimpleOpenAIResponses } from "./openai-responses.js";
interface BedrockProviderModule {
streamBedrock: (
model: Model<"bedrock-converse-stream">,
context: Context,
options?: StreamOptions,
) => AssistantMessageEventStream;
streamSimpleBedrock: (
model: Model<"bedrock-converse-stream">,
context: Context,
options?: SimpleStreamOptions,
) => AssistantMessageEventStream;
}
type DynamicImport = (specifier: string) => Promise<unknown>;
const dynamicImport = new Function("specifier", "return import(specifier);") as DynamicImport;
async function loadBedrockProviderModule(): Promise<BedrockProviderModule> {
const module = await dynamicImport("./amazon-bedrock.js");
return module as BedrockProviderModule;
}
function forwardStream(target: AssistantMessageEventStream, source: AssistantMessageEventStream): void {
(async () => {
for await (const event of source as AsyncIterable<AssistantMessageEvent>) {
target.push(event);
}
target.end();
})();
}
function createLazyLoadErrorMessage(model: Model<"bedrock-converse-stream">, error: unknown): AssistantMessage {
return {
role: "assistant",
content: [],
api: "bedrock-converse-stream",
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "error",
errorMessage: error instanceof Error ? error.message : String(error),
timestamp: Date.now(),
};
}
function streamBedrockLazy(
model: Model<"bedrock-converse-stream">,
context: Context,
options?: StreamOptions,
): AssistantMessageEventStream {
const outer = new AssistantMessageEventStream();
loadBedrockProviderModule()
.then((module) => {
const inner = module.streamBedrock(model, context, options);
forwardStream(outer, inner);
})
.catch((error) => {
const message = createLazyLoadErrorMessage(model, error);
outer.push({ type: "error", reason: "error", error: message });
outer.end(message);
});
return outer;
}
function streamSimpleBedrockLazy(
model: Model<"bedrock-converse-stream">,
context: Context,
options?: SimpleStreamOptions,
): AssistantMessageEventStream {
const outer = new AssistantMessageEventStream();
loadBedrockProviderModule()
.then((module) => {
const inner = module.streamSimpleBedrock(model, context, options);
forwardStream(outer, inner);
})
.catch((error) => {
const message = createLazyLoadErrorMessage(model, error);
outer.push({ type: "error", reason: "error", error: message });
outer.end(message);
});
return outer;
}
export function registerBuiltInApiProviders(): void {
registerApiProvider({
api: "anthropic-messages",
@ -60,8 +162,8 @@ export function registerBuiltInApiProviders(): void {
registerApiProvider({
api: "bedrock-converse-stream",
stream: streamBedrock,
streamSimple: streamSimpleBedrock,
stream: streamBedrockLazy,
streamSimple: streamSimpleBedrockLazy,
});
}

View file

@ -1,5 +1,4 @@
import "./providers/register-builtins.js";
import "./utils/http-proxy.js";
import { getApiProvider } from "./api-registry.js";
import type {

View file

@ -1,13 +0,0 @@
/**
* Set up HTTP proxy according to env variables for `fetch` based SDKs in Node.js.
* Bun has builtin support for this.
*
* This module should be imported early by any code that needs proxy support for fetch().
* ES modules are cached, so importing multiple times is safe - setup only runs once.
*/
if (typeof process !== "undefined" && process.versions?.node) {
import("undici").then((m) => {
const { EnvHttpProxyAgent, setGlobalDispatcher } = m;
setGlobalDispatcher(new EnvHttpProxyAgent());
});
}

View file

@ -9,9 +9,6 @@
* - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud)
*/
// Set up HTTP proxy for fetch() calls (respects HTTP_PROXY, HTTPS_PROXY env vars)
import "../http-proxy.js";
// Anthropic
export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
// GitHub Copilot