@ai-sdk/openai-compatible 2.0.15 → 2.0.17

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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +5 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +23 -6
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +23 -6
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/chat/convert-openai-compatible-chat-usage.ts +55 -0
  10. package/src/chat/convert-to-openai-compatible-chat-messages.test.ts +1238 -0
  11. package/src/chat/convert-to-openai-compatible-chat-messages.ts +246 -0
  12. package/src/chat/get-response-metadata.ts +15 -0
  13. package/src/chat/map-openai-compatible-finish-reason.ts +19 -0
  14. package/src/chat/openai-compatible-api-types.ts +86 -0
  15. package/src/chat/openai-compatible-chat-language-model.test.ts +3292 -0
  16. package/src/chat/openai-compatible-chat-language-model.ts +830 -0
  17. package/src/chat/openai-compatible-chat-options.ts +34 -0
  18. package/src/chat/openai-compatible-metadata-extractor.ts +48 -0
  19. package/src/chat/openai-compatible-prepare-tools.test.ts +336 -0
  20. package/src/chat/openai-compatible-prepare-tools.ts +98 -0
  21. package/src/completion/convert-openai-compatible-completion-usage.ts +46 -0
  22. package/src/completion/convert-to-openai-compatible-completion-prompt.ts +93 -0
  23. package/src/completion/get-response-metadata.ts +15 -0
  24. package/src/completion/map-openai-compatible-finish-reason.ts +19 -0
  25. package/src/completion/openai-compatible-completion-language-model.test.ts +773 -0
  26. package/src/completion/openai-compatible-completion-language-model.ts +390 -0
  27. package/src/completion/openai-compatible-completion-options.ts +33 -0
  28. package/src/embedding/openai-compatible-embedding-model.test.ts +171 -0
  29. package/src/embedding/openai-compatible-embedding-model.ts +166 -0
  30. package/src/embedding/openai-compatible-embedding-options.ts +21 -0
  31. package/src/image/openai-compatible-image-model.test.ts +494 -0
  32. package/src/image/openai-compatible-image-model.ts +205 -0
  33. package/src/image/openai-compatible-image-settings.ts +1 -0
  34. package/src/index.ts +27 -0
  35. package/src/internal/index.ts +4 -0
  36. package/src/openai-compatible-error.ts +30 -0
  37. package/src/openai-compatible-provider.test.ts +329 -0
  38. package/src/openai-compatible-provider.ts +189 -0
  39. package/src/version.ts +5 -0
@@ -0,0 +1,830 @@
1
+ import {
2
+ APICallError,
3
+ InvalidResponseDataError,
4
+ LanguageModelV3,
5
+ LanguageModelV3CallOptions,
6
+ LanguageModelV3Content,
7
+ LanguageModelV3FinishReason,
8
+ LanguageModelV3GenerateResult,
9
+ LanguageModelV3StreamPart,
10
+ LanguageModelV3StreamResult,
11
+ SharedV3ProviderMetadata,
12
+ SharedV3Warning,
13
+ } from '@ai-sdk/provider';
14
+ import {
15
+ combineHeaders,
16
+ createEventSourceResponseHandler,
17
+ createJsonErrorResponseHandler,
18
+ createJsonResponseHandler,
19
+ FetchFunction,
20
+ generateId,
21
+ isParsableJson,
22
+ parseProviderOptions,
23
+ ParseResult,
24
+ postJsonToApi,
25
+ ResponseHandler,
26
+ } from '@ai-sdk/provider-utils';
27
+ import { z } from 'zod/v4';
28
+ import {
29
+ defaultOpenAICompatibleErrorStructure,
30
+ ProviderErrorStructure,
31
+ } from '../openai-compatible-error';
32
+ import { convertOpenAICompatibleChatUsage } from './convert-openai-compatible-chat-usage';
33
+ import { convertToOpenAICompatibleChatMessages } from './convert-to-openai-compatible-chat-messages';
34
+ import { getResponseMetadata } from './get-response-metadata';
35
+ import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason';
36
+ import {
37
+ OpenAICompatibleChatModelId,
38
+ openaiCompatibleProviderOptions,
39
+ } from './openai-compatible-chat-options';
40
+ import { MetadataExtractor } from './openai-compatible-metadata-extractor';
41
+ import { prepareTools } from './openai-compatible-prepare-tools';
42
+
43
+ export type OpenAICompatibleChatConfig = {
44
+ provider: string;
45
+ headers: () => Record<string, string | undefined>;
46
+ url: (options: { modelId: string; path: string }) => string;
47
+ fetch?: FetchFunction;
48
+ includeUsage?: boolean;
49
+ errorStructure?: ProviderErrorStructure<any>;
50
+ metadataExtractor?: MetadataExtractor;
51
+
52
+ /**
53
+ * Whether the model supports structured outputs.
54
+ */
55
+ supportsStructuredOutputs?: boolean;
56
+
57
+ /**
58
+ * The supported URLs for the model.
59
+ */
60
+ supportedUrls?: () => LanguageModelV3['supportedUrls'];
61
+
62
+ /**
63
+ * Optional function to transform the request body before sending it to the API.
64
+ * This is useful for proxy providers that may require a different request format
65
+ * than the official OpenAI API.
66
+ */
67
+ transformRequestBody?: (args: Record<string, any>) => Record<string, any>;
68
+ };
69
+
70
+ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
71
+ readonly specificationVersion = 'v3';
72
+
73
+ readonly supportsStructuredOutputs: boolean;
74
+
75
+ readonly modelId: OpenAICompatibleChatModelId;
76
+ private readonly config: OpenAICompatibleChatConfig;
77
+ private readonly failedResponseHandler: ResponseHandler<APICallError>;
78
+ private readonly chunkSchema; // type inferred via constructor
79
+
80
+ constructor(
81
+ modelId: OpenAICompatibleChatModelId,
82
+ config: OpenAICompatibleChatConfig,
83
+ ) {
84
+ this.modelId = modelId;
85
+ this.config = config;
86
+
87
+ // initialize error handling:
88
+ const errorStructure =
89
+ config.errorStructure ?? defaultOpenAICompatibleErrorStructure;
90
+ this.chunkSchema = createOpenAICompatibleChatChunkSchema(
91
+ errorStructure.errorSchema,
92
+ );
93
+ this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure);
94
+
95
+ this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false;
96
+ }
97
+
98
+ get provider(): string {
99
+ return this.config.provider;
100
+ }
101
+
102
+ private get providerOptionsName(): string {
103
+ return this.config.provider.split('.')[0].trim();
104
+ }
105
+
106
+ get supportedUrls() {
107
+ return this.config.supportedUrls?.() ?? {};
108
+ }
109
+
110
+ private transformRequestBody(args: Record<string, any>): Record<string, any> {
111
+ return this.config.transformRequestBody?.(args) ?? args;
112
+ }
113
+
114
+ private async getArgs({
115
+ prompt,
116
+ maxOutputTokens,
117
+ temperature,
118
+ topP,
119
+ topK,
120
+ frequencyPenalty,
121
+ presencePenalty,
122
+ providerOptions,
123
+ stopSequences,
124
+ responseFormat,
125
+ seed,
126
+ toolChoice,
127
+ tools,
128
+ }: LanguageModelV3CallOptions) {
129
+ const warnings: SharedV3Warning[] = [];
130
+
131
+ // Parse provider options - check for deprecated 'openai-compatible' key
132
+ const deprecatedOptions = await parseProviderOptions({
133
+ provider: 'openai-compatible',
134
+ providerOptions,
135
+ schema: openaiCompatibleProviderOptions,
136
+ });
137
+
138
+ if (deprecatedOptions != null) {
139
+ warnings.push({
140
+ type: 'other',
141
+ message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
142
+ });
143
+ }
144
+
145
+ const compatibleOptions = Object.assign(
146
+ deprecatedOptions ?? {},
147
+ (await parseProviderOptions({
148
+ provider: 'openaiCompatible',
149
+ providerOptions,
150
+ schema: openaiCompatibleProviderOptions,
151
+ })) ?? {},
152
+ (await parseProviderOptions({
153
+ provider: this.providerOptionsName,
154
+ providerOptions,
155
+ schema: openaiCompatibleProviderOptions,
156
+ })) ?? {},
157
+ );
158
+
159
+ const strictJsonSchema = compatibleOptions?.strictJsonSchema ?? true;
160
+
161
+ if (topK != null) {
162
+ warnings.push({ type: 'unsupported', feature: 'topK' });
163
+ }
164
+
165
+ if (
166
+ responseFormat?.type === 'json' &&
167
+ responseFormat.schema != null &&
168
+ !this.supportsStructuredOutputs
169
+ ) {
170
+ warnings.push({
171
+ type: 'unsupported',
172
+ feature: 'responseFormat',
173
+ details:
174
+ 'JSON response format schema is only supported with structuredOutputs',
175
+ });
176
+ }
177
+
178
+ const {
179
+ tools: openaiTools,
180
+ toolChoice: openaiToolChoice,
181
+ toolWarnings,
182
+ } = prepareTools({
183
+ tools,
184
+ toolChoice,
185
+ });
186
+
187
+ return {
188
+ args: {
189
+ // model id:
190
+ model: this.modelId,
191
+
192
+ // model specific settings:
193
+ user: compatibleOptions.user,
194
+
195
+ // standardized settings:
196
+ max_tokens: maxOutputTokens,
197
+ temperature,
198
+ top_p: topP,
199
+ frequency_penalty: frequencyPenalty,
200
+ presence_penalty: presencePenalty,
201
+ response_format:
202
+ responseFormat?.type === 'json'
203
+ ? this.supportsStructuredOutputs === true &&
204
+ responseFormat.schema != null
205
+ ? {
206
+ type: 'json_schema',
207
+ json_schema: {
208
+ schema: responseFormat.schema,
209
+ strict: strictJsonSchema,
210
+ name: responseFormat.name ?? 'response',
211
+ description: responseFormat.description,
212
+ },
213
+ }
214
+ : { type: 'json_object' }
215
+ : undefined,
216
+
217
+ stop: stopSequences,
218
+ seed,
219
+ ...Object.fromEntries(
220
+ Object.entries(
221
+ providerOptions?.[this.providerOptionsName] ?? {},
222
+ ).filter(
223
+ ([key]) =>
224
+ !Object.keys(openaiCompatibleProviderOptions.shape).includes(key),
225
+ ),
226
+ ),
227
+
228
+ reasoning_effort: compatibleOptions.reasoningEffort,
229
+ verbosity: compatibleOptions.textVerbosity,
230
+
231
+ // messages:
232
+ messages: convertToOpenAICompatibleChatMessages(prompt),
233
+
234
+ // tools:
235
+ tools: openaiTools,
236
+ tool_choice: openaiToolChoice,
237
+ },
238
+ warnings: [...warnings, ...toolWarnings],
239
+ };
240
+ }
241
+
242
+ async doGenerate(
243
+ options: LanguageModelV3CallOptions,
244
+ ): Promise<LanguageModelV3GenerateResult> {
245
+ const { args, warnings } = await this.getArgs({ ...options });
246
+
247
+ const transformedBody = this.transformRequestBody(args);
248
+ const body = JSON.stringify(transformedBody);
249
+
250
+ const {
251
+ responseHeaders,
252
+ value: responseBody,
253
+ rawValue: rawResponse,
254
+ } = await postJsonToApi({
255
+ url: this.config.url({
256
+ path: '/chat/completions',
257
+ modelId: this.modelId,
258
+ }),
259
+ headers: combineHeaders(this.config.headers(), options.headers),
260
+ body: transformedBody,
261
+ failedResponseHandler: this.failedResponseHandler,
262
+ successfulResponseHandler: createJsonResponseHandler(
263
+ OpenAICompatibleChatResponseSchema,
264
+ ),
265
+ abortSignal: options.abortSignal,
266
+ fetch: this.config.fetch,
267
+ });
268
+
269
+ const choice = responseBody.choices[0];
270
+ const content: Array<LanguageModelV3Content> = [];
271
+
272
+ // text content:
273
+ const text = choice.message.content;
274
+ if (text != null && text.length > 0) {
275
+ content.push({ type: 'text', text });
276
+ }
277
+
278
+ // reasoning content:
279
+ const reasoning =
280
+ choice.message.reasoning_content ?? choice.message.reasoning;
281
+ if (reasoning != null && reasoning.length > 0) {
282
+ content.push({
283
+ type: 'reasoning',
284
+ text: reasoning,
285
+ });
286
+ }
287
+
288
+ // tool calls:
289
+ if (choice.message.tool_calls != null) {
290
+ for (const toolCall of choice.message.tool_calls) {
291
+ const thoughtSignature =
292
+ toolCall.extra_content?.google?.thought_signature;
293
+ content.push({
294
+ type: 'tool-call',
295
+ toolCallId: toolCall.id ?? generateId(),
296
+ toolName: toolCall.function.name,
297
+ input: toolCall.function.arguments!,
298
+ ...(thoughtSignature
299
+ ? {
300
+ providerMetadata: {
301
+ [this.providerOptionsName]: { thoughtSignature },
302
+ },
303
+ }
304
+ : {}),
305
+ });
306
+ }
307
+ }
308
+
309
+ // provider metadata:
310
+ const providerMetadata: SharedV3ProviderMetadata = {
311
+ [this.providerOptionsName]: {},
312
+ ...(await this.config.metadataExtractor?.extractMetadata?.({
313
+ parsedBody: rawResponse,
314
+ })),
315
+ };
316
+ const completionTokenDetails =
317
+ responseBody.usage?.completion_tokens_details;
318
+ if (completionTokenDetails?.accepted_prediction_tokens != null) {
319
+ providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
320
+ completionTokenDetails?.accepted_prediction_tokens;
321
+ }
322
+ if (completionTokenDetails?.rejected_prediction_tokens != null) {
323
+ providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
324
+ completionTokenDetails?.rejected_prediction_tokens;
325
+ }
326
+
327
+ return {
328
+ content,
329
+ finishReason: {
330
+ unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
331
+ raw: choice.finish_reason ?? undefined,
332
+ },
333
+ usage: convertOpenAICompatibleChatUsage(responseBody.usage),
334
+ providerMetadata,
335
+ request: { body },
336
+ response: {
337
+ ...getResponseMetadata(responseBody),
338
+ headers: responseHeaders,
339
+ body: rawResponse,
340
+ },
341
+ warnings,
342
+ };
343
+ }
344
+
345
+ async doStream(
346
+ options: LanguageModelV3CallOptions,
347
+ ): Promise<LanguageModelV3StreamResult> {
348
+ const { args, warnings } = await this.getArgs({ ...options });
349
+
350
+ const body = this.transformRequestBody({
351
+ ...args,
352
+ stream: true,
353
+
354
+ // only include stream_options when in strict compatibility mode:
355
+ stream_options: this.config.includeUsage
356
+ ? { include_usage: true }
357
+ : undefined,
358
+ });
359
+
360
+ const metadataExtractor =
361
+ this.config.metadataExtractor?.createStreamExtractor();
362
+
363
+ const { responseHeaders, value: response } = await postJsonToApi({
364
+ url: this.config.url({
365
+ path: '/chat/completions',
366
+ modelId: this.modelId,
367
+ }),
368
+ headers: combineHeaders(this.config.headers(), options.headers),
369
+ body,
370
+ failedResponseHandler: this.failedResponseHandler,
371
+ successfulResponseHandler: createEventSourceResponseHandler(
372
+ this.chunkSchema,
373
+ ),
374
+ abortSignal: options.abortSignal,
375
+ fetch: this.config.fetch,
376
+ });
377
+
378
+ const toolCalls: Array<{
379
+ id: string;
380
+ type: 'function';
381
+ function: {
382
+ name: string;
383
+ arguments: string;
384
+ };
385
+ hasFinished: boolean;
386
+ thoughtSignature?: string;
387
+ }> = [];
388
+
389
+ let finishReason: LanguageModelV3FinishReason = {
390
+ unified: 'other',
391
+ raw: undefined,
392
+ };
393
+ let usage: z.infer<typeof openaiCompatibleTokenUsageSchema> | undefined =
394
+ undefined;
395
+ let isFirstChunk = true;
396
+ const providerOptionsName = this.providerOptionsName;
397
+ let isActiveReasoning = false;
398
+ let isActiveText = false;
399
+
400
+ return {
401
+ stream: response.pipeThrough(
402
+ new TransformStream<
403
+ ParseResult<z.infer<typeof this.chunkSchema>>,
404
+ LanguageModelV3StreamPart
405
+ >({
406
+ start(controller) {
407
+ controller.enqueue({ type: 'stream-start', warnings });
408
+ },
409
+
410
+ transform(chunk, controller) {
411
+ // Emit raw chunk if requested (before anything else)
412
+ if (options.includeRawChunks) {
413
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
414
+ }
415
+
416
+ // handle failed chunk parsing / validation:
417
+ if (!chunk.success) {
418
+ finishReason = { unified: 'error', raw: undefined };
419
+ controller.enqueue({ type: 'error', error: chunk.error });
420
+ return;
421
+ }
422
+
423
+ metadataExtractor?.processChunk(chunk.rawValue);
424
+
425
+ // handle error chunks:
426
+ if ('error' in chunk.value) {
427
+ finishReason = { unified: 'error', raw: undefined };
428
+ controller.enqueue({
429
+ type: 'error',
430
+ error: chunk.value.error.message,
431
+ });
432
+ return;
433
+ }
434
+
435
+ // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX
436
+ // remove this workaround when the issue is fixed
437
+ const value = chunk.value as z.infer<typeof chunkBaseSchema>;
438
+
439
+ if (isFirstChunk) {
440
+ isFirstChunk = false;
441
+
442
+ controller.enqueue({
443
+ type: 'response-metadata',
444
+ ...getResponseMetadata(value),
445
+ });
446
+ }
447
+
448
+ if (value.usage != null) {
449
+ usage = value.usage;
450
+ }
451
+
452
+ const choice = value.choices[0];
453
+
454
+ if (choice?.finish_reason != null) {
455
+ finishReason = {
456
+ unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
457
+ raw: choice.finish_reason ?? undefined,
458
+ };
459
+ }
460
+
461
+ if (choice?.delta == null) {
462
+ return;
463
+ }
464
+
465
+ const delta = choice.delta;
466
+
467
+ // enqueue reasoning before text deltas:
468
+ const reasoningContent = delta.reasoning_content ?? delta.reasoning;
469
+ if (reasoningContent) {
470
+ if (!isActiveReasoning) {
471
+ controller.enqueue({
472
+ type: 'reasoning-start',
473
+ id: 'reasoning-0',
474
+ });
475
+ isActiveReasoning = true;
476
+ }
477
+
478
+ controller.enqueue({
479
+ type: 'reasoning-delta',
480
+ id: 'reasoning-0',
481
+ delta: reasoningContent,
482
+ });
483
+ }
484
+
485
+ if (delta.content) {
486
+ // end active reasoning block before text starts
487
+ if (isActiveReasoning) {
488
+ controller.enqueue({
489
+ type: 'reasoning-end',
490
+ id: 'reasoning-0',
491
+ });
492
+ isActiveReasoning = false;
493
+ }
494
+
495
+ if (!isActiveText) {
496
+ controller.enqueue({ type: 'text-start', id: 'txt-0' });
497
+ isActiveText = true;
498
+ }
499
+
500
+ controller.enqueue({
501
+ type: 'text-delta',
502
+ id: 'txt-0',
503
+ delta: delta.content,
504
+ });
505
+ }
506
+
507
+ if (delta.tool_calls != null) {
508
+ // end active reasoning block before tool calls start
509
+ if (isActiveReasoning) {
510
+ controller.enqueue({
511
+ type: 'reasoning-end',
512
+ id: 'reasoning-0',
513
+ });
514
+ isActiveReasoning = false;
515
+ }
516
+
517
+ for (const toolCallDelta of delta.tool_calls) {
518
+ const index = toolCallDelta.index ?? toolCalls.length;
519
+
520
+ if (toolCalls[index] == null) {
521
+ if (toolCallDelta.id == null) {
522
+ throw new InvalidResponseDataError({
523
+ data: toolCallDelta,
524
+ message: `Expected 'id' to be a string.`,
525
+ });
526
+ }
527
+
528
+ if (toolCallDelta.function?.name == null) {
529
+ throw new InvalidResponseDataError({
530
+ data: toolCallDelta,
531
+ message: `Expected 'function.name' to be a string.`,
532
+ });
533
+ }
534
+
535
+ controller.enqueue({
536
+ type: 'tool-input-start',
537
+ id: toolCallDelta.id,
538
+ toolName: toolCallDelta.function.name,
539
+ });
540
+
541
+ toolCalls[index] = {
542
+ id: toolCallDelta.id,
543
+ type: 'function',
544
+ function: {
545
+ name: toolCallDelta.function.name,
546
+ arguments: toolCallDelta.function.arguments ?? '',
547
+ },
548
+ hasFinished: false,
549
+ thoughtSignature:
550
+ toolCallDelta.extra_content?.google?.thought_signature ??
551
+ undefined,
552
+ };
553
+
554
+ const toolCall = toolCalls[index];
555
+
556
+ if (
557
+ toolCall.function?.name != null &&
558
+ toolCall.function?.arguments != null
559
+ ) {
560
+ // send delta if the argument text has already started:
561
+ if (toolCall.function.arguments.length > 0) {
562
+ controller.enqueue({
563
+ type: 'tool-input-delta',
564
+ id: toolCall.id,
565
+ delta: toolCall.function.arguments,
566
+ });
567
+ }
568
+
569
+ // check if tool call is complete
570
+ // (some providers send the full tool call in one chunk):
571
+ if (isParsableJson(toolCall.function.arguments)) {
572
+ controller.enqueue({
573
+ type: 'tool-input-end',
574
+ id: toolCall.id,
575
+ });
576
+
577
+ controller.enqueue({
578
+ type: 'tool-call',
579
+ toolCallId: toolCall.id ?? generateId(),
580
+ toolName: toolCall.function.name,
581
+ input: toolCall.function.arguments,
582
+ ...(toolCall.thoughtSignature
583
+ ? {
584
+ providerMetadata: {
585
+ [providerOptionsName]: {
586
+ thoughtSignature: toolCall.thoughtSignature,
587
+ },
588
+ },
589
+ }
590
+ : {}),
591
+ });
592
+ toolCall.hasFinished = true;
593
+ }
594
+ }
595
+
596
+ continue;
597
+ }
598
+
599
+ // existing tool call, merge if not finished
600
+ const toolCall = toolCalls[index];
601
+
602
+ if (toolCall.hasFinished) {
603
+ continue;
604
+ }
605
+
606
+ if (toolCallDelta.function?.arguments != null) {
607
+ toolCall.function!.arguments +=
608
+ toolCallDelta.function?.arguments ?? '';
609
+ }
610
+
611
+ // send delta
612
+ controller.enqueue({
613
+ type: 'tool-input-delta',
614
+ id: toolCall.id,
615
+ delta: toolCallDelta.function.arguments ?? '',
616
+ });
617
+
618
+ // check if tool call is complete
619
+ if (
620
+ toolCall.function?.name != null &&
621
+ toolCall.function?.arguments != null &&
622
+ isParsableJson(toolCall.function.arguments)
623
+ ) {
624
+ controller.enqueue({
625
+ type: 'tool-input-end',
626
+ id: toolCall.id,
627
+ });
628
+
629
+ controller.enqueue({
630
+ type: 'tool-call',
631
+ toolCallId: toolCall.id ?? generateId(),
632
+ toolName: toolCall.function.name,
633
+ input: toolCall.function.arguments,
634
+ ...(toolCall.thoughtSignature
635
+ ? {
636
+ providerMetadata: {
637
+ [providerOptionsName]: {
638
+ thoughtSignature: toolCall.thoughtSignature,
639
+ },
640
+ },
641
+ }
642
+ : {}),
643
+ });
644
+ toolCall.hasFinished = true;
645
+ }
646
+ }
647
+ }
648
+ },
649
+
650
+ flush(controller) {
651
+ if (isActiveReasoning) {
652
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
653
+ }
654
+
655
+ if (isActiveText) {
656
+ controller.enqueue({ type: 'text-end', id: 'txt-0' });
657
+ }
658
+
659
+ // go through all tool calls and send the ones that are not finished
660
+ for (const toolCall of toolCalls.filter(
661
+ toolCall => !toolCall.hasFinished,
662
+ )) {
663
+ controller.enqueue({
664
+ type: 'tool-input-end',
665
+ id: toolCall.id,
666
+ });
667
+
668
+ controller.enqueue({
669
+ type: 'tool-call',
670
+ toolCallId: toolCall.id ?? generateId(),
671
+ toolName: toolCall.function.name,
672
+ input: toolCall.function.arguments,
673
+ ...(toolCall.thoughtSignature
674
+ ? {
675
+ providerMetadata: {
676
+ [providerOptionsName]: {
677
+ thoughtSignature: toolCall.thoughtSignature,
678
+ },
679
+ },
680
+ }
681
+ : {}),
682
+ });
683
+ }
684
+
685
+ const providerMetadata: SharedV3ProviderMetadata = {
686
+ [providerOptionsName]: {},
687
+ ...metadataExtractor?.buildMetadata(),
688
+ };
689
+ if (
690
+ usage?.completion_tokens_details?.accepted_prediction_tokens !=
691
+ null
692
+ ) {
693
+ providerMetadata[providerOptionsName].acceptedPredictionTokens =
694
+ usage?.completion_tokens_details?.accepted_prediction_tokens;
695
+ }
696
+ if (
697
+ usage?.completion_tokens_details?.rejected_prediction_tokens !=
698
+ null
699
+ ) {
700
+ providerMetadata[providerOptionsName].rejectedPredictionTokens =
701
+ usage?.completion_tokens_details?.rejected_prediction_tokens;
702
+ }
703
+
704
+ controller.enqueue({
705
+ type: 'finish',
706
+ finishReason,
707
+ usage: convertOpenAICompatibleChatUsage(usage),
708
+ providerMetadata,
709
+ });
710
+ },
711
+ }),
712
+ ),
713
+ request: { body },
714
+ response: { headers: responseHeaders },
715
+ };
716
+ }
717
+ }
718
+
719
+ const openaiCompatibleTokenUsageSchema = z
720
+ .object({
721
+ prompt_tokens: z.number().nullish(),
722
+ completion_tokens: z.number().nullish(),
723
+ total_tokens: z.number().nullish(),
724
+ prompt_tokens_details: z
725
+ .object({
726
+ cached_tokens: z.number().nullish(),
727
+ })
728
+ .nullish(),
729
+ completion_tokens_details: z
730
+ .object({
731
+ reasoning_tokens: z.number().nullish(),
732
+ accepted_prediction_tokens: z.number().nullish(),
733
+ rejected_prediction_tokens: z.number().nullish(),
734
+ })
735
+ .nullish(),
736
+ })
737
+ .nullish();
738
+
739
+ // limited version of the schema, focussed on what is needed for the implementation
740
+ // this approach limits breakages when the API changes and increases efficiency
741
+ const OpenAICompatibleChatResponseSchema = z.looseObject({
742
+ id: z.string().nullish(),
743
+ created: z.number().nullish(),
744
+ model: z.string().nullish(),
745
+ choices: z.array(
746
+ z.object({
747
+ message: z.object({
748
+ role: z.literal('assistant').nullish(),
749
+ content: z.string().nullish(),
750
+ reasoning_content: z.string().nullish(),
751
+ reasoning: z.string().nullish(),
752
+ tool_calls: z
753
+ .array(
754
+ z.object({
755
+ id: z.string().nullish(),
756
+ function: z.object({
757
+ name: z.string(),
758
+ arguments: z.string(),
759
+ }),
760
+ // Support for Google Gemini thought signatures via OpenAI compatibility
761
+ extra_content: z
762
+ .object({
763
+ google: z
764
+ .object({
765
+ thought_signature: z.string().nullish(),
766
+ })
767
+ .nullish(),
768
+ })
769
+ .nullish(),
770
+ }),
771
+ )
772
+ .nullish(),
773
+ }),
774
+ finish_reason: z.string().nullish(),
775
+ }),
776
+ ),
777
+ usage: openaiCompatibleTokenUsageSchema,
778
+ });
779
+
780
+ const chunkBaseSchema = z.looseObject({
781
+ id: z.string().nullish(),
782
+ created: z.number().nullish(),
783
+ model: z.string().nullish(),
784
+ choices: z.array(
785
+ z.object({
786
+ delta: z
787
+ .object({
788
+ role: z.enum(['assistant']).nullish(),
789
+ content: z.string().nullish(),
790
+ // Most openai-compatible models set `reasoning_content`, but some
791
+ // providers serving `gpt-oss` set `reasoning`. See #7866
792
+ reasoning_content: z.string().nullish(),
793
+ reasoning: z.string().nullish(),
794
+ tool_calls: z
795
+ .array(
796
+ z.object({
797
+ index: z.number().nullish(), //google does not send index
798
+ id: z.string().nullish(),
799
+ function: z.object({
800
+ name: z.string().nullish(),
801
+ arguments: z.string().nullish(),
802
+ }),
803
+ // Support for Google Gemini thought signatures via OpenAI compatibility
804
+ extra_content: z
805
+ .object({
806
+ google: z
807
+ .object({
808
+ thought_signature: z.string().nullish(),
809
+ })
810
+ .nullish(),
811
+ })
812
+ .nullish(),
813
+ }),
814
+ )
815
+ .nullish(),
816
+ })
817
+ .nullish(),
818
+ finish_reason: z.string().nullish(),
819
+ }),
820
+ ),
821
+ usage: openaiCompatibleTokenUsageSchema,
822
+ });
823
+
824
+ // limited version of the schema, focussed on what is needed for the implementation
825
+ // this approach limits breakages when the API changes and increases efficiency
826
+ const createOpenAICompatibleChatChunkSchema = <
827
+ ERROR_SCHEMA extends z.core.$ZodType,
828
+ >(
829
+ errorSchema: ERROR_SCHEMA,
830
+ ) => z.union([chunkBaseSchema, errorSchema]);