@mariozechner/pi-ai 0.5.27 → 0.5.29

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.
Files changed (42) hide show
  1. package/README.md +355 -275
  2. package/dist/generate.d.ts +22 -0
  3. package/dist/generate.d.ts.map +1 -0
  4. package/dist/generate.js +204 -0
  5. package/dist/generate.js.map +1 -0
  6. package/dist/index.d.ts +7 -8
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +7 -12
  9. package/dist/index.js.map +1 -1
  10. package/dist/models.d.ts +10 -71
  11. package/dist/models.d.ts.map +1 -1
  12. package/dist/models.generated.d.ts +3056 -2659
  13. package/dist/models.generated.d.ts.map +1 -1
  14. package/dist/models.generated.js +3063 -2663
  15. package/dist/models.generated.js.map +1 -1
  16. package/dist/models.js +17 -59
  17. package/dist/models.js.map +1 -1
  18. package/dist/providers/anthropic.d.ts +5 -18
  19. package/dist/providers/anthropic.d.ts.map +1 -1
  20. package/dist/providers/anthropic.js +254 -227
  21. package/dist/providers/anthropic.js.map +1 -1
  22. package/dist/providers/google.d.ts +3 -14
  23. package/dist/providers/google.d.ts.map +1 -1
  24. package/dist/providers/google.js +215 -220
  25. package/dist/providers/google.js.map +1 -1
  26. package/dist/providers/openai-completions.d.ts +4 -14
  27. package/dist/providers/openai-completions.d.ts.map +1 -1
  28. package/dist/providers/openai-completions.js +247 -215
  29. package/dist/providers/openai-completions.js.map +1 -1
  30. package/dist/providers/openai-responses.d.ts +6 -13
  31. package/dist/providers/openai-responses.d.ts.map +1 -1
  32. package/dist/providers/openai-responses.js +242 -244
  33. package/dist/providers/openai-responses.js.map +1 -1
  34. package/dist/providers/utils.d.ts +2 -14
  35. package/dist/providers/utils.d.ts.map +1 -1
  36. package/dist/providers/utils.js +2 -15
  37. package/dist/providers/utils.js.map +1 -1
  38. package/dist/types.d.ts +39 -16
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +1 -0
  41. package/dist/types.js.map +1 -1
  42. package/package.json +1 -1
@@ -1,60 +1,16 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
+ import { QueuedGenerateStream } from "../generate.js";
2
3
  import { calculateCost } from "../models.js";
3
4
  import { transformMessages } from "./utils.js";
4
- export class AnthropicLLM {
5
- client;
6
- modelInfo;
7
- isOAuthToken = false;
8
- constructor(model, apiKey) {
9
- if (!apiKey) {
10
- if (!process.env.ANTHROPIC_API_KEY) {
11
- throw new Error("Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or pass it as an argument.");
12
- }
13
- apiKey = process.env.ANTHROPIC_API_KEY;
14
- }
15
- if (apiKey.includes("sk-ant-oat")) {
16
- const defaultHeaders = {
17
- accept: "application/json",
18
- "anthropic-dangerous-direct-browser-access": "true",
19
- "anthropic-beta": "oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
20
- };
21
- // Clear the env var if we're in Node.js to prevent SDK from using it
22
- if (typeof process !== "undefined" && process.env) {
23
- process.env.ANTHROPIC_API_KEY = undefined;
24
- }
25
- this.client = new Anthropic({
26
- apiKey: null,
27
- authToken: apiKey,
28
- baseURL: model.baseUrl,
29
- defaultHeaders,
30
- dangerouslyAllowBrowser: true,
31
- });
32
- this.isOAuthToken = true;
33
- }
34
- else {
35
- const defaultHeaders = {
36
- accept: "application/json",
37
- "anthropic-dangerous-direct-browser-access": "true",
38
- "anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
39
- };
40
- this.client = new Anthropic({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders });
41
- this.isOAuthToken = false;
42
- }
43
- this.modelInfo = model;
44
- }
45
- getModel() {
46
- return this.modelInfo;
47
- }
48
- getApi() {
49
- return "anthropic-messages";
50
- }
51
- async generate(context, options) {
5
+ export const streamAnthropic = (model, context, options) => {
6
+ const stream = new QueuedGenerateStream();
7
+ (async () => {
52
8
  const output = {
53
9
  role: "assistant",
54
10
  content: [],
55
- api: this.getApi(),
56
- provider: this.modelInfo.provider,
57
- model: this.modelInfo.id,
11
+ api: "anthropic-messages",
12
+ provider: model.provider,
13
+ model: model.id,
58
14
  usage: {
59
15
  input: 0,
60
16
  output: 0,
@@ -65,67 +21,12 @@ export class AnthropicLLM {
65
21
  stopReason: "stop",
66
22
  };
67
23
  try {
68
- const messages = this.convertMessages(context.messages);
69
- const params = {
70
- model: this.modelInfo.id,
71
- messages,
72
- max_tokens: options?.maxTokens || 4096,
73
- stream: true,
74
- };
75
- // For OAuth tokens, we MUST include Claude Code identity
76
- if (this.isOAuthToken) {
77
- params.system = [
78
- {
79
- type: "text",
80
- text: "You are Claude Code, Anthropic's official CLI for Claude.",
81
- cache_control: {
82
- type: "ephemeral",
83
- },
84
- },
85
- ];
86
- if (context.systemPrompt) {
87
- params.system.push({
88
- type: "text",
89
- text: context.systemPrompt,
90
- cache_control: {
91
- type: "ephemeral",
92
- },
93
- });
94
- }
95
- }
96
- else if (context.systemPrompt) {
97
- params.system = context.systemPrompt;
98
- }
99
- if (options?.temperature !== undefined) {
100
- params.temperature = options?.temperature;
101
- }
102
- if (context.tools) {
103
- params.tools = this.convertTools(context.tools);
104
- }
105
- // Only enable thinking if the model supports it
106
- if (options?.thinking?.enabled && this.modelInfo.reasoning) {
107
- params.thinking = {
108
- type: "enabled",
109
- budget_tokens: options.thinking.budgetTokens || 1024,
110
- };
111
- }
112
- if (options?.toolChoice) {
113
- if (typeof options.toolChoice === "string") {
114
- params.tool_choice = { type: options.toolChoice };
115
- }
116
- else {
117
- params.tool_choice = options.toolChoice;
118
- }
119
- }
120
- const stream = this.client.messages.stream({
121
- ...params,
122
- stream: true,
123
- }, {
124
- signal: options?.signal,
125
- });
126
- options?.onEvent?.({ type: "start", model: this.modelInfo.id, provider: this.modelInfo.provider });
24
+ const { client, isOAuthToken } = createClient(model, options?.apiKey);
25
+ const params = buildParams(model, context, isOAuthToken, options);
26
+ const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
27
+ stream.push({ type: "start", partial: output });
127
28
  let currentBlock = null;
128
- for await (const event of stream) {
29
+ for await (const event of anthropicStream) {
129
30
  if (event.type === "content_block_start") {
130
31
  if (event.content_block.type === "text") {
131
32
  currentBlock = {
@@ -133,7 +34,7 @@ export class AnthropicLLM {
133
34
  text: "",
134
35
  };
135
36
  output.content.push(currentBlock);
136
- options?.onEvent?.({ type: "text_start" });
37
+ stream.push({ type: "text_start", partial: output });
137
38
  }
138
39
  else if (event.content_block.type === "thinking") {
139
40
  currentBlock = {
@@ -142,10 +43,10 @@ export class AnthropicLLM {
142
43
  thinkingSignature: "",
143
44
  };
144
45
  output.content.push(currentBlock);
145
- options?.onEvent?.({ type: "thinking_start" });
46
+ stream.push({ type: "thinking_start", partial: output });
146
47
  }
147
48
  else if (event.content_block.type === "tool_use") {
148
- // We wait for the full tool use to be streamed to send the event
49
+ // We wait for the full tool use to be streamed
149
50
  currentBlock = {
150
51
  type: "toolCall",
151
52
  id: event.content_block.id,
@@ -159,16 +60,20 @@ export class AnthropicLLM {
159
60
  if (event.delta.type === "text_delta") {
160
61
  if (currentBlock && currentBlock.type === "text") {
161
62
  currentBlock.text += event.delta.text;
162
- options?.onEvent?.({ type: "text_delta", content: currentBlock.text, delta: event.delta.text });
63
+ stream.push({
64
+ type: "text_delta",
65
+ delta: event.delta.text,
66
+ partial: output,
67
+ });
163
68
  }
164
69
  }
165
70
  else if (event.delta.type === "thinking_delta") {
166
71
  if (currentBlock && currentBlock.type === "thinking") {
167
72
  currentBlock.thinking += event.delta.thinking;
168
- options?.onEvent?.({
73
+ stream.push({
169
74
  type: "thinking_delta",
170
- content: currentBlock.thinking,
171
75
  delta: event.delta.thinking,
76
+ partial: output,
172
77
  });
173
78
  }
174
79
  }
@@ -187,10 +92,18 @@ export class AnthropicLLM {
187
92
  else if (event.type === "content_block_stop") {
188
93
  if (currentBlock) {
189
94
  if (currentBlock.type === "text") {
190
- options?.onEvent?.({ type: "text_end", content: currentBlock.text });
95
+ stream.push({
96
+ type: "text_end",
97
+ content: currentBlock.text,
98
+ partial: output,
99
+ });
191
100
  }
192
101
  else if (currentBlock.type === "thinking") {
193
- options?.onEvent?.({ type: "thinking_end", content: currentBlock.thinking });
102
+ stream.push({
103
+ type: "thinking_end",
104
+ content: currentBlock.thinking,
105
+ partial: output,
106
+ });
194
107
  }
195
108
  else if (currentBlock.type === "toolCall") {
196
109
  const finalToolCall = {
@@ -200,150 +113,264 @@ export class AnthropicLLM {
200
113
  arguments: JSON.parse(currentBlock.partialJson),
201
114
  };
202
115
  output.content.push(finalToolCall);
203
- options?.onEvent?.({ type: "toolCall", toolCall: finalToolCall });
116
+ stream.push({
117
+ type: "toolCall",
118
+ toolCall: finalToolCall,
119
+ partial: output,
120
+ });
204
121
  }
205
122
  currentBlock = null;
206
123
  }
207
124
  }
208
125
  else if (event.type === "message_delta") {
209
126
  if (event.delta.stop_reason) {
210
- output.stopReason = this.mapStopReason(event.delta.stop_reason);
127
+ output.stopReason = mapStopReason(event.delta.stop_reason);
211
128
  }
212
129
  output.usage.input += event.usage.input_tokens || 0;
213
130
  output.usage.output += event.usage.output_tokens || 0;
214
131
  output.usage.cacheRead += event.usage.cache_read_input_tokens || 0;
215
132
  output.usage.cacheWrite += event.usage.cache_creation_input_tokens || 0;
216
- calculateCost(this.modelInfo, output.usage);
133
+ calculateCost(model, output.usage);
217
134
  }
218
135
  }
219
- options?.onEvent?.({ type: "done", reason: output.stopReason, message: output });
220
- return output;
136
+ if (options?.signal?.aborted) {
137
+ throw new Error("Request was aborted");
138
+ }
139
+ stream.push({ type: "done", reason: output.stopReason, message: output });
140
+ stream.end();
221
141
  }
222
142
  catch (error) {
223
143
  output.stopReason = "error";
224
144
  output.error = error instanceof Error ? error.message : JSON.stringify(error);
225
- options?.onEvent?.({ type: "error", error: output.error });
226
- return output;
145
+ stream.push({ type: "error", error: output.error, partial: output });
146
+ stream.end();
147
+ }
148
+ })();
149
+ return stream;
150
+ };
151
+ function createClient(model, apiKey) {
152
+ if (apiKey.includes("sk-ant-oat")) {
153
+ const defaultHeaders = {
154
+ accept: "application/json",
155
+ "anthropic-dangerous-direct-browser-access": "true",
156
+ "anthropic-beta": "oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
157
+ };
158
+ // Clear the env var if we're in Node.js to prevent SDK from using it
159
+ if (typeof process !== "undefined" && process.env) {
160
+ process.env.ANTHROPIC_API_KEY = undefined;
161
+ }
162
+ const client = new Anthropic({
163
+ apiKey: null,
164
+ authToken: apiKey,
165
+ baseURL: model.baseUrl,
166
+ defaultHeaders,
167
+ dangerouslyAllowBrowser: true,
168
+ });
169
+ return { client, isOAuthToken: true };
170
+ }
171
+ else {
172
+ const defaultHeaders = {
173
+ accept: "application/json",
174
+ "anthropic-dangerous-direct-browser-access": "true",
175
+ "anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
176
+ };
177
+ const client = new Anthropic({
178
+ apiKey,
179
+ baseURL: model.baseUrl,
180
+ dangerouslyAllowBrowser: true,
181
+ defaultHeaders,
182
+ });
183
+ return { client, isOAuthToken: false };
184
+ }
185
+ }
186
+ function buildParams(model, context, isOAuthToken, options) {
187
+ const params = {
188
+ model: model.id,
189
+ messages: convertMessages(context.messages, model),
190
+ max_tokens: options?.maxTokens || model.maxTokens,
191
+ stream: true,
192
+ };
193
+ // For OAuth tokens, we MUST include Claude Code identity
194
+ if (isOAuthToken) {
195
+ params.system = [
196
+ {
197
+ type: "text",
198
+ text: "You are Claude Code, Anthropic's official CLI for Claude.",
199
+ cache_control: {
200
+ type: "ephemeral",
201
+ },
202
+ },
203
+ ];
204
+ if (context.systemPrompt) {
205
+ params.system.push({
206
+ type: "text",
207
+ text: context.systemPrompt,
208
+ cache_control: {
209
+ type: "ephemeral",
210
+ },
211
+ });
212
+ }
213
+ }
214
+ else if (context.systemPrompt) {
215
+ params.system = context.systemPrompt;
216
+ }
217
+ if (options?.temperature !== undefined) {
218
+ params.temperature = options.temperature;
219
+ }
220
+ if (context.tools) {
221
+ params.tools = convertTools(context.tools);
222
+ }
223
+ if (options?.thinkingEnabled && model.reasoning) {
224
+ params.thinking = {
225
+ type: "enabled",
226
+ budget_tokens: options.thinkingBudgetTokens || 1024,
227
+ };
228
+ }
229
+ if (options?.toolChoice) {
230
+ if (typeof options.toolChoice === "string") {
231
+ params.tool_choice = { type: options.toolChoice };
232
+ }
233
+ else {
234
+ params.tool_choice = options.toolChoice;
227
235
  }
228
236
  }
229
- convertMessages(messages) {
230
- const params = [];
231
- // Transform messages for cross-provider compatibility
232
- const transformedMessages = transformMessages(messages, this.modelInfo, this.getApi());
233
- for (const msg of transformedMessages) {
234
- if (msg.role === "user") {
235
- // Handle both string and array content
236
- if (typeof msg.content === "string") {
237
+ return params;
238
+ }
239
+ // Sanitize tool call IDs to match Anthropic's required pattern: ^[a-zA-Z0-9_-]+$
240
+ function sanitizeToolCallId(id) {
241
+ // Replace any character that isn't alphanumeric, underscore, or hyphen with underscore
242
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
243
+ }
244
+ function convertMessages(messages, model) {
245
+ const params = [];
246
+ // Transform messages for cross-provider compatibility
247
+ const transformedMessages = transformMessages(messages, model);
248
+ for (const msg of transformedMessages) {
249
+ if (msg.role === "user") {
250
+ if (typeof msg.content === "string") {
251
+ if (msg.content.trim().length > 0) {
237
252
  params.push({
238
253
  role: "user",
239
254
  content: msg.content,
240
255
  });
241
256
  }
242
- else {
243
- // Convert array content to Anthropic format
244
- const blocks = msg.content.map((item) => {
245
- if (item.type === "text") {
246
- return {
247
- type: "text",
248
- text: item.text,
249
- };
250
- }
251
- else {
252
- // Image content
253
- return {
254
- type: "image",
255
- source: {
256
- type: "base64",
257
- media_type: item.mimeType,
258
- data: item.data,
259
- },
260
- };
261
- }
262
- });
263
- const filteredBlocks = !this.modelInfo?.input.includes("image")
264
- ? blocks.filter((b) => b.type !== "image")
265
- : blocks;
266
- params.push({
267
- role: "user",
268
- content: filteredBlocks,
269
- });
270
- }
271
257
  }
272
- else if (msg.role === "assistant") {
273
- const blocks = [];
274
- for (const block of msg.content) {
275
- if (block.type === "text") {
276
- blocks.push({
258
+ else {
259
+ const blocks = msg.content.map((item) => {
260
+ if (item.type === "text") {
261
+ return {
277
262
  type: "text",
278
- text: block.text,
279
- });
263
+ text: item.text,
264
+ };
280
265
  }
281
- else if (block.type === "thinking") {
282
- blocks.push({
283
- type: "thinking",
284
- thinking: block.thinking,
285
- signature: block.thinkingSignature || "",
286
- });
266
+ else {
267
+ return {
268
+ type: "image",
269
+ source: {
270
+ type: "base64",
271
+ media_type: item.mimeType,
272
+ data: item.data,
273
+ },
274
+ };
287
275
  }
288
- else if (block.type === "toolCall") {
289
- blocks.push({
290
- type: "tool_use",
291
- id: block.id,
292
- name: block.name,
293
- input: block.arguments,
294
- });
276
+ });
277
+ let filteredBlocks = !model?.input.includes("image") ? blocks.filter((b) => b.type !== "image") : blocks;
278
+ filteredBlocks = filteredBlocks.filter((b) => {
279
+ if (b.type === "text") {
280
+ return b.text.trim().length > 0;
295
281
  }
296
- }
297
- params.push({
298
- role: "assistant",
299
- content: blocks,
282
+ return true;
300
283
  });
301
- }
302
- else if (msg.role === "toolResult") {
284
+ if (filteredBlocks.length === 0)
285
+ continue;
303
286
  params.push({
304
287
  role: "user",
305
- content: [
306
- {
307
- type: "tool_result",
308
- tool_use_id: msg.toolCallId,
309
- content: msg.content,
310
- is_error: msg.isError,
311
- },
312
- ],
288
+ content: filteredBlocks,
313
289
  });
314
290
  }
315
291
  }
316
- return params;
317
- }
318
- convertTools(tools) {
319
- if (!tools)
320
- return [];
321
- return tools.map((tool) => ({
322
- name: tool.name,
323
- description: tool.description,
324
- input_schema: {
325
- type: "object",
326
- properties: tool.parameters.properties || {},
327
- required: tool.parameters.required || [],
328
- },
329
- }));
292
+ else if (msg.role === "assistant") {
293
+ const blocks = [];
294
+ for (const block of msg.content) {
295
+ if (block.type === "text") {
296
+ if (block.text.trim().length === 0)
297
+ continue;
298
+ blocks.push({
299
+ type: "text",
300
+ text: block.text,
301
+ });
302
+ }
303
+ else if (block.type === "thinking") {
304
+ if (block.thinking.trim().length === 0)
305
+ continue;
306
+ blocks.push({
307
+ type: "thinking",
308
+ thinking: block.thinking,
309
+ signature: block.thinkingSignature || "",
310
+ });
311
+ }
312
+ else if (block.type === "toolCall") {
313
+ blocks.push({
314
+ type: "tool_use",
315
+ id: sanitizeToolCallId(block.id),
316
+ name: block.name,
317
+ input: block.arguments,
318
+ });
319
+ }
320
+ }
321
+ if (blocks.length === 0)
322
+ continue;
323
+ params.push({
324
+ role: "assistant",
325
+ content: blocks,
326
+ });
327
+ }
328
+ else if (msg.role === "toolResult") {
329
+ params.push({
330
+ role: "user",
331
+ content: [
332
+ {
333
+ type: "tool_result",
334
+ tool_use_id: sanitizeToolCallId(msg.toolCallId),
335
+ content: msg.content,
336
+ is_error: msg.isError,
337
+ },
338
+ ],
339
+ });
340
+ }
330
341
  }
331
- mapStopReason(reason) {
332
- switch (reason) {
333
- case "end_turn":
334
- return "stop";
335
- case "max_tokens":
336
- return "length";
337
- case "tool_use":
338
- return "toolUse";
339
- case "refusal":
340
- return "safety";
341
- case "pause_turn": // Stop is good enough -> resubmit
342
- return "stop";
343
- case "stop_sequence":
344
- return "stop"; // We don't supply stop sequences, so this should never happen
345
- default:
346
- return "stop";
342
+ return params;
343
+ }
344
+ function convertTools(tools) {
345
+ if (!tools)
346
+ return [];
347
+ return tools.map((tool) => ({
348
+ name: tool.name,
349
+ description: tool.description,
350
+ input_schema: {
351
+ type: "object",
352
+ properties: tool.parameters.properties || {},
353
+ required: tool.parameters.required || [],
354
+ },
355
+ }));
356
+ }
357
+ function mapStopReason(reason) {
358
+ switch (reason) {
359
+ case "end_turn":
360
+ return "stop";
361
+ case "max_tokens":
362
+ return "length";
363
+ case "tool_use":
364
+ return "toolUse";
365
+ case "refusal":
366
+ return "safety";
367
+ case "pause_turn": // Stop is good enough -> resubmit
368
+ return "stop";
369
+ case "stop_sequence":
370
+ return "stop"; // We don't supply stop sequences, so this should never happen
371
+ default: {
372
+ const _exhaustive = reason;
373
+ throw new Error(`Unhandled stop reason: ${_exhaustive}`);
347
374
  }
348
375
  }
349
376
  }