@oh-my-pi/pi-ai 6.7.670 → 6.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,38 +10,38 @@ Unified LLM API with automatic model discovery, provider configuration, token an
10
10
  - [Installation](#installation)
11
11
  - [Quick Start](#quick-start)
12
12
  - [Tools](#tools)
13
- - [Defining Tools](#defining-tools)
14
- - [Handling Tool Calls](#handling-tool-calls)
15
- - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)
16
- - [Validating Tool Arguments](#validating-tool-arguments)
17
- - [Complete Event Reference](#complete-event-reference)
13
+ - [Defining Tools](#defining-tools)
14
+ - [Handling Tool Calls](#handling-tool-calls)
15
+ - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)
16
+ - [Validating Tool Arguments](#validating-tool-arguments)
17
+ - [Complete Event Reference](#complete-event-reference)
18
18
  - [Image Input](#image-input)
19
19
  - [Thinking/Reasoning](#thinkingreasoning)
20
- - [Unified Interface](#unified-interface-streamsimplecompletesimple)
21
- - [Provider-Specific Options](#provider-specific-options-streamcomplete)
22
- - [Streaming Thinking Content](#streaming-thinking-content)
20
+ - [Unified Interface](#unified-interface-streamsimplecompletesimple)
21
+ - [Provider-Specific Options](#provider-specific-options-streamcomplete)
22
+ - [Streaming Thinking Content](#streaming-thinking-content)
23
23
  - [Stop Reasons](#stop-reasons)
24
24
  - [Error Handling](#error-handling)
25
- - [Aborting Requests](#aborting-requests)
26
- - [Continuing After Abort](#continuing-after-abort)
25
+ - [Aborting Requests](#aborting-requests)
26
+ - [Continuing After Abort](#continuing-after-abort)
27
27
  - [APIs, Models, and Providers](#apis-models-and-providers)
28
- - [Providers and Models](#providers-and-models)
29
- - [Querying Providers and Models](#querying-providers-and-models)
30
- - [Custom Models](#custom-models)
31
- - [OpenAI Compatibility Settings](#openai-compatibility-settings)
32
- - [Type Safety](#type-safety)
28
+ - [Providers and Models](#providers-and-models)
29
+ - [Querying Providers and Models](#querying-providers-and-models)
30
+ - [Custom Models](#custom-models)
31
+ - [OpenAI Compatibility Settings](#openai-compatibility-settings)
32
+ - [Type Safety](#type-safety)
33
33
  - [Cross-Provider Handoffs](#cross-provider-handoffs)
34
34
  - [Context Serialization](#context-serialization)
35
35
  - [Browser Usage](#browser-usage)
36
- - [Environment Variables](#environment-variables-nodejs-only)
37
- - [Checking Environment Variables](#checking-environment-variables)
36
+ - [Environment Variables](#environment-variables-nodejs-only)
37
+ - [Checking Environment Variables](#checking-environment-variables)
38
38
  - [OAuth Providers](#oauth-providers)
39
- - [Vertex AI (ADC)](#vertex-ai-adc)
40
- - [CLI Login](#cli-login)
41
- - [Programmatic OAuth](#programmatic-oauth)
42
- - [Login Flow Example](#login-flow-example)
43
- - [Using OAuth Tokens](#using-oauth-tokens)
44
- - [Provider Notes](#provider-notes)
39
+ - [Vertex AI (ADC)](#vertex-ai-adc)
40
+ - [CLI Login](#cli-login)
41
+ - [Programmatic OAuth](#programmatic-oauth)
42
+ - [Login Flow Example](#login-flow-example)
43
+ - [Using OAuth Tokens](#using-oauth-tokens)
44
+ - [Provider Notes](#provider-notes)
45
45
  - [License](#license)
46
46
 
47
47
  ## Supported Providers
@@ -156,7 +156,7 @@ for (const call of toolCalls) {
156
156
  timeZone: call.arguments.timezone || "UTC",
157
157
  dateStyle: "full",
158
158
  timeStyle: "long",
159
- })
159
+ })
160
160
  : "Unknown tool";
161
161
 
162
162
  // Add tool result to context (supports text and images)
@@ -443,7 +443,7 @@ const response = await completeSimple(
443
443
  },
444
444
  {
445
445
  reasoning: "medium", // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)
446
- }
446
+ },
447
447
  );
448
448
 
449
449
  // Access thinking and text blocks
@@ -563,7 +563,7 @@ const s = stream(
563
563
  },
564
564
  {
565
565
  signal: controller.signal,
566
- }
566
+ },
567
567
  );
568
568
 
569
569
  for await (const event of s) {
@@ -856,7 +856,7 @@ const response = await complete(
856
856
  },
857
857
  {
858
858
  apiKey: "your-api-key",
859
- }
859
+ },
860
860
  );
861
861
  ```
862
862
 
@@ -957,9 +957,9 @@ Official docs: [Application Default Credentials](https://cloud.google.com/docs/a
957
957
  The quickest way to authenticate:
958
958
 
959
959
  ```bash
960
- npx @oh-my-pi/pi-ai login # interactive provider selection
961
- npx @oh-my-pi/pi-ai login anthropic # login to specific provider
962
- npx @oh-my-pi/pi-ai list # list available providers
960
+ bunx @oh-my-pi/pi-ai login # interactive provider selection
961
+ bunx @oh-my-pi/pi-ai login anthropic # login to specific provider
962
+ bunx @oh-my-pi/pi-ai list # list available providers
963
963
  ```
964
964
 
965
965
  Credentials are saved to `auth.json` in the current directory.
@@ -1035,7 +1035,7 @@ const response = await complete(
1035
1035
  {
1036
1036
  messages: [{ role: "user", content: "Hello!" }],
1037
1037
  },
1038
- { apiKey: result.apiKey }
1038
+ { apiKey: result.apiKey },
1039
1039
  );
1040
1040
  ```
1041
1041
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "6.7.670",
3
+ "version": "6.8.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -17,6 +17,7 @@
17
17
  "test": "bun test"
18
18
  },
19
19
  "dependencies": {
20
+ "@oh-my-pi/pi-utils": "6.8.1",
20
21
  "@anthropic-ai/sdk": "0.71.2",
21
22
  "@aws-sdk/client-bedrock-runtime": "^3.968.0",
22
23
  "@bufbuild/protobuf": "^2.10.2",
package/src/cli.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env bun
2
- import "./utils/migrate-env";
3
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
2
  import { createInterface } from "readline";
3
+ import { CliAuthStorage } from "./storage";
4
+ import "./utils/migrate-env";
5
5
  import { loginAnthropic } from "./utils/oauth/anthropic";
6
+ import { loginCursor } from "./utils/oauth/cursor";
6
7
  import { loginGitHubCopilot } from "./utils/oauth/github-copilot";
7
8
  import { loginAntigravity } from "./utils/oauth/google-antigravity";
8
9
  import { loginGeminiCli } from "./utils/oauth/google-gemini-cli";
@@ -10,105 +11,104 @@ import { getOAuthProviders } from "./utils/oauth/index";
10
11
  import { loginOpenAICodex } from "./utils/oauth/openai-codex";
11
12
  import type { OAuthCredentials, OAuthProvider } from "./utils/oauth/types";
12
13
 
13
- const AUTH_FILE = "auth.json";
14
14
  const PROVIDERS = getOAuthProviders();
15
15
 
16
16
  function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
17
- return new Promise((resolve) => rl.question(question, resolve));
18
- }
19
-
20
- function loadAuth(): Record<string, { type: "oauth" } & OAuthCredentials> {
21
- if (!existsSync(AUTH_FILE)) return {};
22
- try {
23
- return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
24
- } catch {
25
- return {};
26
- }
27
- }
28
-
29
- function saveAuth(auth: Record<string, { type: "oauth" } & OAuthCredentials>): void {
30
- writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), "utf-8");
17
+ const { promise, resolve } = Promise.withResolvers<string>();
18
+ rl.question(question, resolve);
19
+ return promise;
31
20
  }
32
21
 
33
22
  async function login(provider: OAuthProvider): Promise<void> {
34
23
  const rl = createInterface({ input: process.stdin, output: process.stdout });
35
24
 
36
25
  const promptFn = (msg: string) => prompt(rl, `${msg} `);
26
+ const storage = new CliAuthStorage();
37
27
 
38
28
  try {
39
29
  let credentials: OAuthCredentials;
40
30
 
41
31
  switch (provider) {
42
32
  case "anthropic":
43
- credentials = await loginAnthropic(
44
- (url) => {
33
+ credentials = await loginAnthropic({
34
+ onAuth(info) {
35
+ const { url } = info;
45
36
  console.log(`\nOpen this URL in your browser:\n${url}\n`);
46
37
  },
47
- async () => {
48
- return await promptFn("Paste the authorization code:");
38
+ onProgress(message) {
39
+ console.log(message);
49
40
  },
50
- );
41
+ });
51
42
  break;
52
43
 
53
44
  case "github-copilot":
54
45
  credentials = await loginGitHubCopilot({
55
- onAuth: (url, instructions) => {
46
+ onAuth(url, instructions) {
56
47
  console.log(`\nOpen this URL in your browser:\n${url}`);
57
48
  if (instructions) console.log(instructions);
58
49
  console.log();
59
50
  },
60
- onPrompt: async (p) => {
51
+ async onPrompt(p) {
61
52
  return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
62
53
  },
63
- onProgress: (msg) => console.log(msg),
64
54
  });
65
55
  break;
66
56
 
67
57
  case "google-gemini-cli":
68
- credentials = await loginGeminiCli(
69
- (info) => {
70
- console.log(`\nOpen this URL in your browser:\n${info.url}`);
71
- if (info.instructions) console.log(info.instructions);
58
+ credentials = await loginGeminiCli({
59
+ onAuth(info) {
60
+ const { url, instructions } = info;
61
+ console.log(`\nOpen this URL in your browser:\n${url}`);
62
+ if (instructions) console.log(instructions);
72
63
  console.log();
73
64
  },
74
- (msg) => console.log(msg),
75
- );
65
+ });
76
66
  break;
77
67
 
78
68
  case "google-antigravity":
79
- credentials = await loginAntigravity(
80
- (info) => {
81
- console.log(`\nOpen this URL in your browser:\n${info.url}`);
82
- if (info.instructions) console.log(info.instructions);
69
+ credentials = await loginAntigravity({
70
+ onAuth(info) {
71
+ const { url, instructions } = info;
72
+ console.log(`\nOpen this URL in your browser:\n${url}`);
73
+ if (instructions) console.log(instructions);
83
74
  console.log();
84
75
  },
85
- (msg) => console.log(msg),
86
- );
76
+ });
87
77
  break;
88
78
  case "openai-codex":
89
79
  credentials = await loginOpenAICodex({
90
- onAuth: (info) => {
91
- console.log(`\nOpen this URL in your browser:\n${info.url}`);
92
- if (info.instructions) console.log(info.instructions);
80
+ onAuth(info) {
81
+ const { url, instructions } = info;
82
+ console.log(`\nOpen this URL in your browser:\n${url}`);
83
+ if (instructions) console.log(instructions);
93
84
  console.log();
94
85
  },
95
- onPrompt: async (p) => {
86
+ async onPrompt(p) {
96
87
  return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
97
88
  },
98
- onProgress: (msg) => console.log(msg),
99
89
  });
100
90
  break;
101
91
 
92
+ case "cursor":
93
+ credentials = await loginCursor(
94
+ (url) => {
95
+ console.log(`\nOpen this URL in your browser:\n${url}\n`);
96
+ },
97
+ () => {
98
+ console.log("Waiting for browser authentication...");
99
+ },
100
+ );
101
+ break;
102
+
102
103
  default:
103
104
  throw new Error(`Unknown provider: ${provider}`);
104
105
  }
105
106
 
106
- const auth = loadAuth();
107
- auth[provider] = { type: "oauth", ...credentials };
108
- saveAuth(auth);
107
+ storage.saveOAuth(provider, credentials);
109
108
 
110
- console.log(`\nCredentials saved to ${AUTH_FILE}`);
109
+ console.log(`\nCredentials saved to ~/.omp/agent/agent.db`);
111
110
  } finally {
111
+ storage.close();
112
112
  rl.close();
113
113
  }
114
114
  }
@@ -118,10 +118,12 @@ async function main(): Promise<void> {
118
118
  const command = args[0];
119
119
 
120
120
  if (!command || command === "help" || command === "--help" || command === "-h") {
121
- console.log(`Usage: npx @oh-my-pi/pi-ai <command> [provider]
121
+ console.log(`Usage: bunx @oh-my-pi/pi-ai <command> [provider]
122
122
 
123
123
  Commands:
124
124
  login [provider] Login to an OAuth provider
125
+ logout [provider] Logout from an OAuth provider
126
+ status Show logged-in providers
125
127
  list List available providers
126
128
 
127
129
  Providers:
@@ -130,15 +132,43 @@ Providers:
130
132
  google-gemini-cli Google Gemini CLI
131
133
  google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS)
132
134
  openai-codex OpenAI Codex (ChatGPT Plus/Pro)
135
+ cursor Cursor (Claude, GPT, etc.)
133
136
 
134
137
  Examples:
135
- npx @oh-my-pi/pi-ai login # interactive provider selection
136
- npx @oh-my-pi/pi-ai login anthropic # login to specific provider
137
- npx @oh-my-pi/pi-ai list # list providers
138
+ bunx @oh-my-pi/pi-ai login # interactive provider selection
139
+ bunx @oh-my-pi/pi-ai login anthropic # login to specific provider
140
+ bunx @oh-my-pi/pi-ai logout anthropic # logout from specific provider
141
+ bunx @oh-my-pi/pi-ai status # show logged-in providers
142
+ bunx @oh-my-pi/pi-ai list # list providers
138
143
  `);
139
144
  return;
140
145
  }
141
146
 
147
+ if (command === "status") {
148
+ const storage = new CliAuthStorage();
149
+ try {
150
+ const providers = storage.listProviders();
151
+ if (providers.length === 0) {
152
+ console.log("No OAuth credentials stored.");
153
+ console.log(`Use 'bunx @oh-my-pi/pi-ai login' to authenticate.`);
154
+ } else {
155
+ console.log("Logged-in providers:\n");
156
+ for (const provider of providers) {
157
+ const oauth = storage.getOAuth(provider);
158
+ if (oauth) {
159
+ const expires = new Date(oauth.expires);
160
+ const expired = Date.now() >= oauth.expires;
161
+ const status = expired ? "(expired)" : `(expires ${expires.toLocaleString()})`;
162
+ console.log(` ${provider.padEnd(20)} ${status}`);
163
+ }
164
+ }
165
+ }
166
+ } finally {
167
+ storage.close();
168
+ }
169
+ return;
170
+ }
171
+
142
172
  if (command === "list") {
143
173
  console.log("Available OAuth providers:\n");
144
174
  for (const p of PROVIDERS) {
@@ -147,6 +177,50 @@ Examples:
147
177
  return;
148
178
  }
149
179
 
180
+ if (command === "logout") {
181
+ let provider = args[1] as OAuthProvider | undefined;
182
+ const storage = new CliAuthStorage();
183
+
184
+ try {
185
+ if (!provider) {
186
+ const providers = storage.listProviders();
187
+ if (providers.length === 0) {
188
+ console.log("No OAuth credentials stored.");
189
+ return;
190
+ }
191
+
192
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
193
+ console.log("Select a provider to logout:\n");
194
+ for (let i = 0; i < providers.length; i++) {
195
+ console.log(` ${i + 1}. ${providers[i]}`);
196
+ }
197
+ console.log();
198
+
199
+ const choice = await prompt(rl, `Enter number (1-${providers.length}): `);
200
+ rl.close();
201
+
202
+ const index = parseInt(choice, 10) - 1;
203
+ if (index < 0 || index >= providers.length) {
204
+ console.error("Invalid selection");
205
+ process.exit(1);
206
+ }
207
+ provider = providers[index] as OAuthProvider;
208
+ }
209
+
210
+ const oauth = storage.getOAuth(provider);
211
+ if (!oauth) {
212
+ console.error(`Not logged in to ${provider}`);
213
+ process.exit(1);
214
+ }
215
+
216
+ storage.deleteProvider(provider);
217
+ console.log(`Logged out from ${provider}`);
218
+ } finally {
219
+ storage.close();
220
+ }
221
+ return;
222
+ }
223
+
150
224
  if (command === "login") {
151
225
  let provider = args[1] as OAuthProvider | undefined;
152
226
 
@@ -171,7 +245,7 @@ Examples:
171
245
 
172
246
  if (!PROVIDERS.some((p) => p.id === provider)) {
173
247
  console.error(`Unknown provider: ${provider}`);
174
- console.error(`Use 'npx @oh-my-pi/pi-ai list' to see available providers`);
248
+ console.error(`Use 'bunx @oh-my-pi/pi-ai list' to see available providers`);
175
249
  process.exit(1);
176
250
  }
177
251
 
@@ -181,7 +255,7 @@ Examples:
181
255
  }
182
256
 
183
257
  console.error(`Unknown command: ${command}`);
184
- console.error(`Use 'npx @oh-my-pi/pi-ai --help' for usage`);
258
+ console.error(`Use 'bunx @oh-my-pi/pi-ai --help' for usage`);
185
259
  process.exit(1);
186
260
  }
187
261
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { createHash } from "node:crypto";
8
8
  import type { Content, ThinkingConfig } from "@google/genai";
9
+ import { abortableSleep } from "@oh-my-pi/pi-utils";
9
10
  import { calculateCost } from "../models";
10
11
  import type {
11
12
  Api,
@@ -301,23 +302,6 @@ function extractErrorMessage(errorText: string): string {
301
302
  return errorText;
302
303
  }
303
304
 
304
- /**
305
- * Sleep for a given number of milliseconds, respecting abort signal.
306
- */
307
- function sleep(ms: number, signal?: AbortSignal): Promise<void> {
308
- return new Promise((resolve, reject) => {
309
- if (signal?.aborted) {
310
- reject(new Error("Request was aborted"));
311
- return;
312
- }
313
- const timeout = setTimeout(resolve, ms);
314
- signal?.addEventListener("abort", () => {
315
- clearTimeout(timeout);
316
- reject(new Error("Request was aborted"));
317
- });
318
- });
319
- }
320
-
321
305
  interface CloudCodeAssistRequest {
322
306
  project: string;
323
307
  model: string;
@@ -468,7 +452,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
468
452
  // Use server-provided delay or exponential backoff
469
453
  const serverDelay = extractRetryDelay(errorText, response);
470
454
  const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt;
471
- await sleep(delayMs, options?.signal);
455
+ await abortableSleep(delayMs, options?.signal);
472
456
  continue;
473
457
  }
474
458
 
@@ -489,7 +473,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
489
473
  // Network errors are retryable
490
474
  if (attempt < MAX_RETRIES) {
491
475
  const delayMs = BASE_DELAY_MS * 2 ** attempt;
492
- await sleep(delayMs, options?.signal);
476
+ await abortableSleep(delayMs, options?.signal);
493
477
  continue;
494
478
  }
495
479
  throw lastError;
@@ -769,7 +753,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
769
753
 
770
754
  if (emptyAttempt > 0) {
771
755
  const backoffMs = EMPTY_STREAM_BASE_DELAY_MS * 2 ** (emptyAttempt - 1);
772
- await sleep(backoffMs, options?.signal);
756
+ await abortableSleep(backoffMs, options?.signal);
773
757
 
774
758
  if (!requestUrl) {
775
759
  throw new Error("Missing request URL");
@@ -1,3 +1,5 @@
1
+ import { readSseData } from "@oh-my-pi/pi-utils";
2
+
1
3
  export type CodexRateLimit = {
2
4
  used_percent?: number;
3
5
  window_minutes?: number;
@@ -74,49 +76,8 @@ export async function* parseCodexSseStream(response: Response): AsyncGenerator<R
74
76
  return;
75
77
  }
76
78
 
77
- const reader = response.body.getReader();
78
- const decoder = new TextDecoder();
79
- let buffer = "";
80
-
81
- while (true) {
82
- const { done, value } = await reader.read();
83
- if (done) break;
84
- buffer += decoder.decode(value, { stream: true });
85
-
86
- let index = buffer.indexOf("\n\n");
87
- while (index !== -1) {
88
- const chunk = buffer.slice(0, index);
89
- buffer = buffer.slice(index + 2);
90
- const event = parseSseChunk(chunk);
91
- if (event) yield event;
92
- index = buffer.indexOf("\n\n");
93
- }
94
- }
95
-
96
- if (buffer.trim()) {
97
- const event = parseSseChunk(buffer);
98
- if (event) yield event;
99
- }
100
- }
101
-
102
- function parseSseChunk(chunk: string): Record<string, unknown> | null {
103
- const lines = chunk.split("\n");
104
- const dataLines: string[] = [];
105
-
106
- for (const line of lines) {
107
- if (line.startsWith("data:")) {
108
- dataLines.push(line.slice(5).trim());
109
- }
110
- }
111
-
112
- if (dataLines.length === 0) return null;
113
- const data = dataLines.join("\n").trim();
114
- if (!data || data === "[DONE]") return null;
115
-
116
- try {
117
- return JSON.parse(data) as Record<string, unknown>;
118
- } catch {
119
- return null;
79
+ for await (const data of readSseData<Record<string, unknown>>(response.body)) {
80
+ yield data;
120
81
  }
121
82
  }
122
83
 
@@ -1,4 +1,5 @@
1
1
  import os from "node:os";
2
+ import { abortableSleep } from "@oh-my-pi/pi-utils";
2
3
  import type {
3
4
  ResponseFunctionToolCall,
4
5
  ResponseInput,
@@ -440,13 +441,13 @@ async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSign
440
441
  }
441
442
  if (signal?.aborted) return response;
442
443
  const delay = getRetryDelayMs(response, attempt);
443
- await new Promise((resolve) => setTimeout(resolve, delay));
444
+ await abortableSleep(delay, signal);
444
445
  } catch (error) {
445
446
  if (attempt >= CODEX_MAX_RETRIES || signal?.aborted) {
446
447
  throw error;
447
448
  }
448
449
  const delay = CODEX_RETRY_DELAY_MS * (attempt + 1);
449
- await new Promise((resolve) => setTimeout(resolve, delay));
450
+ await abortableSleep(delay, signal);
450
451
  }
451
452
  attempt += 1;
452
453
  }