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

@ -11,6 +11,25 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
RUN_BROWSER_SMOKE=0
for file in $STAGED_FILES; do
case "$file" in
packages/ai/*|packages/web-ui/*|package.json|package-lock.json)
RUN_BROWSER_SMOKE=1
break
;;
esac
done
if [ $RUN_BROWSER_SMOKE -eq 1 ]; then
echo "Running browser smoke check..."
npm run check:browser-smoke
if [ $? -ne 0 ]; then
echo "❌ Browser smoke check failed."
exit 1
fi
fi
# Restage files that were previously staged and may have been modified by formatting # Restage files that were previously staged and may have been modified by formatting
for file in $STAGED_FILES; do for file in $STAGED_FILES; do
if [ -f "$file" ]; then if [ -f "$file" ]; then
@ -18,4 +37,4 @@ for file in $STAGED_FILES; do
fi fi
done done
echo "✅ All pre-commit checks passed!" echo "✅ All pre-commit checks passed!"

View file

@ -15,7 +15,8 @@
"build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../mom && npm run build && cd ../web-ui && npm run build && cd ../pods && npm run build", "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../mom && npm run build && cd ../web-ui && npm run build && cd ../pods && npm run build",
"dev": "concurrently --names \"ai,agent,coding-agent,mom,web-ui,tui\" --prefix-colors \"cyan,yellow,red,white,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/mom && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"", "dev": "concurrently --names \"ai,agent,coding-agent,mom,web-ui,tui\" --prefix-colors \"cyan,yellow,red,white,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/mom && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"",
"dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"", "dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"",
"check": "biome check --write --error-on-warnings . && tsgo --noEmit && cd packages/web-ui && npm run check", "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check",
"check:browser-smoke": "sh -c 'esbuild scripts/browser-smoke-entry.ts --bundle --platform=browser --format=esm --log-limit=0 --outfile=/tmp/pi-browser-smoke.js > /tmp/pi-browser-smoke-errors.log 2>&1 || { echo \"Browser smoke check failed. See /tmp/pi-browser-smoke-errors.log\"; exit 1; }'",
"test": "npm run test --workspaces --if-present", "test": "npm run test --workspaces --if-present",
"version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
"version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", "version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",

View file

@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Breaking Changes
- Moved Node OAuth runtime exports off the top-level package entry. Import OAuth login/refresh functions from `@mariozechner/pi-ai/oauth` instead of `@mariozechner/pi-ai` ([#1814](https://github.com/badlogic/pi-mono/issues/1814))
### Added ### Added
- Added `gemini-3.1-flash-lite-preview` fallback model entry for the `google` provider so it remains selectable until upstream model catalogs include it ([#1785](https://github.com/badlogic/pi-mono/issues/1785), thanks [@n-WN](https://github.com/n-WN)). - Added `gemini-3.1-flash-lite-preview` fallback model entry for the `google` provider so it remains selectable until upstream model catalogs include it ([#1785](https://github.com/badlogic/pi-mono/issues/1785), thanks [@n-WN](https://github.com/n-WN)).
@ -9,6 +13,7 @@
### Fixed ### Fixed
- Fixed Gemini 3.1 thinking-level detection in `google` and `google-vertex` providers so `gemini-3.1-*` models use Gemini 3 level-based thinking config instead of budget fallback ([#1785](https://github.com/badlogic/pi-mono/issues/1785), thanks [@n-WN](https://github.com/n-WN)). - Fixed Gemini 3.1 thinking-level detection in `google` and `google-vertex` providers so `gemini-3.1-*` models use Gemini 3 level-based thinking config instead of budget fallback ([#1785](https://github.com/badlogic/pi-mono/issues/1785), thanks [@n-WN](https://github.com/n-WN)).
- Fixed browser bundling failures by lazy-loading the Bedrock provider and removing Node-only side effects from the default browser import graph ([#1814](https://github.com/badlogic/pi-mono/issues/1814))
## [0.55.4] - 2026-03-02 ## [0.55.4] - 2026-03-02

View file

@ -33,6 +33,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
- [Cross-Provider Handoffs](#cross-provider-handoffs) - [Cross-Provider Handoffs](#cross-provider-handoffs)
- [Context Serialization](#context-serialization) - [Context Serialization](#context-serialization)
- [Browser Usage](#browser-usage) - [Browser Usage](#browser-usage)
- [Browser Compatibility Notes](#browser-compatibility-notes)
- [Environment Variables](#environment-variables-nodejs-only) - [Environment Variables](#environment-variables-nodejs-only)
- [Checking Environment Variables](#checking-environment-variables) - [Checking Environment Variables](#checking-environment-variables)
- [OAuth Providers](#oauth-providers) - [OAuth Providers](#oauth-providers)
@ -888,6 +889,13 @@ const response = await complete(model, {
> **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure. > **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure.
### Browser Compatibility Notes
- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments.
- OAuth login flows are not supported in browser environments. Use the `@mariozechner/pi-ai/oauth` entry point in Node.js.
- In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime.
- Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app.
### Environment Variables (Node.js only) ### Environment Variables (Node.js only)
In Node.js environments, you can set environment variables to avoid passing API keys: In Node.js environments, you can set environment variables to avoid passing API keys:
@ -1018,7 +1026,7 @@ Credentials are saved to `auth.json` in the current directory.
### Programmatic OAuth ### Programmatic OAuth
The library provides login and token refresh functions. Credential storage is the caller's responsibility. The library provides login and token refresh functions via the `@mariozechner/pi-ai/oauth` entry point. Credential storage is the caller's responsibility.
```typescript ```typescript
import { import {
@ -1036,13 +1044,13 @@ import {
// Types // Types
type OAuthProvider, // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity' type OAuthProvider, // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity'
type OAuthCredentials, type OAuthCredentials,
} from '@mariozechner/pi-ai'; } from '@mariozechner/pi-ai/oauth';
``` ```
### Login Flow Example ### Login Flow Example
```typescript ```typescript
import { loginGitHubCopilot } from '@mariozechner/pi-ai'; import { loginGitHubCopilot } from '@mariozechner/pi-ai/oauth';
import { writeFileSync } from 'fs'; import { writeFileSync } from 'fs';
const credentials = await loginGitHubCopilot({ const credentials = await loginGitHubCopilot({
@ -1066,7 +1074,8 @@ writeFileSync('auth.json', JSON.stringify(auth, null, 2));
Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired: Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired:
```typescript ```typescript
import { getModel, complete, getOAuthApiKey } from '@mariozechner/pi-ai'; import { getModel, complete } from '@mariozechner/pi-ai';
import { getOAuthApiKey } from '@mariozechner/pi-ai/oauth';
import { readFileSync, writeFileSync } from 'fs'; import { readFileSync, writeFileSync } from 'fs';
// Load your stored credentials // Load your stored credentials

1
packages/ai/oauth.d.ts vendored Normal file
View file

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

1
packages/ai/oauth.js Normal file
View file

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

View file

@ -5,11 +5,23 @@
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./oauth": {
"types": "./oauth.d.ts",
"import": "./oauth.js"
}
},
"bin": { "bin": {
"pi-ai": "./dist/cli.js" "pi-ai": "./dist/cli.js"
}, },
"files": [ "files": [
"dist", "dist",
"oauth.js",
"oauth.d.ts",
"README.md" "README.md"
], ],
"scripts": { "scripts": {

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 _homedir: typeof import("node:os").homedir | null = null;
let _join: typeof import("node:path").join | 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 // Eagerly load in Node.js/Bun environment only
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
import("node:fs").then((m) => { dynamicImport("node:fs").then((m) => {
_existsSync = m.existsSync; _existsSync = (m as typeof import("node:fs")).existsSync;
}); });
import("node:os").then((m) => { dynamicImport("node:os").then((m) => {
_homedir = m.homedir; _homedir = (m as typeof import("node:os")).homedir;
}); });
import("node:path").then((m) => { dynamicImport("node:path").then((m) => {
_join = m.join; _join = (m as typeof import("node:path")).join;
}); });
} }

View file

@ -16,7 +16,16 @@ export * from "./stream.js";
export * from "./types.js"; export * from "./types.js";
export * from "./utils/event-stream.js"; export * from "./utils/event-stream.js";
export * from "./utils/json-parse.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/overflow.js";
export * from "./utils/typebox-helpers.js"; export * from "./utils/typebox-helpers.js";
export * from "./utils/validation.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) import type * as NodeOs from "node:os";
let _os: typeof import("node:os") | null = null; 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)) { if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
import("node:os").then((m) => { dynamicImport("node:os").then((m) => {
_os = 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 { getEnvApiKey } from "../env-api-keys.js";
import { supportsXhigh } from "../models.js"; import { supportsXhigh } from "../models.js";
import type { import type {

View file

@ -1,5 +1,13 @@
import { clearApiProviders, registerApiProvider } from "../api-registry.js"; 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 { streamAnthropic, streamSimpleAnthropic } from "./anthropic.js";
import { streamAzureOpenAIResponses, streamSimpleAzureOpenAIResponses } from "./azure-openai-responses.js"; import { streamAzureOpenAIResponses, streamSimpleAzureOpenAIResponses } from "./azure-openai-responses.js";
import { streamGoogle, streamSimpleGoogle } from "./google.js"; import { streamGoogle, streamSimpleGoogle } from "./google.js";
@ -9,6 +17,100 @@ import { streamOpenAICodexResponses, streamSimpleOpenAICodexResponses } from "./
import { streamOpenAICompletions, streamSimpleOpenAICompletions } from "./openai-completions.js"; import { streamOpenAICompletions, streamSimpleOpenAICompletions } from "./openai-completions.js";
import { streamOpenAIResponses, streamSimpleOpenAIResponses } from "./openai-responses.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 { export function registerBuiltInApiProviders(): void {
registerApiProvider({ registerApiProvider({
api: "anthropic-messages", api: "anthropic-messages",
@ -60,8 +162,8 @@ export function registerBuiltInApiProviders(): void {
registerApiProvider({ registerApiProvider({
api: "bedrock-converse-stream", api: "bedrock-converse-stream",
stream: streamBedrock, stream: streamBedrockLazy,
streamSimple: streamSimpleBedrock, streamSimple: streamSimpleBedrockLazy,
}); });
} }

View file

@ -1,5 +1,4 @@
import "./providers/register-builtins.js"; import "./providers/register-builtins.js";
import "./utils/http-proxy.js";
import { getApiProvider } from "./api-registry.js"; import { getApiProvider } from "./api-registry.js";
import type { 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) * - 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 // Anthropic
export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js"; export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
// GitHub Copilot // GitHub Copilot

View file

@ -56,6 +56,7 @@
"minimatch": "^10.2.3", "minimatch": "^10.2.3",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"undici": "^7.19.1",
"yaml": "^2.8.2" "yaml": "^2.8.2"
}, },
"overrides": { "overrides": {

View file

@ -7,6 +7,9 @@
*/ */
process.title = "pi"; process.title = "pi";
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
import { main } from "./main.js"; import { main } from "./main.js";
setGlobalDispatcher(new EnvHttpProxyAgent());
main(process.argv.slice(2)); main(process.argv.slice(2));

View file

@ -8,13 +8,11 @@
import { import {
getEnvApiKey, getEnvApiKey,
getOAuthApiKey,
getOAuthProvider,
getOAuthProviders,
type OAuthCredentials, type OAuthCredentials,
type OAuthLoginCallbacks, type OAuthLoginCallbacks,
type OAuthProviderId, type OAuthProviderId,
} from "@mariozechner/pi-ai"; } from "@mariozechner/pi-ai";
import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path"; import { dirname, join } from "path";
import lockfile from "proper-lockfile"; import lockfile from "proper-lockfile";

View file

@ -14,11 +14,10 @@ import {
type OpenAICompletionsCompat, type OpenAICompletionsCompat,
type OpenAIResponsesCompat, type OpenAIResponsesCompat,
registerApiProvider, registerApiProvider,
registerOAuthProvider,
resetApiProviders, resetApiProviders,
resetOAuthProviders,
type SimpleStreamOptions, type SimpleStreamOptions,
} from "@mariozechner/pi-ai"; } from "@mariozechner/pi-ai";
import { registerOAuthProvider, resetOAuthProviders } from "@mariozechner/pi-ai/oauth";
import { type Static, Type } from "@sinclair/typebox"; import { type Static, Type } from "@sinclair/typebox";
import AjvModule from "ajv"; import AjvModule from "ajv";
import { existsSync, readFileSync } from "fs"; import { existsSync, readFileSync } from "fs";

View file

@ -1,4 +1,4 @@
import { getOAuthProviders } from "@mariozechner/pi-ai"; import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
import { exec } from "child_process"; import { exec } from "child_process";
import { theme } from "../theme/theme.js"; import { theme } from "../theme/theme.js";

View file

@ -1,4 +1,5 @@
import { getOAuthProviders, type OAuthProviderInterface } from "@mariozechner/pi-ai"; import type { OAuthProviderInterface } from "@mariozechner/pi-ai";
import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
import { Container, getEditorKeybindings, Spacer, TruncatedText } from "@mariozechner/pi-tui"; import { Container, getEditorKeybindings, Spacer, TruncatedText } from "@mariozechner/pi-tui";
import type { AuthStorage } from "../../../core/auth-storage.js"; import type { AuthStorage } from "../../../core/auth-storage.js";
import { theme } from "../theme/theme.js"; import { theme } from "../theme/theme.js";

View file

@ -8,14 +8,7 @@ import * as fs from "node:fs";
import * as os from "node:os"; import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { import type { AssistantMessage, ImageContent, Message, Model, OAuthProviderId } from "@mariozechner/pi-ai";
type AssistantMessage,
getOAuthProviders,
type ImageContent,
type Message,
type Model,
type OAuthProvider,
} from "@mariozechner/pi-ai";
import type { import type {
AutocompleteItem, AutocompleteItem,
EditorAction, EditorAction,
@ -3625,7 +3618,9 @@ export class InteractiveMode {
await this.showLoginDialog(providerId); await this.showLoginDialog(providerId);
} else { } else {
// Logout flow // Logout flow
const providerInfo = getOAuthProviders().find((p) => p.id === providerId); const providerInfo = this.session.modelRegistry.authStorage
.getOAuthProviders()
.find((p) => p.id === providerId);
const providerName = providerInfo?.name || providerId; const providerName = providerInfo?.name || providerId;
try { try {
@ -3648,7 +3643,7 @@ export class InteractiveMode {
} }
private async showLoginDialog(providerId: string): Promise<void> { private async showLoginDialog(providerId: string): Promise<void> {
const providerInfo = getOAuthProviders().find((p) => p.id === providerId); const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
const providerName = providerInfo?.name || providerId; const providerName = providerInfo?.name || providerId;
// Providers that use callback servers (can paste redirect URL) // Providers that use callback servers (can paste redirect URL)
@ -3682,7 +3677,7 @@ export class InteractiveMode {
}; };
try { try {
await this.session.modelRegistry.authStorage.login(providerId as OAuthProvider, { await this.session.modelRegistry.authStorage.login(providerId as OAuthProviderId, {
onAuth: (info: { url: string; instructions?: string }) => { onAuth: (info: { url: string; instructions?: string }) => {
dialog.showAuth(info.url, info.instructions); dialog.showAuth(info.url, info.instructions);

View file

@ -1,7 +1,7 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { registerOAuthProvider } from "@mariozechner/pi-ai"; import { registerOAuthProvider } from "@mariozechner/pi-ai/oauth";
import lockfile from "proper-lockfile"; import lockfile from "proper-lockfile";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js"; import { AuthStorage } from "../src/core/auth-storage.js";

View file

@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import type { Api, Context, Model, OpenAICompletionsCompat } from "@mariozechner/pi-ai"; import type { Api, Context, Model, OpenAICompletionsCompat } from "@mariozechner/pi-ai";
import { getApiProvider, getOAuthProvider } from "@mariozechner/pi-ai"; import { getApiProvider } from "@mariozechner/pi-ai";
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js"; import { AuthStorage } from "../src/core/auth-storage.js";
import { clearApiKeyCache, ModelRegistry } from "../src/core/model-registry.js"; import { clearApiKeyCache, ModelRegistry } from "../src/core/model-registry.js";

View file

@ -6,7 +6,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
import { homedir, tmpdir } from "node:os"; import { homedir, tmpdir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { Agent } from "@mariozechner/pi-agent-core"; import { Agent } from "@mariozechner/pi-agent-core";
import { getModel, getOAuthApiKey, type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai"; import { getModel, type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai";
import { getOAuthApiKey } from "@mariozechner/pi-ai/oauth";
import { AgentSession } from "../src/core/agent-session.js"; import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js"; import { AuthStorage } from "../src/core/auth-storage.js";
import { createExtensionRuntime } from "../src/core/extensions/loader.js"; import { createExtensionRuntime } from "../src/core/extensions/loader.js";

View file

@ -0,0 +1,4 @@
import { complete, getModel } from "@mariozechner/pi-ai";
const model = getModel("google", "gemini-2.5-flash");
console.log(model.id, typeof complete);