@hebo-ai/gateway 0.6.2-rc0 → 0.6.2

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 (134) hide show
  1. package/README.md +3 -3
  2. package/dist/endpoints/chat-completions/converters.js +26 -21
  3. package/dist/endpoints/chat-completions/handler.js +2 -0
  4. package/dist/endpoints/chat-completions/otel.js +1 -1
  5. package/dist/endpoints/chat-completions/schema.d.ts +4 -18
  6. package/dist/endpoints/chat-completions/schema.js +14 -17
  7. package/dist/endpoints/embeddings/handler.js +2 -0
  8. package/dist/endpoints/embeddings/otel.js +5 -0
  9. package/dist/endpoints/embeddings/schema.d.ts +6 -0
  10. package/dist/endpoints/embeddings/schema.js +4 -1
  11. package/dist/endpoints/models/converters.js +3 -3
  12. package/dist/lifecycle.js +2 -2
  13. package/dist/logger/default.js +3 -3
  14. package/dist/logger/index.d.ts +2 -5
  15. package/dist/middleware/common.js +1 -0
  16. package/dist/middleware/utils.js +0 -3
  17. package/dist/models/amazon/middleware.js +8 -5
  18. package/dist/models/anthropic/middleware.js +13 -13
  19. package/dist/models/catalog.js +5 -1
  20. package/dist/models/cohere/middleware.js +7 -5
  21. package/dist/models/google/middleware.d.ts +1 -1
  22. package/dist/models/google/middleware.js +29 -25
  23. package/dist/models/openai/middleware.js +13 -9
  24. package/dist/models/voyage/middleware.js +2 -1
  25. package/dist/providers/bedrock/middleware.js +21 -23
  26. package/dist/providers/registry.js +3 -0
  27. package/dist/telemetry/fetch.js +7 -2
  28. package/dist/telemetry/gen-ai.js +15 -12
  29. package/dist/telemetry/memory.d.ts +1 -1
  30. package/dist/telemetry/memory.js +30 -14
  31. package/dist/telemetry/span.js +1 -1
  32. package/dist/telemetry/stream.js +30 -23
  33. package/dist/utils/env.js +4 -2
  34. package/dist/utils/preset.js +1 -0
  35. package/dist/utils/response.js +3 -1
  36. package/package.json +36 -50
  37. package/src/config.ts +0 -98
  38. package/src/endpoints/chat-completions/converters.test.ts +0 -631
  39. package/src/endpoints/chat-completions/converters.ts +0 -899
  40. package/src/endpoints/chat-completions/handler.test.ts +0 -391
  41. package/src/endpoints/chat-completions/handler.ts +0 -201
  42. package/src/endpoints/chat-completions/index.ts +0 -4
  43. package/src/endpoints/chat-completions/otel.test.ts +0 -315
  44. package/src/endpoints/chat-completions/otel.ts +0 -214
  45. package/src/endpoints/chat-completions/schema.ts +0 -364
  46. package/src/endpoints/embeddings/converters.ts +0 -51
  47. package/src/endpoints/embeddings/handler.test.ts +0 -133
  48. package/src/endpoints/embeddings/handler.ts +0 -137
  49. package/src/endpoints/embeddings/index.ts +0 -4
  50. package/src/endpoints/embeddings/otel.ts +0 -40
  51. package/src/endpoints/embeddings/schema.ts +0 -36
  52. package/src/endpoints/models/converters.ts +0 -56
  53. package/src/endpoints/models/handler.test.ts +0 -122
  54. package/src/endpoints/models/handler.ts +0 -37
  55. package/src/endpoints/models/index.ts +0 -3
  56. package/src/endpoints/models/schema.ts +0 -37
  57. package/src/errors/ai-sdk.ts +0 -99
  58. package/src/errors/gateway.ts +0 -17
  59. package/src/errors/openai.ts +0 -57
  60. package/src/errors/utils.ts +0 -47
  61. package/src/gateway.ts +0 -50
  62. package/src/index.ts +0 -19
  63. package/src/lifecycle.ts +0 -135
  64. package/src/logger/default.ts +0 -105
  65. package/src/logger/index.ts +0 -42
  66. package/src/middleware/common.test.ts +0 -215
  67. package/src/middleware/common.ts +0 -163
  68. package/src/middleware/debug.ts +0 -37
  69. package/src/middleware/matcher.ts +0 -161
  70. package/src/middleware/utils.ts +0 -34
  71. package/src/models/amazon/index.ts +0 -2
  72. package/src/models/amazon/middleware.test.ts +0 -133
  73. package/src/models/amazon/middleware.ts +0 -79
  74. package/src/models/amazon/presets.ts +0 -104
  75. package/src/models/anthropic/index.ts +0 -2
  76. package/src/models/anthropic/middleware.test.ts +0 -643
  77. package/src/models/anthropic/middleware.ts +0 -148
  78. package/src/models/anthropic/presets.ts +0 -191
  79. package/src/models/catalog.ts +0 -13
  80. package/src/models/cohere/index.ts +0 -2
  81. package/src/models/cohere/middleware.test.ts +0 -138
  82. package/src/models/cohere/middleware.ts +0 -76
  83. package/src/models/cohere/presets.ts +0 -186
  84. package/src/models/google/index.ts +0 -2
  85. package/src/models/google/middleware.test.ts +0 -298
  86. package/src/models/google/middleware.ts +0 -137
  87. package/src/models/google/presets.ts +0 -118
  88. package/src/models/meta/index.ts +0 -1
  89. package/src/models/meta/presets.ts +0 -143
  90. package/src/models/openai/index.ts +0 -2
  91. package/src/models/openai/middleware.test.ts +0 -189
  92. package/src/models/openai/middleware.ts +0 -103
  93. package/src/models/openai/presets.ts +0 -280
  94. package/src/models/types.ts +0 -114
  95. package/src/models/voyage/index.ts +0 -2
  96. package/src/models/voyage/middleware.test.ts +0 -28
  97. package/src/models/voyage/middleware.ts +0 -23
  98. package/src/models/voyage/presets.ts +0 -126
  99. package/src/providers/anthropic/canonical.ts +0 -17
  100. package/src/providers/anthropic/index.ts +0 -1
  101. package/src/providers/bedrock/canonical.ts +0 -87
  102. package/src/providers/bedrock/index.ts +0 -2
  103. package/src/providers/bedrock/middleware.test.ts +0 -303
  104. package/src/providers/bedrock/middleware.ts +0 -128
  105. package/src/providers/cohere/canonical.ts +0 -26
  106. package/src/providers/cohere/index.ts +0 -1
  107. package/src/providers/groq/canonical.ts +0 -21
  108. package/src/providers/groq/index.ts +0 -1
  109. package/src/providers/openai/canonical.ts +0 -16
  110. package/src/providers/openai/index.ts +0 -1
  111. package/src/providers/registry.test.ts +0 -44
  112. package/src/providers/registry.ts +0 -165
  113. package/src/providers/types.ts +0 -20
  114. package/src/providers/vertex/canonical.ts +0 -17
  115. package/src/providers/vertex/index.ts +0 -1
  116. package/src/providers/voyage/canonical.ts +0 -16
  117. package/src/providers/voyage/index.ts +0 -1
  118. package/src/telemetry/ai-sdk.ts +0 -46
  119. package/src/telemetry/baggage.ts +0 -27
  120. package/src/telemetry/fetch.ts +0 -62
  121. package/src/telemetry/gen-ai.ts +0 -113
  122. package/src/telemetry/http.ts +0 -62
  123. package/src/telemetry/index.ts +0 -1
  124. package/src/telemetry/memory.ts +0 -36
  125. package/src/telemetry/span.ts +0 -85
  126. package/src/telemetry/stream.ts +0 -64
  127. package/src/types.ts +0 -223
  128. package/src/utils/env.ts +0 -7
  129. package/src/utils/headers.ts +0 -27
  130. package/src/utils/preset.ts +0 -65
  131. package/src/utils/request.test.ts +0 -75
  132. package/src/utils/request.ts +0 -52
  133. package/src/utils/response.ts +0 -84
  134. package/src/utils/url.ts +0 -26
@@ -1,899 +0,0 @@
1
- import type { SharedV3ProviderOptions, SharedV3ProviderMetadata } from "@ai-sdk/provider";
2
- import type {
3
- GenerateTextResult,
4
- StreamTextResult,
5
- FinishReason,
6
- ToolChoice,
7
- ToolCallPart,
8
- ToolResultPart,
9
- ToolSet,
10
- ModelMessage,
11
- UserContent,
12
- AssistantContent,
13
- LanguageModelUsage,
14
- TextStreamPart,
15
- ReasoningOutput,
16
- JSONValue,
17
- AssistantModelMessage,
18
- ToolModelMessage,
19
- UserModelMessage,
20
- TextPart,
21
- ImagePart,
22
- FilePart,
23
- } from "ai";
24
-
25
- import { Output, jsonSchema, tool } from "ai";
26
- import { z } from "zod";
27
-
28
- import type {
29
- ChatCompletionsToolCall,
30
- ChatCompletionsTool,
31
- ChatCompletionsToolChoice,
32
- ChatCompletionsContentPart,
33
- ChatCompletionsMessage,
34
- ChatCompletionsUserMessage,
35
- ChatCompletionsAssistantMessage,
36
- ChatCompletionsToolMessage,
37
- ChatCompletionsFinishReason,
38
- ChatCompletionsUsage,
39
- ChatCompletionsChoice,
40
- ChatCompletionsInputs,
41
- ChatCompletions,
42
- ChatCompletionsAssistantMessageDelta,
43
- ChatCompletionsChoiceDelta,
44
- ChatCompletionsChunk,
45
- ChatCompletionsToolCallDelta,
46
- ChatCompletionsReasoningEffort,
47
- ChatCompletionsReasoningConfig,
48
- ChatCompletionsReasoningDetail,
49
- ChatCompletionsResponseFormat,
50
- ChatCompletionsContentPartText,
51
- ChatCompletionsCacheControl,
52
- } from "./schema";
53
-
54
- import { GatewayError } from "../../errors/gateway";
55
- import { OpenAIError, toOpenAIError } from "../../errors/openai";
56
- import { toResponse } from "../../utils/response";
57
- import { parseDataUrl } from "../../utils/url";
58
-
59
- export type TextCallOptions = {
60
- messages: ModelMessage[];
61
- tools?: ToolSet;
62
- toolChoice?: ToolChoice<ToolSet>;
63
- activeTools?: Array<keyof ToolSet>;
64
- output?: Output.Output;
65
- temperature?: number;
66
- maxOutputTokens?: number;
67
- frequencyPenalty?: number;
68
- presencePenalty?: number;
69
- seed?: number;
70
- stopSequences?: string[];
71
- topP?: number;
72
- providerOptions: SharedV3ProviderOptions;
73
- };
74
-
75
- // --- Request Flow ---
76
-
77
- export function convertToTextCallOptions(params: ChatCompletionsInputs): TextCallOptions {
78
- const {
79
- messages,
80
- tools,
81
- tool_choice,
82
- temperature,
83
- max_tokens,
84
- max_completion_tokens,
85
- response_format,
86
- reasoning_effort,
87
- reasoning,
88
- prompt_cache_key,
89
- prompt_cache_retention,
90
- extra_body,
91
- cache_control,
92
- frequency_penalty,
93
- presence_penalty,
94
- seed,
95
- stop,
96
- top_p,
97
- ...rest
98
- } = params;
99
-
100
- Object.assign(rest, parseReasoningOptions(reasoning_effort, reasoning));
101
- Object.assign(
102
- rest,
103
- parsePromptCachingOptions(
104
- prompt_cache_key,
105
- prompt_cache_retention,
106
- extra_body?.google?.cached_content,
107
- cache_control,
108
- ),
109
- );
110
-
111
- const { toolChoice, activeTools } = convertToToolChoiceOptions(tool_choice);
112
-
113
- return {
114
- messages: convertToModelMessages(messages),
115
- tools: convertToToolSet(tools),
116
- toolChoice,
117
- activeTools,
118
- output: convertToOutput(response_format),
119
- temperature,
120
- maxOutputTokens: max_completion_tokens ?? max_tokens,
121
- frequencyPenalty: frequency_penalty,
122
- presencePenalty: presence_penalty,
123
- seed,
124
- stopSequences: stop ? (Array.isArray(stop) ? stop : [stop]) : undefined,
125
- topP: top_p,
126
- providerOptions: {
127
- unknown: rest,
128
- },
129
- };
130
- }
131
-
132
- function convertToOutput(responseFormat: ChatCompletionsResponseFormat | undefined) {
133
- if (!responseFormat || responseFormat.type === "text") {
134
- return;
135
- }
136
-
137
- const { name, description, schema } = responseFormat.json_schema;
138
- return Output.object({
139
- name,
140
- description,
141
- schema: jsonSchema(schema),
142
- });
143
- }
144
-
145
- export function convertToModelMessages(messages: ChatCompletionsMessage[]): ModelMessage[] {
146
- const modelMessages: ModelMessage[] = [];
147
- const toolById = indexToolMessages(messages);
148
-
149
- for (const message of messages) {
150
- if (message.role === "tool") continue;
151
-
152
- if (message.role === "system") {
153
- if (message.cache_control) {
154
- (message as ModelMessage).providerOptions = {
155
- unknown: { cache_control: message.cache_control },
156
- };
157
- }
158
- modelMessages.push(message);
159
- continue;
160
- }
161
-
162
- if (message.role === "user") {
163
- modelMessages.push(fromChatCompletionsUserMessage(message));
164
- continue;
165
- }
166
-
167
- modelMessages.push(fromChatCompletionsAssistantMessage(message));
168
- const toolResult = fromChatCompletionsToolResultMessage(message, toolById);
169
- if (toolResult) modelMessages.push(toolResult);
170
- }
171
-
172
- return modelMessages;
173
- }
174
-
175
- function indexToolMessages(messages: ChatCompletionsMessage[]) {
176
- const map = new Map<string, ChatCompletionsToolMessage>();
177
- for (const m of messages) {
178
- if (m.role === "tool") map.set(m.tool_call_id, m);
179
- }
180
- return map;
181
- }
182
-
183
- export function fromChatCompletionsUserMessage(
184
- message: ChatCompletionsUserMessage,
185
- ): UserModelMessage {
186
- const out: UserModelMessage = {
187
- role: "user",
188
- content: Array.isArray(message.content)
189
- ? fromChatCompletionsContent(message.content)
190
- : message.content,
191
- };
192
- if (message.cache_control) {
193
- out.providerOptions = {
194
- unknown: { cache_control: message.cache_control },
195
- };
196
- }
197
- return out;
198
- }
199
-
200
- export function fromChatCompletionsAssistantMessage(
201
- message: ChatCompletionsAssistantMessage,
202
- ): AssistantModelMessage {
203
- const { tool_calls, role, content, extra_content, reasoning_details, cache_control } = message;
204
-
205
- const parts: AssistantContent = [];
206
-
207
- if (reasoning_details?.length) {
208
- for (const detail of reasoning_details) {
209
- if (detail.text && detail.type === "reasoning.text") {
210
- parts.push({
211
- type: "reasoning",
212
- text: detail.text,
213
- providerOptions: detail.signature
214
- ? {
215
- unknown: {
216
- signature: detail.signature,
217
- },
218
- }
219
- : undefined,
220
- });
221
- } else if (detail.type === "reasoning.encrypted" && detail.data) {
222
- parts.push({
223
- type: "reasoning",
224
- text: "",
225
- providerOptions: {
226
- unknown: {
227
- redactedData: detail.data,
228
- },
229
- },
230
- });
231
- }
232
- }
233
- }
234
-
235
- if (content !== undefined && content !== null) {
236
- const inputContent =
237
- typeof content === "string"
238
- ? ([{ type: "text", text: content }] as ChatCompletionsContentPartText[])
239
- : content;
240
- for (const part of inputContent) {
241
- if (part.type === "text") {
242
- const textPart: TextPart = {
243
- type: "text",
244
- text: part.text,
245
- };
246
- if (part.cache_control) {
247
- textPart.providerOptions = {
248
- unknown: { cache_control: part.cache_control },
249
- };
250
- }
251
- parts.push(textPart);
252
- }
253
- }
254
- }
255
-
256
- if (tool_calls?.length) {
257
- for (const tc of tool_calls) {
258
- // oxlint-disable-next-line no-shadow
259
- const { id, function: fn, extra_content } = tc;
260
- const out: ToolCallPart = {
261
- type: "tool-call",
262
- toolCallId: id,
263
- toolName: fn.name,
264
- input: parseJsonOrText(fn.arguments).value,
265
- };
266
- if (extra_content) {
267
- out.providerOptions = extra_content as SharedV3ProviderOptions;
268
- }
269
- parts.push(out);
270
- }
271
- }
272
-
273
- const out: AssistantModelMessage = {
274
- role,
275
- content: parts.length > 0 ? parts : (content ?? ""),
276
- };
277
-
278
- if (extra_content) {
279
- out.providerOptions = extra_content as SharedV3ProviderOptions;
280
- }
281
-
282
- if (cache_control) {
283
- ((out.providerOptions ??= { unknown: {} })["unknown"] ??= {})["cache_control"] = cache_control;
284
- }
285
-
286
- return out;
287
- }
288
-
289
- export function fromChatCompletionsToolResultMessage(
290
- message: ChatCompletionsAssistantMessage,
291
- toolById: Map<string, ChatCompletionsToolMessage>,
292
- ): ToolModelMessage | undefined {
293
- const toolCalls = message.tool_calls ?? [];
294
- if (toolCalls.length === 0) return undefined;
295
-
296
- const toolResultParts: ToolResultPart[] = [];
297
- for (const tc of toolCalls) {
298
- const toolMsg = toolById.get(tc.id);
299
- if (!toolMsg) continue;
300
-
301
- toolResultParts.push({
302
- type: "tool-result",
303
- toolCallId: tc.id,
304
- toolName: tc.function.name,
305
- output: parseToolResult(toolMsg.content),
306
- });
307
- }
308
-
309
- return toolResultParts.length > 0 ? { role: "tool", content: toolResultParts } : undefined;
310
- }
311
-
312
- export function fromChatCompletionsContent(content: ChatCompletionsContentPart[]): UserContent {
313
- return content.map((part) => {
314
- switch (part.type) {
315
- case "image_url":
316
- return fromImageUrlPart(part.image_url.url, part.cache_control);
317
- case "file":
318
- return fromFilePart(
319
- part.file.data,
320
- part.file.media_type,
321
- part.file.filename,
322
- part.cache_control,
323
- );
324
- case "input_audio":
325
- return fromFilePart(
326
- part.input_audio.data,
327
- `audio/${part.input_audio.format}`,
328
- undefined,
329
- part.cache_control,
330
- );
331
- default: {
332
- const out: TextPart = {
333
- type: "text" as const,
334
- text: part.text,
335
- };
336
- if (part.cache_control) {
337
- out.providerOptions = {
338
- unknown: { cache_control: part.cache_control },
339
- };
340
- }
341
- return out;
342
- }
343
- }
344
- });
345
- }
346
-
347
- function fromImageUrlPart(url: string, cacheControl?: ChatCompletionsCacheControl) {
348
- if (url.startsWith("data:")) {
349
- const { mimeType, dataStart } = parseDataUrl(url);
350
- if (!mimeType || dataStart <= "data:".length || dataStart >= url.length) {
351
- throw new GatewayError("Invalid data URL", 400);
352
- }
353
- return fromFilePart(url.slice(dataStart), mimeType, undefined, cacheControl);
354
- }
355
-
356
- const out: ImagePart = {
357
- type: "image" as const,
358
- image: new URL(url),
359
- };
360
- if (cacheControl) {
361
- out.providerOptions = {
362
- unknown: { cache_control: cacheControl },
363
- };
364
- }
365
- return out;
366
- }
367
-
368
- function fromFilePart(
369
- base64Data: string,
370
- mediaType: string,
371
- filename?: string,
372
- cacheControl?: ChatCompletionsCacheControl,
373
- ) {
374
- if (mediaType.startsWith("image/")) {
375
- const out: ImagePart = {
376
- type: "image" as const,
377
- image: z.util.base64ToUint8Array(base64Data),
378
- mediaType,
379
- };
380
- if (cacheControl) {
381
- out.providerOptions = {
382
- unknown: { cache_control: cacheControl },
383
- };
384
- }
385
- return out;
386
- }
387
-
388
- const out: FilePart = {
389
- type: "file" as const,
390
- data: z.util.base64ToUint8Array(base64Data),
391
- filename,
392
- mediaType,
393
- };
394
- if (cacheControl) {
395
- out.providerOptions = {
396
- unknown: { cache_control: cacheControl },
397
- };
398
- }
399
- return out;
400
- }
401
-
402
- export const convertToToolSet = (tools: ChatCompletionsTool[] | undefined): ToolSet | undefined => {
403
- if (!tools) {
404
- return;
405
- }
406
-
407
- const toolSet: ToolSet = {};
408
- for (const t of tools) {
409
- toolSet[t.function.name] = tool({
410
- description: t.function.description,
411
- inputSchema: jsonSchema(t.function.parameters),
412
- strict: t.function.strict,
413
- });
414
- }
415
- return toolSet;
416
- };
417
-
418
- export const convertToToolChoiceOptions = (
419
- toolChoice: ChatCompletionsToolChoice | undefined,
420
- ): {
421
- toolChoice?: ToolChoice<ToolSet>;
422
- activeTools?: Array<keyof ToolSet>;
423
- } => {
424
- if (!toolChoice) {
425
- return {};
426
- }
427
-
428
- if (toolChoice === "none" || toolChoice === "auto" || toolChoice === "required") {
429
- return { toolChoice };
430
- }
431
-
432
- // FUTURE: this is right now google specific, which is not supported by AI SDK, until then, we temporarily map it to auto for now https://docs.cloud.google.com/vertex-ai/generative-ai/docs/migrate/openai/overview
433
- if (toolChoice === "validated") {
434
- return { toolChoice: "auto" };
435
- }
436
-
437
- if (toolChoice.type === "allowed_tools") {
438
- return {
439
- toolChoice: toolChoice.allowed_tools.mode,
440
- activeTools: toolChoice.allowed_tools.tools.map((toolRef) => toolRef.function.name),
441
- };
442
- }
443
-
444
- return {
445
- toolChoice: {
446
- type: "tool",
447
- toolName: toolChoice.function.name,
448
- },
449
- };
450
- };
451
-
452
- function parseToolResult(
453
- content: string | ChatCompletionsContentPartText[],
454
- ): ToolResultPart["output"] {
455
- if (Array.isArray(content)) {
456
- return {
457
- type: "content",
458
- value: content.map((part) => ({
459
- type: "text",
460
- text: part.text,
461
- })),
462
- };
463
- }
464
- return parseJsonOrText(content);
465
- }
466
-
467
- function parseJsonOrText(
468
- content: string,
469
- ): { type: "json"; value: JSONValue } | { type: "text"; value: string } {
470
- try {
471
- return { type: "json", value: JSON.parse(content) };
472
- } catch {
473
- return { type: "text", value: content };
474
- }
475
- }
476
-
477
- function parseReasoningOptions(
478
- reasoning_effort: ChatCompletionsReasoningEffort | undefined,
479
- reasoning: ChatCompletionsReasoningConfig | undefined,
480
- ) {
481
- const effort = reasoning?.effort ?? reasoning_effort;
482
- const max_tokens = reasoning?.max_tokens;
483
-
484
- if (reasoning?.enabled === false || effort === "none") {
485
- return { reasoning: { enabled: false }, reasoning_effort: "none" };
486
- }
487
- if (!reasoning && effort === undefined) return {};
488
-
489
- const out: any = { reasoning: {} };
490
-
491
- if (effort) {
492
- out.reasoning.enabled = true;
493
- out.reasoning.effort = effort;
494
- out.reasoning_effort = effort;
495
- }
496
- if (max_tokens) {
497
- out.reasoning.enabled = true;
498
- out.reasoning.max_tokens = max_tokens;
499
- }
500
- if (out.reasoning.enabled) {
501
- out.reasoning.exclude = reasoning?.exclude;
502
- }
503
-
504
- return out;
505
- }
506
-
507
- function parsePromptCachingOptions(
508
- prompt_cache_key: string | undefined,
509
- prompt_cache_retention: "in_memory" | "24h" | undefined,
510
- cached_content: string | undefined,
511
- cache_control: ChatCompletionsCacheControl | undefined,
512
- ) {
513
- const out: Record<string, unknown> = {};
514
-
515
- const syncedCacheKey = prompt_cache_key ?? cached_content;
516
- const syncedCachedContent = cached_content ?? prompt_cache_key;
517
-
518
- let syncedCacheRetention = prompt_cache_retention;
519
- if (!syncedCacheRetention && cache_control?.ttl) {
520
- syncedCacheRetention = cache_control.ttl === "24h" ? "24h" : "in_memory";
521
- }
522
-
523
- let syncedCacheControl = cache_control;
524
- if (!syncedCacheControl && syncedCacheRetention) {
525
- syncedCacheControl = {
526
- type: "ephemeral",
527
- ttl: syncedCacheRetention === "24h" ? "24h" : "5m",
528
- };
529
- }
530
-
531
- if (syncedCacheKey) out["prompt_cache_key"] = syncedCacheKey;
532
- if (syncedCacheRetention) out["prompt_cache_retention"] = syncedCacheRetention;
533
- if (syncedCachedContent) out["cached_content"] = syncedCachedContent;
534
- if (syncedCacheControl) out["cache_control"] = syncedCacheControl;
535
-
536
- return out;
537
- }
538
-
539
- // --- Response Flow ---
540
-
541
- export function toChatCompletions(
542
- result: GenerateTextResult<ToolSet, Output.Output>,
543
- model: string,
544
- ): ChatCompletions {
545
- return {
546
- id: "chatcmpl-" + crypto.randomUUID(),
547
- object: "chat.completion",
548
- created: Math.floor(Date.now() / 1000),
549
- model,
550
- choices: [
551
- {
552
- index: 0,
553
- message: toChatCompletionsAssistantMessage(result),
554
- finish_reason: toChatCompletionsFinishReason(result.finishReason),
555
- } satisfies ChatCompletionsChoice,
556
- ],
557
- usage: result.totalUsage ? toChatCompletionsUsage(result.totalUsage) : null,
558
- provider_metadata: result.providerMetadata,
559
- };
560
- }
561
- export function toChatCompletionsResponse(
562
- result: GenerateTextResult<ToolSet, Output.Output>,
563
- model: string,
564
- responseInit?: ResponseInit,
565
- ): Response {
566
- return toResponse(toChatCompletions(result, model), responseInit);
567
- }
568
-
569
- export function toChatCompletionsStream<E extends boolean = false>(
570
- result: StreamTextResult<ToolSet, Output.Output>,
571
- model: string,
572
- wrapErrors?: E,
573
- ): ReadableStream<ChatCompletionsChunk | (E extends true ? OpenAIError : Error)> {
574
- return result.fullStream.pipeThrough(new ChatCompletionsStream(model, wrapErrors));
575
- }
576
-
577
- export function toChatCompletionsStreamResponse(
578
- result: StreamTextResult<ToolSet, Output.Output>,
579
- model: string,
580
- responseInit?: ResponseInit,
581
- ): Response {
582
- return toResponse(toChatCompletionsStream(result, model, true), responseInit);
583
- }
584
-
585
- export class ChatCompletionsStream<E extends boolean = false> extends TransformStream<
586
- TextStreamPart<ToolSet>,
587
- ChatCompletionsChunk | (E extends true ? OpenAIError : Error)
588
- > {
589
- constructor(model: string, wrapErrors?: E) {
590
- const streamId = `chatcmpl-${crypto.randomUUID()}`;
591
- const creationTime = Math.floor(Date.now() / 1000);
592
- let toolCallIndexCounter = 0;
593
- const reasoningIdToIndex = new Map<string, number>();
594
- let finishProviderMetadata: SharedV3ProviderMetadata | undefined;
595
-
596
- const createChunk = (
597
- delta: ChatCompletionsAssistantMessageDelta,
598
- provider_metadata?: SharedV3ProviderMetadata,
599
- finish_reason?: ChatCompletionsFinishReason,
600
- usage?: ChatCompletionsUsage,
601
- ): ChatCompletionsChunk => {
602
- if (provider_metadata) {
603
- delta.extra_content = provider_metadata;
604
- }
605
- return {
606
- id: streamId,
607
- object: "chat.completion.chunk",
608
- created: creationTime,
609
- model,
610
- choices: [
611
- {
612
- index: 0,
613
- delta,
614
- finish_reason: finish_reason ?? null,
615
- } satisfies ChatCompletionsChoiceDelta,
616
- ],
617
- usage: usage ?? null,
618
- };
619
- };
620
-
621
- super({
622
- transform(part, controller) {
623
- switch (part.type) {
624
- case "text-delta": {
625
- controller.enqueue(
626
- createChunk({ role: "assistant", content: part.text }, part.providerMetadata),
627
- );
628
- break;
629
- }
630
-
631
- case "reasoning-delta": {
632
- let index = reasoningIdToIndex.get(part.id);
633
- if (index === undefined) {
634
- index = reasoningIdToIndex.size;
635
- reasoningIdToIndex.set(part.id, index);
636
- }
637
-
638
- controller.enqueue(
639
- createChunk(
640
- {
641
- reasoning: part.text,
642
- reasoning_details: [
643
- toReasoningDetail(
644
- {
645
- type: "reasoning",
646
- text: part.text,
647
- providerMetadata: part.providerMetadata,
648
- },
649
- part.id,
650
- index,
651
- ),
652
- ],
653
- },
654
- part.providerMetadata,
655
- ),
656
- );
657
- break;
658
- }
659
-
660
- case "tool-call": {
661
- const toolCall = toChatCompletionsToolCall(
662
- part.toolCallId,
663
- part.toolName,
664
- part.input,
665
- part.providerMetadata,
666
- ) as ChatCompletionsToolCallDelta;
667
- toolCall.index = toolCallIndexCounter++;
668
- controller.enqueue(
669
- createChunk({
670
- tool_calls: [toolCall],
671
- }),
672
- );
673
- break;
674
- }
675
-
676
- case "finish-step": {
677
- finishProviderMetadata = part.providerMetadata;
678
- break;
679
- }
680
-
681
- case "finish": {
682
- controller.enqueue(
683
- createChunk(
684
- {},
685
- finishProviderMetadata,
686
- toChatCompletionsFinishReason(part.finishReason),
687
- toChatCompletionsUsage(part.totalUsage),
688
- ),
689
- );
690
- break;
691
- }
692
-
693
- case "error": {
694
- let err: Error | OpenAIError;
695
- if (wrapErrors) {
696
- err = toOpenAIError(part.error);
697
- } else if (part.error instanceof Error) {
698
- err = part.error;
699
- } else {
700
- err = new Error(String(part.error));
701
- }
702
- controller.enqueue(err as E extends true ? OpenAIError : Error);
703
- }
704
- }
705
- },
706
- });
707
- }
708
- }
709
-
710
- export const toChatCompletionsAssistantMessage = (
711
- result: GenerateTextResult<ToolSet, Output.Output>,
712
- ): ChatCompletionsAssistantMessage => {
713
- const message: ChatCompletionsAssistantMessage = {
714
- role: "assistant",
715
- content: null,
716
- };
717
-
718
- if (result.toolCalls && result.toolCalls.length > 0) {
719
- message.tool_calls = result.toolCalls.map((toolCall) =>
720
- toChatCompletionsToolCall(
721
- toolCall.toolCallId,
722
- toolCall.toolName,
723
- toolCall.input,
724
- toolCall.providerMetadata,
725
- ),
726
- );
727
- }
728
-
729
- const reasoningDetails: ChatCompletionsReasoningDetail[] = [];
730
-
731
- for (const part of result.content) {
732
- if (part.type === "text") {
733
- if (message.content === null) {
734
- message.content = part.text;
735
- } else {
736
- message.content += part.text;
737
- }
738
- if (part.providerMetadata) {
739
- message.extra_content = part.providerMetadata;
740
- }
741
- } else if (part.type === "reasoning") {
742
- reasoningDetails.push(
743
- toReasoningDetail(part, `reasoning-${crypto.randomUUID()}`, reasoningDetails.length),
744
- );
745
- }
746
- }
747
-
748
- if (result.reasoningText) {
749
- message.reasoning = result.reasoningText;
750
- }
751
-
752
- if (reasoningDetails.length > 0) {
753
- message.reasoning_details = reasoningDetails;
754
- }
755
-
756
- if (!message.content && !message.tool_calls) {
757
- // some models return just reasoning without tool calls or content
758
- message.content = "";
759
- }
760
-
761
- return message;
762
- };
763
-
764
- export function toReasoningDetail(
765
- reasoning: ReasoningOutput,
766
- id: string,
767
- index: number,
768
- ): ChatCompletionsReasoningDetail {
769
- const providerMetadata = reasoning.providerMetadata ?? {};
770
-
771
- let redactedData: string | undefined;
772
- let signature: string | undefined;
773
-
774
- for (const metadata of Object.values(providerMetadata)) {
775
- if (metadata && typeof metadata === "object") {
776
- if ("redactedData" in metadata && typeof metadata["redactedData"] === "string") {
777
- redactedData = metadata["redactedData"];
778
- }
779
- if ("signature" in metadata && typeof metadata["signature"] === "string") {
780
- signature = metadata["signature"];
781
- }
782
- }
783
- }
784
-
785
- if (redactedData) {
786
- return {
787
- id,
788
- index,
789
- type: "reasoning.encrypted",
790
- data: redactedData,
791
- format: "unknown",
792
- };
793
- }
794
-
795
- return {
796
- id,
797
- index,
798
- type: "reasoning.text",
799
- text: reasoning.text,
800
- signature,
801
- format: "unknown",
802
- };
803
- }
804
-
805
- export function toChatCompletionsUsage(usage: LanguageModelUsage): ChatCompletionsUsage {
806
- const out: ChatCompletionsUsage = {};
807
-
808
- const prompt = usage.inputTokens;
809
- if (prompt !== undefined) out.prompt_tokens = prompt;
810
-
811
- const completion = usage.outputTokens;
812
- if (completion !== undefined) out.completion_tokens = completion;
813
-
814
- if (prompt !== undefined || completion !== undefined || usage.totalTokens !== undefined) {
815
- out.total_tokens = usage.totalTokens ?? (prompt ?? 0) + (completion ?? 0);
816
- }
817
-
818
- const reasoning = usage.outputTokenDetails?.reasoningTokens;
819
- if (reasoning !== undefined) out.completion_tokens_details = { reasoning_tokens: reasoning };
820
-
821
- const cached = usage.inputTokenDetails?.cacheReadTokens;
822
- const cacheWrite = usage.inputTokenDetails?.cacheWriteTokens;
823
- if (cached !== undefined || cacheWrite !== undefined) {
824
- out.prompt_tokens_details = {};
825
- if (cached !== undefined) {
826
- out.prompt_tokens_details.cached_tokens = cached;
827
- }
828
- if (cacheWrite !== undefined) {
829
- out.prompt_tokens_details.cache_write_tokens = cacheWrite;
830
- }
831
- }
832
-
833
- return out;
834
- }
835
-
836
- export function toChatCompletionsToolCall(
837
- id: string,
838
- name: string,
839
- args: unknown,
840
- providerMetadata?: SharedV3ProviderMetadata,
841
- ): ChatCompletionsToolCall {
842
- const out: ChatCompletionsToolCall = {
843
- id,
844
- type: "function",
845
- function: {
846
- name: normalizeToolName(name),
847
- arguments: typeof args === "string" ? args : JSON.stringify(stripEmptyKeys(args)),
848
- },
849
- };
850
-
851
- if (providerMetadata) {
852
- out.extra_content = providerMetadata;
853
- }
854
-
855
- return out;
856
- }
857
-
858
- function normalizeToolName(name: string): string {
859
- // some models hallucinate invalid characters
860
- // normalize to valid characters [^A-Za-z0-9_-.] (non regex for perf)
861
- // https://modelcontextprotocol.io/specification/draft/server/tools#tool-names
862
- let out = "";
863
- for (let i = 0; i < name.length; i++) {
864
- if (out.length === 128) break;
865
-
866
- // oxlint-disable-next-line unicorn/prefer-code-point
867
- const c = name.charCodeAt(i);
868
-
869
- if (
870
- (c >= 48 && c <= 57) ||
871
- (c >= 65 && c <= 90) ||
872
- (c >= 97 && c <= 122) ||
873
- c === 95 ||
874
- c === 45 ||
875
- c === 46
876
- ) {
877
- out += name[i];
878
- } else {
879
- out += "_";
880
- }
881
- }
882
- return out;
883
- }
884
-
885
- function stripEmptyKeys(obj: unknown) {
886
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) return obj;
887
- // some models hallucinate empty parameters
888
- delete (obj as Record<string, unknown>)[""];
889
- return obj;
890
- }
891
-
892
- export const toChatCompletionsFinishReason = (
893
- finishReason: FinishReason,
894
- ): ChatCompletionsFinishReason => {
895
- if (finishReason === "error" || finishReason === "other") {
896
- return "stop";
897
- }
898
- return (finishReason as string).replaceAll("-", "_") as ChatCompletionsFinishReason;
899
- };