@ai-sdk/openai-compatible 3.0.0-beta.4 → 3.0.0-beta.57

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 (36) hide show
  1. package/CHANGELOG.md +440 -8
  2. package/README.md +2 -0
  3. package/dist/index.d.ts +69 -8
  4. package/dist/index.js +548 -428
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +20 -2
  7. package/dist/internal/index.js +91 -91
  8. package/dist/internal/index.js.map +1 -1
  9. package/docs/index.mdx +1 -0
  10. package/package.json +16 -17
  11. package/src/chat/convert-openai-compatible-chat-usage.ts +1 -1
  12. package/src/chat/convert-to-openai-compatible-chat-messages.ts +94 -73
  13. package/src/chat/map-openai-compatible-finish-reason.ts +1 -1
  14. package/src/chat/openai-compatible-api-types.ts +3 -5
  15. package/src/chat/openai-compatible-chat-language-model.ts +205 -191
  16. package/src/chat/openai-compatible-metadata-extractor.ts +1 -1
  17. package/src/chat/openai-compatible-prepare-tools.ts +2 -3
  18. package/src/completion/convert-openai-compatible-completion-usage.ts +1 -1
  19. package/src/completion/convert-to-openai-compatible-completion-prompt.ts +1 -2
  20. package/src/completion/map-openai-compatible-finish-reason.ts +1 -1
  21. package/src/completion/openai-compatible-completion-language-model.ts +52 -17
  22. package/src/embedding/openai-compatible-embedding-model.ts +36 -10
  23. package/src/image/openai-compatible-image-model.ts +35 -13
  24. package/src/index.ts +3 -3
  25. package/src/openai-compatible-error.ts +1 -2
  26. package/src/openai-compatible-provider.ts +18 -5
  27. package/src/utils/to-camel-case.ts +43 -0
  28. package/dist/index.d.mts +0 -290
  29. package/dist/index.mjs +0 -1742
  30. package/dist/index.mjs.map +0 -1
  31. package/dist/internal/index.d.mts +0 -193
  32. package/dist/internal/index.mjs +0 -340
  33. package/dist/internal/index.mjs.map +0 -1
  34. /package/src/chat/{openai-compatible-chat-options.ts → openai-compatible-chat-language-model-options.ts} +0 -0
  35. /package/src/completion/{openai-compatible-completion-options.ts → openai-compatible-completion-language-model-options.ts} +0 -0
  36. /package/src/embedding/{openai-compatible-embedding-options.ts → openai-compatible-embedding-model-options.ts} +0 -0
@@ -1,6 +1,5 @@
1
- import {
1
+ import type {
2
2
  APICallError,
3
- InvalidResponseDataError,
4
3
  LanguageModelV4,
5
4
  LanguageModelV4CallOptions,
6
5
  LanguageModelV4Content,
@@ -8,6 +7,7 @@ import {
8
7
  LanguageModelV4GenerateResult,
9
8
  LanguageModelV4StreamPart,
10
9
  LanguageModelV4StreamResult,
10
+ LanguageModelV4Usage,
11
11
  SharedV4ProviderMetadata,
12
12
  SharedV4Warning,
13
13
  } from '@ai-sdk/provider';
@@ -16,33 +16,51 @@ import {
16
16
  createEventSourceResponseHandler,
17
17
  createJsonErrorResponseHandler,
18
18
  createJsonResponseHandler,
19
- FetchFunction,
20
19
  generateId,
21
- isParsableJson,
20
+ isCustomReasoning,
22
21
  parseProviderOptions,
23
- ParseResult,
24
22
  postJsonToApi,
25
- ResponseHandler,
23
+ serializeModelOptions,
24
+ StreamingToolCallTracker,
25
+ WORKFLOW_SERIALIZE,
26
+ WORKFLOW_DESERIALIZE,
27
+ type StreamingToolCallDelta,
28
+ type FetchFunction,
29
+ type ParseResult,
30
+ type ResponseHandler,
26
31
  } from '@ai-sdk/provider-utils';
27
32
  import { z } from 'zod/v4';
33
+ import {
34
+ resolveProviderOptionsKey,
35
+ toCamelCase,
36
+ warnIfDeprecatedProviderOptionsKey,
37
+ } from '../utils/to-camel-case';
28
38
  import {
29
39
  defaultOpenAICompatibleErrorStructure,
30
- ProviderErrorStructure,
40
+ type ProviderErrorStructure,
31
41
  } from '../openai-compatible-error';
32
42
  import { convertOpenAICompatibleChatUsage } from './convert-openai-compatible-chat-usage';
33
43
  import { convertToOpenAICompatibleChatMessages } from './convert-to-openai-compatible-chat-messages';
34
44
  import { getResponseMetadata } from './get-response-metadata';
35
45
  import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason';
36
46
  import {
37
- OpenAICompatibleChatModelId,
38
47
  openaiCompatibleLanguageModelChatOptions,
39
- } from './openai-compatible-chat-options';
40
- import { MetadataExtractor } from './openai-compatible-metadata-extractor';
48
+ type OpenAICompatibleChatModelId,
49
+ } from './openai-compatible-chat-language-model-options';
50
+ import type { MetadataExtractor } from './openai-compatible-metadata-extractor';
41
51
  import { prepareTools } from './openai-compatible-prepare-tools';
42
52
 
53
+ type OpenAICompatibleStreamingToolCallDelta = StreamingToolCallDelta & {
54
+ extra_content?: {
55
+ google?: {
56
+ thought_signature?: string | null;
57
+ } | null;
58
+ } | null;
59
+ };
60
+
43
61
  export type OpenAICompatibleChatConfig = {
44
62
  provider: string;
45
- headers: () => Record<string, string | undefined>;
63
+ headers?: () => Record<string, string | undefined>;
46
64
  url: (options: { modelId: string; path: string }) => string;
47
65
  fetch?: FetchFunction;
48
66
  includeUsage?: boolean;
@@ -65,6 +83,20 @@ export type OpenAICompatibleChatConfig = {
65
83
  * than the official OpenAI API.
66
84
  */
67
85
  transformRequestBody?: (args: Record<string, any>) => Record<string, any>;
86
+
87
+ /**
88
+ * Optional usage converter for OpenAI-compatible providers with different
89
+ * token accounting semantics.
90
+ */
91
+ convertUsage?: (
92
+ usage: z.infer<typeof openaiCompatibleTokenUsageSchema>,
93
+ ) => LanguageModelV4Usage;
94
+ };
95
+
96
+ type PendingToolCall = {
97
+ id: string | null;
98
+ bufferedArguments: string;
99
+ extraContent: OpenAICompatibleStreamingToolCallDelta['extra_content'];
68
100
  };
69
101
 
70
102
  export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
@@ -73,10 +105,27 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
73
105
  readonly supportsStructuredOutputs: boolean;
74
106
 
75
107
  readonly modelId: OpenAICompatibleChatModelId;
76
- private readonly config: OpenAICompatibleChatConfig;
108
+ protected readonly config: OpenAICompatibleChatConfig;
77
109
  private readonly failedResponseHandler: ResponseHandler<APICallError>;
78
110
  private readonly chunkSchema; // type inferred via constructor
79
111
 
112
+ static [WORKFLOW_SERIALIZE](model: OpenAICompatibleChatLanguageModel) {
113
+ return serializeModelOptions({
114
+ modelId: model.modelId,
115
+ config: model.config,
116
+ });
117
+ }
118
+
119
+ static [WORKFLOW_DESERIALIZE](options: {
120
+ modelId: string;
121
+ config: OpenAICompatibleChatConfig;
122
+ }) {
123
+ return new OpenAICompatibleChatLanguageModel(
124
+ options.modelId,
125
+ options.config,
126
+ );
127
+ }
128
+
80
129
  constructor(
81
130
  modelId: OpenAICompatibleChatModelId,
82
131
  config: OpenAICompatibleChatConfig,
@@ -111,6 +160,15 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
111
160
  return this.config.transformRequestBody?.(args) ?? args;
112
161
  }
113
162
 
163
+ private convertUsage(
164
+ usage: z.infer<typeof openaiCompatibleTokenUsageSchema>,
165
+ ): LanguageModelV4Usage {
166
+ return (
167
+ this.config.convertUsage?.(usage) ??
168
+ convertOpenAICompatibleChatUsage(usage)
169
+ );
170
+ }
171
+
114
172
  private async getArgs({
115
173
  prompt,
116
174
  maxOutputTokens,
@@ -119,6 +177,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
119
177
  topK,
120
178
  frequencyPenalty,
121
179
  presencePenalty,
180
+ reasoning,
122
181
  providerOptions,
123
182
  stopSequences,
124
183
  responseFormat,
@@ -137,11 +196,19 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
137
196
 
138
197
  if (deprecatedOptions != null) {
139
198
  warnings.push({
140
- type: 'other',
141
- message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
199
+ type: 'deprecated',
200
+ setting: "providerOptions key 'openai-compatible'",
201
+ message: "Use 'openaiCompatible' instead.",
142
202
  });
143
203
  }
144
204
 
205
+ // Warn when the raw (non-camelCase) provider name is used
206
+ warnIfDeprecatedProviderOptionsKey({
207
+ rawName: this.providerOptionsName,
208
+ providerOptions,
209
+ warnings,
210
+ });
211
+
145
212
  const compatibleOptions = Object.assign(
146
213
  deprecatedOptions ?? {},
147
214
  (await parseProviderOptions({
@@ -154,6 +221,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
154
221
  providerOptions,
155
222
  schema: openaiCompatibleLanguageModelChatOptions,
156
223
  })) ?? {},
224
+ (await parseProviderOptions({
225
+ provider: toCamelCase(this.providerOptionsName),
226
+ providerOptions,
227
+ schema: openaiCompatibleLanguageModelChatOptions,
228
+ })) ?? {},
157
229
  );
158
230
 
159
231
  const strictJsonSchema = compatibleOptions?.strictJsonSchema ?? true;
@@ -184,7 +256,13 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
184
256
  toolChoice,
185
257
  });
186
258
 
259
+ const metadataKey = resolveProviderOptionsKey(
260
+ this.providerOptionsName,
261
+ providerOptions,
262
+ );
263
+
187
264
  return {
265
+ metadataKey,
188
266
  args: {
189
267
  // model id:
190
268
  model: this.modelId,
@@ -217,9 +295,10 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
217
295
  stop: stopSequences,
218
296
  seed,
219
297
  ...Object.fromEntries(
220
- Object.entries(
221
- providerOptions?.[this.providerOptionsName] ?? {},
222
- ).filter(
298
+ Object.entries({
299
+ ...providerOptions?.[this.providerOptionsName],
300
+ ...providerOptions?.[toCamelCase(this.providerOptionsName)],
301
+ }).filter(
223
302
  ([key]) =>
224
303
  !Object.keys(
225
304
  openaiCompatibleLanguageModelChatOptions.shape,
@@ -227,7 +306,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
227
306
  ),
228
307
  ),
229
308
 
230
- reasoning_effort: compatibleOptions.reasoningEffort,
309
+ reasoning_effort:
310
+ compatibleOptions.reasoningEffort ??
311
+ (isCustomReasoning(reasoning) && reasoning !== 'none'
312
+ ? reasoning
313
+ : undefined),
231
314
  verbosity: compatibleOptions.textVerbosity,
232
315
 
233
316
  // messages:
@@ -244,7 +327,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
244
327
  async doGenerate(
245
328
  options: LanguageModelV4CallOptions,
246
329
  ): Promise<LanguageModelV4GenerateResult> {
247
- const { args, warnings } = await this.getArgs({ ...options });
330
+ const { args, warnings, metadataKey } = await this.getArgs({ ...options });
248
331
 
249
332
  const transformedBody = this.transformRequestBody(args);
250
333
  const body = JSON.stringify(transformedBody);
@@ -258,7 +341,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
258
341
  path: '/chat/completions',
259
342
  modelId: this.modelId,
260
343
  }),
261
- headers: combineHeaders(this.config.headers(), options.headers),
344
+ headers: combineHeaders(this.config.headers?.(), options.headers),
262
345
  body: transformedBody,
263
346
  failedResponseHandler: this.failedResponseHandler,
264
347
  successfulResponseHandler: createJsonResponseHandler(
@@ -300,7 +383,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
300
383
  ...(thoughtSignature
301
384
  ? {
302
385
  providerMetadata: {
303
- [this.providerOptionsName]: { thoughtSignature },
386
+ [metadataKey]: { thoughtSignature },
304
387
  },
305
388
  }
306
389
  : {}),
@@ -310,7 +393,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
310
393
 
311
394
  // provider metadata:
312
395
  const providerMetadata: SharedV4ProviderMetadata = {
313
- [this.providerOptionsName]: {},
396
+ [metadataKey]: {},
314
397
  ...(await this.config.metadataExtractor?.extractMetadata?.({
315
398
  parsedBody: rawResponse,
316
399
  })),
@@ -318,11 +401,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
318
401
  const completionTokenDetails =
319
402
  responseBody.usage?.completion_tokens_details;
320
403
  if (completionTokenDetails?.accepted_prediction_tokens != null) {
321
- providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
404
+ providerMetadata[metadataKey].acceptedPredictionTokens =
322
405
  completionTokenDetails?.accepted_prediction_tokens;
323
406
  }
324
407
  if (completionTokenDetails?.rejected_prediction_tokens != null) {
325
- providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
408
+ providerMetadata[metadataKey].rejectedPredictionTokens =
326
409
  completionTokenDetails?.rejected_prediction_tokens;
327
410
  }
328
411
 
@@ -332,7 +415,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
332
415
  unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
333
416
  raw: choice.finish_reason ?? undefined,
334
417
  },
335
- usage: convertOpenAICompatibleChatUsage(responseBody.usage),
418
+ usage: this.convertUsage(responseBody.usage),
336
419
  providerMetadata,
337
420
  request: { body },
338
421
  response: {
@@ -347,7 +430,9 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
347
430
  async doStream(
348
431
  options: LanguageModelV4CallOptions,
349
432
  ): Promise<LanguageModelV4StreamResult> {
350
- const { args, warnings } = await this.getArgs({ ...options });
433
+ const { args, warnings, metadataKey } = await this.getArgs({
434
+ ...options,
435
+ });
351
436
 
352
437
  const body = this.transformRequestBody({
353
438
  ...args,
@@ -367,7 +452,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
367
452
  path: '/chat/completions',
368
453
  modelId: this.modelId,
369
454
  }),
370
- headers: combineHeaders(this.config.headers(), options.headers),
455
+ headers: combineHeaders(this.config.headers?.(), options.headers),
371
456
  body,
372
457
  failedResponseHandler: this.failedResponseHandler,
373
458
  successfulResponseHandler: createEventSourceResponseHandler(
@@ -377,16 +462,66 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
377
462
  fetch: this.config.fetch,
378
463
  });
379
464
 
380
- const toolCalls: Array<{
381
- id: string;
382
- type: 'function';
383
- function: {
384
- name: string;
385
- arguments: string;
386
- };
387
- hasFinished: boolean;
388
- thoughtSignature?: string;
389
- }> = [];
465
+ const providerOptionsName = metadataKey;
466
+ let toolCallTracker: StreamingToolCallTracker<OpenAICompatibleStreamingToolCallDelta>;
467
+
468
+ // Buffers tool-call deltas by `index` until `function.name` is known.
469
+ // Some OpenAI-compatible providers send the first delta without
470
+ // `function.name`, which the shared tracker rejects on first chunk.
471
+ const pendingToolCalls = new Map<number, PendingToolCall>();
472
+ const forwardedToolCallIndices = new Set<number>();
473
+
474
+ const processToolCallDelta = (
475
+ toolCallDelta: OpenAICompatibleStreamingToolCallDelta,
476
+ ) => {
477
+ const index = toolCallDelta.index;
478
+
479
+ if (index == null || forwardedToolCallIndices.has(index)) {
480
+ toolCallTracker.processDelta(toolCallDelta);
481
+ return;
482
+ }
483
+
484
+ let pending = pendingToolCalls.get(index);
485
+ if (pending == null) {
486
+ pending = {
487
+ id: toolCallDelta.id ?? null,
488
+ bufferedArguments: '',
489
+ extraContent: toolCallDelta.extra_content ?? null,
490
+ };
491
+ pendingToolCalls.set(index, pending);
492
+ } else {
493
+ if (pending.id == null && toolCallDelta.id != null) {
494
+ pending.id = toolCallDelta.id;
495
+ }
496
+ if (
497
+ pending.extraContent == null &&
498
+ toolCallDelta.extra_content != null
499
+ ) {
500
+ pending.extraContent = toolCallDelta.extra_content;
501
+ }
502
+ }
503
+
504
+ const argumentsDelta = toolCallDelta.function?.arguments;
505
+ if (argumentsDelta != null) {
506
+ pending.bufferedArguments += argumentsDelta;
507
+ }
508
+
509
+ const name = toolCallDelta.function?.name;
510
+ if (name != null) {
511
+ const forwardDelta: OpenAICompatibleStreamingToolCallDelta = {
512
+ index,
513
+ id: pending.id,
514
+ function: {
515
+ name,
516
+ arguments: pending.bufferedArguments,
517
+ },
518
+ extra_content: pending.extraContent ?? undefined,
519
+ };
520
+ toolCallTracker.processDelta(forwardDelta);
521
+ pendingToolCalls.delete(index);
522
+ forwardedToolCallIndices.add(index);
523
+ }
524
+ };
390
525
 
391
526
  let finishReason: LanguageModelV4FinishReason = {
392
527
  unified: 'other',
@@ -395,9 +530,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
395
530
  let usage: z.infer<typeof openaiCompatibleTokenUsageSchema> | undefined =
396
531
  undefined;
397
532
  let isFirstChunk = true;
398
- const providerOptionsName = this.providerOptionsName;
399
533
  let isActiveReasoning = false;
400
534
  let isActiveText = false;
535
+ const convertUsage = (
536
+ usage: z.infer<typeof openaiCompatibleTokenUsageSchema>,
537
+ ) => this.convertUsage(usage);
401
538
 
402
539
  return {
403
540
  stream: response.pipeThrough(
@@ -406,6 +543,22 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
406
543
  LanguageModelV4StreamPart
407
544
  >({
408
545
  start(controller) {
546
+ toolCallTracker =
547
+ new StreamingToolCallTracker<OpenAICompatibleStreamingToolCallDelta>(
548
+ controller,
549
+ {
550
+ generateId,
551
+ extractMetadata: delta => {
552
+ const thoughtSignature =
553
+ delta.extra_content?.google?.thought_signature;
554
+
555
+ return thoughtSignature
556
+ ? { [providerOptionsName]: { thoughtSignature } }
557
+ : undefined;
558
+ },
559
+ buildToolCallProviderMetadata: metadata => metadata,
560
+ },
561
+ );
409
562
  controller.enqueue({ type: 'stream-start', warnings });
410
563
  },
411
564
 
@@ -517,134 +670,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
517
670
  }
518
671
 
519
672
  for (const toolCallDelta of delta.tool_calls) {
520
- const index = toolCallDelta.index ?? toolCalls.length;
521
-
522
- if (toolCalls[index] == null) {
523
- if (toolCallDelta.id == null) {
524
- throw new InvalidResponseDataError({
525
- data: toolCallDelta,
526
- message: `Expected 'id' to be a string.`,
527
- });
528
- }
529
-
530
- if (toolCallDelta.function?.name == null) {
531
- throw new InvalidResponseDataError({
532
- data: toolCallDelta,
533
- message: `Expected 'function.name' to be a string.`,
534
- });
535
- }
536
-
537
- controller.enqueue({
538
- type: 'tool-input-start',
539
- id: toolCallDelta.id,
540
- toolName: toolCallDelta.function.name,
541
- });
542
-
543
- toolCalls[index] = {
544
- id: toolCallDelta.id,
545
- type: 'function',
546
- function: {
547
- name: toolCallDelta.function.name,
548
- arguments: toolCallDelta.function.arguments ?? '',
549
- },
550
- hasFinished: false,
551
- thoughtSignature:
552
- toolCallDelta.extra_content?.google?.thought_signature ??
553
- undefined,
554
- };
555
-
556
- const toolCall = toolCalls[index];
557
-
558
- if (
559
- toolCall.function?.name != null &&
560
- toolCall.function?.arguments != null
561
- ) {
562
- // send delta if the argument text has already started:
563
- if (toolCall.function.arguments.length > 0) {
564
- controller.enqueue({
565
- type: 'tool-input-delta',
566
- id: toolCall.id,
567
- delta: toolCall.function.arguments,
568
- });
569
- }
570
-
571
- // check if tool call is complete
572
- // (some providers send the full tool call in one chunk):
573
- if (isParsableJson(toolCall.function.arguments)) {
574
- controller.enqueue({
575
- type: 'tool-input-end',
576
- id: toolCall.id,
577
- });
578
-
579
- controller.enqueue({
580
- type: 'tool-call',
581
- toolCallId: toolCall.id ?? generateId(),
582
- toolName: toolCall.function.name,
583
- input: toolCall.function.arguments,
584
- ...(toolCall.thoughtSignature
585
- ? {
586
- providerMetadata: {
587
- [providerOptionsName]: {
588
- thoughtSignature: toolCall.thoughtSignature,
589
- },
590
- },
591
- }
592
- : {}),
593
- });
594
- toolCall.hasFinished = true;
595
- }
596
- }
597
-
598
- continue;
599
- }
600
-
601
- // existing tool call, merge if not finished
602
- const toolCall = toolCalls[index];
603
-
604
- if (toolCall.hasFinished) {
605
- continue;
606
- }
607
-
608
- if (toolCallDelta.function?.arguments != null) {
609
- toolCall.function!.arguments +=
610
- toolCallDelta.function?.arguments ?? '';
611
- }
612
-
613
- // send delta
614
- controller.enqueue({
615
- type: 'tool-input-delta',
616
- id: toolCall.id,
617
- delta: toolCallDelta.function.arguments ?? '',
618
- });
619
-
620
- // check if tool call is complete
621
- if (
622
- toolCall.function?.name != null &&
623
- toolCall.function?.arguments != null &&
624
- isParsableJson(toolCall.function.arguments)
625
- ) {
626
- controller.enqueue({
627
- type: 'tool-input-end',
628
- id: toolCall.id,
629
- });
630
-
631
- controller.enqueue({
632
- type: 'tool-call',
633
- toolCallId: toolCall.id ?? generateId(),
634
- toolName: toolCall.function.name,
635
- input: toolCall.function.arguments,
636
- ...(toolCall.thoughtSignature
637
- ? {
638
- providerMetadata: {
639
- [providerOptionsName]: {
640
- thoughtSignature: toolCall.thoughtSignature,
641
- },
642
- },
643
- }
644
- : {}),
645
- });
646
- toolCall.hasFinished = true;
647
- }
673
+ processToolCallDelta(toolCallDelta);
648
674
  }
649
675
  }
650
676
  },
@@ -658,31 +684,19 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
658
684
  controller.enqueue({ type: 'text-end', id: 'txt-0' });
659
685
  }
660
686
 
661
- // go through all tool calls and send the ones that are not finished
662
- for (const toolCall of toolCalls.filter(
663
- toolCall => !toolCall.hasFinished,
664
- )) {
665
- controller.enqueue({
666
- type: 'tool-input-end',
667
- id: toolCall.id,
668
- });
669
-
670
- controller.enqueue({
671
- type: 'tool-call',
672
- toolCallId: toolCall.id ?? generateId(),
673
- toolName: toolCall.function.name,
674
- input: toolCall.function.arguments,
675
- ...(toolCall.thoughtSignature
676
- ? {
677
- providerMetadata: {
678
- [providerOptionsName]: {
679
- thoughtSignature: toolCall.thoughtSignature,
680
- },
681
- },
682
- }
683
- : {}),
687
+ // Forward any tool-call deltas that never received a
688
+ // `function.name`. The tracker will throw on the missing name,
689
+ // preserving the original invalid-response semantics.
690
+ for (const [index, pending] of pendingToolCalls) {
691
+ toolCallTracker.processDelta({
692
+ index,
693
+ id: pending.id,
694
+ function: { arguments: pending.bufferedArguments },
684
695
  });
685
696
  }
697
+ pendingToolCalls.clear();
698
+
699
+ toolCallTracker.flush();
686
700
 
687
701
  const providerMetadata: SharedV4ProviderMetadata = {
688
702
  [providerOptionsName]: {},
@@ -706,7 +720,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV4 {
706
720
  controller.enqueue({
707
721
  type: 'finish',
708
722
  finishReason,
709
- usage: convertOpenAICompatibleChatUsage(usage),
723
+ usage: convertUsage(usage),
710
724
  providerMetadata,
711
725
  });
712
726
  },
@@ -787,7 +801,7 @@ const chunkBaseSchema = z.looseObject({
787
801
  z.object({
788
802
  delta: z
789
803
  .object({
790
- role: z.enum(['assistant']).nullish(),
804
+ role: z.enum(['assistant', '']).nullish(),
791
805
  content: z.string().nullish(),
792
806
  // Most openai-compatible models set `reasoning_content`, but some
793
807
  // providers serving `gpt-oss` set `reasoning`. See #7866
@@ -1,4 +1,4 @@
1
- import { SharedV4ProviderMetadata } from '@ai-sdk/provider';
1
+ import type { SharedV4ProviderMetadata } from '@ai-sdk/provider';
2
2
 
3
3
  /**
4
4
  * Extracts provider-specific metadata from API responses.
@@ -1,9 +1,8 @@
1
1
  import {
2
- LanguageModelV4CallOptions,
3
- SharedV4Warning,
4
2
  UnsupportedFunctionalityError,
3
+ type LanguageModelV4CallOptions,
4
+ type SharedV4Warning,
5
5
  } from '@ai-sdk/provider';
6
-
7
6
  export function prepareTools({
8
7
  tools,
9
8
  toolChoice,
@@ -1,4 +1,4 @@
1
- import { LanguageModelV4Usage } from '@ai-sdk/provider';
1
+ import type { LanguageModelV4Usage } from '@ai-sdk/provider';
2
2
 
3
3
  export function convertOpenAICompatibleCompletionUsage(
4
4
  usage:
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  InvalidPromptError,
3
- LanguageModelV4Prompt,
4
3
  UnsupportedFunctionalityError,
4
+ type LanguageModelV4Prompt,
5
5
  } from '@ai-sdk/provider';
6
-
7
6
  export function convertToOpenAICompatibleCompletionPrompt({
8
7
  prompt,
9
8
  user = 'user',
@@ -1,4 +1,4 @@
1
- import { LanguageModelV4FinishReason } from '@ai-sdk/provider';
1
+ import type { LanguageModelV4FinishReason } from '@ai-sdk/provider';
2
2
 
3
3
  export function mapOpenAICompatibleFinishReason(
4
4
  finishReason: string | null | undefined,