@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,631 +0,0 @@
1
- import type { GenerateTextResult, ToolSet, Output, LanguageModelUsage } from "ai";
2
-
3
- import { describe, expect, test } from "bun:test";
4
-
5
- import type { ChatCompletionsToolMessage } from "./schema";
6
-
7
- import {
8
- convertToTextCallOptions,
9
- toChatCompletionsAssistantMessage,
10
- toChatCompletionsToolCall,
11
- toChatCompletionsUsage,
12
- fromChatCompletionsAssistantMessage,
13
- fromChatCompletionsToolResultMessage,
14
- } from "./converters";
15
-
16
- describe("Chat Completions Converters", () => {
17
- describe("fromChatCompletionsToolResultMessage", () => {
18
- test("should handle tool message with string content", () => {
19
- const assistantMessage = {
20
- role: "assistant" as const,
21
- tool_calls: [
22
- {
23
- id: "call_1",
24
- type: "function" as const,
25
- function: { name: "test_tool", arguments: "{}" },
26
- },
27
- ],
28
- };
29
- const toolById = new Map<string, ChatCompletionsToolMessage>([
30
- ["call_1", { role: "tool", content: "hello world", tool_call_id: "call_1" }],
31
- ]);
32
-
33
- const result = fromChatCompletionsToolResultMessage(assistantMessage, toolById);
34
- expect(result).toBeDefined();
35
- expect(result?.content[0]).toMatchObject({
36
- type: "tool-result",
37
- toolCallId: "call_1",
38
- output: { type: "text", value: "hello world" },
39
- });
40
- });
41
-
42
- test("should handle tool message with content parts array", () => {
43
- const assistantMessage = {
44
- role: "assistant" as const,
45
- tool_calls: [
46
- {
47
- id: "call_1",
48
- type: "function" as const,
49
- function: { name: "test_tool", arguments: "{}" },
50
- },
51
- ],
52
- };
53
- const toolById = new Map<string, ChatCompletionsToolMessage>([
54
- [
55
- "call_1",
56
- {
57
- role: "tool",
58
- content: [
59
- { type: "text", text: "part 1" },
60
- { type: "text", text: " part 2" },
61
- ],
62
- tool_call_id: "call_1",
63
- },
64
- ],
65
- ]);
66
-
67
- const result = fromChatCompletionsToolResultMessage(assistantMessage, toolById);
68
- expect(result).toBeDefined();
69
- expect(result?.content[0]).toMatchObject({
70
- type: "tool-result",
71
- toolCallId: "call_1",
72
- output: {
73
- type: "content",
74
- value: [
75
- { type: "text", text: "part 1" },
76
- { type: "text", text: " part 2" },
77
- ],
78
- },
79
- });
80
- });
81
-
82
- test("should handle tool message with content parts array containing JSON string", () => {
83
- const assistantMessage = {
84
- role: "assistant" as const,
85
- tool_calls: [
86
- {
87
- id: "call_1",
88
- type: "function" as const,
89
- function: { name: "test_tool", arguments: "{}" },
90
- },
91
- ],
92
- };
93
- const toolById = new Map<string, ChatCompletionsToolMessage>([
94
- [
95
- "call_1",
96
- {
97
- role: "tool",
98
- content: [{ type: "text", text: '{"result": "success"}' }],
99
- tool_call_id: "call_1",
100
- },
101
- ],
102
- ]);
103
-
104
- const result = fromChatCompletionsToolResultMessage(assistantMessage, toolById);
105
- expect(result).toBeDefined();
106
- expect(result?.content[0]).toMatchObject({
107
- type: "tool-result",
108
- toolCallId: "call_1",
109
- output: {
110
- type: "content",
111
- value: [{ type: "text", text: '{"result": "success"}' }],
112
- },
113
- });
114
- });
115
- });
116
-
117
- describe("toChatCompletionsAssistantMessage", () => {
118
- test("should pass through providerMetadata to extra_content", () => {
119
- const mockResult: GenerateTextResult<ToolSet, Output.Output> = {
120
- content: [
121
- {
122
- type: "text",
123
- text: "hello",
124
- providerMetadata: {
125
- vertex: {
126
- thought_signature: "signature-abc",
127
- },
128
- },
129
- } as any,
130
- ],
131
- toolCalls: [],
132
- };
133
-
134
- const message = toChatCompletionsAssistantMessage(mockResult);
135
-
136
- expect(message.extra_content).toEqual({
137
- vertex: {
138
- thought_signature: "signature-abc",
139
- },
140
- });
141
- });
142
-
143
- test("should pass through providerMetadata to tool calls", () => {
144
- const mockResult: GenerateTextResult<ToolSet, Output.Output> = {
145
- content: [],
146
- toolCalls: [
147
- {
148
- toolCallId: "call_123",
149
- toolName: "get_weather",
150
- input: { location: "London" },
151
- providerMetadata: {
152
- vertex: { thought_signature: "tool-signature" },
153
- },
154
- },
155
- ],
156
- };
157
-
158
- const message = toChatCompletionsAssistantMessage(mockResult);
159
-
160
- expect(message.tool_calls![0].extra_content).toEqual({
161
- vertex: { thought_signature: "tool-signature" },
162
- });
163
- });
164
-
165
- test("should extract reasoning_details from reasoning parts", () => {
166
- const mockResult: GenerateTextResult<ToolSet, Output.Output> = {
167
- content: [
168
- {
169
- type: "reasoning",
170
- text: "I am thinking...",
171
- providerMetadata: {
172
- anthropic: {
173
- signature: "sig-123",
174
- },
175
- },
176
- } as any,
177
- {
178
- type: "text",
179
- text: "Final answer.",
180
- } as any,
181
- ],
182
- reasoningText: "I am thinking...",
183
- toolCalls: [],
184
- };
185
-
186
- const message = toChatCompletionsAssistantMessage(mockResult);
187
-
188
- expect(message.reasoning).toBe("I am thinking...");
189
- expect(message.reasoning_details![0]).toMatchObject({
190
- type: "reasoning.text",
191
- text: "I am thinking...",
192
- signature: "sig-123",
193
- format: "unknown",
194
- index: 0,
195
- });
196
- expect(message.reasoning_details![0].id).toStartWith("reasoning-");
197
- expect(message.content).toBe("Final answer.");
198
- });
199
-
200
- test("should handle redacted/encrypted reasoning", () => {
201
- const mockResult: GenerateTextResult<ToolSet, Output.Output> = {
202
- content: [
203
- {
204
- type: "reasoning",
205
- text: "",
206
- providerMetadata: {
207
- anthropic: {
208
- redactedData: "encrypted-content",
209
- },
210
- },
211
- } as any,
212
- ],
213
- toolCalls: [],
214
- };
215
-
216
- const message = toChatCompletionsAssistantMessage(mockResult);
217
-
218
- expect(message.reasoning_details![0]).toMatchObject({
219
- type: "reasoning.encrypted",
220
- data: "encrypted-content",
221
- });
222
- expect((message.reasoning_details![0] as any).text).toBeUndefined();
223
- expect(message.reasoning_details![0].signature).toBeUndefined();
224
- });
225
- });
226
-
227
- describe("fromChatCompletionsAssistantMessage", () => {
228
- test("should convert reasoning_details back to reasoning parts with unknown providerOptions", () => {
229
- const message = fromChatCompletionsAssistantMessage({
230
- role: "assistant",
231
- content: "The result is 42.",
232
- reasoning_details: [
233
- {
234
- type: "reasoning.text",
235
- text: "Thinking hard...",
236
- signature: "sig-xyz",
237
- format: "unknown",
238
- index: 0,
239
- },
240
- ],
241
- });
242
-
243
- expect(Array.isArray(message.content)).toBe(true);
244
- const content = message.content as any[];
245
- expect(content).toHaveLength(2);
246
- expect(content[0]).toEqual({
247
- type: "reasoning",
248
- text: "Thinking hard...",
249
- providerOptions: {
250
- unknown: {
251
- signature: "sig-xyz",
252
- },
253
- },
254
- });
255
- expect(content[1]).toEqual({
256
- type: "text",
257
- text: "The result is 42.",
258
- });
259
- });
260
-
261
- test("should convert reasoning.encrypted back to reasoning parts", () => {
262
- const message = fromChatCompletionsAssistantMessage({
263
- role: "assistant",
264
- content: "Hello",
265
- reasoning_details: [
266
- {
267
- type: "reasoning.encrypted",
268
- data: "secret-data",
269
- format: "unknown",
270
- index: 0,
271
- },
272
- ],
273
- });
274
-
275
- expect(Array.isArray(message.content)).toBe(true);
276
- const content = message.content as any[];
277
- expect(content[0]).toEqual({
278
- type: "reasoning",
279
- text: "",
280
- providerOptions: {
281
- unknown: {
282
- redactedData: "secret-data",
283
- },
284
- },
285
- });
286
- });
287
-
288
- test("should handle both content and tool_calls", () => {
289
- const message = fromChatCompletionsAssistantMessage({
290
- role: "assistant",
291
- content: "I will call a tool.",
292
- tool_calls: [
293
- {
294
- id: "call_1",
295
- type: "function",
296
- function: {
297
- name: "my_tool",
298
- arguments: "{}",
299
- },
300
- },
301
- ],
302
- });
303
-
304
- expect(Array.isArray(message.content)).toBe(true);
305
- const content = message.content as any[];
306
- expect(content).toHaveLength(2);
307
- expect(content[0]).toEqual({
308
- type: "text",
309
- text: "I will call a tool.",
310
- });
311
- expect(content[1]).toEqual({
312
- type: "tool-call",
313
- toolCallId: "call_1",
314
- toolName: "my_tool",
315
- input: {},
316
- });
317
- });
318
- });
319
-
320
- describe("convertToTextCallOptions", () => {
321
- test("should use max_completion_tokens when present", () => {
322
- const result = convertToTextCallOptions({
323
- messages: [{ role: "user", content: "hi" }],
324
- max_completion_tokens: 200,
325
- });
326
- expect(result.maxOutputTokens).toBe(200);
327
- });
328
-
329
- test("should use max_tokens when max_completion_tokens is absent", () => {
330
- const result = convertToTextCallOptions({
331
- messages: [{ role: "user", content: "hi" }],
332
- max_tokens: 100,
333
- });
334
- expect(result.maxOutputTokens).toBe(100);
335
- });
336
-
337
- test("should favor max_completion_tokens over max_tokens when both are present", () => {
338
- const result = convertToTextCallOptions({
339
- messages: [{ role: "user", content: "hi" }],
340
- max_tokens: 100,
341
- max_completion_tokens: 200,
342
- });
343
- expect(result.maxOutputTokens).toBe(200);
344
- });
345
-
346
- test("should handle neither being present", () => {
347
- const result = convertToTextCallOptions({
348
- messages: [{ role: "user", content: "hi" }],
349
- });
350
- expect(result.maxOutputTokens).toBeUndefined();
351
- });
352
-
353
- test("should convert response_format json_schema to output.object", async () => {
354
- const result = convertToTextCallOptions({
355
- messages: [{ role: "user", content: "hi" }],
356
- response_format: {
357
- type: "json_schema",
358
- json_schema: {
359
- name: "weather",
360
- description: "Structured weather response",
361
- schema: {
362
- type: "object",
363
- properties: {
364
- city: { type: "string" },
365
- },
366
- required: ["city"],
367
- additionalProperties: false,
368
- },
369
- strict: true,
370
- },
371
- },
372
- });
373
-
374
- expect(result.output?.name).toBe("object");
375
-
376
- const parsed = await result.output!.parseCompleteOutput(
377
- {
378
- text: '{"city":"San Francisco"}',
379
- },
380
- {
381
- response: {} as any,
382
- usage: {} as any,
383
- finishReason: "stop",
384
- },
385
- );
386
-
387
- expect(parsed).toEqual({ city: "San Francisco" });
388
- });
389
-
390
- test("should treat response_format text as default text output", () => {
391
- const result = convertToTextCallOptions({
392
- messages: [{ role: "user", content: "hi" }],
393
- response_format: {
394
- type: "text",
395
- },
396
- });
397
-
398
- expect(result.output).toBeUndefined();
399
- });
400
-
401
- test("should convert input_audio content parts to file user content", () => {
402
- const result = convertToTextCallOptions({
403
- messages: [
404
- {
405
- role: "user",
406
- content: [
407
- {
408
- type: "input_audio",
409
- input_audio: {
410
- data: "aGVsbG8=",
411
- format: "wav",
412
- },
413
- },
414
- ],
415
- },
416
- ],
417
- });
418
-
419
- const userMessage = result.messages[0] as any;
420
- expect(userMessage.role).toBe("user");
421
- expect(Array.isArray(userMessage.content)).toBe(true);
422
-
423
- const [part] = userMessage.content as any[];
424
- expect(part.type).toBe("file");
425
- expect(part.mediaType).toBe("audio/wav");
426
- expect(part.data).toBeInstanceOf(Uint8Array);
427
- expect(Array.from(part.data)).toEqual([104, 101, 108, 108, 111]);
428
- });
429
-
430
- test("should map tool_choice 'validated' to 'auto'", () => {
431
- const result = convertToTextCallOptions({
432
- messages: [{ role: "user", content: "hi" }],
433
- tool_choice: "validated",
434
- });
435
- expect(result.toolChoice).toBe("auto");
436
- });
437
-
438
- test("should map allowed_tools to activeTools and auto mode", () => {
439
- const result = convertToTextCallOptions({
440
- messages: [{ role: "user", content: "hi" }],
441
- tool_choice: {
442
- type: "allowed_tools",
443
- allowed_tools: {
444
- mode: "auto",
445
- tools: [
446
- {
447
- type: "function",
448
- function: { name: "get_weather" },
449
- },
450
- ],
451
- },
452
- },
453
- });
454
-
455
- expect(result.toolChoice).toBe("auto");
456
- expect(result.activeTools).toEqual(["get_weather"]);
457
- });
458
-
459
- test("should map allowed_tools required mode to required", () => {
460
- const result = convertToTextCallOptions({
461
- messages: [{ role: "user", content: "hi" }],
462
- tool_choice: {
463
- type: "allowed_tools",
464
- allowed_tools: {
465
- mode: "required",
466
- tools: [
467
- {
468
- type: "function",
469
- function: { name: "get_weather" },
470
- },
471
- ],
472
- },
473
- },
474
- });
475
-
476
- expect(result.toolChoice).toBe("required");
477
- expect(result.activeTools).toEqual(["get_weather"]);
478
- });
479
-
480
- test("should convert function tools into tool set entries", () => {
481
- const result = convertToTextCallOptions({
482
- messages: [{ role: "user", content: "hi" }],
483
- tools: [
484
- {
485
- type: "function",
486
- function: {
487
- name: "get_weather",
488
- description: "Get weather",
489
- parameters: {
490
- type: "object",
491
- properties: {},
492
- },
493
- },
494
- },
495
- ],
496
- });
497
-
498
- expect(result.tools).toBeDefined();
499
- expect(Object.keys(result.tools!)).toEqual(["get_weather"]);
500
- });
501
-
502
- test("should map prompt cache options into providerOptions.unknown", () => {
503
- const result = convertToTextCallOptions({
504
- messages: [{ role: "system", content: "You are concise." }],
505
- prompt_cache_key: "tenant:docs:v1",
506
- prompt_cache_retention: "24h",
507
- });
508
-
509
- expect(result.providerOptions).toEqual({
510
- unknown: {
511
- prompt_cache_key: "tenant:docs:v1",
512
- prompt_cache_retention: "24h",
513
- cached_content: "tenant:docs:v1",
514
- cache_control: {
515
- type: "ephemeral",
516
- ttl: "24h",
517
- },
518
- },
519
- });
520
- });
521
-
522
- test("should sync retention from cache_control ttl", () => {
523
- const result = convertToTextCallOptions({
524
- messages: [{ role: "system", content: "You are concise." }],
525
- cache_control: {
526
- type: "ephemeral",
527
- ttl: "5m",
528
- },
529
- });
530
-
531
- expect(result.providerOptions).toEqual({
532
- unknown: {
533
- prompt_cache_retention: "in_memory",
534
- cache_control: {
535
- type: "ephemeral",
536
- ttl: "5m",
537
- },
538
- },
539
- });
540
- });
541
-
542
- test("should preserve cache_control on message and content parts", () => {
543
- const result = convertToTextCallOptions({
544
- messages: [
545
- {
546
- role: "system",
547
- content: "Policy block",
548
- cache_control: { type: "ephemeral", ttl: "1h" },
549
- },
550
- {
551
- role: "user",
552
- content: [{ type: "text", text: "Question", cache_control: { type: "ephemeral" } }],
553
- },
554
- ],
555
- });
556
-
557
- expect((result.messages[0] as any).providerOptions.unknown.cache_control).toEqual({
558
- type: "ephemeral",
559
- ttl: "1h",
560
- });
561
- expect((result.messages[1] as any).content[0].providerOptions.unknown.cache_control).toEqual({
562
- type: "ephemeral",
563
- });
564
- });
565
- });
566
-
567
- describe("toChatCompletionsUsage", () => {
568
- test("should include cached token details", () => {
569
- const usage = toChatCompletionsUsage({
570
- inputTokens: 100,
571
- outputTokens: 20,
572
- totalTokens: 120,
573
- inputTokenDetails: {
574
- cacheReadTokens: 60,
575
- cacheWriteTokens: 10,
576
- },
577
- } as LanguageModelUsage);
578
-
579
- expect(usage.prompt_tokens_details).toEqual({
580
- cached_tokens: 60,
581
- cache_write_tokens: 10,
582
- });
583
- });
584
- });
585
-
586
- describe("toChatCompletionsToolCall", () => {
587
- test("should filter top-level empty-string keys from object arguments", () => {
588
- const call = toChatCompletionsToolCall("call_1", "my_tool", {
589
- "": {},
590
- city: "San Francisco",
591
- nested: {
592
- "": {},
593
- country: "US",
594
- },
595
- });
596
-
597
- expect(call.function.arguments).toBe(
598
- JSON.stringify({
599
- city: "San Francisco",
600
- nested: {
601
- "": {},
602
- country: "US",
603
- },
604
- }),
605
- );
606
- });
607
-
608
- test("should pass through JSON string arguments unchanged", () => {
609
- const call = toChatCompletionsToolCall(
610
- "call_1",
611
- "my_tool",
612
- '{"":{},"city":"San Francisco","nested":{"":{},"country":"US"}}',
613
- );
614
-
615
- expect(call.function.arguments).toBe(
616
- '{"":{},"city":"San Francisco","nested":{"":{},"country":"US"}}',
617
- );
618
- });
619
-
620
- test("should normalize invalid tool names", () => {
621
- const call = toChatCompletionsToolCall("call_1", "bad. Tool- name1!@", {});
622
- expect(call.function.name).toBe("bad._Tool-_name1__");
623
- });
624
-
625
- test("should truncate tool names longer than 128 chars", () => {
626
- const call = toChatCompletionsToolCall("call_1", "a".repeat(200), {});
627
- expect(call.function.name).toHaveLength(128);
628
- expect(call.function.name).toBe("a".repeat(128));
629
- });
630
- });
631
- });