@yeshwanthyk/ai 0.1.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.
Files changed (94) hide show
  1. package/README.md +1142 -0
  2. package/dist/agent/agent-loop.d.ts +16 -0
  3. package/dist/agent/agent-loop.d.ts.map +1 -0
  4. package/dist/agent/agent-loop.js +307 -0
  5. package/dist/agent/agent-loop.js.map +1 -0
  6. package/dist/agent/index.d.ts +4 -0
  7. package/dist/agent/index.d.ts.map +1 -0
  8. package/dist/agent/index.js +3 -0
  9. package/dist/agent/index.js.map +1 -0
  10. package/dist/agent/tools/calculate.d.ts +15 -0
  11. package/dist/agent/tools/calculate.d.ts.map +1 -0
  12. package/dist/agent/tools/calculate.js +23 -0
  13. package/dist/agent/tools/calculate.js.map +1 -0
  14. package/dist/agent/tools/get-current-time.d.ts +15 -0
  15. package/dist/agent/tools/get-current-time.d.ts.map +1 -0
  16. package/dist/agent/tools/get-current-time.js +38 -0
  17. package/dist/agent/tools/get-current-time.js.map +1 -0
  18. package/dist/agent/tools/index.d.ts +3 -0
  19. package/dist/agent/tools/index.d.ts.map +1 -0
  20. package/dist/agent/tools/index.js +3 -0
  21. package/dist/agent/tools/index.js.map +1 -0
  22. package/dist/agent/types.d.ts +69 -0
  23. package/dist/agent/types.d.ts.map +1 -0
  24. package/dist/agent/types.js +2 -0
  25. package/dist/agent/types.js.map +1 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +15 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/models.d.ts +11 -0
  31. package/dist/models.d.ts.map +1 -0
  32. package/dist/models.generated.d.ts +7406 -0
  33. package/dist/models.generated.d.ts.map +1 -0
  34. package/dist/models.generated.js +7268 -0
  35. package/dist/models.generated.js.map +1 -0
  36. package/dist/models.js +35 -0
  37. package/dist/models.js.map +1 -0
  38. package/dist/providers/anthropic.d.ts +12 -0
  39. package/dist/providers/anthropic.d.ts.map +1 -0
  40. package/dist/providers/anthropic.js +538 -0
  41. package/dist/providers/anthropic.js.map +1 -0
  42. package/dist/providers/google.d.ts +12 -0
  43. package/dist/providers/google.d.ts.map +1 -0
  44. package/dist/providers/google.js +427 -0
  45. package/dist/providers/google.js.map +1 -0
  46. package/dist/providers/openai-completions.d.ts +12 -0
  47. package/dist/providers/openai-completions.d.ts.map +1 -0
  48. package/dist/providers/openai-completions.js +540 -0
  49. package/dist/providers/openai-completions.js.map +1 -0
  50. package/dist/providers/openai-responses.d.ts +14 -0
  51. package/dist/providers/openai-responses.d.ts.map +1 -0
  52. package/dist/providers/openai-responses.js +553 -0
  53. package/dist/providers/openai-responses.js.map +1 -0
  54. package/dist/providers/transform-messages.d.ts +3 -0
  55. package/dist/providers/transform-messages.d.ts.map +1 -0
  56. package/dist/providers/transform-messages.js +111 -0
  57. package/dist/providers/transform-messages.js.map +1 -0
  58. package/dist/stream.d.ts +11 -0
  59. package/dist/stream.d.ts.map +1 -0
  60. package/dist/stream.js +204 -0
  61. package/dist/stream.js.map +1 -0
  62. package/dist/types.d.ts +203 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +4 -0
  65. package/dist/types.js.map +1 -0
  66. package/dist/utils/event-stream.d.ts +19 -0
  67. package/dist/utils/event-stream.d.ts.map +1 -0
  68. package/dist/utils/event-stream.js +77 -0
  69. package/dist/utils/event-stream.js.map +1 -0
  70. package/dist/utils/json-parse.d.ts +9 -0
  71. package/dist/utils/json-parse.d.ts.map +1 -0
  72. package/dist/utils/json-parse.js +29 -0
  73. package/dist/utils/json-parse.js.map +1 -0
  74. package/dist/utils/oauth/anthropic.d.ts +20 -0
  75. package/dist/utils/oauth/anthropic.d.ts.map +1 -0
  76. package/dist/utils/oauth/anthropic.js +103 -0
  77. package/dist/utils/oauth/anthropic.js.map +1 -0
  78. package/dist/utils/overflow.d.ts +51 -0
  79. package/dist/utils/overflow.d.ts.map +1 -0
  80. package/dist/utils/overflow.js +106 -0
  81. package/dist/utils/overflow.js.map +1 -0
  82. package/dist/utils/sanitize-unicode.d.ts +22 -0
  83. package/dist/utils/sanitize-unicode.d.ts.map +1 -0
  84. package/dist/utils/sanitize-unicode.js +26 -0
  85. package/dist/utils/sanitize-unicode.js.map +1 -0
  86. package/dist/utils/typebox-helpers.d.ts +17 -0
  87. package/dist/utils/typebox-helpers.d.ts.map +1 -0
  88. package/dist/utils/typebox-helpers.js +21 -0
  89. package/dist/utils/typebox-helpers.js.map +1 -0
  90. package/dist/utils/validation.d.ts +18 -0
  91. package/dist/utils/validation.d.ts.map +1 -0
  92. package/dist/utils/validation.js +69 -0
  93. package/dist/utils/validation.js.map +1 -0
  94. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,1142 @@
1
+ # @yeshwanthyk/ai
2
+
3
+ Unified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.
4
+
5
+ **Note**: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows.
6
+
7
+ ## Supported Providers
8
+
9
+ - **OpenAI**
10
+ - **Anthropic**
11
+ - **Google**
12
+ - **Mistral**
13
+ - **Groq**
14
+ - **Cerebras**
15
+ - **xAI**
16
+ - **OpenRouter**
17
+ - **GitHub Copilot** (requires OAuth, see below)
18
+ - **Any OpenAI-compatible API**: Ollama, vLLM, LM Studio, etc.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @yeshwanthyk/ai
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { Type, getModel, stream, complete, Context, Tool, StringEnum } from '@yeshwanthyk/ai';
30
+
31
+ // Fully typed with auto-complete support for both providers and models
32
+ const model = getModel('openai', 'gpt-4o-mini');
33
+
34
+ // Define tools with TypeBox schemas for type safety and validation
35
+ const tools: Tool[] = [{
36
+ name: 'get_time',
37
+ description: 'Get the current time',
38
+ parameters: Type.Object({
39
+ timezone: Type.Optional(Type.String({ description: 'Optional timezone (e.g., America/New_York)' }))
40
+ })
41
+ }];
42
+
43
+ // Build a conversation context (easily serializable and transferable between models)
44
+ const context: Context = {
45
+ systemPrompt: 'You are a helpful assistant.',
46
+ messages: [{ role: 'user', content: 'What time is it?' }],
47
+ tools
48
+ };
49
+
50
+ // Option 1: Streaming with all event types
51
+ const s = stream(model, context);
52
+
53
+ for await (const event of s) {
54
+ switch (event.type) {
55
+ case 'start':
56
+ console.log(`Starting with ${event.partial.model}`);
57
+ break;
58
+ case 'text_start':
59
+ console.log('\n[Text started]');
60
+ break;
61
+ case 'text_delta':
62
+ process.stdout.write(event.delta);
63
+ break;
64
+ case 'text_end':
65
+ console.log('\n[Text ended]');
66
+ break;
67
+ case 'thinking_start':
68
+ console.log('[Model is thinking...]');
69
+ break;
70
+ case 'thinking_delta':
71
+ process.stdout.write(event.delta);
72
+ break;
73
+ case 'thinking_end':
74
+ console.log('[Thinking complete]');
75
+ break;
76
+ case 'toolcall_start':
77
+ console.log(`\n[Tool call started: index ${event.contentIndex}]`);
78
+ break;
79
+ case 'toolcall_delta':
80
+ // Partial tool arguments are being streamed
81
+ const partialCall = event.partial.content[event.contentIndex];
82
+ if (partialCall.type === 'toolCall') {
83
+ console.log(`[Streaming args for ${partialCall.name}]`);
84
+ }
85
+ break;
86
+ case 'toolcall_end':
87
+ console.log(`\nTool called: ${event.toolCall.name}`);
88
+ console.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`);
89
+ break;
90
+ case 'done':
91
+ console.log(`\nFinished: ${event.reason}`);
92
+ break;
93
+ case 'error':
94
+ console.error(`Error: ${event.error}`);
95
+ break;
96
+ }
97
+ }
98
+
99
+ // Get the final message after streaming, add it to the context
100
+ const finalMessage = await s.result();
101
+ context.messages.push(finalMessage);
102
+
103
+ // Handle tool calls if any
104
+ const toolCalls = finalMessage.content.filter(b => b.type === 'toolCall');
105
+ for (const call of toolCalls) {
106
+ // Execute the tool
107
+ const result = call.name === 'get_time'
108
+ ? new Date().toLocaleString('en-US', {
109
+ timeZone: call.arguments.timezone || 'UTC',
110
+ dateStyle: 'full',
111
+ timeStyle: 'long'
112
+ })
113
+ : 'Unknown tool';
114
+
115
+ // Add tool result to context (supports text and images)
116
+ context.messages.push({
117
+ role: 'toolResult',
118
+ toolCallId: call.id,
119
+ toolName: call.name,
120
+ content: [{ type: 'text', text: result }],
121
+ isError: false,
122
+ timestamp: Date.now()
123
+ });
124
+ }
125
+
126
+ // Continue if there were tool calls
127
+ if (toolCalls.length > 0) {
128
+ const continuation = await complete(model, context);
129
+ context.messages.push(continuation);
130
+ console.log('After tool execution:', continuation.content);
131
+ }
132
+
133
+ console.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage.output} out`);
134
+ console.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`);
135
+
136
+ // Option 2: Get complete response without streaming
137
+ const response = await complete(model, context);
138
+
139
+ for (const block of response.content) {
140
+ if (block.type === 'text') {
141
+ console.log(block.text);
142
+ } else if (block.type === 'toolCall') {
143
+ console.log(`Tool: ${block.name}(${JSON.stringify(block.arguments)})`);
144
+ }
145
+ }
146
+ ```
147
+
148
+ ## Tools
149
+
150
+ Tools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using AJV. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems.
151
+
152
+ ### Defining Tools
153
+
154
+ ```typescript
155
+ import { Type, Tool, StringEnum } from '@yeshwanthyk/ai';
156
+
157
+ // Define tool parameters with TypeBox
158
+ const weatherTool: Tool = {
159
+ name: 'get_weather',
160
+ description: 'Get current weather for a location',
161
+ parameters: Type.Object({
162
+ location: Type.String({ description: 'City name or coordinates' }),
163
+ units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })
164
+ })
165
+ };
166
+
167
+ // Note: For Google API compatibility, use StringEnum helper instead of Type.Enum
168
+ // Type.Enum generates anyOf/const patterns that Google doesn't support
169
+
170
+ const bookMeetingTool: Tool = {
171
+ name: 'book_meeting',
172
+ description: 'Schedule a meeting',
173
+ parameters: Type.Object({
174
+ title: Type.String({ minLength: 1 }),
175
+ startTime: Type.String({ format: 'date-time' }),
176
+ endTime: Type.String({ format: 'date-time' }),
177
+ attendees: Type.Array(Type.String({ format: 'email' }), { minItems: 1 })
178
+ })
179
+ };
180
+ ```
181
+
182
+ ### Handling Tool Calls
183
+
184
+ Tool results use content blocks and can include both text and images:
185
+
186
+ ```typescript
187
+ import { readFileSync } from 'fs';
188
+
189
+ const context: Context = {
190
+ messages: [{ role: 'user', content: 'What is the weather in London?' }],
191
+ tools: [weatherTool]
192
+ };
193
+
194
+ const response = await complete(model, context);
195
+
196
+ // Check for tool calls in the response
197
+ for (const block of response.content) {
198
+ if (block.type === 'toolCall') {
199
+ // Execute your tool with the arguments
200
+ // See "Validating Tool Arguments" section for validation
201
+ const result = await executeWeatherApi(block.arguments);
202
+
203
+ // Add tool result with text content
204
+ context.messages.push({
205
+ role: 'toolResult',
206
+ toolCallId: block.id,
207
+ toolName: block.name,
208
+ content: [{ type: 'text', text: JSON.stringify(result) }],
209
+ isError: false,
210
+ timestamp: Date.now()
211
+ });
212
+ }
213
+ }
214
+
215
+ // Tool results can also include images (for vision-capable models)
216
+ const imageBuffer = readFileSync('chart.png');
217
+ context.messages.push({
218
+ role: 'toolResult',
219
+ toolCallId: 'tool_xyz',
220
+ toolName: 'generate_chart',
221
+ content: [
222
+ { type: 'text', text: 'Generated chart showing temperature trends' },
223
+ { type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' }
224
+ ],
225
+ isError: false,
226
+ timestamp: Date.now()
227
+ });
228
+ ```
229
+
230
+ ### Streaming Tool Calls with Partial JSON
231
+
232
+ During streaming, tool call arguments are progressively parsed as they arrive. This enables real-time UI updates before the complete arguments are available:
233
+
234
+ ```typescript
235
+ const s = stream(model, context);
236
+
237
+ for await (const event of s) {
238
+ if (event.type === 'toolcall_delta') {
239
+ const toolCall = event.partial.content[event.contentIndex];
240
+
241
+ // toolCall.arguments contains partially parsed JSON during streaming
242
+ // This allows for progressive UI updates
243
+ if (toolCall.type === 'toolCall' && toolCall.arguments) {
244
+ // BE DEFENSIVE: arguments may be incomplete
245
+ // Example: Show file path being written even before content is complete
246
+ if (toolCall.name === 'write_file' && toolCall.arguments.path) {
247
+ console.log(`Writing to: ${toolCall.arguments.path}`);
248
+
249
+ // Content might be partial or missing
250
+ if (toolCall.arguments.content) {
251
+ console.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ if (event.type === 'toolcall_end') {
258
+ // Here toolCall.arguments is complete (but not yet validated)
259
+ const toolCall = event.toolCall;
260
+ console.log(`Tool completed: ${toolCall.name}`, toolCall.arguments);
261
+ }
262
+ }
263
+ ```
264
+
265
+ **Important notes about partial tool arguments:**
266
+ - During `toolcall_delta` events, `arguments` contains the best-effort parse of partial JSON
267
+ - Fields may be missing or incomplete - always check for existence before use
268
+ - String values may be truncated mid-word
269
+ - Arrays may be incomplete
270
+ - Nested objects may be partially populated
271
+ - At minimum, `arguments` will be an empty object `{}`, never `undefined`
272
+ - The Google provider does not support function call streaming. Instead, you will receive a single `toolcall_delta` event with the full arguments.
273
+
274
+ ### Validating Tool Arguments
275
+
276
+ When using `agentLoop`, tool arguments are automatically validated against your TypeBox schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry.
277
+
278
+ When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:
279
+
280
+ ```typescript
281
+ import { stream, validateToolCall, Tool } from '@yeshwanthyk/ai';
282
+
283
+ const tools: Tool[] = [weatherTool, calculatorTool];
284
+ const s = stream(model, { messages, tools });
285
+
286
+ for await (const event of s) {
287
+ if (event.type === 'toolcall_end') {
288
+ const toolCall = event.toolCall;
289
+
290
+ try {
291
+ // Validate arguments against the tool's schema (throws on invalid args)
292
+ const validatedArgs = validateToolCall(tools, toolCall);
293
+ const result = await executeMyTool(toolCall.name, validatedArgs);
294
+ // ... add tool result to context
295
+ } catch (error) {
296
+ // Validation failed - return error as tool result so model can retry
297
+ context.messages.push({
298
+ role: 'toolResult',
299
+ toolCallId: toolCall.id,
300
+ toolName: toolCall.name,
301
+ content: [{ type: 'text', text: error.message }],
302
+ isError: true,
303
+ timestamp: Date.now()
304
+ });
305
+ }
306
+ }
307
+ }
308
+ ```
309
+
310
+ ### Complete Event Reference
311
+
312
+ All streaming events emitted during assistant message generation:
313
+
314
+ | Event Type | Description | Key Properties |
315
+ |------------|-------------|----------------|
316
+ | `start` | Stream begins | `partial`: Initial assistant message structure |
317
+ | `text_start` | Text block starts | `contentIndex`: Position in content array |
318
+ | `text_delta` | Text chunk received | `delta`: New text, `contentIndex`: Position |
319
+ | `text_end` | Text block complete | `content`: Full text, `contentIndex`: Position |
320
+ | `thinking_start` | Thinking block starts | `contentIndex`: Position in content array |
321
+ | `thinking_delta` | Thinking chunk received | `delta`: New text, `contentIndex`: Position |
322
+ | `thinking_end` | Thinking block complete | `content`: Full thinking, `contentIndex`: Position |
323
+ | `toolcall_start` | Tool call begins | `contentIndex`: Position in content array |
324
+ | `toolcall_delta` | Tool arguments streaming | `delta`: JSON chunk, `partial.content[contentIndex].arguments`: Partial parsed args |
325
+ | `toolcall_end` | Tool call complete | `toolCall`: Complete validated tool call with `id`, `name`, `arguments` |
326
+ | `done` | Stream complete | `reason`: Stop reason ("stop", "length", "toolUse"), `message`: Final assistant message |
327
+ | `error` | Error occurred | `reason`: Error type ("error" or "aborted"), `error`: AssistantMessage with partial content |
328
+
329
+ ## Image Input
330
+
331
+ 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.
332
+
333
+ ```typescript
334
+ import { readFileSync } from 'fs';
335
+ import { getModel, complete } from '@yeshwanthyk/ai';
336
+
337
+ const model = getModel('openai', 'gpt-4o-mini');
338
+
339
+ // Check if model supports images
340
+ if (model.input.includes('image')) {
341
+ console.log('Model supports vision');
342
+ }
343
+
344
+ const imageBuffer = readFileSync('image.png');
345
+ const base64Image = imageBuffer.toString('base64');
346
+
347
+ const response = await complete(model, {
348
+ messages: [{
349
+ role: 'user',
350
+ content: [
351
+ { type: 'text', text: 'What is in this image?' },
352
+ { type: 'image', data: base64Image, mimeType: 'image/png' }
353
+ ]
354
+ }]
355
+ });
356
+
357
+ // Access the response
358
+ for (const block of response.content) {
359
+ if (block.type === 'text') {
360
+ console.log(block.text);
361
+ }
362
+ }
363
+ ```
364
+
365
+ ## Thinking/Reasoning
366
+
367
+ Many models support thinking/reasoning capabilities where they can show their internal thought process. You can check if a model supports reasoning via the `reasoning` property. If you pass reasoning options to a non-reasoning model, they are silently ignored.
368
+
369
+ ### Unified Interface (streamSimple/completeSimple)
370
+
371
+ ```typescript
372
+ import { getModel, streamSimple, completeSimple } from '@yeshwanthyk/ai';
373
+
374
+ // Many models across providers support thinking/reasoning
375
+ const model = getModel('anthropic', 'claude-sonnet-4-20250514');
376
+ // or getModel('openai', 'gpt-5-mini');
377
+ // or getModel('google', 'gemini-2.5-flash');
378
+ // or getModel('xai', 'grok-code-fast-1');
379
+ // or getModel('groq', 'openai/gpt-oss-20b');
380
+ // or getModel('cerebras', 'gpt-oss-120b');
381
+ // or getModel('openrouter', 'z-ai/glm-4.5v');
382
+
383
+ // Check if model supports reasoning
384
+ if (model.reasoning) {
385
+ console.log('Model supports reasoning/thinking');
386
+ }
387
+
388
+ // Use the simplified reasoning option
389
+ const response = await completeSimple(model, {
390
+ messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13' }]
391
+ }, {
392
+ reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)
393
+ });
394
+
395
+ // Access thinking and text blocks
396
+ for (const block of response.content) {
397
+ if (block.type === 'thinking') {
398
+ console.log('Thinking:', block.thinking);
399
+ } else if (block.type === 'text') {
400
+ console.log('Response:', block.text);
401
+ }
402
+ }
403
+ ```
404
+
405
+ ### Provider-Specific Options (stream/complete)
406
+
407
+ For fine-grained control, use the provider-specific options:
408
+
409
+ ```typescript
410
+ import { getModel, complete } from '@yeshwanthyk/ai';
411
+
412
+ // OpenAI Reasoning (o1, o3, gpt-5)
413
+ const openaiModel = getModel('openai', 'gpt-5-mini');
414
+ await complete(openaiModel, context, {
415
+ reasoningEffort: 'medium',
416
+ reasoningSummary: 'detailed' // OpenAI Responses API only
417
+ });
418
+
419
+ // Anthropic Thinking (Claude Sonnet 4)
420
+ const anthropicModel = getModel('anthropic', 'claude-sonnet-4-20250514');
421
+ await complete(anthropicModel, context, {
422
+ thinkingEnabled: true,
423
+ thinkingBudgetTokens: 8192 // Optional token limit
424
+ });
425
+
426
+ // Google Gemini Thinking
427
+ const googleModel = getModel('google', 'gemini-2.5-flash');
428
+ await complete(googleModel, context, {
429
+ thinking: {
430
+ enabled: true,
431
+ budgetTokens: 8192 // -1 for dynamic, 0 to disable
432
+ }
433
+ });
434
+ ```
435
+
436
+ ### Streaming Thinking Content
437
+
438
+ When streaming, thinking content is delivered through specific events:
439
+
440
+ ```typescript
441
+ const s = streamSimple(model, context, { reasoning: 'high' });
442
+
443
+ for await (const event of s) {
444
+ switch (event.type) {
445
+ case 'thinking_start':
446
+ console.log('[Model started thinking]');
447
+ break;
448
+ case 'thinking_delta':
449
+ process.stdout.write(event.delta); // Stream thinking content
450
+ break;
451
+ case 'thinking_end':
452
+ console.log('\n[Thinking complete]');
453
+ break;
454
+ }
455
+ }
456
+ ```
457
+
458
+ ## Stop Reasons
459
+
460
+ Every `AssistantMessage` includes a `stopReason` field that indicates how the generation ended:
461
+
462
+ - `"stop"` - Normal completion, the model finished its response
463
+ - `"length"` - Output hit the maximum token limit
464
+ - `"toolUse"` - Model is calling tools and expects tool results
465
+ - `"error"` - An error occurred during generation
466
+ - `"aborted"` - Request was cancelled via abort signal
467
+
468
+ ## Error Handling
469
+
470
+ When a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event:
471
+
472
+ ```typescript
473
+ // In streaming
474
+ for await (const event of stream) {
475
+ if (event.type === 'error') {
476
+ // event.reason is either "error" or "aborted"
477
+ // event.error is the AssistantMessage with partial content
478
+ console.error(`Error (${event.reason}):`, event.error.errorMessage);
479
+ console.log('Partial content:', event.error.content);
480
+ }
481
+ }
482
+
483
+ // The final message will have the error details
484
+ const message = await stream.result();
485
+ if (message.stopReason === 'error' || message.stopReason === 'aborted') {
486
+ console.error('Request failed:', message.errorMessage);
487
+ // message.content contains any partial content received before the error
488
+ // message.usage contains partial token counts and costs
489
+ }
490
+ ```
491
+
492
+ ### Aborting Requests
493
+
494
+ The abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:
495
+
496
+ ```typescript
497
+ import { getModel, stream } from '@yeshwanthyk/ai';
498
+
499
+ const model = getModel('openai', 'gpt-4o-mini');
500
+ const controller = new AbortController();
501
+
502
+ // Abort after 2 seconds
503
+ setTimeout(() => controller.abort(), 2000);
504
+
505
+ const s = stream(model, {
506
+ messages: [{ role: 'user', content: 'Write a long story' }]
507
+ }, {
508
+ signal: controller.signal
509
+ });
510
+
511
+ for await (const event of s) {
512
+ if (event.type === 'text_delta') {
513
+ process.stdout.write(event.delta);
514
+ } else if (event.type === 'error') {
515
+ // event.reason tells you if it was "error" or "aborted"
516
+ console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);
517
+ }
518
+ }
519
+
520
+ // Get results (may be partial if aborted)
521
+ const response = await s.result();
522
+ if (response.stopReason === 'aborted') {
523
+ console.log('Request was aborted:', response.errorMessage);
524
+ console.log('Partial content received:', response.content);
525
+ console.log('Tokens used:', response.usage);
526
+ }
527
+ ```
528
+
529
+ ### Continuing After Abort
530
+
531
+ Aborted messages can be added to the conversation context and continued in subsequent requests:
532
+
533
+ ```typescript
534
+ const context = {
535
+ messages: [
536
+ { role: 'user', content: 'Explain quantum computing in detail' }
537
+ ]
538
+ };
539
+
540
+ // First request gets aborted after 2 seconds
541
+ const controller1 = new AbortController();
542
+ setTimeout(() => controller1.abort(), 2000);
543
+
544
+ const partial = await complete(model, context, { signal: controller1.signal });
545
+
546
+ // Add the partial response to context
547
+ context.messages.push(partial);
548
+ context.messages.push({ role: 'user', content: 'Please continue' });
549
+
550
+ // Continue the conversation
551
+ const continuation = await complete(model, context);
552
+ ```
553
+
554
+ ## APIs, Models, and Providers
555
+
556
+ The library implements 4 API interfaces, each with its own streaming function and options:
557
+
558
+ - **`anthropic-messages`**: Anthropic's Messages API (`streamAnthropic`, `AnthropicOptions`)
559
+ - **`google-generative-ai`**: Google's Generative AI API (`streamGoogle`, `GoogleOptions`)
560
+ - **`openai-completions`**: OpenAI's Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`)
561
+ - **`openai-responses`**: OpenAI's Responses API (`streamOpenAIResponses`, `OpenAIResponsesOptions`)
562
+
563
+ ### Providers and Models
564
+
565
+ A **provider** offers models through a specific API. For example:
566
+ - **Anthropic** models use the `anthropic-messages` API
567
+ - **Google** models use the `google-generative-ai` API
568
+ - **OpenAI** models use the `openai-responses` API
569
+ - **Mistral, xAI, Cerebras, Groq, etc.** models use the `openai-completions` API (OpenAI-compatible)
570
+
571
+ ### Querying Providers and Models
572
+
573
+ ```typescript
574
+ import { getProviders, getModels, getModel } from '@yeshwanthyk/ai';
575
+
576
+ // Get all available providers
577
+ const providers = getProviders();
578
+ console.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...]
579
+
580
+ // Get all models from a provider (fully typed)
581
+ const anthropicModels = getModels('anthropic');
582
+ for (const model of anthropicModels) {
583
+ console.log(`${model.id}: ${model.name}`);
584
+ console.log(` API: ${model.api}`); // 'anthropic-messages'
585
+ console.log(` Context: ${model.contextWindow} tokens`);
586
+ console.log(` Vision: ${model.input.includes('image')}`);
587
+ console.log(` Reasoning: ${model.reasoning}`);
588
+ }
589
+
590
+ // Get a specific model (both provider and model ID are auto-completed in IDEs)
591
+ const model = getModel('openai', 'gpt-4o-mini');
592
+ console.log(`Using ${model.name} via ${model.api} API`);
593
+ ```
594
+
595
+ ### Custom Models
596
+
597
+ You can create custom models for local inference servers or custom endpoints:
598
+
599
+ ```typescript
600
+ import { Model, stream } from '@yeshwanthyk/ai';
601
+
602
+ // Example: Ollama using OpenAI-compatible API
603
+ const ollamaModel: Model<'openai-completions'> = {
604
+ id: 'llama-3.1-8b',
605
+ name: 'Llama 3.1 8B (Ollama)',
606
+ api: 'openai-completions',
607
+ provider: 'ollama',
608
+ baseUrl: 'http://localhost:11434/v1',
609
+ reasoning: false,
610
+ input: ['text'],
611
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
612
+ contextWindow: 128000,
613
+ maxTokens: 32000
614
+ };
615
+
616
+ // Example: LiteLLM proxy with explicit compat settings
617
+ const litellmModel: Model<'openai-completions'> = {
618
+ id: 'gpt-4o',
619
+ name: 'GPT-4o (via LiteLLM)',
620
+ api: 'openai-completions',
621
+ provider: 'litellm',
622
+ baseUrl: 'http://localhost:4000/v1',
623
+ reasoning: false,
624
+ input: ['text', 'image'],
625
+ cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
626
+ contextWindow: 128000,
627
+ maxTokens: 16384,
628
+ compat: {
629
+ supportsStore: false, // LiteLLM doesn't support the store field
630
+ }
631
+ };
632
+
633
+ // Example: Custom endpoint with headers (bypassing Cloudflare bot detection)
634
+ const proxyModel: Model<'anthropic-messages'> = {
635
+ id: 'claude-sonnet-4',
636
+ name: 'Claude Sonnet 4 (Proxied)',
637
+ api: 'anthropic-messages',
638
+ provider: 'custom-proxy',
639
+ baseUrl: 'https://proxy.example.com/v1',
640
+ reasoning: true,
641
+ input: ['text', 'image'],
642
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
643
+ contextWindow: 200000,
644
+ maxTokens: 8192,
645
+ headers: {
646
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
647
+ 'X-Custom-Auth': 'bearer-token-here'
648
+ }
649
+ };
650
+
651
+ // Use the custom model
652
+ const response = await stream(ollamaModel, context, {
653
+ apiKey: 'dummy' // Ollama doesn't need a real key
654
+ });
655
+ ```
656
+
657
+ ### OpenAI Compatibility Settings
658
+
659
+ The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for known providers (Cerebras, xAI, Mistral, Chutes, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field:
660
+
661
+ ```typescript
662
+ interface OpenAICompat {
663
+ supportsStore?: boolean; // Whether provider supports the `store` field (default: true)
664
+ supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
665
+ supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
666
+ maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
667
+ }
668
+ ```
669
+
670
+ If `compat` is not set, the library falls back to URL-based detection. If `compat` is partially set, unspecified fields use the detected defaults. This is useful for:
671
+
672
+ - **LiteLLM proxies**: May not support `store` field
673
+ - **Custom inference servers**: May use non-standard field names
674
+ - **Self-hosted endpoints**: May have different feature support
675
+
676
+ ### Type Safety
677
+
678
+ Models are typed by their API, ensuring type-safe options:
679
+
680
+ ```typescript
681
+ // TypeScript knows this is an Anthropic model
682
+ const claude = getModel('anthropic', 'claude-sonnet-4-20250514');
683
+
684
+ // So these options are type-checked for AnthropicOptions
685
+ await stream(claude, context, {
686
+ thinkingEnabled: true, // ✓ Valid for anthropic-messages
687
+ thinkingBudgetTokens: 2048, // ✓ Valid for anthropic-messages
688
+ // reasoningEffort: 'high' // ✗ TypeScript error: not valid for anthropic-messages
689
+ });
690
+ ```
691
+
692
+ ## Cross-Provider Handoffs
693
+
694
+ The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results.
695
+
696
+ ### How It Works
697
+
698
+ When messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:
699
+
700
+ - **User and tool result messages** are passed through unchanged
701
+ - **Assistant messages from the same provider/API** are preserved as-is
702
+ - **Assistant messages from different providers** have their thinking blocks converted to text with `<thinking>` tags
703
+ - **Tool calls and regular text** are preserved unchanged
704
+
705
+ ### Example: Multi-Provider Conversation
706
+
707
+ ```typescript
708
+ import { getModel, complete, Context } from '@yeshwanthyk/ai';
709
+
710
+ // Start with Claude
711
+ const claude = getModel('anthropic', 'claude-sonnet-4-20250514');
712
+ const context: Context = {
713
+ messages: []
714
+ };
715
+
716
+ context.messages.push({ role: 'user', content: 'What is 25 * 18?' });
717
+ const claudeResponse = await complete(claude, context, {
718
+ thinkingEnabled: true
719
+ });
720
+ context.messages.push(claudeResponse);
721
+
722
+ // Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text
723
+ const gpt5 = getModel('openai', 'gpt-5-mini');
724
+ context.messages.push({ role: 'user', content: 'Is that calculation correct?' });
725
+ const gptResponse = await complete(gpt5, context);
726
+ context.messages.push(gptResponse);
727
+
728
+ // Switch to Gemini
729
+ const gemini = getModel('google', 'gemini-2.5-flash');
730
+ context.messages.push({ role: 'user', content: 'What was the original question?' });
731
+ const geminiResponse = await complete(gemini, context);
732
+ ```
733
+
734
+ ### Provider Compatibility
735
+
736
+ All providers can handle messages from other providers, including:
737
+ - Text content
738
+ - Tool calls and tool results (including images in tool results)
739
+ - Thinking/reasoning blocks (transformed to tagged text for cross-provider compatibility)
740
+ - Aborted messages with partial content
741
+
742
+ This enables flexible workflows where you can:
743
+ - Start with a fast model for initial responses
744
+ - Switch to a more capable model for complex reasoning
745
+ - Use specialized models for specific tasks
746
+ - Maintain conversation continuity across provider outages
747
+
748
+ ## Context Serialization
749
+
750
+ The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:
751
+
752
+ ```typescript
753
+ import { Context, getModel, complete } from '@yeshwanthyk/ai';
754
+
755
+ // Create and use a context
756
+ const context: Context = {
757
+ systemPrompt: 'You are a helpful assistant.',
758
+ messages: [
759
+ { role: 'user', content: 'What is TypeScript?' }
760
+ ]
761
+ };
762
+
763
+ const model = getModel('openai', 'gpt-4o-mini');
764
+ const response = await complete(model, context);
765
+ context.messages.push(response);
766
+
767
+ // Serialize the entire context
768
+ const serialized = JSON.stringify(context);
769
+ console.log('Serialized context size:', serialized.length, 'bytes');
770
+
771
+ // Save to database, localStorage, file, etc.
772
+ localStorage.setItem('conversation', serialized);
773
+
774
+ // Later: deserialize and continue the conversation
775
+ const restored: Context = JSON.parse(localStorage.getItem('conversation')!);
776
+ restored.messages.push({ role: 'user', content: 'Tell me more about its type system' });
777
+
778
+ // Continue with any model
779
+ const newModel = getModel('anthropic', 'claude-3-5-haiku-20241022');
780
+ const continuation = await complete(newModel, restored);
781
+ ```
782
+
783
+ > **Note**: If the context contains images (encoded as base64 as shown in the Image Input section), those will also be serialized.
784
+
785
+ ## Agent API
786
+
787
+ The Agent API provides a higher-level interface for building agents with tools. It handles tool execution, validation, and provides detailed event streaming for interactive applications.
788
+
789
+ ### Event System
790
+
791
+ The Agent API streams events during execution, allowing you to build reactive UIs and track agent progress. The agent processes prompts in **turns**, where each turn consists of:
792
+ 1. An assistant message (the LLM's response)
793
+ 2. Optional tool executions if the assistant calls tools
794
+ 3. Tool result messages that are fed back to the LLM
795
+
796
+ This continues until the assistant produces a response without tool calls.
797
+
798
+ ### Event Flow Example
799
+
800
+ Given a prompt asking to calculate two expressions and sum them:
801
+
802
+ ```typescript
803
+ import { agentLoop, AgentContext, calculateTool } from '@yeshwanthyk/ai';
804
+
805
+ const context: AgentContext = {
806
+ systemPrompt: 'You are a helpful math assistant.',
807
+ messages: [],
808
+ tools: [calculateTool]
809
+ };
810
+
811
+ const stream = agentLoop(
812
+ { role: 'user', content: 'Calculate 15 * 20 and 30 * 40, then sum the results', timestamp: Date.now() },
813
+ context,
814
+ { model: getModel('openai', 'gpt-4o-mini') }
815
+ );
816
+
817
+ // Expected event sequence:
818
+ // 1. agent_start - Agent begins processing
819
+ // 2. turn_start - First turn begins
820
+ // 3. message_start - User message starts
821
+ // 4. message_end - User message ends
822
+ // 5. message_start - Assistant message starts
823
+ // 6. message_update - Assistant streams response with tool calls
824
+ // 7. message_end - Assistant message ends
825
+ // 8. tool_execution_start - First calculation (15 * 20)
826
+ // 9. tool_execution_update - Streaming progress (for long-running tools)
827
+ // 10. tool_execution_end - Result: 300
828
+ // 11. tool_execution_start - Second calculation (30 * 40)
829
+ // 12. tool_execution_update - Streaming progress
830
+ // 13. tool_execution_end - Result: 1200
831
+ // 12. message_start - Tool result message for first calculation
832
+ // 13. message_end - Tool result message ends
833
+ // 14. message_start - Tool result message for second calculation
834
+ // 15. message_end - Tool result message ends
835
+ // 16. turn_end - First turn ends with 2 tool results
836
+ // 17. turn_start - Second turn begins
837
+ // 18. message_start - Assistant message starts
838
+ // 19. message_update - Assistant streams response with sum calculation
839
+ // 20. message_end - Assistant message ends
840
+ // 21. tool_execution_start - Sum calculation (300 + 1200)
841
+ // 22. tool_execution_end - Result: 1500
842
+ // 23. message_start - Tool result message for sum
843
+ // 24. message_end - Tool result message ends
844
+ // 25. turn_end - Second turn ends with 1 tool result
845
+ // 26. turn_start - Third turn begins
846
+ // 27. message_start - Final assistant message starts
847
+ // 28. message_update - Assistant streams final answer
848
+ // 29. message_end - Final assistant message ends
849
+ // 30. turn_end - Third turn ends with 0 tool results
850
+ // 31. agent_end - Agent completes with all messages
851
+ ```
852
+
853
+ ### Handling Events
854
+
855
+ ```typescript
856
+ for await (const event of stream) {
857
+ switch (event.type) {
858
+ case 'agent_start':
859
+ console.log('Agent started');
860
+ break;
861
+
862
+ case 'turn_start':
863
+ console.log('New turn started');
864
+ break;
865
+
866
+ case 'message_start':
867
+ console.log(`${event.message.role} message started`);
868
+ break;
869
+
870
+ case 'message_update':
871
+ // Only for assistant messages during streaming
872
+ if (event.message.content.some(c => c.type === 'text')) {
873
+ console.log('Assistant:', event.message.content);
874
+ }
875
+ break;
876
+
877
+ case 'tool_execution_start':
878
+ console.log(`Calling ${event.toolName} with:`, event.args);
879
+ break;
880
+
881
+ case 'tool_execution_update':
882
+ // Streaming progress for long-running tools (e.g., bash output)
883
+ console.log(`Progress:`, event.partialResult.content);
884
+ break;
885
+
886
+ case 'tool_execution_end':
887
+ if (event.isError) {
888
+ console.error(`Tool failed:`, event.result);
889
+ } else {
890
+ console.log(`Tool result:`, event.result.content);
891
+ }
892
+ break;
893
+
894
+ case 'turn_end':
895
+ console.log(`Turn ended with ${event.toolResults.length} tool calls`);
896
+ break;
897
+
898
+ case 'agent_end':
899
+ console.log(`Agent completed with ${event.messages.length} new messages`);
900
+ break;
901
+ }
902
+ }
903
+
904
+ // Get all messages generated during this agent execution
905
+ // These include the user message and can be directly appended to context.messages
906
+ const messages = await stream.result();
907
+ context.messages.push(...messages);
908
+ ```
909
+
910
+ ### Continuing from Existing Context
911
+
912
+ Use `agentLoopContinue` to resume an agent loop without adding a new user message. This is useful for:
913
+ - Retrying after context overflow (after compaction reduces context size)
914
+ - Resuming from tool results that were added manually to the context
915
+
916
+ ```typescript
917
+ import { agentLoopContinue, AgentContext } from '@yeshwanthyk/ai';
918
+
919
+ // Context already has messages - last must be 'user' or 'toolResult'
920
+ const context: AgentContext = {
921
+ systemPrompt: 'You are helpful.',
922
+ messages: [userMessage, assistantMessage, toolResult],
923
+ tools: [myTool]
924
+ };
925
+
926
+ // Continue processing from the tool result
927
+ const stream = agentLoopContinue(context, { model });
928
+
929
+ for await (const event of stream) {
930
+ // Same events as agentLoop, but no user message events emitted
931
+ }
932
+
933
+ const newMessages = await stream.result();
934
+ ```
935
+
936
+ **Validation**: Throws if context has no messages or if the last message is an assistant message.
937
+
938
+ ### Defining Tools with TypeBox
939
+
940
+ Tools use TypeBox schemas for runtime validation and type inference:
941
+
942
+ ```typescript
943
+ import { Type, Static, AgentTool, AgentToolResult, StringEnum } from '@yeshwanthyk/ai';
944
+
945
+ const weatherSchema = Type.Object({
946
+ city: Type.String({ minLength: 1 }),
947
+ units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })
948
+ });
949
+
950
+ type WeatherParams = Static<typeof weatherSchema>;
951
+
952
+ const weatherTool: AgentTool<typeof weatherSchema, { temp: number }> = {
953
+ label: 'Get Weather',
954
+ name: 'get_weather',
955
+ description: 'Get current weather for a city',
956
+ parameters: weatherSchema,
957
+ execute: async (toolCallId, args, signal, onUpdate) => {
958
+ // args is fully typed: { city: string, units: 'celsius' | 'fahrenheit' }
959
+ // signal: AbortSignal for cancellation
960
+ // onUpdate: Optional callback for streaming progress (emits tool_execution_update events)
961
+ const temp = Math.round(Math.random() * 30);
962
+ return {
963
+ content: [{ type: 'text', text: `Temperature in ${args.city}: ${temp}°${args.units[0].toUpperCase()}` }],
964
+ details: { temp }
965
+ };
966
+ }
967
+ };
968
+
969
+ // Tools can also return images alongside text
970
+ const chartTool: AgentTool<typeof Type.Object({ data: Type.Array(Type.Number()) })> = {
971
+ label: 'Generate Chart',
972
+ name: 'generate_chart',
973
+ description: 'Generate a chart from data',
974
+ parameters: Type.Object({ data: Type.Array(Type.Number()) }),
975
+ execute: async (toolCallId, args) => {
976
+ const chartImage = await generateChartImage(args.data);
977
+ return {
978
+ content: [
979
+ { type: 'text', text: `Generated chart with ${args.data.length} data points` },
980
+ { type: 'image', data: chartImage.toString('base64'), mimeType: 'image/png' }
981
+ ]
982
+ };
983
+ }
984
+ };
985
+
986
+ // Tools can stream progress via the onUpdate callback (emits tool_execution_update events)
987
+ const bashTool: AgentTool<typeof Type.Object({ command: Type.String() }), { exitCode: number }> = {
988
+ label: 'Run Bash',
989
+ name: 'bash',
990
+ description: 'Execute a bash command',
991
+ parameters: Type.Object({ command: Type.String() }),
992
+ execute: async (toolCallId, args, signal, onUpdate) => {
993
+ let output = '';
994
+ const child = spawn('bash', ['-c', args.command]);
995
+
996
+ child.stdout.on('data', (data) => {
997
+ output += data.toString();
998
+ // Stream partial output to UI via tool_execution_update events
999
+ onUpdate?.({
1000
+ content: [{ type: 'text', text: output }],
1001
+ details: { exitCode: -1 } // Not finished yet
1002
+ });
1003
+ });
1004
+
1005
+ const exitCode = await new Promise<number>((resolve) => {
1006
+ child.on('close', resolve);
1007
+ });
1008
+
1009
+ return {
1010
+ content: [{ type: 'text', text: output }],
1011
+ details: { exitCode }
1012
+ };
1013
+ }
1014
+ };
1015
+ ```
1016
+
1017
+ ### Validation and Error Handling
1018
+
1019
+ Tool arguments are automatically validated using AJV with the TypeBox schema. Invalid arguments result in detailed error messages:
1020
+
1021
+ ```typescript
1022
+ // If the LLM calls with invalid arguments:
1023
+ // get_weather({ city: '', units: 'kelvin' })
1024
+
1025
+ // The tool execution will fail with:
1026
+ /*
1027
+ Validation failed for tool "get_weather":
1028
+ - city: must NOT have fewer than 1 characters
1029
+ - units: must be equal to one of the allowed values
1030
+
1031
+ Received arguments:
1032
+ {
1033
+ "city": "",
1034
+ "units": "kelvin"
1035
+ }
1036
+ */
1037
+ ```
1038
+
1039
+ ### Built-in Example Tools
1040
+
1041
+ The library includes example tools for common operations:
1042
+
1043
+ ```typescript
1044
+ import { calculateTool, getCurrentTimeTool } from '@yeshwanthyk/ai';
1045
+
1046
+ const context: AgentContext = {
1047
+ systemPrompt: 'You are a helpful assistant.',
1048
+ messages: [],
1049
+ tools: [calculateTool, getCurrentTimeTool]
1050
+ };
1051
+ ```
1052
+
1053
+ ## Browser Usage
1054
+
1055
+ The library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:
1056
+
1057
+ ```typescript
1058
+ import { getModel, complete } from '@yeshwanthyk/ai';
1059
+
1060
+ // API key must be passed explicitly in browser
1061
+ const model = getModel('anthropic', 'claude-3-5-haiku-20241022');
1062
+
1063
+ const response = await complete(model, {
1064
+ messages: [{ role: 'user', content: 'Hello!' }]
1065
+ }, {
1066
+ apiKey: 'your-api-key'
1067
+ });
1068
+ ```
1069
+
1070
+ > **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.
1071
+
1072
+ ### Environment Variables (Node.js only)
1073
+
1074
+ In Node.js environments, you can set environment variables to avoid passing API keys:
1075
+
1076
+ ```bash
1077
+ OPENAI_API_KEY=sk-...
1078
+ ANTHROPIC_API_KEY=sk-ant-...
1079
+ GEMINI_API_KEY=...
1080
+ MISTRAL_API_KEY=...
1081
+ GROQ_API_KEY=gsk_...
1082
+ CEREBRAS_API_KEY=csk-...
1083
+ XAI_API_KEY=xai-...
1084
+ ZAI_API_KEY=...
1085
+ OPENROUTER_API_KEY=sk-or-...
1086
+ ```
1087
+
1088
+ When set, the library automatically uses these keys:
1089
+
1090
+ ```typescript
1091
+ // Uses OPENAI_API_KEY from environment
1092
+ const model = getModel('openai', 'gpt-4o-mini');
1093
+ const response = await complete(model, context);
1094
+
1095
+ // Or override with explicit key
1096
+ const response = await complete(model, context, {
1097
+ apiKey: 'sk-different-key'
1098
+ });
1099
+ ```
1100
+
1101
+ ### Programmatic API Key Management
1102
+
1103
+ You can also set and get API keys programmatically:
1104
+
1105
+ ```typescript
1106
+ import { setApiKey, getApiKey } from '@yeshwanthyk/ai';
1107
+
1108
+ // Set API key for a provider
1109
+ setApiKey('openai', 'sk-...');
1110
+ setApiKey('anthropic', 'sk-ant-...');
1111
+
1112
+ // Get API key for a provider (checks both programmatic and env vars)
1113
+ const key = getApiKey('openai');
1114
+ ```
1115
+
1116
+ ## GitHub Copilot
1117
+
1118
+ GitHub Copilot is available as a provider, requiring OAuth authentication via GitHub's device flow.
1119
+
1120
+ **Using with `the coding-agent app`**: Use `/login` and select "GitHub Copilot" to authenticate. All models are automatically enabled after login. Token stored in `~/.pi/agent/oauth.json`.
1121
+
1122
+ **Using standalone**: If you have a valid Copilot OAuth token (e.g., from the coding agent's `oauth.json`):
1123
+
1124
+ ```typescript
1125
+ import { getModel, complete } from '@yeshwanthyk/ai';
1126
+
1127
+ const model = getModel('github-copilot', 'gpt-4o');
1128
+
1129
+ const response = await complete(model, {
1130
+ messages: [{ role: 'user', content: 'Hello!' }]
1131
+ }, {
1132
+ apiKey: 'tid=...;exp=...;proxy-ep=...' // OAuth token from ~/.pi/agent/oauth.json
1133
+ });
1134
+ ```
1135
+
1136
+ **Note**: OAuth tokens expire and need periodic refresh. The coding agent handles this automatically.
1137
+
1138
+ If you get "The requested model is not supported" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click "Enable".
1139
+
1140
+ ## License
1141
+
1142
+ MIT