@hebo-ai/gateway 0.4.1 → 0.5.0-beta.0
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 +2 -3
- package/dist/endpoints/chat-completions/converters.d.ts +3 -1
- package/dist/endpoints/chat-completions/converters.js +121 -90
- package/dist/endpoints/chat-completions/handler.js +2 -4
- package/dist/endpoints/chat-completions/otel.js +7 -0
- package/dist/endpoints/chat-completions/schema.d.ts +400 -76
- package/dist/endpoints/chat-completions/schema.js +80 -36
- package/dist/endpoints/embeddings/handler.js +2 -4
- package/dist/endpoints/embeddings/schema.d.ts +1 -1
- package/dist/endpoints/embeddings/schema.js +1 -1
- package/dist/errors/gateway.js +1 -0
- package/dist/lifecycle.js +7 -12
- package/dist/logger/default.d.ts +0 -1
- package/dist/logger/default.js +30 -6
- package/dist/middleware/utils.js +1 -0
- package/dist/models/amazon/middleware.js +1 -0
- package/dist/models/anthropic/middleware.d.ts +2 -0
- package/dist/models/anthropic/middleware.js +77 -16
- package/dist/models/google/middleware.js +17 -0
- package/dist/models/google/presets.d.ts +387 -0
- package/dist/models/google/presets.js +9 -2
- package/dist/models/openai/middleware.js +1 -0
- package/dist/models/types.d.ts +1 -1
- package/dist/models/types.js +1 -0
- package/dist/providers/bedrock/index.d.ts +1 -0
- package/dist/providers/bedrock/index.js +1 -0
- package/dist/providers/bedrock/middleware.d.ts +2 -0
- package/dist/providers/bedrock/middleware.js +35 -0
- package/dist/telemetry/http.js +0 -3
- package/dist/types.d.ts +10 -20
- package/dist/utils/request.d.ts +1 -3
- package/dist/utils/request.js +3 -26
- package/dist/utils/response.d.ts +1 -1
- package/dist/utils/response.js +3 -3
- package/package.json +19 -21
- package/src/endpoints/chat-completions/converters.test.ts +219 -0
- package/src/endpoints/chat-completions/converters.ts +144 -104
- package/src/endpoints/chat-completions/handler.test.ts +87 -0
- package/src/endpoints/chat-completions/handler.ts +2 -5
- package/src/endpoints/chat-completions/otel.ts +6 -0
- package/src/endpoints/chat-completions/schema.ts +85 -43
- package/src/endpoints/embeddings/handler.ts +5 -5
- package/src/endpoints/embeddings/schema.ts +1 -1
- package/src/errors/gateway.ts +2 -0
- package/src/lifecycle.ts +7 -11
- package/src/logger/default.ts +34 -8
- package/src/middleware/utils.ts +1 -0
- package/src/models/amazon/middleware.ts +1 -0
- package/src/models/anthropic/middleware.test.ts +332 -1
- package/src/models/anthropic/middleware.ts +83 -19
- package/src/models/google/middleware.test.ts +31 -0
- package/src/models/google/middleware.ts +18 -0
- package/src/models/google/presets.ts +13 -2
- package/src/models/openai/middleware.ts +1 -0
- package/src/models/types.ts +1 -0
- package/src/providers/bedrock/index.ts +1 -0
- package/src/providers/bedrock/middleware.test.ts +73 -0
- package/src/providers/bedrock/middleware.ts +43 -0
- package/src/telemetry/http.ts +0 -3
- package/src/types.ts +19 -23
- package/src/utils/request.ts +5 -33
- package/src/utils/response.ts +3 -3
|
@@ -11,16 +11,16 @@ import type {
|
|
|
11
11
|
UserContent,
|
|
12
12
|
AssistantContent,
|
|
13
13
|
LanguageModelUsage,
|
|
14
|
-
Output,
|
|
15
14
|
TextStreamPart,
|
|
16
15
|
ReasoningOutput,
|
|
16
|
+
JSONValue,
|
|
17
17
|
AssistantModelMessage,
|
|
18
18
|
ToolModelMessage,
|
|
19
19
|
UserModelMessage,
|
|
20
20
|
} from "ai";
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { Output, jsonSchema, tool } from "ai";
|
|
23
|
+
import { z } from "zod";
|
|
24
24
|
|
|
25
25
|
import type {
|
|
26
26
|
ChatCompletionsToolCall,
|
|
@@ -44,6 +44,8 @@ import type {
|
|
|
44
44
|
ChatCompletionsReasoningEffort,
|
|
45
45
|
ChatCompletionsReasoningConfig,
|
|
46
46
|
ChatCompletionsReasoningDetail,
|
|
47
|
+
ChatCompletionsResponseFormat,
|
|
48
|
+
ChatCompletionsContentPartText,
|
|
47
49
|
} from "./schema";
|
|
48
50
|
|
|
49
51
|
import { GatewayError } from "../../errors/gateway";
|
|
@@ -54,6 +56,7 @@ export type TextCallOptions = {
|
|
|
54
56
|
messages: ModelMessage[];
|
|
55
57
|
tools?: ToolSet;
|
|
56
58
|
toolChoice?: ToolChoice<ToolSet>;
|
|
59
|
+
output?: Output.Output;
|
|
57
60
|
temperature?: number;
|
|
58
61
|
maxOutputTokens?: number;
|
|
59
62
|
frequencyPenalty?: number;
|
|
@@ -74,6 +77,7 @@ export function convertToTextCallOptions(params: ChatCompletionsInputs): TextCal
|
|
|
74
77
|
temperature,
|
|
75
78
|
max_tokens,
|
|
76
79
|
max_completion_tokens,
|
|
80
|
+
response_format,
|
|
77
81
|
reasoning_effort,
|
|
78
82
|
reasoning,
|
|
79
83
|
frequency_penalty,
|
|
@@ -90,6 +94,7 @@ export function convertToTextCallOptions(params: ChatCompletionsInputs): TextCal
|
|
|
90
94
|
messages: convertToModelMessages(messages),
|
|
91
95
|
tools: convertToToolSet(tools),
|
|
92
96
|
toolChoice: convertToToolChoice(tool_choice),
|
|
97
|
+
output: convertToOutput(response_format),
|
|
93
98
|
temperature,
|
|
94
99
|
maxOutputTokens: max_completion_tokens ?? max_tokens,
|
|
95
100
|
frequencyPenalty: frequency_penalty,
|
|
@@ -103,6 +108,19 @@ export function convertToTextCallOptions(params: ChatCompletionsInputs): TextCal
|
|
|
103
108
|
};
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
function convertToOutput(responseFormat: ChatCompletionsResponseFormat | undefined) {
|
|
112
|
+
if (!responseFormat || responseFormat.type === "text") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { name, description, schema } = responseFormat.json_schema;
|
|
117
|
+
return Output.object({
|
|
118
|
+
name,
|
|
119
|
+
description,
|
|
120
|
+
schema: jsonSchema(schema),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
export function convertToModelMessages(messages: ChatCompletionsMessage[]): ModelMessage[] {
|
|
107
125
|
const modelMessages: ModelMessage[] = [];
|
|
108
126
|
const toolById = indexToolMessages(messages);
|
|
@@ -154,65 +172,73 @@ export function fromChatCompletionsAssistantMessage(
|
|
|
154
172
|
|
|
155
173
|
const parts: AssistantContent = [];
|
|
156
174
|
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
redactedData: detail.data,
|
|
179
|
-
},
|
|
175
|
+
if (reasoning_details?.length) {
|
|
176
|
+
for (const detail of reasoning_details) {
|
|
177
|
+
if (detail.text && detail.type === "reasoning.text") {
|
|
178
|
+
parts.push({
|
|
179
|
+
type: "reasoning",
|
|
180
|
+
text: detail.text,
|
|
181
|
+
providerOptions: detail.signature
|
|
182
|
+
? {
|
|
183
|
+
unknown: {
|
|
184
|
+
signature: detail.signature,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
: undefined,
|
|
188
|
+
});
|
|
189
|
+
} else if (detail.type === "reasoning.encrypted" && detail.data) {
|
|
190
|
+
parts.push({
|
|
191
|
+
type: "reasoning",
|
|
192
|
+
text: "",
|
|
193
|
+
providerOptions: {
|
|
194
|
+
unknown: {
|
|
195
|
+
redactedData: detail.data,
|
|
180
196
|
},
|
|
181
|
-
}
|
|
182
|
-
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
183
199
|
}
|
|
184
200
|
}
|
|
201
|
+
}
|
|
185
202
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
out.providerOptions = extra_content;
|
|
198
|
-
}
|
|
199
|
-
parts.push(out);
|
|
203
|
+
if (content !== undefined && content !== null) {
|
|
204
|
+
const inputContent =
|
|
205
|
+
typeof content === "string"
|
|
206
|
+
? ([{ type: "text", text: content }] as ChatCompletionsContentPartText[])
|
|
207
|
+
: content;
|
|
208
|
+
for (const part of inputContent) {
|
|
209
|
+
if (part.type === "text") {
|
|
210
|
+
parts.push({
|
|
211
|
+
type: "text",
|
|
212
|
+
text: part.text,
|
|
213
|
+
});
|
|
200
214
|
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (tool_calls?.length) {
|
|
219
|
+
for (const tc of tool_calls) {
|
|
220
|
+
// eslint-disable-next-line no-shadow
|
|
221
|
+
const { id, function: fn, extra_content } = tc;
|
|
222
|
+
const out: ToolCallPart = {
|
|
223
|
+
type: "tool-call",
|
|
224
|
+
toolCallId: id,
|
|
225
|
+
toolName: fn.name,
|
|
226
|
+
input: parseJsonOrText(fn.arguments).value,
|
|
227
|
+
};
|
|
228
|
+
if (extra_content) {
|
|
229
|
+
out.providerOptions = extra_content as SharedV3ProviderOptions;
|
|
230
|
+
}
|
|
231
|
+
parts.push(out);
|
|
206
232
|
}
|
|
207
233
|
}
|
|
208
234
|
|
|
209
235
|
const out: AssistantModelMessage = {
|
|
210
|
-
role
|
|
211
|
-
content:
|
|
236
|
+
role,
|
|
237
|
+
content: parts.length > 0 ? parts : (content ?? ""),
|
|
212
238
|
};
|
|
213
239
|
|
|
214
240
|
if (extra_content) {
|
|
215
|
-
out.providerOptions = extra_content;
|
|
241
|
+
out.providerOptions = extra_content as SharedV3ProviderOptions;
|
|
216
242
|
}
|
|
217
243
|
|
|
218
244
|
return out;
|
|
@@ -234,7 +260,7 @@ export function fromChatCompletionsToolResultMessage(
|
|
|
234
260
|
type: "tool-result",
|
|
235
261
|
toolCallId: tc.id,
|
|
236
262
|
toolName: tc.function.name,
|
|
237
|
-
output:
|
|
263
|
+
output: parseToolResult(toolMsg.content),
|
|
238
264
|
});
|
|
239
265
|
}
|
|
240
266
|
|
|
@@ -243,48 +269,48 @@ export function fromChatCompletionsToolResultMessage(
|
|
|
243
269
|
|
|
244
270
|
export function fromChatCompletionsContent(content: ChatCompletionsContentPart[]): UserContent {
|
|
245
271
|
return content.map((part) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
mediaType: mimeType,
|
|
256
|
-
}
|
|
257
|
-
: {
|
|
258
|
-
type: "file" as const,
|
|
259
|
-
data: convertBase64ToUint8Array(base64Data),
|
|
260
|
-
mediaType: mimeType,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
type: "image" as const,
|
|
266
|
-
image: new URL(url),
|
|
267
|
-
};
|
|
272
|
+
switch (part.type) {
|
|
273
|
+
case "image_url":
|
|
274
|
+
return fromImageUrlPart(part.image_url.url);
|
|
275
|
+
case "file":
|
|
276
|
+
return fromFilePart(part.file.data, part.file.media_type, part.file.filename);
|
|
277
|
+
case "input_audio":
|
|
278
|
+
return fromFilePart(part.input_audio.data, `audio/${part.input_audio.format}`);
|
|
279
|
+
default:
|
|
280
|
+
return part;
|
|
268
281
|
}
|
|
269
|
-
if (part.type === "file") {
|
|
270
|
-
let { data, media_type, filename } = part.file;
|
|
271
|
-
return media_type.startsWith("image/")
|
|
272
|
-
? {
|
|
273
|
-
type: "image" as const,
|
|
274
|
-
image: convertBase64ToUint8Array(data),
|
|
275
|
-
mediaType: media_type,
|
|
276
|
-
}
|
|
277
|
-
: {
|
|
278
|
-
type: "file" as const,
|
|
279
|
-
data: convertBase64ToUint8Array(data),
|
|
280
|
-
filename,
|
|
281
|
-
mediaType: media_type,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
return part;
|
|
285
282
|
});
|
|
286
283
|
}
|
|
287
284
|
|
|
285
|
+
function fromImageUrlPart(url: string) {
|
|
286
|
+
if (url.startsWith("data:")) {
|
|
287
|
+
const { mimeType, base64Data } = parseDataUrl(url);
|
|
288
|
+
return fromFilePart(base64Data, mimeType);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
type: "image" as const,
|
|
293
|
+
image: new URL(url),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function fromFilePart(base64Data: string, mediaType: string, filename?: string) {
|
|
298
|
+
if (mediaType.startsWith("image/")) {
|
|
299
|
+
return {
|
|
300
|
+
type: "image" as const,
|
|
301
|
+
image: z.util.base64ToUint8Array(base64Data),
|
|
302
|
+
mediaType,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
type: "file" as const,
|
|
308
|
+
data: z.util.base64ToUint8Array(base64Data),
|
|
309
|
+
filename,
|
|
310
|
+
mediaType,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
288
314
|
export const convertToToolSet = (tools: ChatCompletionsTool[] | undefined): ToolSet | undefined => {
|
|
289
315
|
if (!tools) {
|
|
290
316
|
return;
|
|
@@ -311,17 +337,39 @@ export const convertToToolChoice = (
|
|
|
311
337
|
return toolChoice;
|
|
312
338
|
}
|
|
313
339
|
|
|
340
|
+
// 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
|
|
341
|
+
if (toolChoice === "validated") {
|
|
342
|
+
return "auto";
|
|
343
|
+
}
|
|
344
|
+
|
|
314
345
|
return {
|
|
315
346
|
type: "tool",
|
|
316
347
|
toolName: toolChoice.function.name,
|
|
317
348
|
};
|
|
318
349
|
};
|
|
319
350
|
|
|
320
|
-
function
|
|
351
|
+
function parseToolResult(
|
|
352
|
+
content: string | ChatCompletionsContentPartText[],
|
|
353
|
+
): ToolResultPart["output"] {
|
|
354
|
+
if (Array.isArray(content)) {
|
|
355
|
+
return {
|
|
356
|
+
type: "content",
|
|
357
|
+
value: content.map((part) => ({
|
|
358
|
+
type: "text",
|
|
359
|
+
text: part.text,
|
|
360
|
+
})),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return parseJsonOrText(content);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseJsonOrText(
|
|
367
|
+
content: string,
|
|
368
|
+
): { type: "json"; value: JSONValue } | { type: "text"; value: string } {
|
|
321
369
|
try {
|
|
322
|
-
return { type: "json"
|
|
370
|
+
return { type: "json", value: JSON.parse(content) };
|
|
323
371
|
} catch {
|
|
324
|
-
return { type: "text"
|
|
372
|
+
return { type: "text", value: content };
|
|
325
373
|
}
|
|
326
374
|
}
|
|
327
375
|
|
|
@@ -379,8 +427,6 @@ export function toChatCompletions(
|
|
|
379
427
|
result: GenerateTextResult<ToolSet, Output.Output>,
|
|
380
428
|
model: string,
|
|
381
429
|
): ChatCompletions {
|
|
382
|
-
const finish_reason = toChatCompletionsFinishReason(result.finishReason);
|
|
383
|
-
|
|
384
430
|
return {
|
|
385
431
|
id: "chatcmpl-" + crypto.randomUUID(),
|
|
386
432
|
object: "chat.completion",
|
|
@@ -390,7 +436,7 @@ export function toChatCompletions(
|
|
|
390
436
|
{
|
|
391
437
|
index: 0,
|
|
392
438
|
message: toChatCompletionsAssistantMessage(result),
|
|
393
|
-
finish_reason,
|
|
439
|
+
finish_reason: toChatCompletionsFinishReason(result.finishReason),
|
|
394
440
|
} satisfies ChatCompletionsChoice,
|
|
395
441
|
],
|
|
396
442
|
usage: result.totalUsage ? toChatCompletionsUsage(result.totalUsage) : null,
|
|
@@ -430,6 +476,7 @@ export class ChatCompletionsStream<E extends boolean = false> extends TransformS
|
|
|
430
476
|
const creationTime = Math.floor(Date.now() / 1000);
|
|
431
477
|
let toolCallIndexCounter = 0;
|
|
432
478
|
const reasoningIdToIndex = new Map<string, number>();
|
|
479
|
+
let finishProviderMetadata: SharedV3ProviderMetadata | undefined;
|
|
433
480
|
|
|
434
481
|
const createChunk = (
|
|
435
482
|
delta: ChatCompletionsAssistantMessageDelta,
|
|
@@ -512,14 +559,7 @@ export class ChatCompletionsStream<E extends boolean = false> extends TransformS
|
|
|
512
559
|
}
|
|
513
560
|
|
|
514
561
|
case "finish-step": {
|
|
515
|
-
|
|
516
|
-
createChunk(
|
|
517
|
-
{},
|
|
518
|
-
part.providerMetadata,
|
|
519
|
-
toChatCompletionsFinishReason(part.finishReason),
|
|
520
|
-
toChatCompletionsUsage(part.usage),
|
|
521
|
-
),
|
|
522
|
-
);
|
|
562
|
+
finishProviderMetadata = part.providerMetadata;
|
|
523
563
|
break;
|
|
524
564
|
}
|
|
525
565
|
|
|
@@ -527,7 +567,7 @@ export class ChatCompletionsStream<E extends boolean = false> extends TransformS
|
|
|
527
567
|
controller.enqueue(
|
|
528
568
|
createChunk(
|
|
529
569
|
{},
|
|
530
|
-
|
|
570
|
+
finishProviderMetadata,
|
|
531
571
|
toChatCompletionsFinishReason(part.finishReason),
|
|
532
572
|
toChatCompletionsUsage(part.totalUsage),
|
|
533
573
|
),
|
|
@@ -12,6 +12,7 @@ describe("Chat Completions Handler", () => {
|
|
|
12
12
|
const mockLanguageModel = new MockLanguageModelV3({
|
|
13
13
|
// eslint-disable-next-line require-await
|
|
14
14
|
doGenerate: async (options) => {
|
|
15
|
+
const isStructuredOutput = options.responseFormat?.type === "json";
|
|
15
16
|
const isToolCall = options.tools && options.tools.length > 0;
|
|
16
17
|
|
|
17
18
|
if (isToolCall) {
|
|
@@ -34,6 +35,24 @@ describe("Chat Completions Handler", () => {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
if (isStructuredOutput) {
|
|
39
|
+
return {
|
|
40
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
41
|
+
usage: {
|
|
42
|
+
inputTokens: { total: 10, noCache: 10, cacheRead: 20, cacheWrite: 0 },
|
|
43
|
+
outputTokens: { total: 20, text: 20, reasoning: 10 },
|
|
44
|
+
},
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: '{"city":"San Francisco","temp_c":18}',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
providerMetadata: { provider: { key: "value" } },
|
|
52
|
+
warnings: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
return {
|
|
38
57
|
finishReason: { unified: "stop", raw: "stop" },
|
|
39
58
|
usage: {
|
|
@@ -184,6 +203,31 @@ describe("Chat Completions Handler", () => {
|
|
|
184
203
|
});
|
|
185
204
|
});
|
|
186
205
|
|
|
206
|
+
test("should accept input_audio content parts", async () => {
|
|
207
|
+
const request = postJson(baseUrl, {
|
|
208
|
+
model: "openai/gpt-oss-20b",
|
|
209
|
+
messages: [
|
|
210
|
+
{
|
|
211
|
+
role: "user",
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "input_audio",
|
|
215
|
+
input_audio: {
|
|
216
|
+
data: "aGVsbG8=",
|
|
217
|
+
format: "wav",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const res = await endpoint.handler(request);
|
|
226
|
+
expect(res.status).toBe(200);
|
|
227
|
+
const data = await parseResponse(res);
|
|
228
|
+
expect(data.model).toBe("openai/gpt-oss-20b");
|
|
229
|
+
});
|
|
230
|
+
|
|
187
231
|
test("should generate completion with tool calls successfully", async () => {
|
|
188
232
|
const request = postJson(baseUrl, {
|
|
189
233
|
model: "openai/gpt-oss-20b",
|
|
@@ -299,4 +343,47 @@ describe("Chat Completions Handler", () => {
|
|
|
299
343
|
const data = await parseResponse(res);
|
|
300
344
|
expect(data.model).toBe("openai/gpt-oss-20b");
|
|
301
345
|
});
|
|
346
|
+
|
|
347
|
+
test("should generate non-streaming structured output", async () => {
|
|
348
|
+
const request = postJson(baseUrl, {
|
|
349
|
+
model: "openai/gpt-oss-20b",
|
|
350
|
+
messages: [{ role: "user", content: "Return weather as JSON" }],
|
|
351
|
+
response_format: {
|
|
352
|
+
type: "json_schema",
|
|
353
|
+
json_schema: {
|
|
354
|
+
name: "weather",
|
|
355
|
+
schema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: {
|
|
358
|
+
city: { type: "string" },
|
|
359
|
+
temp_c: { type: "number" },
|
|
360
|
+
},
|
|
361
|
+
required: ["city", "temp_c"],
|
|
362
|
+
additionalProperties: false,
|
|
363
|
+
},
|
|
364
|
+
strict: true,
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const res = await endpoint.handler(request);
|
|
370
|
+
expect(res.status).toBe(200);
|
|
371
|
+
const data = await parseResponse(res);
|
|
372
|
+
expect(data.choices[0].message.content).toBe('{"city":"San Francisco","temp_c":18}');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('should accept response_format type "text"', async () => {
|
|
376
|
+
const request = postJson(baseUrl, {
|
|
377
|
+
model: "openai/gpt-oss-20b",
|
|
378
|
+
messages: [{ role: "user", content: "Say hi" }],
|
|
379
|
+
response_format: {
|
|
380
|
+
type: "text",
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const res = await endpoint.handler(request);
|
|
385
|
+
expect(res.status).toBe(200);
|
|
386
|
+
const data = await parseResponse(res);
|
|
387
|
+
expect(data.choices[0].message.content).toBe("Hello from AI");
|
|
388
|
+
});
|
|
302
389
|
});
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
recordTokenUsage,
|
|
30
30
|
} from "../../telemetry/gen-ai";
|
|
31
31
|
import { addSpanEvent, setSpanAttributes } from "../../telemetry/span";
|
|
32
|
-
import { resolveRequestId } from "../../utils/headers";
|
|
33
32
|
import { prepareForwardHeaders } from "../../utils/request";
|
|
34
33
|
import { convertToTextCallOptions, toChatCompletions, toChatCompletionsStream } from "./converters";
|
|
35
34
|
import {
|
|
@@ -52,8 +51,6 @@ export const chatCompletions = (config: GatewayConfig): Endpoint => {
|
|
|
52
51
|
throw new GatewayError("Method Not Allowed", 405);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const requestId = resolveRequestId(ctx.request);
|
|
56
|
-
|
|
57
54
|
// Parse + validate input.
|
|
58
55
|
try {
|
|
59
56
|
ctx.body = await ctx.request.json();
|
|
@@ -107,7 +104,7 @@ export const chatCompletions = (config: GatewayConfig): Endpoint => {
|
|
|
107
104
|
const textOptions = convertToTextCallOptions(inputs);
|
|
108
105
|
logger.trace(
|
|
109
106
|
{
|
|
110
|
-
requestId,
|
|
107
|
+
requestId: ctx.requestId,
|
|
111
108
|
options: textOptions,
|
|
112
109
|
},
|
|
113
110
|
"[chat] AI SDK options",
|
|
@@ -178,7 +175,7 @@ export const chatCompletions = (config: GatewayConfig): Endpoint => {
|
|
|
178
175
|
},
|
|
179
176
|
...textOptions,
|
|
180
177
|
});
|
|
181
|
-
logger.trace({ requestId, result }, "[chat] AI SDK result");
|
|
178
|
+
logger.trace({ requestId: ctx.requestId, result }, "[chat] AI SDK result");
|
|
182
179
|
addSpanEvent("hebo.ai-sdk.completed");
|
|
183
180
|
|
|
184
181
|
// Transform result.
|
|
@@ -41,6 +41,12 @@ const toMessageParts = (message: ChatCompletionsMessage): Record<string, unknown
|
|
|
41
41
|
parts.push(toTextPart(part.text));
|
|
42
42
|
} else if (part.type === "image_url") {
|
|
43
43
|
parts.push({ type: "image", content: part.image_url.url });
|
|
44
|
+
} else if (part.type === "input_audio") {
|
|
45
|
+
parts.push({
|
|
46
|
+
type: "audio",
|
|
47
|
+
content: "[REDACTED_BINARY_DATA]",
|
|
48
|
+
format: part.input_audio.format,
|
|
49
|
+
});
|
|
44
50
|
} else {
|
|
45
51
|
parts.push({
|
|
46
52
|
type: "file",
|