@ai-sdk/openai 4.0.11 → 4.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.js +69 -54
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +61 -49
- package/dist/internal/index.js.map +1 -1
- package/package.json +4 -4
- package/src/openai-provider.ts +7 -4
- package/src/transcription/openai-transcription-model.ts +101 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@ai-sdk/provider": "4.0.3",
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.9"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "22.19.19",
|
|
43
43
|
"tsup": "^8.5.1",
|
|
44
44
|
"typescript": "5.8.3",
|
|
45
45
|
"zod": "3.25.76",
|
|
46
|
-
"@ai-
|
|
47
|
-
"@
|
|
46
|
+
"@vercel/ai-tsconfig": "0.0.0",
|
|
47
|
+
"@ai-sdk/test-server": "2.0.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"zod": "^3.25.76 || ^4.1.8"
|
package/src/openai-provider.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import {
|
|
14
14
|
loadApiKey,
|
|
15
15
|
loadOptionalSetting,
|
|
16
|
+
validateBaseURL,
|
|
16
17
|
withoutTrailingSlash,
|
|
17
18
|
withUserAgentSuffix,
|
|
18
19
|
type FetchFunction,
|
|
@@ -175,10 +176,12 @@ export function createOpenAI(
|
|
|
175
176
|
): OpenAIProvider {
|
|
176
177
|
const baseURL =
|
|
177
178
|
withoutTrailingSlash(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
validateBaseURL(
|
|
180
|
+
loadOptionalSetting({
|
|
181
|
+
settingValue: options.baseURL,
|
|
182
|
+
environmentVariableName: 'OPENAI_BASE_URL',
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
182
185
|
) ?? 'https://api.openai.com/v1';
|
|
183
186
|
|
|
184
187
|
const providerName = options.name ?? 'openai';
|
|
@@ -10,16 +10,18 @@ import {
|
|
|
10
10
|
convertBase64ToUint8Array,
|
|
11
11
|
convertToBase64,
|
|
12
12
|
createJsonResponseHandler,
|
|
13
|
-
|
|
13
|
+
connectToWebSocket,
|
|
14
14
|
mediaTypeToExtension,
|
|
15
15
|
parseProviderOptions,
|
|
16
16
|
postFormDataToApi,
|
|
17
|
-
readWebSocketMessageText,
|
|
18
17
|
safeParseJSON,
|
|
19
18
|
serializeModelOptions,
|
|
20
19
|
toWebSocketUrl,
|
|
21
20
|
WORKFLOW_DESERIALIZE,
|
|
22
21
|
WORKFLOW_SERIALIZE,
|
|
22
|
+
waitForWebSocketBufferDrain,
|
|
23
|
+
type WebSocketConnection,
|
|
24
|
+
type WebSocketLike,
|
|
23
25
|
} from '@ai-sdk/provider-utils';
|
|
24
26
|
import type { OpenAIConfig } from '../openai-config';
|
|
25
27
|
import { openaiFailedResponseHandler } from '../openai-error';
|
|
@@ -390,22 +392,21 @@ function createOpenAIRealtimeTranscriptionStream({
|
|
|
390
392
|
|
|
391
393
|
return new ReadableStream({
|
|
392
394
|
start: controller => {
|
|
393
|
-
const
|
|
394
|
-
const ws = new WebSocketConstructor(
|
|
395
|
-
url,
|
|
396
|
-
getOpenAIRealtimeProtocols(headers),
|
|
397
|
-
{ headers },
|
|
398
|
-
);
|
|
395
|
+
const realtimeConnection = getOpenAIRealtimeConnection(headers);
|
|
399
396
|
let audioReader:
|
|
400
397
|
| ReadableStreamDefaultReader<Uint8Array | string>
|
|
401
398
|
| undefined;
|
|
399
|
+
let connection: WebSocketConnection | undefined;
|
|
402
400
|
|
|
403
401
|
cleanup = (closeCode?: number) => {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
402
|
+
if (audioReader != null) {
|
|
403
|
+
void audioReader.cancel().catch(() => {});
|
|
404
|
+
} else {
|
|
405
|
+
// pre-open failure or abort: cancel the caller's audio stream so an
|
|
406
|
+
// upstream producer piping into it does not hang:
|
|
407
|
+
void audio.cancel().catch(() => {});
|
|
408
|
+
}
|
|
409
|
+
connection?.close(closeCode);
|
|
409
410
|
};
|
|
410
411
|
|
|
411
412
|
const finishWithError = (error: unknown) => {
|
|
@@ -431,89 +432,86 @@ function createOpenAIRealtimeTranscriptionStream({
|
|
|
431
432
|
cleanup(1000);
|
|
432
433
|
};
|
|
433
434
|
|
|
434
|
-
const
|
|
435
|
-
finishWithError(abortSignal?.reason ?? new Error('Aborted'));
|
|
436
|
-
};
|
|
437
|
-
if (abortSignal?.aborted) {
|
|
438
|
-
abort();
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
abortSignal?.addEventListener('abort', abort, { once: true });
|
|
442
|
-
|
|
443
|
-
const sendAudio = async () => {
|
|
435
|
+
const sendAudio = async (socket: WebSocketLike) => {
|
|
444
436
|
audioReader = audio.getReader();
|
|
445
437
|
try {
|
|
446
438
|
while (true) {
|
|
447
439
|
const { done, value } = await audioReader.read();
|
|
448
440
|
if (done || finished) break;
|
|
449
|
-
|
|
441
|
+
socket.send(
|
|
450
442
|
JSON.stringify({
|
|
451
443
|
type: 'input_audio_buffer.append',
|
|
452
444
|
audio: convertToBase64(value),
|
|
453
445
|
}),
|
|
454
446
|
);
|
|
447
|
+
// backpressure: pause reads while the socket buffer is full
|
|
448
|
+
await waitForWebSocketBufferDrain(socket);
|
|
455
449
|
}
|
|
456
450
|
} finally {
|
|
457
451
|
audioReader.releaseLock();
|
|
452
|
+
// unlocked again: cleanup must cancel `audio`, not the reader
|
|
453
|
+
audioReader = undefined;
|
|
458
454
|
}
|
|
459
455
|
if (!finished) {
|
|
460
|
-
|
|
456
|
+
socket.send(JSON.stringify({ type: 'input_audio_buffer.commit' }));
|
|
461
457
|
}
|
|
462
458
|
};
|
|
463
459
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
460
|
+
connection = connectToWebSocket({
|
|
461
|
+
url,
|
|
462
|
+
protocols: realtimeConnection.protocols,
|
|
463
|
+
headers: realtimeConnection.headers,
|
|
464
|
+
webSocket,
|
|
465
|
+
abortSignal,
|
|
466
|
+
onAbort: finishWithError,
|
|
467
|
+
onProcessingError: finishWithError,
|
|
468
|
+
onOpen: socket => {
|
|
469
|
+
controller.enqueue({ type: 'stream-start', warnings });
|
|
470
|
+
socket.send(JSON.stringify(sessionUpdate));
|
|
471
|
+
void sendAudio(socket).catch(finishWithError);
|
|
472
|
+
},
|
|
473
|
+
onMessageText: async text => {
|
|
474
|
+
const parsed = await safeParseJSON({ text });
|
|
475
|
+
if (!parsed.success) return;
|
|
476
|
+
const raw = parsed.value as OpenAIRealtimeTranscriptionEvent;
|
|
469
477
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const parsed = await safeParseJSON({ text });
|
|
474
|
-
if (!parsed.success) return;
|
|
475
|
-
const raw = parsed.value as OpenAIRealtimeTranscriptionEvent;
|
|
478
|
+
if (includeRawChunks) {
|
|
479
|
+
controller.enqueue({ type: 'raw', rawValue: raw });
|
|
480
|
+
}
|
|
476
481
|
|
|
477
|
-
|
|
478
|
-
|
|
482
|
+
switch (raw.type) {
|
|
483
|
+
case 'conversation.item.input_audio_transcription.delta': {
|
|
484
|
+
controller.enqueue({
|
|
485
|
+
type: 'transcript-delta',
|
|
486
|
+
id: raw.item_id,
|
|
487
|
+
delta: raw.delta ?? '',
|
|
488
|
+
});
|
|
489
|
+
break;
|
|
479
490
|
}
|
|
480
491
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
type: 'transcript-delta',
|
|
485
|
-
id: raw.item_id,
|
|
486
|
-
delta: raw.delta ?? '',
|
|
487
|
-
});
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
case 'conversation.item.input_audio_transcription.completed': {
|
|
492
|
-
finish(raw.transcript ?? '', raw.item_id);
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
case 'error': {
|
|
497
|
-
finishWithError(
|
|
498
|
-
new Error(raw.error?.message ?? 'OpenAI realtime error'),
|
|
499
|
-
);
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
492
|
+
case 'conversation.item.input_audio_transcription.completed': {
|
|
493
|
+
finish(raw.transcript ?? '', raw.item_id);
|
|
494
|
+
break;
|
|
502
495
|
}
|
|
503
|
-
})
|
|
504
|
-
.catch(finishWithError);
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
ws.onerror = () => {
|
|
508
|
-
finishWithError(new Error('OpenAI realtime transcription error'));
|
|
509
|
-
};
|
|
510
496
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
497
|
+
case 'error': {
|
|
498
|
+
finishWithError(
|
|
499
|
+
new Error(raw.error?.message ?? 'OpenAI realtime error'),
|
|
500
|
+
);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
onSocketError: () => {
|
|
506
|
+
finishWithError(new Error('OpenAI realtime transcription error'));
|
|
507
|
+
},
|
|
508
|
+
onClose: () => {
|
|
509
|
+
if (finished) return;
|
|
510
|
+
finished = true;
|
|
511
|
+
cleanup();
|
|
512
|
+
controller.close();
|
|
513
|
+
},
|
|
514
|
+
});
|
|
517
515
|
},
|
|
518
516
|
|
|
519
517
|
cancel: () => {
|
|
@@ -564,15 +562,36 @@ function buildOpenAIRealtimeTranscriptionSession({
|
|
|
564
562
|
};
|
|
565
563
|
}
|
|
566
564
|
|
|
567
|
-
|
|
565
|
+
// The bearer token rides the `openai-insecure-api-key` subprotocol (native
|
|
566
|
+
// `WebSocket` cannot send headers) and the Authorization header is stripped:
|
|
567
|
+
// OpenAI rejects handshakes that send both auth channels.
|
|
568
|
+
function getOpenAIRealtimeConnection(
|
|
568
569
|
headers: Record<string, string | undefined>,
|
|
569
|
-
):
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
570
|
+
): {
|
|
571
|
+
protocols: string[];
|
|
572
|
+
headers: Record<string, string | undefined>;
|
|
573
|
+
} {
|
|
574
|
+
// last case-variant wins: combineHeaders keeps case-distinct keys and
|
|
575
|
+
// spreads per-call headers after configuration headers
|
|
576
|
+
let authorization: string | undefined;
|
|
577
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
578
|
+
if (key.toLowerCase() === 'authorization' && value != null) {
|
|
579
|
+
authorization = value;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// the HTTP auth scheme is case-insensitive
|
|
583
|
+
const token = authorization?.match(/^bearer\s+(.+)$/i)?.[1];
|
|
584
|
+
|
|
585
|
+
if (token == null) {
|
|
586
|
+
return { protocols: ['realtime'], headers };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
protocols: ['realtime', `openai-insecure-api-key.${token}`],
|
|
591
|
+
headers: Object.fromEntries(
|
|
592
|
+
Object.entries(headers).filter(
|
|
593
|
+
([key]) => key.toLowerCase() !== 'authorization',
|
|
594
|
+
),
|
|
595
|
+
),
|
|
596
|
+
};
|
|
578
597
|
}
|