@oh-my-pi/pi-ai 8.1.0 → 8.2.0

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
@@ -231,7 +231,7 @@ const bookMeetingTool: Tool = {
231
231
  Tool results use content blocks and can include both text and images:
232
232
 
233
233
  ```typescript
234
- import { readFileSync } from "fs";
234
+ import * as fs from "node:fs";
235
235
 
236
236
  const context: Context = {
237
237
  messages: [{ role: "user", content: "What is the weather in London?" }],
@@ -260,7 +260,7 @@ for (const block of response.content) {
260
260
  }
261
261
 
262
262
  // Tool results can also include images (for vision-capable models)
263
- const imageBuffer = readFileSync("chart.png");
263
+ const imageBuffer = fs.readFileSync("chart.png");
264
264
  context.messages.push({
265
265
  role: "toolResult",
266
266
  toolCallId: "tool_xyz",
@@ -379,7 +379,7 @@ All streaming events emitted during assistant message generation:
379
379
  Models with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.
380
380
 
381
381
  ```typescript
382
- import { readFileSync } from "fs";
382
+ import * as fs from "node:fs";
383
383
  import { getModel, complete } from "@oh-my-pi/pi-ai";
384
384
 
385
385
  const model = getModel("openai", "gpt-4o-mini");
@@ -389,7 +389,7 @@ if (model.input.includes("image")) {
389
389
  console.log("Model supports vision");
390
390
  }
391
391
 
392
- const imageBuffer = readFileSync("image.png");
392
+ const imageBuffer = fs.readFileSync("image.png");
393
393
  const base64Image = imageBuffer.toString("base64");
394
394
 
395
395
  const response = await complete(model, {
@@ -551,10 +551,9 @@ The abort signal allows you to cancel in-progress requests. Aborted requests hav
551
551
  import { getModel, stream } from "@oh-my-pi/pi-ai";
552
552
 
553
553
  const model = getModel("openai", "gpt-4o-mini");
554
- const controller = new AbortController();
555
554
 
556
555
  // Abort after 2 seconds
557
- setTimeout(() => controller.abort(), 2000);
556
+ const signal = AbortSignal.timeout(2000);
558
557
 
559
558
  const s = stream(
560
559
  model,
@@ -562,7 +561,7 @@ const s = stream(
562
561
  messages: [{ role: "user", content: "Write a long story" }],
563
562
  },
564
563
  {
565
- signal: controller.signal,
564
+ signal,
566
565
  },
567
566
  );
568
567
 
@@ -1022,7 +1021,7 @@ await loginOpenAICodex({
1022
1021
 
1023
1022
  ```typescript
1024
1023
  import { loginGitHubCopilot } from "@oh-my-pi/pi-ai";
1025
- import { writeFileSync } from "fs";
1024
+ import * as fs from "node:fs";
1026
1025
 
1027
1026
  const credentials = await loginGitHubCopilot({
1028
1027
  onAuth: (url, instructions) => {
@@ -1037,7 +1036,7 @@ const credentials = await loginGitHubCopilot({
1037
1036
 
1038
1037
  // Store credentials yourself
1039
1038
  const auth = { "github-copilot": { type: "oauth", ...credentials } };
1040
- writeFileSync("auth.json", JSON.stringify(auth, null, 2));
1039
+ fs.writeFileSync("auth.json", JSON.stringify(auth, null, 2));
1041
1040
  ```
1042
1041
 
1043
1042
  ### Using OAuth Tokens
@@ -1046,10 +1045,10 @@ Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired:
1046
1045
 
1047
1046
  ```typescript
1048
1047
  import { getModel, complete, getOAuthApiKey } from "@oh-my-pi/pi-ai";
1049
- import { readFileSync, writeFileSync } from "fs";
1048
+ import * as fs from "node:fs";
1050
1049
 
1051
1050
  // Load your stored credentials
1052
- const auth = JSON.parse(readFileSync("auth.json", "utf-8"));
1051
+ const auth = JSON.parse(fs.readFileSync("auth.json", "utf-8"));
1053
1052
 
1054
1053
  // Get API key (refreshes if expired)
1055
1054
  const result = await getOAuthApiKey("github-copilot", auth);
@@ -1057,7 +1056,7 @@ if (!result) throw new Error("Not logged in");
1057
1056
 
1058
1057
  // Save refreshed credentials
1059
1058
  auth["github-copilot"] = { type: "oauth", ...result.newCredentials };
1060
- writeFileSync("auth.json", JSON.stringify(auth, null, 2));
1059
+ fs.writeFileSync("auth.json", JSON.stringify(auth, null, 2));
1061
1060
 
1062
1061
  // Use the API key
1063
1062
  const model = getModel("github-copilot", "gpt-4o");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -10,13 +10,37 @@
10
10
  "types": "./src/index.ts",
11
11
  "import": "./src/index.ts"
12
12
  },
13
+ "./models": {
14
+ "types": "./src/models.ts",
15
+ "import": "./src/models.ts"
16
+ },
17
+ "./models.generated": {
18
+ "types": "./src/models.generated.ts",
19
+ "import": "./src/models.generated.ts"
20
+ },
21
+ "./stream": {
22
+ "types": "./src/stream.ts",
23
+ "import": "./src/stream.ts"
24
+ },
25
+ "./types": {
26
+ "types": "./src/types.ts",
27
+ "import": "./src/types.ts"
28
+ },
29
+ "./usage": {
30
+ "types": "./src/usage.ts",
31
+ "import": "./src/usage.ts"
32
+ },
33
+ "./storage": {
34
+ "types": "./src/storage.ts",
35
+ "import": "./src/storage.ts"
36
+ },
37
+ "./providers/*": {
38
+ "types": "./src/providers/*.ts",
39
+ "import": "./src/providers/*.ts"
40
+ },
13
41
  "./utils/*": {
14
42
  "types": "./src/utils/*.ts",
15
43
  "import": "./src/utils/*.ts"
16
- },
17
- "./*": {
18
- "types": "./src/*",
19
- "import": "./src/*"
20
44
  }
21
45
  },
22
46
  "bin": {
@@ -27,25 +51,25 @@
27
51
  "README.md"
28
52
  ],
29
53
  "scripts": {
54
+ "check": "tsgo -p tsconfig.json",
30
55
  "generate-models": "bun scripts/generate-models.ts",
31
- "test": "bun test",
32
- "prepublishOnly": "cp tsconfig.publish.json tsconfig.json"
56
+ "test": "bun test"
33
57
  },
34
58
  "dependencies": {
35
- "@oh-my-pi/pi-utils": "workspace:*",
36
- "@anthropic-ai/sdk": "0.71.2",
37
- "@aws-sdk/client-bedrock-runtime": "^3.968.0",
59
+ "@oh-my-pi/pi-utils": "8.2.0",
60
+ "@anthropic-ai/sdk": "^0.71.2",
61
+ "@aws-sdk/client-bedrock-runtime": "^3.975.0",
38
62
  "@bufbuild/protobuf": "^2.10.2",
39
63
  "@connectrpc/connect": "^2.1.1",
40
64
  "@connectrpc/connect-node": "^2.1.1",
41
- "@google/genai": "1.34.0",
42
- "@mistralai/mistralai": "1.10.0",
65
+ "@google/genai": "^1.38.0",
66
+ "@mistralai/mistralai": "^1.13.0",
43
67
  "@sinclair/typebox": "^0.34.41",
44
68
  "ajv": "^8.17.1",
45
69
  "ajv-formats": "^3.0.1",
46
70
  "chalk": "^5.6.2",
47
71
  "json5": "^2.2.3",
48
- "openai": "6.10.0",
72
+ "openai": "^6.16.0",
49
73
  "partial-json": "^0.1.7",
50
74
  "zod-to-json-schema": "^3.24.6"
51
75
  },
@@ -69,6 +93,6 @@
69
93
  "bun": ">=1.0.0"
70
94
  },
71
95
  "devDependencies": {
72
- "@types/node": "^24.3.0"
96
+ "@types/node": "^25.0.10"
73
97
  }
74
98
  }
package/src/cli.ts CHANGED
@@ -23,7 +23,7 @@ async function login(provider: OAuthProvider): Promise<void> {
23
23
  const rl = createInterface({ input: process.stdin, output: process.stdout });
24
24
 
25
25
  const promptFn = (msg: string) => prompt(rl, `${msg} `);
26
- const storage = new CliAuthStorage();
26
+ const storage = await CliAuthStorage.create();
27
27
 
28
28
  try {
29
29
  let credentials: OAuthCredentials;
@@ -91,7 +91,7 @@ async function login(provider: OAuthProvider): Promise<void> {
91
91
 
92
92
  case "cursor":
93
93
  credentials = await loginCursor(
94
- (url) => {
94
+ url => {
95
95
  console.log(`\nOpen this URL in your browser:\n${url}\n`);
96
96
  },
97
97
  () => {
@@ -145,7 +145,7 @@ Examples:
145
145
  }
146
146
 
147
147
  if (command === "status") {
148
- const storage = new CliAuthStorage();
148
+ const storage = await CliAuthStorage.create();
149
149
  try {
150
150
  const providers = storage.listProviders();
151
151
  if (providers.length === 0) {
@@ -179,7 +179,7 @@ Examples:
179
179
 
180
180
  if (command === "logout") {
181
181
  let provider = args[1] as OAuthProvider | undefined;
182
- const storage = new CliAuthStorage();
182
+ const storage = await CliAuthStorage.create();
183
183
 
184
184
  try {
185
185
  if (!provider) {
@@ -243,7 +243,7 @@ Examples:
243
243
  provider = PROVIDERS[index].id;
244
244
  }
245
245
 
246
- if (!PROVIDERS.some((p) => p.id === provider)) {
246
+ if (!PROVIDERS.some(p => p.id === provider)) {
247
247
  console.error(`Unknown provider: ${provider}`);
248
248
  console.error(`Use 'bunx @oh-my-pi/pi-ai list' to see available providers`);
249
249
  process.exit(1);
@@ -259,7 +259,7 @@ Examples:
259
259
  process.exit(1);
260
260
  }
261
261
 
262
- main().catch((err) => {
262
+ main().catch(err => {
263
263
  console.error("Error:", err.message);
264
264
  process.exit(1);
265
265
  });
@@ -17,8 +17,7 @@ import {
17
17
  type ToolConfiguration,
18
18
  ToolResultStatus,
19
19
  } from "@aws-sdk/client-bedrock-runtime";
20
-
21
- import { calculateCost } from "@oh-my-pi/pi-ai/models";
20
+ import { calculateCost } from "../models";
22
21
  import type {
23
22
  Api,
24
23
  AssistantMessage,
@@ -34,10 +33,10 @@ import type {
34
33
  Tool,
35
34
  ToolCall,
36
35
  ToolResultMessage,
37
- } from "@oh-my-pi/pi-ai/types";
38
- import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
39
- import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
40
- import { sanitizeSurrogates } from "@oh-my-pi/pi-ai/utils/sanitize-unicode";
36
+ } from "../types";
37
+ import { AssistantMessageEventStream } from "../utils/event-stream";
38
+ import { parseStreamingJson } from "../utils/json-parse";
39
+ import { sanitizeSurrogates } from "../utils/sanitize-unicode";
41
40
  import { transformMessages } from "./transform-messages";
42
41
 
43
42
  export interface BedrockOptions extends StreamOptions {
@@ -200,7 +199,7 @@ function handleContentBlockDelta(
200
199
  ): void {
201
200
  const contentBlockIndex = event.contentBlockIndex!;
202
201
  const delta = event.delta;
203
- let index = blocks.findIndex((b) => b.index === contentBlockIndex);
202
+ let index = blocks.findIndex(b => b.index === contentBlockIndex);
204
203
  let block = blocks[index];
205
204
 
206
205
  if (delta?.text !== undefined) {
@@ -271,7 +270,7 @@ function handleContentBlockStop(
271
270
  output: AssistantMessage,
272
271
  stream: AssistantMessageEventStream,
273
272
  ): void {
274
- const index = blocks.findIndex((b) => b.index === event.contentBlockIndex);
273
+ const index = blocks.findIndex(b => b.index === event.contentBlockIndex);
275
274
  const block = blocks[index];
276
275
  if (!block) return;
277
276
  delete (block as Block).index;
@@ -351,7 +350,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
351
350
  });
352
351
  } else {
353
352
  const contentBlocks = m.content
354
- .map((c) => {
353
+ .map(c => {
355
354
  switch (c.type) {
356
355
  case "text":
357
356
  return { text: sanitizeSurrogates(c.text) };
@@ -361,7 +360,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
361
360
  throw new Error("Unknown user content type");
362
361
  }
363
362
  })
364
- .filter((block) => {
363
+ .filter(block => {
365
364
  // Filter out empty text blocks
366
365
  if ("text" in block && block.text) {
367
366
  return block.text.trim().length > 0;
@@ -442,7 +441,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
442
441
  toolResults.push({
443
442
  toolResult: {
444
443
  toolUseId: sanitizeToolCallId(m.toolCallId),
445
- content: m.content.map((c) =>
444
+ content: m.content.map(c =>
446
445
  c.type === "image"
447
446
  ? { image: createImageBlock(c.mimeType, c.data) }
448
447
  : { text: sanitizeSurrogates(c.text) },
@@ -458,7 +457,7 @@ function convertMessages(context: Context, model: Model<"bedrock-converse-stream
458
457
  toolResults.push({
459
458
  toolResult: {
460
459
  toolUseId: sanitizeToolCallId(nextMsg.toolCallId),
461
- content: nextMsg.content.map((c) =>
460
+ content: nextMsg.content.map(c =>
462
461
  c.type === "image"
463
462
  ? { image: createImageBlock(c.mimeType, c.data) }
464
463
  : { text: sanitizeSurrogates(c.text) },
@@ -500,7 +499,7 @@ function convertToolConfig(
500
499
  ): ToolConfiguration | undefined {
501
500
  if (!tools?.length || toolChoice === "none") return undefined;
502
501
 
503
- const bedrockTools: BedrockTool[] = tools.map((tool) => ({
502
+ const bedrockTools: BedrockTool[] = tools.map(tool => ({
504
503
  toolSpec: {
505
504
  name: tool.name,
506
505
  description: tool.description,
@@ -4,8 +4,8 @@ import type {
4
4
  MessageCreateParamsStreaming,
5
5
  MessageParam,
6
6
  } from "@anthropic-ai/sdk/resources/messages";
7
- import { calculateCost } from "@oh-my-pi/pi-ai/models";
8
- import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "@oh-my-pi/pi-ai/stream";
7
+ import { calculateCost } from "../models";
8
+ import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
9
9
  import type {
10
10
  Api,
11
11
  AssistantMessage,
@@ -21,12 +21,11 @@ import type {
21
21
  Tool,
22
22
  ToolCall,
23
23
  ToolResultMessage,
24
- } from "@oh-my-pi/pi-ai/types";
25
- import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
26
- import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
27
- import { formatErrorMessageWithRetryAfter } from "@oh-my-pi/pi-ai/utils/retry-after";
28
- import { sanitizeSurrogates } from "@oh-my-pi/pi-ai/utils/sanitize-unicode";
29
-
24
+ } from "../types";
25
+ import { AssistantMessageEventStream } from "../utils/event-stream";
26
+ import { parseStreamingJson } from "../utils/json-parse";
27
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
28
+ import { sanitizeSurrogates } from "../utils/sanitize-unicode";
30
29
  import { transformMessages } from "./transform-messages";
31
30
 
32
31
  // Stealth mode: Mimic Claude Code headers and tool prefixing.
@@ -89,13 +88,13 @@ function convertContentBlocks(content: (TextContent | ImageContent)[]):
89
88
  }
90
89
  > {
91
90
  // If only text blocks, return as concatenated string for simplicity
92
- const hasImages = content.some((c) => c.type === "image");
91
+ const hasImages = content.some(c => c.type === "image");
93
92
  if (!hasImages) {
94
- return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join("\n"));
93
+ return sanitizeSurrogates(content.map(c => (c as TextContent).text).join("\n"));
95
94
  }
96
95
 
97
96
  // If we have images, convert to content block array
98
- const blocks = content.map((block) => {
97
+ const blocks = content.map(block => {
99
98
  if (block.type === "text") {
100
99
  return {
101
100
  type: "text" as const,
@@ -113,7 +112,7 @@ function convertContentBlocks(content: (TextContent | ImageContent)[]):
113
112
  });
114
113
 
115
114
  // If only images (no text), add placeholder text block
116
- const hasText = blocks.some((b) => b.type === "text");
115
+ const hasText = blocks.some(b => b.type === "text");
117
116
  if (!hasText) {
118
117
  blocks.unshift({
119
118
  type: "text" as const,
@@ -218,7 +217,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
218
217
  }
219
218
  } else if (event.type === "content_block_delta") {
220
219
  if (event.delta.type === "text_delta") {
221
- const index = blocks.findIndex((b) => b.index === event.index);
220
+ const index = blocks.findIndex(b => b.index === event.index);
222
221
  const block = blocks[index];
223
222
  if (block && block.type === "text") {
224
223
  block.text += event.delta.text;
@@ -230,7 +229,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
230
229
  });
231
230
  }
232
231
  } else if (event.delta.type === "thinking_delta") {
233
- const index = blocks.findIndex((b) => b.index === event.index);
232
+ const index = blocks.findIndex(b => b.index === event.index);
234
233
  const block = blocks[index];
235
234
  if (block && block.type === "thinking") {
236
235
  block.thinking += event.delta.thinking;
@@ -242,7 +241,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
242
241
  });
243
242
  }
244
243
  } else if (event.delta.type === "input_json_delta") {
245
- const index = blocks.findIndex((b) => b.index === event.index);
244
+ const index = blocks.findIndex(b => b.index === event.index);
246
245
  const block = blocks[index];
247
246
  if (block && block.type === "toolCall") {
248
247
  block.partialJson += event.delta.partial_json;
@@ -255,7 +254,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
255
254
  });
256
255
  }
257
256
  } else if (event.delta.type === "signature_delta") {
258
- const index = blocks.findIndex((b) => b.index === event.index);
257
+ const index = blocks.findIndex(b => b.index === event.index);
259
258
  const block = blocks[index];
260
259
  if (block && block.type === "thinking") {
261
260
  block.thinkingSignature = block.thinkingSignature || "";
@@ -263,7 +262,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
263
262
  }
264
263
  }
265
264
  } else if (event.type === "content_block_stop") {
266
- const index = blocks.findIndex((b) => b.index === event.index);
265
+ const index = blocks.findIndex(b => b.index === event.index);
267
266
  const block = blocks[index];
268
267
  if (block) {
269
268
  delete (block as any).index;
@@ -360,7 +359,7 @@ function isAnthropicBaseUrl(baseUrl?: string): boolean {
360
359
  export function normalizeExtraBetas(betas?: string[] | string): string[] {
361
360
  if (!betas) return [];
362
361
  const raw = Array.isArray(betas) ? betas : betas.split(",");
363
- return raw.map((beta) => beta.trim()).filter((beta) => beta.length > 0);
362
+ return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
364
363
  }
365
364
 
366
365
  // Build deduplicated beta header string
@@ -406,7 +405,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
406
405
  "X-App",
407
406
  "Authorization",
408
407
  "X-Api-Key",
409
- ].map((key) => key.toLowerCase()),
408
+ ].map(key => key.toLowerCase()),
410
409
  );
411
410
  const modelHeaders = Object.fromEntries(
412
411
  Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
@@ -687,7 +686,7 @@ function applyPromptCaching(params: MessageCreateParamsStreaming): void {
687
686
  // 3. Cache penultimate user message for conversation history caching
688
687
  const userIndexes = params.messages
689
688
  .map((message, index) => (message.role === "user" ? index : -1))
690
- .filter((index) => index >= 0);
689
+ .filter(index => index >= 0);
691
690
 
692
691
  if (userIndexes.length >= 2) {
693
692
  const penultimateUserIndex = userIndexes[userIndexes.length - 2];
@@ -748,7 +747,7 @@ function convertMessages(
748
747
  });
749
748
  }
750
749
  } else if (Array.isArray(msg.content)) {
751
- const blocks: Array<ContentBlockParam & CacheControlBlock> = msg.content.map((item) => {
750
+ const blocks: Array<ContentBlockParam & CacheControlBlock> = msg.content.map(item => {
752
751
  if (item.type === "text") {
753
752
  return {
754
753
  type: "text",
@@ -764,8 +763,8 @@ function convertMessages(
764
763
  },
765
764
  };
766
765
  });
767
- let filteredBlocks = !model?.input.includes("image") ? blocks.filter((b) => b.type !== "image") : blocks;
768
- filteredBlocks = filteredBlocks.filter((b) => {
766
+ let filteredBlocks = !model?.input.includes("image") ? blocks.filter(b => b.type !== "image") : blocks;
767
+ filteredBlocks = filteredBlocks.filter(b => {
769
768
  if (b.type === "text") {
770
769
  return b.text.trim().length > 0;
771
770
  }
@@ -786,7 +785,7 @@ function convertMessages(
786
785
  // block with a missing/invalid signature (e.g., from aborted stream), we must skip
787
786
  // the entire message to avoid API rejection. Checking the first non-empty block.
788
787
  const firstContentBlock = msg.content.find(
789
- (b) =>
788
+ b =>
790
789
  (b.type === "text" && b.text.trim().length > 0) ||
791
790
  (b.type === "thinking" && b.thinking.trim().length > 0) ||
792
791
  b.type === "toolCall",
@@ -889,7 +888,7 @@ function convertMessages(
889
888
  }
890
889
 
891
890
  // Final validation: filter out any messages with invalid content
892
- return params.filter((msg) => {
891
+ return params.filter(msg => {
893
892
  if (!msg.content) return false;
894
893
  if (typeof msg.content === "string") return msg.content.length > 0;
895
894
  if (Array.isArray(msg.content)) return msg.content.length > 0;
@@ -900,7 +899,7 @@ function convertMessages(
900
899
  function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
901
900
  if (!tools) return [];
902
901
 
903
- return tools.map((tool) => {
902
+ return tools.map(tool => {
904
903
  const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
905
904
 
906
905
  return {