@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.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/chat/convert-openai-compatible-chat-usage.ts +55 -0
- package/src/chat/convert-to-openai-compatible-chat-messages.test.ts +1238 -0
- package/src/chat/convert-to-openai-compatible-chat-messages.ts +246 -0
- package/src/chat/get-response-metadata.ts +15 -0
- package/src/chat/map-openai-compatible-finish-reason.ts +19 -0
- package/src/chat/openai-compatible-api-types.ts +86 -0
- package/src/chat/openai-compatible-chat-language-model.test.ts +3292 -0
- package/src/chat/openai-compatible-chat-language-model.ts +830 -0
- package/src/chat/openai-compatible-chat-options.ts +34 -0
- package/src/chat/openai-compatible-metadata-extractor.ts +48 -0
- package/src/chat/openai-compatible-prepare-tools.test.ts +336 -0
- package/src/chat/openai-compatible-prepare-tools.ts +98 -0
- package/src/completion/convert-openai-compatible-completion-usage.ts +46 -0
- package/src/completion/convert-to-openai-compatible-completion-prompt.ts +93 -0
- package/src/completion/get-response-metadata.ts +15 -0
- package/src/completion/map-openai-compatible-finish-reason.ts +19 -0
- package/src/completion/openai-compatible-completion-language-model.test.ts +773 -0
- package/src/completion/openai-compatible-completion-language-model.ts +390 -0
- package/src/completion/openai-compatible-completion-options.ts +33 -0
- package/src/embedding/openai-compatible-embedding-model.test.ts +171 -0
- package/src/embedding/openai-compatible-embedding-model.ts +166 -0
- package/src/embedding/openai-compatible-embedding-options.ts +21 -0
- package/src/image/openai-compatible-image-model.test.ts +494 -0
- package/src/image/openai-compatible-image-model.ts +205 -0
- package/src/image/openai-compatible-image-settings.ts +1 -0
- package/src/index.ts +27 -0
- package/src/internal/index.ts +4 -0
- package/src/openai-compatible-error.ts +30 -0
- package/src/openai-compatible-provider.test.ts +329 -0
- package/src/openai-compatible-provider.ts +189 -0
- 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]);
|