@aeye/openai 0.4.0 → 0.7.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 (3) hide show
  1. package/README.md +1076 -1069
  2. package/aeye-openai.md +193 -189
  3. package/package.json +72 -72
package/README.md CHANGED
@@ -1,1069 +1,1076 @@
1
- # @aeye/openai
2
-
3
- OpenAI provider for the @aeye (AI TypeScript) framework. This package provides a comprehensive integration with OpenAI's API, supporting the full range of capabilities including chat completions, image generation, speech synthesis, transcription, and embeddings.
4
-
5
- ## Features
6
-
7
- - **Chat**: GPT-5.x, GPT-6, GPT-4.1 and other chat models, through Chat Completions or the Responses API
8
- - **Vision**: Image and file (PDF) understanding
9
- - **Reasoning**: Efforts `none` through `max`, reasoning summaries, and encrypted reasoning carried across turns
10
- - **Hosted Tools**: Web search, file search, code interpreter, image generation, and remote MCP servers run by OpenAI
11
- - **Image Generation**: GPT Image (`gpt-image-2` by default) for generating and editing images
12
- - **Speech-to-Text**: `gpt-transcribe`, diarization with known speakers, and Whisper
13
- - **Text-to-Speech**: TTS models with built-in or custom voices
14
- - **Audio Output**: Spoken answers from audio-capable chat models
15
- - **Embeddings**: Text embeddings for semantic search and similarity
16
- - **Decisions**: Probabilities, choices, and scores from `/v1/decisions` (`gpt-6-luna`)
17
- - **Batches**: Chat requests as one batch at about half the price
18
- - **Function Calling**: Tool/function calling with automatic schema conversion, free-text and grammar tools, tool search, async tools, and allowed tools
19
- - **Structured Outputs**: JSON mode and schema-based structured outputs
20
- - **Streaming**: Real-time streaming responses for chat completions
21
- - **Multi-modal**: Support for text, images, audio, and files in conversations
22
- - **Extensible**: Base class for OpenAI-compatible providers (Azure OpenAI, OpenRouter, etc.)
23
-
24
- ## Installation
25
-
26
- ```bash
27
- npm install @aeye/openai @aeye/ai @aeye/core openai zod
28
- ```
29
-
30
- Requires Node >= 22 and the `openai` SDK v7.
31
-
32
- ## Quick Start
33
-
34
- ```typescript
35
- import { OpenAIProvider } from '@aeye/openai';
36
-
37
- // Create provider instance
38
- const provider = new OpenAIProvider({
39
- apiKey: process.env.OPENAI_API_KEY!,
40
- });
41
-
42
- // Create executor
43
- const executor = provider.createExecutor();
44
-
45
- // Make a request
46
- const response = await executor(
47
- {
48
- messages: [
49
- { role: 'user', content: 'Tell me a joke about TypeScript' }
50
- ]
51
- },
52
- {},
53
- { model: 'gpt-5.4-mini' }
54
- );
55
-
56
- console.log(response.content);
57
- ```
58
-
59
- ## Configuration
60
-
61
- ### Basic Configuration
62
-
63
- ```typescript
64
- import { OpenAIProvider, OpenAIConfig } from '@aeye/openai';
65
-
66
- const config: OpenAIConfig = {
67
- apiKey: process.env.OPENAI_API_KEY!,
68
- organization: 'org-123456', // Optional: for multi-org accounts
69
- };
70
-
71
- const provider = new OpenAIProvider(config);
72
- ```
73
-
74
- ### Chat Completions or Responses API
75
-
76
- ```typescript
77
- const provider = new OpenAIProvider({
78
- apiKey: process.env.OPENAI_API_KEY!,
79
- api: 'auto', // 'auto' (default) | 'chat' | 'responses'
80
- });
81
- ```
82
-
83
- With `'auto'`, chat uses Chat Completions unless the model is Responses-only (codex, pre-5.6 pro models, deep-research, computer-use), the request has `hostedTools`, a tool sets `format`, `defer`, or `async`, `reason` sets `mode`, `summary`, or `context`, or the request has tools and a reasoning `effort` other than `none`. Through Responses, reasoning comes back encrypted (unless `store` is on) and is replayed on later turns.
84
-
85
- ### Custom Base URL (for Azure OpenAI or compatible APIs)
86
-
87
- ```typescript
88
- const provider = new OpenAIProvider({
89
- apiKey: process.env.AZURE_API_KEY!,
90
- baseURL: 'https://your-resource.openai.azure.com/openai/deployments/your-deployment',
91
- });
92
- ```
93
-
94
- ## Usage Examples
95
-
96
- ### Chat Completions
97
-
98
- #### Basic Chat
99
-
100
- ```typescript
101
- const executor = provider.createExecutor();
102
-
103
- const response = await executor(
104
- {
105
- messages: [
106
- { role: 'system', content: 'You are a helpful assistant.' },
107
- { role: 'user', content: 'What is the capital of France?' }
108
- ],
109
- temperature: 0.7,
110
- maxTokens: 150,
111
- },
112
- {},
113
- { model: 'gpt-5.4-mini' }
114
- );
115
-
116
- console.log(response.content);
117
- console.log('Tokens used:', response.usage.totalTokens);
118
- ```
119
-
120
- #### Streaming Chat
121
-
122
- ```typescript
123
- const streamer = provider.createStreamer();
124
-
125
- for await (const chunk of streamer(
126
- {
127
- messages: [
128
- { role: 'user', content: 'Write a short story about a robot' }
129
- ]
130
- },
131
- {},
132
- { model: 'gpt-5.4-mini' }
133
- )) {
134
- if (chunk.content) {
135
- process.stdout.write(chunk.content);
136
- }
137
- }
138
- ```
139
-
140
- #### Multi-turn Conversation
141
-
142
- ```typescript
143
- const conversation = [
144
- { role: 'system' as const, content: 'You are a helpful assistant.' },
145
- { role: 'user' as const, content: 'What is TypeScript?' },
146
- ];
147
-
148
- // First response
149
- let response = await executor(
150
- { messages: conversation },
151
- {},
152
- { model: 'gpt-5.4-mini' }
153
- );
154
-
155
- conversation.push({ role: 'assistant', content: response.content });
156
- conversation.push({ role: 'user', content: 'Can you give me an example?' });
157
-
158
- // Second response
159
- response = await executor(
160
- { messages: conversation },
161
- {},
162
- { model: 'gpt-5.4-mini' }
163
- );
164
- ```
165
-
166
- ### Vision
167
-
168
- #### Analyze an Image
169
-
170
- ```typescript
171
- const response = await executor(
172
- {
173
- messages: [
174
- {
175
- role: 'user',
176
- content: [
177
- { type: 'text', content: 'What is in this image?' },
178
- {
179
- type: 'image',
180
- content: 'https://example.com/image.jpg',
181
- // Or use base64: content: 'data:image/jpeg;base64,/9j/4AAQ...'
182
- }
183
- ]
184
- }
185
- ]
186
- },
187
- {},
188
- { model: 'gpt-5.4-mini' }
189
- );
190
- ```
191
-
192
- #### Multiple Images
193
-
194
- ```typescript
195
- const response = await executor(
196
- {
197
- messages: [
198
- {
199
- role: 'user',
200
- content: [
201
- { type: 'text', content: 'Compare these two images' },
202
- { type: 'image', content: 'https://example.com/image1.jpg' },
203
- { type: 'image', content: 'https://example.com/image2.jpg' }
204
- ]
205
- }
206
- ]
207
- },
208
- {},
209
- { model: 'gpt-5.4-mini' }
210
- );
211
- ```
212
-
213
- ### Function Calling
214
-
215
- #### Define and Use Tools
216
-
217
- ```typescript
218
- import z from 'zod';
219
-
220
- const response = await executor(
221
- {
222
- messages: [
223
- { role: 'user', content: 'What is the weather like in San Francisco?' }
224
- ],
225
- tools: [
226
- {
227
- name: 'get_current_weather',
228
- description: 'Get the current weather in a given location',
229
- parameters: z.object({
230
- location: z.string().describe('The city and state, e.g., San Francisco, CA'),
231
- unit: z.enum(['celsius', 'fahrenheit']).optional(),
232
- }),
233
- }
234
- ],
235
- toolChoice: 'auto', // 'auto', 'required', 'none', { tool: 'tool_name' }, or { allowed: ['tool_name'], required? }
236
- },
237
- {},
238
- { model: 'gpt-5.4-mini' }
239
- );
240
-
241
- if (response.toolCalls) {
242
- for (const toolCall of response.toolCalls) {
243
- console.log('Tool:', toolCall.name);
244
- console.log('Arguments:', JSON.parse(toolCall.arguments));
245
-
246
- // Execute the function and send result back
247
- const functionResult = await getWeather(JSON.parse(toolCall.arguments));
248
-
249
- const finalResponse = await executor(
250
- {
251
- messages: [
252
- { role: 'user', content: 'What is the weather like in San Francisco?' },
253
- { role: 'assistant', content: '', toolCalls: [toolCall] },
254
- {
255
- role: 'tool',
256
- content: JSON.stringify(functionResult),
257
- toolCallId: toolCall.id,
258
- }
259
- ]
260
- },
261
- {},
262
- { model: 'gpt-5.4-mini' }
263
- );
264
- }
265
- }
266
- ```
267
-
268
- #### Force Specific Tool
269
-
270
- ```typescript
271
- const response = await executor(
272
- {
273
- messages: [
274
- { role: 'user', content: 'Get the weather for Boston' }
275
- ],
276
- tools: [
277
- {
278
- name: 'get_weather',
279
- description: 'Get weather information',
280
- parameters: z.object({
281
- location: z.string(),
282
- }),
283
- }
284
- ],
285
- toolChoice: { tool: 'get_weather' }, // Force this specific tool
286
- },
287
- {},
288
- { model: 'gpt-5.4-mini' }
289
- );
290
- ```
291
-
292
- ### Structured Outputs
293
-
294
- #### JSON Mode
295
-
296
- ```typescript
297
- const response = await executor(
298
- {
299
- messages: [
300
- {
301
- role: 'user',
302
- content: 'List three famous scientists and their main contributions in JSON format'
303
- }
304
- ],
305
- responseFormat: 'json', // Ensures valid JSON output
306
- },
307
- {},
308
- { model: 'gpt-5.4-mini' }
309
- );
310
-
311
- const scientists = JSON.parse(response.content);
312
- ```
313
-
314
- #### Schema-based Structured Output
315
-
316
- ```typescript
317
- import z from 'zod';
318
-
319
- const PersonSchema = z.object({
320
- name: z.string(),
321
- age: z.number(),
322
- occupation: z.string(),
323
- hobbies: z.array(z.string()),
324
- });
325
-
326
- const response = await executor(
327
- {
328
- messages: [
329
- {
330
- role: 'user',
331
- content: 'Generate a profile for a fictional software engineer'
332
- }
333
- ],
334
- responseFormat: PersonSchema, // Strict schema enforcement
335
- },
336
- {},
337
- { model: 'gpt-5.4-mini' }
338
- );
339
-
340
- // Response is guaranteed to match the schema
341
- const person = JSON.parse(response.content);
342
- console.log(person.name, person.age); // Type-safe
343
- ```
344
-
345
- ### Image Generation (GPT Image)
346
-
347
- #### Generate an Image
348
-
349
- ```typescript
350
- const imageResponse = await provider.generateImage(
351
- {
352
- prompt: 'A serene landscape with mountains and a lake at sunset',
353
- model: 'gpt-image-2', // the default
354
- size: '1024x1024', // or 'auto'; other sizes snap to the nearest the model supports
355
- quality: 'high', // 'auto' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' (per model)
356
- outputFormat: 'webp', // 'png' | 'jpeg' | 'webp'
357
- outputCompression: 80, // 0-100, for jpeg and webp
358
- background: 'transparent',
359
- n: 1,
360
- },
361
- {}
362
- );
363
-
364
- const base64Image = imageResponse.images[0].b64_json;
365
- ```
366
-
367
- #### Edit with Reference Images
368
-
369
- ```typescript
370
- const edited = await provider.editImage(
371
- {
372
- prompt: 'Put the product from the second image on the table in the first',
373
- image: fs.readFileSync('room.png'), // source image
374
- references: [fs.readFileSync('product.png')], // more input images
375
- mask: fs.readFileSync('mask.png'), // optional, over the source image
376
- inputFidelity: 'high', // keep input details closely
377
- },
378
- {}
379
- );
380
- ```
381
-
382
- Usage reports the quality and size actually produced, so per-image pricing applies; GPT Image 2 and later report output image tokens in `usage.image.tokens`.
383
-
384
- ### Speech Synthesis (TTS)
385
-
386
- #### Generate Speech
387
-
388
- ```typescript
389
- import fs from 'fs';
390
- import { Readable } from 'stream';
391
-
392
- const speechResponse = await provider.generateSpeech(
393
- {
394
- text: 'Hello! This is a text-to-speech example using OpenAI.',
395
- model: 'gpt-4o-mini-tts', // the default
396
- voice: 'alloy', // any built-in voice name or a custom voice id
397
- instructions: 'Speak warmly and a little slowly.',
398
- speed: 1.0, // 0.25 to 4.0
399
- responseFormat: 'mp3', // mp3, opus, aac, flac, wav, pcm
400
- },
401
- {}
402
- );
403
-
404
- // Save to file
405
- Readable.fromWeb(speechResponse.audio).pipe(fs.createWriteStream('output.mp3'));
406
- console.log('Audio saved to output.mp3');
407
- ```
408
-
409
- #### Different Voices
410
-
411
- ```typescript
412
- const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
413
-
414
- for (const voice of voices) {
415
- const response = await provider.generateSpeech(
416
- {
417
- text: `This is the ${voice} voice speaking.`,
418
- voice,
419
- model: 'gpt-4o-mini-tts',
420
- },
421
- {}
422
- );
423
-
424
- Readable.fromWeb(response.audio).pipe(fs.createWriteStream(`voice_${voice}.mp3`));
425
- }
426
- ```
427
-
428
- ### Transcription
429
-
430
- #### Transcribe Audio File
431
-
432
- ```typescript
433
- import fs from 'fs';
434
-
435
- const audioBuffer = fs.readFileSync('audio.mp3');
436
-
437
- const transcription = await provider.transcribe(
438
- {
439
- audio: audioBuffer,
440
- model: 'gpt-transcribe', // the default
441
- language: 'en', // Optional: ISO-639-1 language code
442
- prompt: 'This is a podcast about AI and technology', // Optional: context
443
- },
444
- {}
445
- );
446
-
447
- console.log('Transcription:', transcription.text);
448
- ```
449
-
450
- #### Transcribe with Timestamps
451
-
452
- ```typescript
453
- const transcription = await provider.transcribe(
454
- {
455
- audio: audioBuffer,
456
- model: 'whisper-1',
457
- responseFormat: 'verbose_json',
458
- timestampGranularities: ['word', 'segment'],
459
- },
460
- {}
461
- );
462
-
463
- console.log('Full text:', transcription.text);
464
-
465
- // Segment-level timestamps
466
- transcription.segments?.forEach(segment => {
467
- console.log(`${segment.text}: ${segment.start}s - ${segment.end}s`);
468
- });
469
- ```
470
-
471
- #### Speaker Diarization
472
-
473
- ```typescript
474
- const transcription = await provider.transcribe(
475
- {
476
- audio: fs.readFileSync('meeting.mp3'),
477
- model: 'gpt-4o-transcribe-diarize',
478
- responseFormat: 'diarized_json',
479
- knownSpeakers: [
480
- { name: 'Alice', audio: fs.readFileSync('alice.wav') },
481
- { name: 'Bob', audio: fs.readFileSync('bob.wav') },
482
- ],
483
- },
484
- {}
485
- );
486
-
487
- transcription.segments?.forEach(segment => {
488
- console.log(`${segment.speaker}: ${segment.text}`);
489
- });
490
- ```
491
-
492
- #### Different Output Formats
493
-
494
- ```typescript
495
- // Plain text
496
- const text = await provider.transcribe(
497
- { audio: audioBuffer, responseFormat: 'text' },
498
- {}
499
- );
500
-
501
- // SRT subtitles
502
- const srt = await provider.transcribe(
503
- { audio: audioBuffer, responseFormat: 'srt' },
504
- {}
505
- );
506
-
507
- // VTT subtitles
508
- const vtt = await provider.transcribe(
509
- { audio: audioBuffer, responseFormat: 'vtt' },
510
- {}
511
- );
512
- ```
513
-
514
- ### Embeddings
515
-
516
- #### Generate Embeddings
517
-
518
- ```typescript
519
- const embeddingResponse = await provider.embed(
520
- {
521
- texts: [
522
- 'The quick brown fox jumps over the lazy dog',
523
- 'Machine learning is a subset of artificial intelligence',
524
- 'OpenAI develops advanced AI models'
525
- ],
526
- model: 'text-embedding-3-small', // or 'text-embedding-3-large'
527
- },
528
- {}
529
- );
530
-
531
- embeddingResponse.embeddings.forEach((item, i) => {
532
- console.log(`Embedding ${i}:`, item.embedding.length, 'dimensions');
533
- });
534
-
535
- console.log('Tokens used:', embeddingResponse.usage?.text?.input);
536
- ```
537
-
538
- #### Custom Dimensions (for smaller embeddings)
539
-
540
- ```typescript
541
- const embeddingResponse = await provider.embed(
542
- {
543
- texts: ['Some text to embed'],
544
- model: 'text-embedding-3-large',
545
- dimensions: 256, // Reduce from default 3072 to 256
546
- },
547
- {}
548
- );
549
- ```
550
-
551
- #### Calculate Similarity
552
-
553
- ```typescript
554
- function cosineSimilarity(a: number[], b: number[]): number {
555
- const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
556
- const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
557
- const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
558
- return dotProduct / (magnitudeA * magnitudeB);
559
- }
560
-
561
- const response = await provider.embed(
562
- {
563
- texts: [
564
- 'I love programming in TypeScript',
565
- 'TypeScript is my favorite language',
566
- 'I enjoy cooking Italian food'
567
- ],
568
- model: 'text-embedding-3-small',
569
- },
570
- {}
571
- );
572
-
573
- const [emb1, emb2, emb3] = response.embeddings.map(e => e.embedding);
574
-
575
- console.log('Similarity 1-2:', cosineSimilarity(emb1, emb2)); // High similarity
576
- console.log('Similarity 1-3:', cosineSimilarity(emb1, emb3)); // Low similarity
577
- ```
578
-
579
- ### Reasoning
580
-
581
- ```typescript
582
- const response = await executor(
583
- {
584
- messages: [
585
- {
586
- role: 'user',
587
- content: 'Solve this logic puzzle: There are 100 doors, all closed. You make 100 passes. On pass 1, toggle all doors. On pass 2, toggle every 2nd door. On pass 3, toggle every 3rd door, etc. Which doors are open after 100 passes?'
588
- }
589
- ],
590
- reason: {
591
- effort: 'high', // 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
592
- summary: 'auto', // a readable summary (uses the Responses API)
593
- },
594
- },
595
- {},
596
- { model: 'gpt-5.5' }
597
- );
598
-
599
- console.log('Reasoning:', response.reasoning?.content);
600
- console.log('Answer:', response.content);
601
- ```
602
-
603
- - The effort is clamped to the model's accepted efforts when its `ModelInfo.reasoningEfforts` lists them.
604
- - `mode`, `summary`, or `context`, or an effort other than `none` with tools, send the request to the Responses API. There, reasoning comes back encrypted (`openai-responses-v1` details) unless `store` is on; keep the assistant message's `reasoning` in the conversation and it is replayed on the next turn.
605
- - `reason.maxTokens` and `reason.exclude` are not used by OpenAI.
606
-
607
- ### Hosted Tools
608
-
609
- Tools OpenAI runs itself go in `hostedTools` and use the Responses API:
610
-
611
- ```typescript
612
- const response = await executor(
613
- {
614
- messages: [{ role: 'user', content: 'Summarize this week\'s TypeScript news.' }],
615
- hostedTools: [
616
- { type: 'web_search', allowedDomains: ['devblogs.microsoft.com'] },
617
- { type: 'code_interpreter' },
618
- { type: 'file_search', vectorStoreIds: ['vs_123'], maxResults: 5 },
619
- { type: 'mcp', label: 'docs', url: 'https://example.com/mcp', allowedTools: ['search'] },
620
- { type: 'image_generation' },
621
- ],
622
- },
623
- {},
624
- { model: 'gpt-5.4-mini' }
625
- );
626
-
627
- console.log(response.citations); // [{ url, title, start, end }]
628
- console.log(response.usage?.tools); // { web_search: 1, ... }
629
- ```
630
-
631
- `web_fetch` is not available on OpenAI (web search fetches pages itself), and web search can only allow domains, not block them (`blockedDomains` is rejected). Images from `image_generation` come back on `response.images`.
632
-
633
- ### Audio Output
634
-
635
- ```typescript
636
- const response = await executor(
637
- {
638
- messages: [{ role: 'user', content: 'Say hello in French.' }],
639
- modalities: ['text', 'audio'],
640
- audio: { voice: 'alloy', format: 'mp3' }, // streaming defaults to pcm
641
- },
642
- {},
643
- { model: 'gpt-audio-1.5' }
644
- );
645
-
646
- console.log(response.audio?.transcript); // response.audio.data is base64
647
- ```
648
-
649
- ### Other Request Options
650
-
651
- - `seed`, `verbosity` (`'low' | 'medium' | 'high'`), `serviceTier` (`'auto' | 'default' | 'flex' | 'fast' | 'scale'`), and `metadata` are passed through. Chat Completions sends `metadata` only when `store` is on.
652
- - `moderate: true` runs inline moderation (`omni-moderation-latest`); verdicts come back on `response.moderation`.
653
- - Content parts take `cacheBreakpoint` (a prompt cache breakpoint), `detail` for images, and `name` for files.
654
- - `response.serviceTier` reports the tier used (`priority` reads as `fast`).
655
-
656
- ### Tool Features
657
-
658
- - `toolChoice: { allowed: [...], required? }` is sent as `allowed_tools` on both APIs: every tool is still sent (keeping the prompt cacheable), but only the allowed ones may be called.
659
- - A tool with `format: { type: 'text' }` or `format: { type: 'grammar', syntax: 'lark' | 'regex', definition }` is a Responses custom tool: the model writes free text, constrained by the grammar, and it arrives as `{ input }` arguments (`TEXT_TOOL_SCHEMA` from `@aeye/core`). With `api: 'chat'` it is a one-field function whose description carries the grammar.
660
- - `defer: true` sends the tool with `defer_loading` and adds the hosted `tool_search` tool, so it is loaded only when the model searches for it.
661
- - `async: true` marks an async tool; the model keeps working while it runs.
662
-
663
- ```typescript
664
- import { TEXT_TOOL_SCHEMA } from '@aeye/core';
665
-
666
- const response = await executor(
667
- {
668
- messages: [{ role: 'user', content: 'How many orders did customer 42 place?' }],
669
- tools: [{
670
- name: 'sql',
671
- description: 'Run a read-only SQL query',
672
- parameters: TEXT_TOOL_SCHEMA,
673
- format: { type: 'grammar', syntax: 'regex', definition: 'SELECT [^;]+;' },
674
- }],
675
- },
676
- {},
677
- { model: 'gpt-5.4-mini' }
678
- );
679
-
680
- console.log(response.toolCalls?.[0]?.arguments); // {"input":"SELECT COUNT(*) FROM orders WHERE customer_id = 42;"}
681
- ```
682
-
683
- ### Decisions
684
-
685
- Typed answers from `/v1/decisions` (default model `gpt-6-luna`, or `defaultModels.decision`), through `ai.decide`:
686
-
687
- ```typescript
688
- import { AI } from '@aeye/ai';
689
-
690
- const ai = AI.with().providers({ openai: provider }).create();
691
-
692
- const { answers } = await ai.decide.get({
693
- input: 'I was charged twice for my order.',
694
- questions: {
695
- billing: { type: 'predicate', instructions: 'Is this a billing problem?' },
696
- urgency: { type: 'score', instructions: 'How urgent is it?', levels: ['Can wait', 'This week', 'Now'] },
697
- },
698
- });
699
- ```
700
-
701
- Decisions report through the `decide` hooks.
702
-
703
- ### Batches
704
-
705
- `ai.batch` uploads chat requests as a JSONL file and runs them as one batch with a 24 hour window, at about half the price. Results come back in request order; a failed request has `error`.
706
-
707
- ```typescript
708
- const job = await ai.batch.submit(
709
- questions.map(question => ({ messages: [{ role: 'user', content: question }] })),
710
- { metadata: { model: 'gpt-5.4-mini' } },
711
- );
712
-
713
- const done = await ai.batch.wait(job, {});
714
- done.results?.forEach(result => console.log(result.response?.content ?? result.error));
715
- ```
716
-
717
- ### Advanced Features
718
-
719
- #### Abort Requests
720
-
721
- ```typescript
722
- const controller = new AbortController();
723
-
724
- // Set timeout
725
- setTimeout(() => controller.abort(), 5000);
726
-
727
- try {
728
- const response = await executor(
729
- { messages: [{ role: 'user', content: 'Tell me a long story' }] },
730
- {},
731
- { model: 'gpt-5.4-mini' },
732
- controller.signal
733
- );
734
- } catch (error) {
735
- if (error.name === 'AbortError') {
736
- console.log('Request was aborted');
737
- }
738
- }
739
- ```
740
-
741
- #### List Available Models
742
-
743
- ```typescript
744
- const models = await provider.listModels();
745
-
746
- models.forEach(model => {
747
- console.log(`${model.id}: ${model.capabilities}`);
748
- console.log(` Context window: ${model.contextWindow}`);
749
- console.log(` Pricing: $${model.pricing.text.input}/1M input tokens`);
750
- });
751
- ```
752
-
753
- #### Check Provider Health
754
-
755
- ```typescript
756
- const isHealthy = await provider.checkHealth();
757
- console.log('OpenAI API is', isHealthy ? 'accessible' : 'not accessible');
758
- ```
759
-
760
- #### Default Metadata
761
-
762
- ```typescript
763
- // Set default metadata for all requests
764
- provider.defaultMetadata = {
765
- model: 'gpt-5.4-mini',
766
- user: 'user-123',
767
- };
768
-
769
- // Now model is automatically set
770
- const response = await executor(
771
- { messages: [{ role: 'user', content: 'Hello' }] },
772
- {}
773
- // No need to pass { model: 'gpt-5.4-mini' }
774
- );
775
- ```
776
-
777
- ## Extending for Custom Providers
778
-
779
- The `OpenAIProvider` class can be extended to support OpenAI-compatible APIs like Azure OpenAI, OpenRouter, or custom deployments.
780
-
781
- ### Example: Custom Provider
782
-
783
- ```typescript
784
- import type { Request } from '@aeye/core';
785
- import { OpenAIProvider, OpenAIConfig } from '@aeye/openai';
786
- import OpenAI from 'openai';
787
-
788
- interface CustomConfig extends OpenAIConfig {
789
- customOption?: string;
790
- }
791
-
792
- class CustomProvider extends OpenAIProvider<CustomConfig> {
793
- readonly name = 'custom-provider';
794
-
795
- protected createClient(config: CustomConfig): OpenAI {
796
- return new OpenAI({
797
- apiKey: config.apiKey,
798
- baseURL: 'https://api.custom-provider.com/v1',
799
- defaultHeaders: {
800
- 'X-Custom-Header': config.customOption || '',
801
- },
802
- });
803
- }
804
-
805
- protected augmentChatRequest<TParams extends OpenAI.Chat.ChatCompletionCreateParams>(
806
- params: TParams,
807
- request: Request,
808
- config: CustomConfig,
809
- ) {
810
- // Add custom parameters (mutate in place)
811
- Object.assign(params, { custom_param: config.customOption });
812
- }
813
- }
814
-
815
- // Use the custom provider
816
- const customProvider = new CustomProvider({
817
- apiKey: process.env.CUSTOM_API_KEY!,
818
- customOption: 'value',
819
- });
820
- ```
821
-
822
- ### Example: Azure OpenAI
823
-
824
- ```typescript
825
- class AzureOpenAIProvider extends OpenAIProvider {
826
- readonly name = 'azure-openai';
827
-
828
- protected createClient(config: OpenAIConfig): OpenAI {
829
- return new OpenAI({
830
- apiKey: config.apiKey,
831
- baseURL: config.baseURL, // Azure endpoint
832
- defaultQuery: { 'api-version': '2024-02-01' },
833
- });
834
- }
835
- }
836
-
837
- const azureProvider = new AzureOpenAIProvider({
838
- apiKey: process.env.AZURE_OPENAI_KEY!,
839
- baseURL: 'https://your-resource.openai.azure.com/openai/deployments/your-deployment',
840
- });
841
- ```
842
-
843
- ## Configuration Options
844
-
845
- ### OpenAIConfig
846
-
847
- | Property | Type | Required | Description |
848
- |----------|------|----------|-------------|
849
- | `apiKey` | `string` | Yes | OpenAI API key |
850
- | `baseURL` | `string` | No | Custom base URL for API endpoint |
851
- | `organization` | `string` | No | OpenAI organization ID |
852
- | `project` | `string` | No | OpenAI project ID |
853
- | `store` | `boolean` | No | Let OpenAI store requests (default `false`) |
854
- | `api` | `'auto' \| 'chat' \| 'responses'` | No | Which API chat uses (default `'auto'`) |
855
- | `retry` / `retryEvents` | `RetryConfig` / `RetryEvents` | No | Retry behavior and events |
856
- | `hooks` | `OpenAIHooks` | No | `beforeRequest` / `afterRequest` / `onError` for `chat`, `responses`, `imageGenerate`, `imageEdit`, `transcribe`, `speech`, `embed` |
857
- | `defaultModels` | `object` | No | Per-operation defaults; built in: `gpt-image-2` (generate, edit), `gpt-5.4-mini` (image analysis), `gpt-transcribe`, `gpt-4o-mini-tts`, `text-embedding-3-small`, `gpt-6-luna` (decision); `rerank` has no OpenAI default |
858
-
859
- ### Request Parameters
860
-
861
- #### Chat Completion Request
862
-
863
- | Parameter | Type | Description |
864
- |-----------|------|-------------|
865
- | `messages` | `Message[]` | Array of conversation messages |
866
- | `temperature` | `number` | Sampling temperature (0-2) |
867
- | `topP` | `number` | Nucleus sampling parameter |
868
- | `maxTokens` | `number` | Maximum tokens to generate |
869
- | `stop` | `string \| string[]` | Stop sequences |
870
- | `tools` | `Tool[]` | Function definitions for tool calling |
871
- | `toolChoice` | `'auto' \| 'required' \| 'none' \| { tool: string } \| { allowed: string[]; required?: boolean }` | Tool selection mode |
872
- | `responseFormat` | `'text' \| 'json' \| ZodSchema` | Response format constraint |
873
- | `reason` | `Reason` | `{ effort, summary, mode, context }` |
874
- | `seed` / `verbosity` / `serviceTier` / `metadata` | | Passed through |
875
- | `modalities` / `audio` | | Audio output |
876
- | `hostedTools` | `HostedTool[]` | Tools OpenAI runs (Responses API) |
877
- | `moderate` | `boolean` | Inline moderation |
878
-
879
- #### Image Generation Request
880
-
881
- | Parameter | Type | Description |
882
- |-----------|------|-------------|
883
- | `prompt` | `string` | Text description of desired image |
884
- | `model` | `string` | GPT Image model (default gpt-image-2) |
885
- | `size` | `string` | `WIDTHxHEIGHT` or `'auto'`; snaps to the model's sizes |
886
- | `quality` | `'auto' \| 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'` | Image quality (models accept subsets) |
887
- | `background` | `'transparent' \| 'opaque' \| 'auto'` | Background |
888
- | `outputFormat` | `'png' \| 'jpeg' \| 'webp'` | Encoding |
889
- | `outputCompression` | `number` | 0-100, for jpeg and webp |
890
- | `moderation` | `'auto' \| 'low'` | Moderation strictness (generate) |
891
- | `references` / `mask` / `inputFidelity` | | Extra input images, mask, and detail preservation (edit) |
892
- | `n` | `number` | Number of images to generate |
893
- | `streamCount` | `number` | Partial images to stream |
894
-
895
- #### Transcription Request
896
-
897
- | Parameter | Type | Description |
898
- |-----------|------|-------------|
899
- | `audio` | `Buffer \| string \| Blob` | Audio file to transcribe |
900
- | `model` | `string` | Model to use (default gpt-transcribe) |
901
- | `language` | `string` | ISO-639-1 language code |
902
- | `prompt` | `string` | Context for better accuracy |
903
- | `responseFormat` | `'json' \| 'text' \| 'srt' \| 'vtt' \| 'verbose_json' \| 'diarized_json'` | Output format |
904
- | `knownSpeakers` | `{ name, audio }[]` | Reference clips for diarization |
905
- | `timestampGranularities` | `('word' \| 'segment')[]` | Timestamp detail level |
906
- | `temperature` | `number` | Sampling temperature |
907
-
908
- #### Speech Synthesis Request
909
-
910
- | Parameter | Type | Description |
911
- |-----------|------|-------------|
912
- | `text` | `string` | Text to convert to speech |
913
- | `model` | `string` | Model to use (default gpt-4o-mini-tts) |
914
- | `voice` | `string` | Built-in voice name or custom voice id (default alloy) |
915
- | `instructions` | `string` | Style instructions |
916
- | `speed` | `number` | Playback speed (0.25-4.0) |
917
- | `responseFormat` | `'mp3' \| 'opus' \| 'aac' \| 'flac' \| 'wav' \| 'pcm'` | Audio format |
918
-
919
- #### Embedding Request
920
-
921
- | Parameter | Type | Description |
922
- |-----------|------|-------------|
923
- | `texts` | `string[]` | Texts to embed |
924
- | `model` | `string` | Model to use (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002) |
925
- | `dimensions` | `number` | Output dimensions (embedding-3 models only) |
926
- | `encodingFormat` | `'float' \| 'base64'` | Encoding format |
927
-
928
- ## Error Handling
929
-
930
- The provider throws specific error types for different failure scenarios:
931
-
932
- ```typescript
933
- import { ProviderError, RateLimitError, ProviderAuthError } from '@aeye/openai';
934
-
935
- try {
936
- const response = await executor(
937
- { messages: [{ role: 'user', content: 'Hello' }] },
938
- {},
939
- { model: 'gpt-5.4-mini' }
940
- );
941
- } catch (error) {
942
- if (error instanceof RateLimitError) {
943
- console.error('Rate limit exceeded');
944
- if (error.retryAfter) {
945
- console.log(`Retry after ${error.retryAfter} seconds`);
946
- }
947
- } else if (error instanceof ProviderAuthError) {
948
- console.error('Authentication failed - check your API key');
949
- } else if (error instanceof ProviderError) {
950
- console.error(`Provider error: ${error.message}`);
951
- console.error('Original error:', error.cause);
952
- }
953
- }
954
- ```
955
-
956
- ### Error Types
957
-
958
- - **`ProviderError`**: Base error for all provider-related errors
959
- - **`ProviderAuthError`**: Authentication/authorization failures
960
- - **`RateLimitError`**: Rate limit exceeded (includes `retryAfter` if available)
961
- - **`ProviderRateLimitError`**: Specialized rate limit error
962
- - **`ProviderQuotaError`**: Quota/usage limit exceeded
963
-
964
- ## API Reference
965
-
966
- ### OpenAIProvider
967
-
968
- Main provider class implementing the @aeye Provider interface.
969
-
970
- **Constructor**: `new OpenAIProvider(config: OpenAIConfig)`
971
-
972
- **Methods**:
973
- - `listModels(config?: TConfig): Promise<ModelInfo[]>` - List available models
974
- - `checkHealth(config?: TConfig): Promise<boolean>` - Check API health
975
- - `createExecutor<TContext, TMetadata>(config?: TConfig): Executor` - Create chat executor
976
- - `createStreamer<TContext, TMetadata>(config?: TConfig): Streamer` - Create streaming executor
977
- - `generateImage<TContext>(request, ctx, config?): Promise<ImageGenerationResponse>` - Generate images
978
- - `editImage<TContext>(request, ctx, config?): Promise<ImageGenerationResponse>` - Edit images
979
- - `transcribe<TContext>(request, ctx, config?): Promise<TranscriptionResponse>` - Transcribe audio
980
- - `speech(request, ctx, config?): Promise<SpeechResponse>` - Generate speech
981
- - `embed<TContext>(request, ctx, config?): Promise<EmbeddingResponse>` - Generate embeddings
982
- - `decide(request, ctx, config?): Promise<DecisionResponse>` - Answer decision questions
983
- - `submitBatch(requests, ctx, config?): Promise<BatchJob>` / `getBatch(job, ctx, config?): Promise<BatchJob>` - Run and read batches
984
-
985
- **Protected Methods** (for extending):
986
- - `createClient(config): OpenAI` - Create OpenAI client
987
- - `convertModel(model): ModelInfo` - Convert model format
988
- - `convertMessages(request): OpenAI.ChatCompletionMessageParam[]` - Convert messages
989
- - `convertTools(request): OpenAI.ChatCompletionTool[]` - Convert tools
990
- - `convertToolChoice(request): OpenAI.ChatCompletionToolChoiceOption` - Convert tool choice
991
- - `convertResponseFormat(request): ResponseFormat` - Convert response format
992
- - `augmentChatRequest(params, request, config)` - Add to Chat Completions params (mutate in place)
993
- - `augmentChatMessage(chatMessage, message, config)` / `augmentChatContent(...)` - Add to converted messages and content parts
994
- - `augmentChatResponse(completion, response, config)` / `augmentChatChunk(chunk, ...)` - Read extra fields from responses and chunks
995
- - `augmentImageGenerateRequest?` / `augmentImageEditRequest?` / `augmentTranscriptionRequest?` / `augmentSpeechRequest?` / `augmentEmbeddingRequest?` `(params, request, config)` - Optional hooks to add to those params
996
- - `usesResponses(request, modelId, config, model?)` - Whether a chat request goes to the Responses API
997
- - `createBatch(batch, run)` / `retrieveBatch(job, run)` - Submit and read batches (OpenRouter overrides these for inline batches)
998
-
999
- ## Model Support
1000
-
1001
- ### Chat Models
1002
-
1003
- - **GPT-6**: `gpt-6-luna`, `gpt-6-sol`, `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6.1-terra`
1004
- - **GPT-5.x**: `gpt-5.6-*`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `chat-latest`
1005
- - **Pro and codex** (Responses API): `gpt-5.5-pro`, `gpt-5.4-pro`, `gpt-5.3-codex`
1006
- - **GPT-4.1 / GPT-4o** (retiring): `gpt-4.1`, `gpt-4.1-mini`, `gpt-4o`, `gpt-4o-mini`
1007
- - **Audio**: `gpt-audio-1.5`
1008
-
1009
- ### Image Models
1010
-
1011
- - **GPT Image**: `gpt-image-2`, `gpt-image-2.5-*`
1012
-
1013
- ### Audio Models
1014
-
1015
- - **Transcription**: `gpt-transcribe`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, `whisper-1`
1016
- - **TTS**: `gpt-4o-mini-tts`, `tts-1`, `tts-1-hd`
1017
-
1018
- The `@aeye/models` registry records each model's `retiresAt` date; retired models are never selected.
1019
-
1020
- ### Embedding Models
1021
-
1022
- - **Embedding v3**: `text-embedding-3-small`, `text-embedding-3-large`
1023
- - **Embedding v2**: `text-embedding-ada-002`
1024
-
1025
- ## Best Practices
1026
-
1027
- 1. **API Key Security**: Never hardcode API keys. Use environment variables or secure key management.
1028
-
1029
- 2. **Error Handling**: Always wrap API calls in try-catch blocks and handle rate limits gracefully.
1030
-
1031
- 3. **Streaming for Long Responses**: Use streaming for better user experience with lengthy responses.
1032
-
1033
- 4. **Token Management**: Monitor token usage to control costs. Use `maxTokens` to limit response length.
1034
-
1035
- 5. **Model Selection**: Choose the appropriate model for your use case:
1036
- - GPT-6 or GPT-5.5 for complex tasks requiring high accuracy
1037
- - `gpt-5.4-mini` / `gpt-5.4-nano` for simpler tasks and faster responses
1038
- - A higher `reason.effort` for reasoning-heavy problems
1039
-
1040
- 6. **Function Calling**: Use structured schemas with Zod for type-safe function definitions.
1041
-
1042
- 7. **Retry Logic**: Implement exponential backoff for rate limit errors.
1043
-
1044
- 8. **Context Window**: Be aware of model context limits and truncate messages if needed.
1045
-
1046
- ## Related Packages
1047
-
1048
- - **[@aeye/core](../core)**: Core @aeye framework types and interfaces
1049
- - **[@aeye/ai](../ai)**: AI abstractions and utilities
1050
- - **[@aeye/anthropic](../anthropic)**: Anthropic Claude provider
1051
- - **[@aeye/openrouter](../openrouter)**: OpenRouter multi-provider gateway
1052
-
1053
- ## License
1054
-
1055
- GPL-3.0
1056
-
1057
- ## Contributing
1058
-
1059
- Contributions are welcome! Please see the main [@aeye repository](https://github.com/ClickerMonkey/aeye) for contribution guidelines.
1060
-
1061
- ## Support
1062
-
1063
- For issues and questions:
1064
- - GitHub Issues: https://github.com/ClickerMonkey/aeye/issues
1065
- - Documentation: https://github.com/ClickerMonkey/aeye
1066
-
1067
- ## Changelog
1068
-
1069
- See [CHANGELOG.md](CHANGELOG.md) for version history and updates.
1
+ # @aeye/openai
2
+
3
+ OpenAI provider for the @aeye (AI TypeScript) framework. This package provides a comprehensive integration with OpenAI's API, supporting the full range of capabilities including chat completions, image generation, speech synthesis, transcription, and embeddings.
4
+
5
+ ## Features
6
+
7
+ - **Chat**: GPT-5.x, GPT-6, GPT-4.1 and other chat models, through Chat Completions or the Responses API
8
+ - **Vision**: Image and file (PDF) understanding
9
+ - **Reasoning**: Efforts `none` through `max`, reasoning summaries, and encrypted reasoning carried across turns
10
+ - **Hosted Tools**: Web search, file search, code interpreter, image generation, and remote MCP servers run by OpenAI
11
+ - **Image Generation**: GPT Image (`gpt-image-2` by default) for generating and editing images
12
+ - **Speech-to-Text**: `gpt-transcribe`, diarization with known speakers, and Whisper
13
+ - **Text-to-Speech**: TTS models with built-in or custom voices
14
+ - **Audio Output**: Spoken answers from audio-capable chat models
15
+ - **Embeddings**: Text embeddings for semantic search and similarity
16
+ - **Decisions**: Probabilities, choices, and scores from `/v1/decisions` (`gpt-6-luna`)
17
+ - **Batches**: Chat requests as one batch at about half the price
18
+ - **Function Calling**: Tool/function calling with automatic schema conversion, free-text and grammar tools, tool search, async tools, and allowed tools
19
+ - **Structured Outputs**: JSON mode and schema-based structured outputs
20
+ - **Streaming**: Real-time streaming responses for chat completions
21
+ - **Multi-modal**: Support for text, images, audio, and files in conversations
22
+ - **Extensible**: Base class for OpenAI-compatible providers (Azure OpenAI, OpenRouter, etc.)
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @aeye/openai @aeye/ai @aeye/core openai zod
28
+ ```
29
+
30
+ Requires Node >= 22 and the `openai` SDK v7.
31
+
32
+ ## Quick Start
33
+
34
+ ```typescript
35
+ import { OpenAIProvider } from '@aeye/openai';
36
+
37
+ // Create provider instance
38
+ const provider = new OpenAIProvider({
39
+ apiKey: process.env.OPENAI_API_KEY!,
40
+ });
41
+
42
+ // Create executor
43
+ const executor = provider.createExecutor();
44
+
45
+ // Make a request
46
+ const response = await executor(
47
+ {
48
+ messages: [
49
+ { role: 'user', content: 'Tell me a joke about TypeScript' }
50
+ ]
51
+ },
52
+ {},
53
+ { model: 'gpt-5.4-mini' }
54
+ );
55
+
56
+ console.log(response.content);
57
+ ```
58
+
59
+ ## Configuration
60
+
61
+ ### Basic Configuration
62
+
63
+ ```typescript
64
+ import { OpenAIProvider, OpenAIConfig } from '@aeye/openai';
65
+
66
+ const config: OpenAIConfig = {
67
+ apiKey: process.env.OPENAI_API_KEY!,
68
+ organization: 'org-123456', // Optional: for multi-org accounts
69
+ };
70
+
71
+ const provider = new OpenAIProvider(config);
72
+ ```
73
+
74
+ ### Chat Completions or Responses API
75
+
76
+ ```typescript
77
+ const provider = new OpenAIProvider({
78
+ apiKey: process.env.OPENAI_API_KEY!,
79
+ api: 'auto', // 'auto' (default) | 'chat' | 'responses'
80
+ });
81
+ ```
82
+
83
+ With `'auto'`, chat uses Chat Completions unless the model is Responses-only (codex, pre-5.6 pro models, deep-research, computer-use), the request has `hostedTools`, a tool sets `format`, `defer`, or `async`, `reason` sets `mode`, `summary`, or `context`, or the request has tools and a reasoning `effort` other than `none`. Through Responses, reasoning comes back encrypted (unless `store` is on) and is replayed on later turns.
84
+
85
+ ### Custom Base URL (for Azure OpenAI or compatible APIs)
86
+
87
+ ```typescript
88
+ const provider = new OpenAIProvider({
89
+ apiKey: process.env.AZURE_API_KEY!,
90
+ baseURL: 'https://your-resource.openai.azure.com/openai/deployments/your-deployment',
91
+ });
92
+ ```
93
+
94
+ ## Usage Examples
95
+
96
+ ### Chat Completions
97
+
98
+ #### Basic Chat
99
+
100
+ ```typescript
101
+ const executor = provider.createExecutor();
102
+
103
+ const response = await executor(
104
+ {
105
+ messages: [
106
+ { role: 'system', content: 'You are a helpful assistant.' },
107
+ { role: 'user', content: 'What is the capital of France?' }
108
+ ],
109
+ temperature: 0.7,
110
+ maxTokens: 150,
111
+ },
112
+ {},
113
+ { model: 'gpt-5.4-mini' }
114
+ );
115
+
116
+ console.log(response.content);
117
+ console.log('Tokens used:', response.usage.totalTokens);
118
+ ```
119
+
120
+ #### Streaming Chat
121
+
122
+ ```typescript
123
+ const streamer = provider.createStreamer();
124
+
125
+ for await (const chunk of streamer(
126
+ {
127
+ messages: [
128
+ { role: 'user', content: 'Write a short story about a robot' }
129
+ ]
130
+ },
131
+ {},
132
+ { model: 'gpt-5.4-mini' }
133
+ )) {
134
+ if (chunk.content) {
135
+ process.stdout.write(chunk.content);
136
+ }
137
+ }
138
+ ```
139
+
140
+ #### Multi-turn Conversation
141
+
142
+ ```typescript
143
+ const conversation = [
144
+ { role: 'system' as const, content: 'You are a helpful assistant.' },
145
+ { role: 'user' as const, content: 'What is TypeScript?' },
146
+ ];
147
+
148
+ // First response
149
+ let response = await executor(
150
+ { messages: conversation },
151
+ {},
152
+ { model: 'gpt-5.4-mini' }
153
+ );
154
+
155
+ conversation.push({ role: 'assistant', content: response.content });
156
+ conversation.push({ role: 'user', content: 'Can you give me an example?' });
157
+
158
+ // Second response
159
+ response = await executor(
160
+ { messages: conversation },
161
+ {},
162
+ { model: 'gpt-5.4-mini' }
163
+ );
164
+ ```
165
+
166
+ ### Vision
167
+
168
+ #### Analyze an Image
169
+
170
+ ```typescript
171
+ const response = await executor(
172
+ {
173
+ messages: [
174
+ {
175
+ role: 'user',
176
+ content: [
177
+ { type: 'text', content: 'What is in this image?' },
178
+ {
179
+ type: 'image',
180
+ content: 'https://example.com/image.jpg',
181
+ // Or use base64: content: 'data:image/jpeg;base64,/9j/4AAQ...'
182
+ }
183
+ ]
184
+ }
185
+ ]
186
+ },
187
+ {},
188
+ { model: 'gpt-5.4-mini' }
189
+ );
190
+ ```
191
+
192
+ #### Multiple Images
193
+
194
+ ```typescript
195
+ const response = await executor(
196
+ {
197
+ messages: [
198
+ {
199
+ role: 'user',
200
+ content: [
201
+ { type: 'text', content: 'Compare these two images' },
202
+ { type: 'image', content: 'https://example.com/image1.jpg' },
203
+ { type: 'image', content: 'https://example.com/image2.jpg' }
204
+ ]
205
+ }
206
+ ]
207
+ },
208
+ {},
209
+ { model: 'gpt-5.4-mini' }
210
+ );
211
+ ```
212
+
213
+ ### Function Calling
214
+
215
+ #### Define and Use Tools
216
+
217
+ ```typescript
218
+ import z from 'zod';
219
+
220
+ const response = await executor(
221
+ {
222
+ messages: [
223
+ { role: 'user', content: 'What is the weather like in San Francisco?' }
224
+ ],
225
+ tools: [
226
+ {
227
+ name: 'get_current_weather',
228
+ description: 'Get the current weather in a given location',
229
+ parameters: z.object({
230
+ location: z.string().describe('The city and state, e.g., San Francisco, CA'),
231
+ unit: z.enum(['celsius', 'fahrenheit']).optional(),
232
+ }),
233
+ }
234
+ ],
235
+ toolChoice: 'auto', // 'auto', 'required', 'none', { tool: 'tool_name' }, or { allowed: ['tool_name'], required? }
236
+ },
237
+ {},
238
+ { model: 'gpt-5.4-mini' }
239
+ );
240
+
241
+ if (response.toolCalls) {
242
+ for (const toolCall of response.toolCalls) {
243
+ console.log('Tool:', toolCall.name);
244
+ console.log('Arguments:', JSON.parse(toolCall.arguments));
245
+
246
+ // Execute the function and send result back
247
+ const functionResult = await getWeather(JSON.parse(toolCall.arguments));
248
+
249
+ const finalResponse = await executor(
250
+ {
251
+ messages: [
252
+ { role: 'user', content: 'What is the weather like in San Francisco?' },
253
+ { role: 'assistant', content: '', toolCalls: [toolCall] },
254
+ {
255
+ role: 'tool',
256
+ content: JSON.stringify(functionResult),
257
+ toolCallId: toolCall.id,
258
+ }
259
+ ]
260
+ },
261
+ {},
262
+ { model: 'gpt-5.4-mini' }
263
+ );
264
+ }
265
+ }
266
+ ```
267
+
268
+ #### Force Specific Tool
269
+
270
+ ```typescript
271
+ const response = await executor(
272
+ {
273
+ messages: [
274
+ { role: 'user', content: 'Get the weather for Boston' }
275
+ ],
276
+ tools: [
277
+ {
278
+ name: 'get_weather',
279
+ description: 'Get weather information',
280
+ parameters: z.object({
281
+ location: z.string(),
282
+ }),
283
+ }
284
+ ],
285
+ toolChoice: { tool: 'get_weather' }, // Force this specific tool
286
+ },
287
+ {},
288
+ { model: 'gpt-5.4-mini' }
289
+ );
290
+ ```
291
+
292
+ ### Structured Outputs
293
+
294
+ #### JSON Mode
295
+
296
+ ```typescript
297
+ const response = await executor(
298
+ {
299
+ messages: [
300
+ {
301
+ role: 'user',
302
+ content: 'List three famous scientists and their main contributions in JSON format'
303
+ }
304
+ ],
305
+ responseFormat: 'json', // Ensures valid JSON output
306
+ },
307
+ {},
308
+ { model: 'gpt-5.4-mini' }
309
+ );
310
+
311
+ const scientists = JSON.parse(response.content);
312
+ ```
313
+
314
+ #### Schema-based Structured Output
315
+
316
+ ```typescript
317
+ import z from 'zod';
318
+
319
+ const PersonSchema = z.object({
320
+ name: z.string(),
321
+ age: z.number(),
322
+ occupation: z.string(),
323
+ hobbies: z.array(z.string()),
324
+ });
325
+
326
+ const response = await executor(
327
+ {
328
+ messages: [
329
+ {
330
+ role: 'user',
331
+ content: 'Generate a profile for a fictional software engineer'
332
+ }
333
+ ],
334
+ responseFormat: PersonSchema, // Strict schema enforcement
335
+ },
336
+ {},
337
+ { model: 'gpt-5.4-mini' }
338
+ );
339
+
340
+ // Response is guaranteed to match the schema
341
+ const person = JSON.parse(response.content);
342
+ console.log(person.name, person.age); // Type-safe
343
+ ```
344
+
345
+ A schema that fits OpenAI's strict dialect is sent with `strict: true`. One that doesn't (nested more than 10 levels, for example) goes non-strict, and through the Responses API it is marked `strict: false`, since Responses treats an unflagged `json_schema` as strict.
346
+
347
+ ### Image Generation (GPT Image)
348
+
349
+ #### Generate an Image
350
+
351
+ ```typescript
352
+ const imageResponse = await provider.generateImage(
353
+ {
354
+ prompt: 'A serene landscape with mountains and a lake at sunset',
355
+ model: 'gpt-image-2', // the default
356
+ size: '1024x1024', // or 'auto'; other sizes snap to the nearest the model supports
357
+ quality: 'high', // 'auto' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' (per model)
358
+ outputFormat: 'webp', // 'png' | 'jpeg' | 'webp'
359
+ outputCompression: 80, // 0-100, for jpeg and webp
360
+ background: 'transparent',
361
+ n: 1,
362
+ },
363
+ {}
364
+ );
365
+
366
+ const base64Image = imageResponse.images[0].b64_json;
367
+ ```
368
+
369
+ #### Edit with Reference Images
370
+
371
+ ```typescript
372
+ const edited = await provider.editImage(
373
+ {
374
+ prompt: 'Put the product from the second image on the table in the first',
375
+ image: fs.readFileSync('room.png'), // source image
376
+ references: [fs.readFileSync('product.png')], // more input images
377
+ mask: fs.readFileSync('mask.png'), // optional, over the source image
378
+ inputFidelity: 'high', // keep input details closely
379
+ },
380
+ {}
381
+ );
382
+ ```
383
+
384
+ Usage reports the quality and size actually produced, so per-image pricing applies; GPT Image 2 and later report output image tokens in `usage.image.tokens`.
385
+
386
+ ### Speech Synthesis (TTS)
387
+
388
+ #### Generate Speech
389
+
390
+ ```typescript
391
+ import fs from 'fs';
392
+ import { Readable } from 'stream';
393
+
394
+ const speechResponse = await provider.generateSpeech(
395
+ {
396
+ text: 'Hello! This is a text-to-speech example using OpenAI.',
397
+ model: 'gpt-4o-mini-tts', // the default
398
+ voice: 'alloy', // any built-in voice name or a custom voice id
399
+ instructions: 'Speak warmly and a little slowly.',
400
+ speed: 1.0, // 0.25 to 4.0
401
+ responseFormat: 'mp3', // mp3, opus, aac, flac, wav, pcm
402
+ },
403
+ {}
404
+ );
405
+
406
+ // Save to file
407
+ Readable.fromWeb(speechResponse.audio).pipe(fs.createWriteStream('output.mp3'));
408
+ console.log('Audio saved to output.mp3');
409
+ ```
410
+
411
+ #### Different Voices
412
+
413
+ ```typescript
414
+ const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
415
+
416
+ for (const voice of voices) {
417
+ const response = await provider.generateSpeech(
418
+ {
419
+ text: `This is the ${voice} voice speaking.`,
420
+ voice,
421
+ model: 'gpt-4o-mini-tts',
422
+ },
423
+ {}
424
+ );
425
+
426
+ Readable.fromWeb(response.audio).pipe(fs.createWriteStream(`voice_${voice}.mp3`));
427
+ }
428
+ ```
429
+
430
+ ### Transcription
431
+
432
+ #### Transcribe Audio File
433
+
434
+ ```typescript
435
+ import fs from 'fs';
436
+
437
+ const audioBuffer = fs.readFileSync('audio.mp3');
438
+
439
+ const transcription = await provider.transcribe(
440
+ {
441
+ audio: audioBuffer,
442
+ model: 'gpt-transcribe', // the default
443
+ language: 'en', // Optional: ISO-639-1 language code
444
+ prompt: 'This is a podcast about AI and technology', // Optional: context
445
+ },
446
+ {}
447
+ );
448
+
449
+ console.log('Transcription:', transcription.text);
450
+ ```
451
+
452
+ #### Transcribe with Timestamps
453
+
454
+ ```typescript
455
+ const transcription = await provider.transcribe(
456
+ {
457
+ audio: audioBuffer,
458
+ model: 'whisper-1',
459
+ responseFormat: 'verbose_json',
460
+ timestampGranularities: ['word', 'segment'],
461
+ },
462
+ {}
463
+ );
464
+
465
+ console.log('Full text:', transcription.text);
466
+
467
+ // Segment-level timestamps
468
+ transcription.segments?.forEach(segment => {
469
+ console.log(`${segment.text}: ${segment.start}s - ${segment.end}s`);
470
+ });
471
+ ```
472
+
473
+ #### Speaker Diarization
474
+
475
+ ```typescript
476
+ const transcription = await provider.transcribe(
477
+ {
478
+ audio: fs.readFileSync('meeting.mp3'),
479
+ model: 'gpt-4o-transcribe-diarize',
480
+ responseFormat: 'diarized_json',
481
+ knownSpeakers: [
482
+ { name: 'Alice', audio: fs.readFileSync('alice.wav') },
483
+ { name: 'Bob', audio: fs.readFileSync('bob.wav') },
484
+ ],
485
+ },
486
+ {}
487
+ );
488
+
489
+ transcription.segments?.forEach(segment => {
490
+ console.log(`${segment.speaker}: ${segment.text}`);
491
+ });
492
+ ```
493
+
494
+ #### Different Output Formats
495
+
496
+ ```typescript
497
+ // Plain text
498
+ const text = await provider.transcribe(
499
+ { audio: audioBuffer, responseFormat: 'text' },
500
+ {}
501
+ );
502
+
503
+ // SRT subtitles
504
+ const srt = await provider.transcribe(
505
+ { audio: audioBuffer, responseFormat: 'srt' },
506
+ {}
507
+ );
508
+
509
+ // VTT subtitles
510
+ const vtt = await provider.transcribe(
511
+ { audio: audioBuffer, responseFormat: 'vtt' },
512
+ {}
513
+ );
514
+ ```
515
+
516
+ ### Embeddings
517
+
518
+ #### Generate Embeddings
519
+
520
+ ```typescript
521
+ const embeddingResponse = await provider.embed(
522
+ {
523
+ texts: [
524
+ 'The quick brown fox jumps over the lazy dog',
525
+ 'Machine learning is a subset of artificial intelligence',
526
+ 'OpenAI develops advanced AI models'
527
+ ],
528
+ model: 'text-embedding-3-small', // or 'text-embedding-3-large'
529
+ },
530
+ {}
531
+ );
532
+
533
+ embeddingResponse.embeddings.forEach((item, i) => {
534
+ console.log(`Embedding ${i}:`, item.embedding.length, 'dimensions');
535
+ });
536
+
537
+ console.log('Tokens used:', embeddingResponse.usage?.text?.input);
538
+ ```
539
+
540
+ #### Custom Dimensions (for smaller embeddings)
541
+
542
+ ```typescript
543
+ const embeddingResponse = await provider.embed(
544
+ {
545
+ texts: ['Some text to embed'],
546
+ model: 'text-embedding-3-large',
547
+ dimensions: 256, // Reduce from default 3072 to 256
548
+ },
549
+ {}
550
+ );
551
+ ```
552
+
553
+ #### Calculate Similarity
554
+
555
+ ```typescript
556
+ function cosineSimilarity(a: number[], b: number[]): number {
557
+ const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
558
+ const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
559
+ const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
560
+ return dotProduct / (magnitudeA * magnitudeB);
561
+ }
562
+
563
+ const response = await provider.embed(
564
+ {
565
+ texts: [
566
+ 'I love programming in TypeScript',
567
+ 'TypeScript is my favorite language',
568
+ 'I enjoy cooking Italian food'
569
+ ],
570
+ model: 'text-embedding-3-small',
571
+ },
572
+ {}
573
+ );
574
+
575
+ const [emb1, emb2, emb3] = response.embeddings.map(e => e.embedding);
576
+
577
+ console.log('Similarity 1-2:', cosineSimilarity(emb1, emb2)); // High similarity
578
+ console.log('Similarity 1-3:', cosineSimilarity(emb1, emb3)); // Low similarity
579
+ ```
580
+
581
+ ### Reasoning
582
+
583
+ ```typescript
584
+ const response = await executor(
585
+ {
586
+ messages: [
587
+ {
588
+ role: 'user',
589
+ content: 'Solve this logic puzzle: There are 100 doors, all closed. You make 100 passes. On pass 1, toggle all doors. On pass 2, toggle every 2nd door. On pass 3, toggle every 3rd door, etc. Which doors are open after 100 passes?'
590
+ }
591
+ ],
592
+ reason: {
593
+ effort: 'high', // 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
594
+ summary: 'auto', // a readable summary (uses the Responses API)
595
+ },
596
+ },
597
+ {},
598
+ { model: 'gpt-5.5' }
599
+ );
600
+
601
+ console.log('Reasoning:', response.reasoning?.content);
602
+ console.log('Answer:', response.content);
603
+ ```
604
+
605
+ - The effort is clamped to the model's accepted efforts when its `ModelInfo.reasoningEfforts` lists them.
606
+ - `mode`, `summary`, or `context`, or an effort other than `none` with tools, send the request to the Responses API. There, reasoning comes back encrypted (`openai-responses-v1` details) unless `store` is on; keep the assistant message's `reasoning` in the conversation and it is replayed on the next turn.
607
+ - `reason.maxTokens` and `reason.exclude` are not used by OpenAI.
608
+ - Only reasoning OpenAI produced (`reasoning.provider` is `'openai'`, or unset) is replayed; reasoning relayed by OpenRouter or made on Bedrock is left out, since OpenAI can't decrypt it.
609
+ - An assistant turn of only tool calls is sent with no content rather than an empty text part.
610
+
611
+ ### Hosted Tools
612
+
613
+ Tools OpenAI runs itself go in `hostedTools` and use the Responses API:
614
+
615
+ ```typescript
616
+ const response = await executor(
617
+ {
618
+ messages: [{ role: 'user', content: 'Summarize this week\'s TypeScript news.' }],
619
+ hostedTools: [
620
+ { type: 'web_search', allowedDomains: ['devblogs.microsoft.com'] },
621
+ { type: 'code_interpreter' },
622
+ { type: 'file_search', vectorStoreIds: ['vs_123'], maxResults: 5 },
623
+ { type: 'mcp', label: 'docs', url: 'https://example.com/mcp', allowedTools: ['search'] },
624
+ { type: 'image_generation' },
625
+ ],
626
+ },
627
+ {},
628
+ { model: 'gpt-5.4-mini' }
629
+ );
630
+
631
+ console.log(response.citations); // [{ url, title, start, end }]
632
+ console.log(response.usage?.tools); // { web_search: 1, ... }
633
+ ```
634
+
635
+ `web_fetch` is not available on OpenAI (web search fetches pages itself), and web search can only allow domains, not block them (`blockedDomains` is rejected). Images from `image_generation` come back on `response.images`.
636
+
637
+ ### Audio Output
638
+
639
+ ```typescript
640
+ const response = await executor(
641
+ {
642
+ messages: [{ role: 'user', content: 'Say hello in French.' }],
643
+ modalities: ['text', 'audio'],
644
+ audio: { voice: 'alloy', format: 'mp3' }, // streaming defaults to pcm
645
+ },
646
+ {},
647
+ { model: 'gpt-audio-1.5' }
648
+ );
649
+
650
+ console.log(response.audio?.transcript); // response.audio.data is base64
651
+ ```
652
+
653
+ ### Other Request Options
654
+
655
+ - `seed`, `verbosity` (`'low' | 'medium' | 'high'`), `serviceTier` (`'auto' | 'default' | 'flex' | 'fast' | 'scale'`), and `metadata` are passed through. Chat Completions sends `metadata` only when `store` is on.
656
+ - `moderate: true` runs inline moderation (`omni-moderation-latest`); verdicts come back on `response.moderation`.
657
+ - Content parts take `cacheBreakpoint` (a prompt cache breakpoint), `detail` for images, and `name` for files.
658
+ - `response.serviceTier` reports the tier used (`priority` reads as `fast`).
659
+
660
+ ### Tool Features
661
+
662
+ - `toolChoice: { allowed: [...], required? }` is sent as `allowed_tools` on both APIs: every tool is still sent (keeping the prompt cacheable), but only the allowed ones may be called.
663
+ - A tool with `format: { type: 'text' }` or `format: { type: 'grammar', syntax: 'lark' | 'regex', definition }` is a Responses custom tool: the model writes free text, constrained by the grammar, and it arrives as `{ input }` arguments (`TEXT_TOOL_SCHEMA` from `@aeye/core`). With `api: 'chat'` it is a one-field function whose description carries the grammar.
664
+ - `defer: true` sends the tool with `defer_loading` and adds the hosted `tool_search` tool, so it is loaded only when the model searches for it.
665
+ - `async: true` marks an async tool; the model keeps working while it runs.
666
+
667
+ ```typescript
668
+ import { TEXT_TOOL_SCHEMA } from '@aeye/core';
669
+
670
+ const response = await executor(
671
+ {
672
+ messages: [{ role: 'user', content: 'How many orders did customer 42 place?' }],
673
+ tools: [{
674
+ name: 'sql',
675
+ description: 'Run a read-only SQL query',
676
+ parameters: TEXT_TOOL_SCHEMA,
677
+ format: { type: 'grammar', syntax: 'regex', definition: 'SELECT [^;]+;' },
678
+ }],
679
+ },
680
+ {},
681
+ { model: 'gpt-5.4-mini' }
682
+ );
683
+
684
+ console.log(response.toolCalls?.[0]?.arguments); // {"input":"SELECT COUNT(*) FROM orders WHERE customer_id = 42;"}
685
+ ```
686
+
687
+ ### Decisions
688
+
689
+ Typed answers from `/v1/decisions` (default model `gpt-6-luna`, or `defaultModels.decision`), through `ai.decide`:
690
+
691
+ ```typescript
692
+ import { AI } from '@aeye/ai';
693
+
694
+ const ai = AI.with().providers({ openai: provider }).create();
695
+
696
+ const { answers } = await ai.decide.get({
697
+ input: 'I was charged twice for my order.',
698
+ questions: {
699
+ billing: { type: 'predicate', instructions: 'Is this a billing problem?' },
700
+ urgency: { type: 'score', instructions: 'How urgent is it?', levels: ['Can wait', 'This week', 'Now'] },
701
+ },
702
+ });
703
+ ```
704
+
705
+ Decisions report through the `decide` hooks.
706
+
707
+ ### Batches
708
+
709
+ `ai.batch` uploads chat requests as a JSONL file and runs them as one batch with a 24 hour window, at about half the price. Results come back in request order; a failed request has `error`.
710
+
711
+ ```typescript
712
+ const job = await ai.batch.submit(
713
+ questions.map(question => ({ messages: [{ role: 'user', content: question }] })),
714
+ { metadata: { model: 'gpt-5.4-mini' } },
715
+ );
716
+
717
+ const done = await ai.batch.wait(job, {});
718
+ done.results?.forEach(result => console.log(result.response?.content ?? result.error));
719
+ ```
720
+
721
+ ### Advanced Features
722
+
723
+ #### Abort Requests
724
+
725
+ ```typescript
726
+ const controller = new AbortController();
727
+
728
+ // Set timeout
729
+ setTimeout(() => controller.abort(), 5000);
730
+
731
+ try {
732
+ const response = await executor(
733
+ { messages: [{ role: 'user', content: 'Tell me a long story' }] },
734
+ {},
735
+ { model: 'gpt-5.4-mini' },
736
+ controller.signal
737
+ );
738
+ } catch (error) {
739
+ if (error.name === 'AbortError') {
740
+ console.log('Request was aborted');
741
+ }
742
+ }
743
+ ```
744
+
745
+ #### List Available Models
746
+
747
+ ```typescript
748
+ const models = await provider.listModels();
749
+
750
+ models.forEach(model => {
751
+ console.log(`${model.id}: ${model.capabilities}`);
752
+ console.log(` Context window: ${model.contextWindow}`);
753
+ console.log(` Pricing: $${model.pricing.text.input}/1M input tokens`);
754
+ });
755
+ ```
756
+
757
+ #### Check Provider Health
758
+
759
+ ```typescript
760
+ const isHealthy = await provider.checkHealth();
761
+ console.log('OpenAI API is', isHealthy ? 'accessible' : 'not accessible');
762
+ ```
763
+
764
+ #### Default Metadata
765
+
766
+ ```typescript
767
+ // Set default metadata for all requests
768
+ provider.defaultMetadata = {
769
+ model: 'gpt-5.4-mini',
770
+ user: 'user-123',
771
+ };
772
+
773
+ // Now model is automatically set
774
+ const response = await executor(
775
+ { messages: [{ role: 'user', content: 'Hello' }] },
776
+ {}
777
+ // No need to pass { model: 'gpt-5.4-mini' }
778
+ );
779
+ ```
780
+
781
+ ## Extending for Custom Providers
782
+
783
+ The `OpenAIProvider` class can be extended to support OpenAI-compatible APIs like Azure OpenAI, OpenRouter, or custom deployments.
784
+
785
+ ### Example: Custom Provider
786
+
787
+ ```typescript
788
+ import type { Request } from '@aeye/core';
789
+ import { OpenAIProvider, OpenAIConfig } from '@aeye/openai';
790
+ import OpenAI from 'openai';
791
+
792
+ interface CustomConfig extends OpenAIConfig {
793
+ customOption?: string;
794
+ }
795
+
796
+ class CustomProvider extends OpenAIProvider<CustomConfig> {
797
+ readonly name = 'custom-provider';
798
+
799
+ protected createClient(config: CustomConfig): OpenAI {
800
+ return new OpenAI({
801
+ apiKey: config.apiKey,
802
+ baseURL: 'https://api.custom-provider.com/v1',
803
+ defaultHeaders: {
804
+ 'X-Custom-Header': config.customOption || '',
805
+ },
806
+ });
807
+ }
808
+
809
+ protected augmentChatRequest<TParams extends OpenAI.Chat.ChatCompletionCreateParams>(
810
+ params: TParams,
811
+ request: Request,
812
+ config: CustomConfig,
813
+ ) {
814
+ // Add custom parameters (mutate in place)
815
+ Object.assign(params, { custom_param: config.customOption });
816
+ }
817
+ }
818
+
819
+ // Use the custom provider
820
+ const customProvider = new CustomProvider({
821
+ apiKey: process.env.CUSTOM_API_KEY!,
822
+ customOption: 'value',
823
+ });
824
+ ```
825
+
826
+ ### Example: Azure OpenAI
827
+
828
+ ```typescript
829
+ class AzureOpenAIProvider extends OpenAIProvider {
830
+ readonly name = 'azure-openai';
831
+
832
+ protected createClient(config: OpenAIConfig): OpenAI {
833
+ return new OpenAI({
834
+ apiKey: config.apiKey,
835
+ baseURL: config.baseURL, // Azure endpoint
836
+ defaultQuery: { 'api-version': '2024-02-01' },
837
+ });
838
+ }
839
+ }
840
+
841
+ const azureProvider = new AzureOpenAIProvider({
842
+ apiKey: process.env.AZURE_OPENAI_KEY!,
843
+ baseURL: 'https://your-resource.openai.azure.com/openai/deployments/your-deployment',
844
+ });
845
+ ```
846
+
847
+ ## Configuration Options
848
+
849
+ ### OpenAIConfig
850
+
851
+ | Property | Type | Required | Description |
852
+ |----------|------|----------|-------------|
853
+ | `apiKey` | `string` | Yes | OpenAI API key |
854
+ | `baseURL` | `string` | No | Custom base URL for API endpoint |
855
+ | `organization` | `string` | No | OpenAI organization ID |
856
+ | `project` | `string` | No | OpenAI project ID |
857
+ | `store` | `boolean` | No | Let OpenAI store requests (default `false`) |
858
+ | `api` | `'auto' \| 'chat' \| 'responses'` | No | Which API chat uses (default `'auto'`) |
859
+ | `retry` / `retryEvents` | `RetryConfig` / `RetryEvents` | No | Retry behavior and events |
860
+ | `hooks` | `OpenAIHooks` | No | `beforeRequest` / `afterRequest` / `onError` for `chat`, `responses`, `imageGenerate`, `imageEdit`, `transcribe`, `speech`, `embed`, `rerank`, `decide` |
861
+ | `defaultModels` | `object` | No | Per-operation defaults; built in: `gpt-image-2` (generate, edit), `gpt-5.4-mini` (image analysis), `gpt-transcribe`, `gpt-4o-mini-tts`, `text-embedding-3-small`, `gpt-6-luna` (decision); `rerank` has no OpenAI default |
862
+
863
+ ### Request Parameters
864
+
865
+ #### Chat Completion Request
866
+
867
+ | Parameter | Type | Description |
868
+ |-----------|------|-------------|
869
+ | `messages` | `Message[]` | Array of conversation messages |
870
+ | `temperature` | `number` | Sampling temperature (0-2) |
871
+ | `topP` | `number` | Nucleus sampling parameter |
872
+ | `maxTokens` | `number` | Maximum tokens to generate |
873
+ | `stop` | `string \| string[]` | Stop sequences |
874
+ | `tools` | `Tool[]` | Function definitions for tool calling |
875
+ | `toolsOneAtATime` | `boolean` | Sends `parallel_tool_calls: false`. Otherwise Chat Completions leaves the parameter out (parallel calls are the default, and o3 rejects the parameter) |
876
+ | `toolChoice` | `'auto' \| 'required' \| 'none' \| { tool: string } \| { allowed: string[]; required?: boolean }` | Tool selection mode |
877
+ | `responseFormat` | `'text' \| 'json' \| ZodSchema` | Response format constraint |
878
+ | `reason` | `Reason` | `{ effort, summary, mode, context }` |
879
+ | `seed` / `verbosity` / `serviceTier` / `metadata` | | Passed through |
880
+ | `modalities` / `audio` | | Audio output |
881
+ | `hostedTools` | `HostedTool[]` | Tools OpenAI runs (Responses API) |
882
+ | `moderate` | `boolean` | Inline moderation |
883
+
884
+ #### Image Generation Request
885
+
886
+ | Parameter | Type | Description |
887
+ |-----------|------|-------------|
888
+ | `prompt` | `string` | Text description of desired image |
889
+ | `model` | `string` | GPT Image model (default gpt-image-2) |
890
+ | `size` | `string` | `WIDTHxHEIGHT` or `'auto'`; snaps to the model's sizes |
891
+ | `quality` | `'auto' \| 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'` | Image quality (models accept subsets) |
892
+ | `background` | `'transparent' \| 'opaque' \| 'auto'` | Background |
893
+ | `outputFormat` | `'png' \| 'jpeg' \| 'webp'` | Encoding |
894
+ | `outputCompression` | `number` | 0-100, for jpeg and webp |
895
+ | `moderation` | `'auto' \| 'low'` | Moderation strictness (generate) |
896
+ | `references` / `mask` / `inputFidelity` | | Extra input images, mask, and detail preservation (edit) |
897
+ | `n` | `number` | Number of images to generate |
898
+ | `streamCount` | `number` | Partial images to stream |
899
+
900
+ #### Transcription Request
901
+
902
+ | Parameter | Type | Description |
903
+ |-----------|------|-------------|
904
+ | `audio` | `Buffer \| string \| Blob` | Audio file to transcribe |
905
+ | `model` | `string` | Model to use (default gpt-transcribe) |
906
+ | `language` | `string` | ISO-639-1 language code |
907
+ | `prompt` | `string` | Context for better accuracy |
908
+ | `responseFormat` | `'json' \| 'text' \| 'srt' \| 'vtt' \| 'verbose_json' \| 'diarized_json'` | Output format |
909
+ | `knownSpeakers` | `{ name, audio }[]` | Reference clips for diarization |
910
+ | `timestampGranularities` | `('word' \| 'segment')[]` | Timestamp detail level |
911
+ | `temperature` | `number` | Sampling temperature |
912
+
913
+ #### Speech Synthesis Request
914
+
915
+ | Parameter | Type | Description |
916
+ |-----------|------|-------------|
917
+ | `text` | `string` | Text to convert to speech |
918
+ | `model` | `string` | Model to use (default gpt-4o-mini-tts) |
919
+ | `voice` | `string` | Built-in voice name or custom voice id (default alloy) |
920
+ | `instructions` | `string` | Style instructions |
921
+ | `speed` | `number` | Playback speed (0.25-4.0) |
922
+ | `responseFormat` | `'mp3' \| 'opus' \| 'aac' \| 'flac' \| 'wav' \| 'pcm'` | Audio format |
923
+
924
+ #### Embedding Request
925
+
926
+ | Parameter | Type | Description |
927
+ |-----------|------|-------------|
928
+ | `texts` | `string[]` | Texts to embed |
929
+ | `model` | `string` | Model to use (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002) |
930
+ | `dimensions` | `number` | Output dimensions (embedding-3 models only) |
931
+ | `encodingFormat` | `'float' \| 'base64'` | Encoding format |
932
+
933
+ ## Error Handling
934
+
935
+ The provider throws specific error types for different failure scenarios:
936
+
937
+ ```typescript
938
+ import { ProviderError, RateLimitError, ProviderAuthError } from '@aeye/openai';
939
+
940
+ try {
941
+ const response = await executor(
942
+ { messages: [{ role: 'user', content: 'Hello' }] },
943
+ {},
944
+ { model: 'gpt-5.4-mini' }
945
+ );
946
+ } catch (error) {
947
+ if (error instanceof RateLimitError) {
948
+ console.error('Rate limit exceeded');
949
+ if (error.retryAfter) {
950
+ console.log(`Retry after ${error.retryAfter} seconds`);
951
+ }
952
+ } else if (error instanceof ProviderAuthError) {
953
+ console.error('Authentication failed - check your API key');
954
+ } else if (error instanceof ProviderError) {
955
+ console.error(`Provider error: ${error.message}`);
956
+ console.error('Original error:', error.cause);
957
+ }
958
+ }
959
+ ```
960
+
961
+ ### Error Types
962
+
963
+ - **`ProviderError`**: Base error for all provider-related errors
964
+ - **`ProviderAuthError`**: Authentication/authorization failures
965
+ - **`RateLimitError`**: Rate limit exceeded (includes `retryAfter` if available)
966
+ - **`ProviderRateLimitError`**: Specialized rate limit error
967
+ - **`ProviderQuotaError`**: Quota/usage limit exceeded
968
+
969
+ ## API Reference
970
+
971
+ ### OpenAIProvider
972
+
973
+ Main provider class implementing the @aeye Provider interface.
974
+
975
+ **Constructor**: `new OpenAIProvider(config: OpenAIConfig)`
976
+
977
+ **Methods**:
978
+ - `listModels(config?: TConfig): Promise<ModelInfo[]>` - List available models
979
+ - `checkHealth(config?: TConfig): Promise<boolean>` - Check API health
980
+ - `createExecutor<TContext, TMetadata>(config?: TConfig): Executor` - Create chat executor
981
+ - `createStreamer<TContext, TMetadata>(config?: TConfig): Streamer` - Create streaming executor
982
+ - `generateImage<TContext>(request, ctx, config?): Promise<ImageGenerationResponse>` - Generate images
983
+ - `editImage<TContext>(request, ctx, config?): Promise<ImageGenerationResponse>` - Edit images
984
+ - `transcribe<TContext>(request, ctx, config?): Promise<TranscriptionResponse>` - Transcribe audio
985
+ - `speech(request, ctx, config?): Promise<SpeechResponse>` - Generate speech
986
+ - `embed<TContext>(request, ctx, config?): Promise<EmbeddingResponse>` - Generate embeddings
987
+ - `decide(request, ctx, config?): Promise<DecisionResponse>` - Answer decision questions
988
+ - `submitBatch(requests, ctx, config?): Promise<BatchJob>` / `getBatch(job, ctx, config?): Promise<BatchJob>` - Run and read batches
989
+
990
+ **Protected Methods** (for extending):
991
+ - `createClient(config): OpenAI` - Create OpenAI client
992
+ - `convertModel(model): ModelInfo` - Convert model format
993
+ - `convertMessages(request): OpenAI.ChatCompletionMessageParam[]` - Convert messages
994
+ - `convertTools(request): OpenAI.ChatCompletionTool[]` - Convert tools
995
+ - `convertToolChoice(request): OpenAI.ChatCompletionToolChoiceOption` - Convert tool choice
996
+ - `convertResponseFormat(request): ResponseFormat` - Convert response format
997
+ - `augmentChatRequest(params, request, config)` - Add to Chat Completions params (mutate in place)
998
+ - `chatHeaders(params)` - Extra HTTP headers for a Chat Completions request (none by default; OpenRouter adds a feature flag here)
999
+ - `augmentChatMessage(chatMessage, message, config)` / `augmentChatContent(...)` - Add to converted messages and content parts
1000
+ - `augmentChatResponse(completion, response, config)` / `augmentChatChunk(chunk, ...)` - Read extra fields from responses and chunks
1001
+ - `augmentImageGenerateRequest?` / `augmentImageEditRequest?` / `augmentTranscriptionRequest?` / `augmentSpeechRequest?` / `augmentEmbeddingRequest?` `(params, request, config)` - Optional hooks to add to those params
1002
+ - `usesResponses(request, modelId, config, model?)` - Whether a chat request goes to the Responses API
1003
+ - `createBatch(batch, run)` / `retrieveBatch(job, run)` - Submit and read batches (OpenRouter overrides these for inline batches)
1004
+ - `send(op, run, request, params, call)` / `finish(op, run, request, params, response)` - Run an API call with retries inside the operation's `beforeRequest` / `onError` hooks, then report its response to `afterRequest` (OpenRouter's `rerank` and `decide` use these)
1005
+
1006
+ ## Model Support
1007
+
1008
+ ### Chat Models
1009
+
1010
+ - **GPT-6**: `gpt-6-luna`, `gpt-6-sol`, `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6.1-terra`
1011
+ - **GPT-5.x**: `gpt-5.6-*`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `chat-latest`
1012
+ - **Pro and codex** (Responses API): `gpt-5.5-pro`, `gpt-5.4-pro`, `gpt-5.3-codex`
1013
+ - **GPT-4.1 / GPT-4o** (retiring): `gpt-4.1`, `gpt-4.1-mini`, `gpt-4o`, `gpt-4o-mini`
1014
+ - **Audio**: `gpt-audio-1.5`
1015
+
1016
+ ### Image Models
1017
+
1018
+ - **GPT Image**: `gpt-image-2`, `gpt-image-2.5-*`
1019
+
1020
+ ### Audio Models
1021
+
1022
+ - **Transcription**: `gpt-transcribe`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, `whisper-1`
1023
+ - **TTS**: `gpt-4o-mini-tts`, `tts-1`, `tts-1-hd`
1024
+
1025
+ The `@aeye/models` registry records each model's `retiresAt` date; retired models are never selected.
1026
+
1027
+ ### Embedding Models
1028
+
1029
+ - **Embedding v3**: `text-embedding-3-small`, `text-embedding-3-large`
1030
+ - **Embedding v2**: `text-embedding-ada-002`
1031
+
1032
+ ## Best Practices
1033
+
1034
+ 1. **API Key Security**: Never hardcode API keys. Use environment variables or secure key management.
1035
+
1036
+ 2. **Error Handling**: Always wrap API calls in try-catch blocks and handle rate limits gracefully.
1037
+
1038
+ 3. **Streaming for Long Responses**: Use streaming for better user experience with lengthy responses.
1039
+
1040
+ 4. **Token Management**: Monitor token usage to control costs. Use `maxTokens` to limit response length.
1041
+
1042
+ 5. **Model Selection**: Choose the appropriate model for your use case:
1043
+ - GPT-6 or GPT-5.5 for complex tasks requiring high accuracy
1044
+ - `gpt-5.4-mini` / `gpt-5.4-nano` for simpler tasks and faster responses
1045
+ - A higher `reason.effort` for reasoning-heavy problems
1046
+
1047
+ 6. **Function Calling**: Use structured schemas with Zod for type-safe function definitions.
1048
+
1049
+ 7. **Retry Logic**: Implement exponential backoff for rate limit errors.
1050
+
1051
+ 8. **Context Window**: Be aware of model context limits and truncate messages if needed.
1052
+
1053
+ ## Related Packages
1054
+
1055
+ - **[@aeye/core](../core)**: Core @aeye framework types and interfaces
1056
+ - **[@aeye/ai](../ai)**: AI abstractions and utilities
1057
+ - **[@aeye/anthropic](../anthropic)**: Anthropic Claude provider
1058
+ - **[@aeye/openrouter](../openrouter)**: OpenRouter multi-provider gateway
1059
+
1060
+ ## License
1061
+
1062
+ GPL-3.0
1063
+
1064
+ ## Contributing
1065
+
1066
+ Contributions are welcome! Please see the main [@aeye repository](https://github.com/ClickerMonkey/aeye) for contribution guidelines.
1067
+
1068
+ ## Support
1069
+
1070
+ For issues and questions:
1071
+ - GitHub Issues: https://github.com/ClickerMonkey/aeye/issues
1072
+ - Documentation: https://github.com/ClickerMonkey/aeye
1073
+
1074
+ ## Changelog
1075
+
1076
+ See [CHANGELOG.md](CHANGELOG.md) for version history and updates.