@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.
Files changed (62) hide show
  1. package/README.md +2 -3
  2. package/dist/endpoints/chat-completions/converters.d.ts +3 -1
  3. package/dist/endpoints/chat-completions/converters.js +121 -90
  4. package/dist/endpoints/chat-completions/handler.js +2 -4
  5. package/dist/endpoints/chat-completions/otel.js +7 -0
  6. package/dist/endpoints/chat-completions/schema.d.ts +400 -76
  7. package/dist/endpoints/chat-completions/schema.js +80 -36
  8. package/dist/endpoints/embeddings/handler.js +2 -4
  9. package/dist/endpoints/embeddings/schema.d.ts +1 -1
  10. package/dist/endpoints/embeddings/schema.js +1 -1
  11. package/dist/errors/gateway.js +1 -0
  12. package/dist/lifecycle.js +7 -12
  13. package/dist/logger/default.d.ts +0 -1
  14. package/dist/logger/default.js +30 -6
  15. package/dist/middleware/utils.js +1 -0
  16. package/dist/models/amazon/middleware.js +1 -0
  17. package/dist/models/anthropic/middleware.d.ts +2 -0
  18. package/dist/models/anthropic/middleware.js +77 -16
  19. package/dist/models/google/middleware.js +17 -0
  20. package/dist/models/google/presets.d.ts +387 -0
  21. package/dist/models/google/presets.js +9 -2
  22. package/dist/models/openai/middleware.js +1 -0
  23. package/dist/models/types.d.ts +1 -1
  24. package/dist/models/types.js +1 -0
  25. package/dist/providers/bedrock/index.d.ts +1 -0
  26. package/dist/providers/bedrock/index.js +1 -0
  27. package/dist/providers/bedrock/middleware.d.ts +2 -0
  28. package/dist/providers/bedrock/middleware.js +35 -0
  29. package/dist/telemetry/http.js +0 -3
  30. package/dist/types.d.ts +10 -20
  31. package/dist/utils/request.d.ts +1 -3
  32. package/dist/utils/request.js +3 -26
  33. package/dist/utils/response.d.ts +1 -1
  34. package/dist/utils/response.js +3 -3
  35. package/package.json +19 -21
  36. package/src/endpoints/chat-completions/converters.test.ts +219 -0
  37. package/src/endpoints/chat-completions/converters.ts +144 -104
  38. package/src/endpoints/chat-completions/handler.test.ts +87 -0
  39. package/src/endpoints/chat-completions/handler.ts +2 -5
  40. package/src/endpoints/chat-completions/otel.ts +6 -0
  41. package/src/endpoints/chat-completions/schema.ts +85 -43
  42. package/src/endpoints/embeddings/handler.ts +5 -5
  43. package/src/endpoints/embeddings/schema.ts +1 -1
  44. package/src/errors/gateway.ts +2 -0
  45. package/src/lifecycle.ts +7 -11
  46. package/src/logger/default.ts +34 -8
  47. package/src/middleware/utils.ts +1 -0
  48. package/src/models/amazon/middleware.ts +1 -0
  49. package/src/models/anthropic/middleware.test.ts +332 -1
  50. package/src/models/anthropic/middleware.ts +83 -19
  51. package/src/models/google/middleware.test.ts +31 -0
  52. package/src/models/google/middleware.ts +18 -0
  53. package/src/models/google/presets.ts +13 -2
  54. package/src/models/openai/middleware.ts +1 -0
  55. package/src/models/types.ts +1 -0
  56. package/src/providers/bedrock/index.ts +1 -0
  57. package/src/providers/bedrock/middleware.test.ts +73 -0
  58. package/src/providers/bedrock/middleware.ts +43 -0
  59. package/src/telemetry/http.ts +0 -3
  60. package/src/types.ts +19 -23
  61. package/src/utils/request.ts +5 -33
  62. package/src/utils/response.ts +3 -3
@@ -1,3 +1,3 @@
1
- export declare const prepareResponseInit: (request: Request) => ResponseInit;
1
+ export declare const prepareResponseInit: (requestId: string) => ResponseInit;
2
2
  export declare const mergeResponseInit: (defaultHeaders: HeadersInit, responseInit?: ResponseInit) => ResponseInit;
3
3
  export declare const toResponse: (result: ReadableStream | Uint8Array<ArrayBuffer> | object | string, responseInit?: ResponseInit) => Response;
@@ -1,4 +1,4 @@
1
- import { REQUEST_ID_HEADER, resolveRequestId } from "./headers";
1
+ import { REQUEST_ID_HEADER } from "./headers";
2
2
  const TEXT_ENCODER = new TextEncoder();
3
3
  class JsonToSseTransformStream extends TransformStream {
4
4
  constructor() {
@@ -12,8 +12,8 @@ class JsonToSseTransformStream extends TransformStream {
12
12
  });
13
13
  }
14
14
  }
15
- export const prepareResponseInit = (request) => ({
16
- headers: { [REQUEST_ID_HEADER]: resolveRequestId(request) },
15
+ export const prepareResponseInit = (requestId) => ({
16
+ headers: { [REQUEST_ID_HEADER]: requestId },
17
17
  });
18
18
  export const mergeResponseInit = (defaultHeaders, responseInit) => {
19
19
  const headers = new Headers(defaultHeaders);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hebo-ai/gateway",
3
- "version": "0.4.1",
3
+ "version": "0.5.0-beta.0",
4
4
  "description": "AI gateway as a framework. For full control over models, routing & lifecycle. OpenAI-compatible /chat/completions, /embeddings & /models.",
5
5
  "keywords": [
6
6
  "ai",
@@ -154,40 +154,38 @@
154
154
  "test": "bun test"
155
155
  },
156
156
  "dependencies": {
157
- "@ai-sdk/provider": "^3.0.7",
158
- "@ai-sdk/provider-utils": "^4.0.13",
159
- "ai": "^6.0.67",
160
- "serialize-error": "^13.0.1",
157
+ "@ai-sdk/provider": "^3.0.8",
158
+ "ai": "^6.0.94",
161
159
  "zod": "^4.3.6"
162
160
  },
163
161
  "devDependencies": {
164
- "@ai-sdk/amazon-bedrock": "^4.0.41",
165
- "@ai-sdk/anthropic": "^3.0.31",
166
- "@ai-sdk/cohere": "^3.0.16",
167
- "@ai-sdk/google-vertex": "^4.0.37",
168
- "@ai-sdk/groq": "^3.0.19",
169
- "@ai-sdk/openai": "^3.0.23",
170
- "@aws-sdk/credential-providers": "^3.981.0",
162
+ "@ai-sdk/amazon-bedrock": "^4.0.62",
163
+ "@ai-sdk/anthropic": "^3.0.46",
164
+ "@ai-sdk/cohere": "^3.0.21",
165
+ "@ai-sdk/google-vertex": "^4.0.61",
166
+ "@ai-sdk/groq": "^3.0.24",
167
+ "@ai-sdk/openai": "^3.0.30",
168
+ "@aws-sdk/credential-providers": "^3.994.0",
171
169
  "@langfuse/otel": "^4.6.1",
172
170
  "@mjackson/node-fetch-server": "^0.7.0",
173
171
  "@opentelemetry/api": "^1.9.0",
174
172
  "@opentelemetry/context-async-hooks": "^2.5.1",
175
173
  "@opentelemetry/sdk-trace-base": "^2.5.1",
176
- "@tanstack/react-router": "^1.157.16",
177
- "@tanstack/react-start": "^1.157.16",
174
+ "@tanstack/react-router": "^1.161.3",
175
+ "@tanstack/react-start": "^1.161.3",
178
176
  "@types/bun": "latest",
179
- "@types/react": "^19.2.10",
177
+ "@types/react": "^19.2.14",
180
178
  "@types/react-dom": "^19.2.3",
181
- "elysia": "^1.4.22",
182
- "hono": "^4.11.7",
183
- "lefthook": "^2.0.16",
179
+ "elysia": "^1.4.25",
180
+ "hono": "^4.12.0",
181
+ "lefthook": "^2.1.1",
184
182
  "next": "^16.1.6",
185
183
  "oxfmt": "^0.24.0",
186
- "oxlint": "^1.42.0",
187
- "pino": "^10.3.0",
184
+ "oxlint": "^1.49.0",
185
+ "pino": "^10.3.1",
188
186
  "typescript": "^5.9.3",
189
187
  "vite": "^7.3.1",
190
- "vite-tsconfig-paths": "^6.0.5",
188
+ "vite-tsconfig-paths": "^6.1.1",
191
189
  "voyage-ai-provider": "^3.0.0"
192
190
  },
193
191
  "peerDependencies": {
@@ -2,13 +2,116 @@ import type { GenerateTextResult, ToolSet, Output } from "ai";
2
2
 
3
3
  import { describe, expect, test } from "bun:test";
4
4
 
5
+ import type { ChatCompletionsToolMessage } from "./schema";
6
+
5
7
  import {
6
8
  convertToTextCallOptions,
7
9
  toChatCompletionsAssistantMessage,
8
10
  fromChatCompletionsAssistantMessage,
11
+ fromChatCompletionsToolResultMessage,
9
12
  } from "./converters";
10
13
 
11
14
  describe("Chat Completions Converters", () => {
15
+ describe("fromChatCompletionsToolResultMessage", () => {
16
+ test("should handle tool message with string content", () => {
17
+ const assistantMessage = {
18
+ role: "assistant" as const,
19
+ tool_calls: [
20
+ {
21
+ id: "call_1",
22
+ type: "function" as const,
23
+ function: { name: "test_tool", arguments: "{}" },
24
+ },
25
+ ],
26
+ };
27
+ const toolById = new Map<string, ChatCompletionsToolMessage>([
28
+ ["call_1", { role: "tool", content: "hello world", tool_call_id: "call_1" }],
29
+ ]);
30
+
31
+ const result = fromChatCompletionsToolResultMessage(assistantMessage, toolById);
32
+ expect(result).toBeDefined();
33
+ expect(result?.content[0]).toMatchObject({
34
+ type: "tool-result",
35
+ toolCallId: "call_1",
36
+ output: { type: "text", value: "hello world" },
37
+ });
38
+ });
39
+
40
+ test("should handle tool message with content parts array", () => {
41
+ const assistantMessage = {
42
+ role: "assistant" as const,
43
+ tool_calls: [
44
+ {
45
+ id: "call_1",
46
+ type: "function" as const,
47
+ function: { name: "test_tool", arguments: "{}" },
48
+ },
49
+ ],
50
+ };
51
+ const toolById = new Map<string, ChatCompletionsToolMessage>([
52
+ [
53
+ "call_1",
54
+ {
55
+ role: "tool",
56
+ content: [
57
+ { type: "text", text: "part 1" },
58
+ { type: "text", text: " part 2" },
59
+ ],
60
+ tool_call_id: "call_1",
61
+ },
62
+ ],
63
+ ]);
64
+
65
+ const result = fromChatCompletionsToolResultMessage(assistantMessage, toolById);
66
+ expect(result).toBeDefined();
67
+ expect(result?.content[0]).toMatchObject({
68
+ type: "tool-result",
69
+ toolCallId: "call_1",
70
+ output: {
71
+ type: "content",
72
+ value: [
73
+ { type: "text", text: "part 1" },
74
+ { type: "text", text: " part 2" },
75
+ ],
76
+ },
77
+ });
78
+ });
79
+
80
+ test("should handle tool message with content parts array containing JSON string", () => {
81
+ const assistantMessage = {
82
+ role: "assistant" as const,
83
+ tool_calls: [
84
+ {
85
+ id: "call_1",
86
+ type: "function" as const,
87
+ function: { name: "test_tool", arguments: "{}" },
88
+ },
89
+ ],
90
+ };
91
+ const toolById = new Map<string, ChatCompletionsToolMessage>([
92
+ [
93
+ "call_1",
94
+ {
95
+ role: "tool",
96
+ content: [{ type: "text", text: '{"result": "success"}' }],
97
+ tool_call_id: "call_1",
98
+ },
99
+ ],
100
+ ]);
101
+
102
+ const result = fromChatCompletionsToolResultMessage(assistantMessage, toolById);
103
+ expect(result).toBeDefined();
104
+ expect(result?.content[0]).toMatchObject({
105
+ type: "tool-result",
106
+ toolCallId: "call_1",
107
+ output: {
108
+ type: "content",
109
+ value: [{ type: "text", text: '{"result": "success"}' }],
110
+ },
111
+ });
112
+ });
113
+ });
114
+
12
115
  describe("toChatCompletionsAssistantMessage", () => {
13
116
  test("should pass through providerMetadata to extra_content", () => {
14
117
  const mockResult: GenerateTextResult<ToolSet, Output.Output> = {
@@ -202,6 +305,37 @@ describe("Chat Completions Converters", () => {
202
305
  },
203
306
  });
204
307
  });
308
+
309
+ test("should handle both content and tool_calls", () => {
310
+ const message = fromChatCompletionsAssistantMessage({
311
+ role: "assistant",
312
+ content: "I will call a tool.",
313
+ tool_calls: [
314
+ {
315
+ id: "call_1",
316
+ type: "function",
317
+ function: {
318
+ name: "my_tool",
319
+ arguments: "{}",
320
+ },
321
+ },
322
+ ],
323
+ });
324
+
325
+ expect(Array.isArray(message.content)).toBe(true);
326
+ const content = message.content as any[];
327
+ expect(content).toHaveLength(2);
328
+ expect(content[0]).toEqual({
329
+ type: "text",
330
+ text: "I will call a tool.",
331
+ });
332
+ expect(content[1]).toEqual({
333
+ type: "tool-call",
334
+ toolCallId: "call_1",
335
+ toolName: "my_tool",
336
+ input: {},
337
+ });
338
+ });
205
339
  });
206
340
 
207
341
  describe("convertToTextCallOptions", () => {
@@ -236,5 +370,90 @@ describe("Chat Completions Converters", () => {
236
370
  });
237
371
  expect(result.maxOutputTokens).toBeUndefined();
238
372
  });
373
+
374
+ test("should convert response_format json_schema to output.object", async () => {
375
+ const result = convertToTextCallOptions({
376
+ messages: [{ role: "user", content: "hi" }],
377
+ response_format: {
378
+ type: "json_schema",
379
+ json_schema: {
380
+ name: "weather",
381
+ description: "Structured weather response",
382
+ schema: {
383
+ type: "object",
384
+ properties: {
385
+ city: { type: "string" },
386
+ },
387
+ required: ["city"],
388
+ additionalProperties: false,
389
+ },
390
+ strict: true,
391
+ },
392
+ },
393
+ });
394
+
395
+ expect(result.output?.name).toBe("object");
396
+
397
+ const parsed = await result.output!.parseCompleteOutput(
398
+ {
399
+ text: '{"city":"San Francisco"}',
400
+ },
401
+ {
402
+ response: {} as any,
403
+ usage: {} as any,
404
+ finishReason: "stop",
405
+ },
406
+ );
407
+
408
+ expect(parsed).toEqual({ city: "San Francisco" });
409
+ });
410
+
411
+ test("should treat response_format text as default text output", () => {
412
+ const result = convertToTextCallOptions({
413
+ messages: [{ role: "user", content: "hi" }],
414
+ response_format: {
415
+ type: "text",
416
+ },
417
+ });
418
+
419
+ expect(result.output).toBeUndefined();
420
+ });
421
+
422
+ test("should convert input_audio content parts to file user content", () => {
423
+ const result = convertToTextCallOptions({
424
+ messages: [
425
+ {
426
+ role: "user",
427
+ content: [
428
+ {
429
+ type: "input_audio",
430
+ input_audio: {
431
+ data: "aGVsbG8=",
432
+ format: "wav",
433
+ },
434
+ },
435
+ ],
436
+ },
437
+ ],
438
+ });
439
+
440
+ const userMessage = result.messages[0] as any;
441
+ expect(userMessage.role).toBe("user");
442
+ expect(Array.isArray(userMessage.content)).toBe(true);
443
+
444
+ const [part] = userMessage.content as any[];
445
+ expect(part.type).toBe("file");
446
+ expect(part.mediaType).toBe("audio/wav");
447
+ expect(part.data).toBeInstanceOf(Uint8Array);
448
+ expect(Array.from(part.data)).toEqual([104, 101, 108, 108, 111]);
449
+ });
450
+
451
+ test("should map tool_choice 'validated' to 'auto'", () => {
452
+ const result = convertToTextCallOptions({
453
+ messages: [{ role: "user", content: "hi" }],
454
+ tool_choice: "validated",
455
+ });
456
+ expect(result.toolChoice).toBe("auto");
457
+ });
239
458
  });
240
459
  });