@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.
- package/README.md +3 -3
- package/dist/endpoints/chat-completions/converters.js +26 -21
- package/dist/endpoints/chat-completions/handler.js +2 -0
- package/dist/endpoints/chat-completions/otel.js +1 -1
- package/dist/endpoints/chat-completions/schema.d.ts +4 -18
- package/dist/endpoints/chat-completions/schema.js +14 -17
- package/dist/endpoints/embeddings/handler.js +2 -0
- package/dist/endpoints/embeddings/otel.js +5 -0
- package/dist/endpoints/embeddings/schema.d.ts +6 -0
- package/dist/endpoints/embeddings/schema.js +4 -1
- package/dist/endpoints/models/converters.js +3 -3
- package/dist/lifecycle.js +2 -2
- package/dist/logger/default.js +3 -3
- package/dist/logger/index.d.ts +2 -5
- package/dist/middleware/common.js +1 -0
- package/dist/middleware/utils.js +0 -3
- package/dist/models/amazon/middleware.js +8 -5
- package/dist/models/anthropic/middleware.js +13 -13
- package/dist/models/catalog.js +5 -1
- package/dist/models/cohere/middleware.js +7 -5
- package/dist/models/google/middleware.d.ts +1 -1
- package/dist/models/google/middleware.js +29 -25
- package/dist/models/openai/middleware.js +13 -9
- package/dist/models/voyage/middleware.js +2 -1
- package/dist/providers/bedrock/middleware.js +21 -23
- package/dist/providers/registry.js +3 -0
- package/dist/telemetry/fetch.js +7 -2
- package/dist/telemetry/gen-ai.js +15 -12
- package/dist/telemetry/memory.d.ts +1 -1
- package/dist/telemetry/memory.js +30 -14
- package/dist/telemetry/span.js +1 -1
- package/dist/telemetry/stream.js +30 -23
- package/dist/utils/env.js +4 -2
- package/dist/utils/preset.js +1 -0
- package/dist/utils/response.js +3 -1
- package/package.json +36 -50
- package/src/config.ts +0 -98
- package/src/endpoints/chat-completions/converters.test.ts +0 -631
- package/src/endpoints/chat-completions/converters.ts +0 -899
- package/src/endpoints/chat-completions/handler.test.ts +0 -391
- package/src/endpoints/chat-completions/handler.ts +0 -201
- package/src/endpoints/chat-completions/index.ts +0 -4
- package/src/endpoints/chat-completions/otel.test.ts +0 -315
- package/src/endpoints/chat-completions/otel.ts +0 -214
- package/src/endpoints/chat-completions/schema.ts +0 -364
- package/src/endpoints/embeddings/converters.ts +0 -51
- package/src/endpoints/embeddings/handler.test.ts +0 -133
- package/src/endpoints/embeddings/handler.ts +0 -137
- package/src/endpoints/embeddings/index.ts +0 -4
- package/src/endpoints/embeddings/otel.ts +0 -40
- package/src/endpoints/embeddings/schema.ts +0 -36
- package/src/endpoints/models/converters.ts +0 -56
- package/src/endpoints/models/handler.test.ts +0 -122
- package/src/endpoints/models/handler.ts +0 -37
- package/src/endpoints/models/index.ts +0 -3
- package/src/endpoints/models/schema.ts +0 -37
- package/src/errors/ai-sdk.ts +0 -99
- package/src/errors/gateway.ts +0 -17
- package/src/errors/openai.ts +0 -57
- package/src/errors/utils.ts +0 -47
- package/src/gateway.ts +0 -50
- package/src/index.ts +0 -19
- package/src/lifecycle.ts +0 -135
- package/src/logger/default.ts +0 -105
- package/src/logger/index.ts +0 -42
- package/src/middleware/common.test.ts +0 -215
- package/src/middleware/common.ts +0 -163
- package/src/middleware/debug.ts +0 -37
- package/src/middleware/matcher.ts +0 -161
- package/src/middleware/utils.ts +0 -34
- package/src/models/amazon/index.ts +0 -2
- package/src/models/amazon/middleware.test.ts +0 -133
- package/src/models/amazon/middleware.ts +0 -79
- package/src/models/amazon/presets.ts +0 -104
- package/src/models/anthropic/index.ts +0 -2
- package/src/models/anthropic/middleware.test.ts +0 -643
- package/src/models/anthropic/middleware.ts +0 -148
- package/src/models/anthropic/presets.ts +0 -191
- package/src/models/catalog.ts +0 -13
- package/src/models/cohere/index.ts +0 -2
- package/src/models/cohere/middleware.test.ts +0 -138
- package/src/models/cohere/middleware.ts +0 -76
- package/src/models/cohere/presets.ts +0 -186
- package/src/models/google/index.ts +0 -2
- package/src/models/google/middleware.test.ts +0 -298
- package/src/models/google/middleware.ts +0 -137
- package/src/models/google/presets.ts +0 -118
- package/src/models/meta/index.ts +0 -1
- package/src/models/meta/presets.ts +0 -143
- package/src/models/openai/index.ts +0 -2
- package/src/models/openai/middleware.test.ts +0 -189
- package/src/models/openai/middleware.ts +0 -103
- package/src/models/openai/presets.ts +0 -280
- package/src/models/types.ts +0 -114
- package/src/models/voyage/index.ts +0 -2
- package/src/models/voyage/middleware.test.ts +0 -28
- package/src/models/voyage/middleware.ts +0 -23
- package/src/models/voyage/presets.ts +0 -126
- package/src/providers/anthropic/canonical.ts +0 -17
- package/src/providers/anthropic/index.ts +0 -1
- package/src/providers/bedrock/canonical.ts +0 -87
- package/src/providers/bedrock/index.ts +0 -2
- package/src/providers/bedrock/middleware.test.ts +0 -303
- package/src/providers/bedrock/middleware.ts +0 -128
- package/src/providers/cohere/canonical.ts +0 -26
- package/src/providers/cohere/index.ts +0 -1
- package/src/providers/groq/canonical.ts +0 -21
- package/src/providers/groq/index.ts +0 -1
- package/src/providers/openai/canonical.ts +0 -16
- package/src/providers/openai/index.ts +0 -1
- package/src/providers/registry.test.ts +0 -44
- package/src/providers/registry.ts +0 -165
- package/src/providers/types.ts +0 -20
- package/src/providers/vertex/canonical.ts +0 -17
- package/src/providers/vertex/index.ts +0 -1
- package/src/providers/voyage/canonical.ts +0 -16
- package/src/providers/voyage/index.ts +0 -1
- package/src/telemetry/ai-sdk.ts +0 -46
- package/src/telemetry/baggage.ts +0 -27
- package/src/telemetry/fetch.ts +0 -62
- package/src/telemetry/gen-ai.ts +0 -113
- package/src/telemetry/http.ts +0 -62
- package/src/telemetry/index.ts +0 -1
- package/src/telemetry/memory.ts +0 -36
- package/src/telemetry/span.ts +0 -85
- package/src/telemetry/stream.ts +0 -64
- package/src/types.ts +0 -223
- package/src/utils/env.ts +0 -7
- package/src/utils/headers.ts +0 -27
- package/src/utils/preset.ts +0 -65
- package/src/utils/request.test.ts +0 -75
- package/src/utils/request.ts +0 -52
- package/src/utils/response.ts +0 -84
- 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
|
-
});
|