@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
package/src/lifecycle.ts DELETED
@@ -1,135 +0,0 @@
1
- import type {
2
- GatewayConfig,
3
- GatewayContext,
4
- OnRequestHookContext,
5
- OnResponseHookContext,
6
- } from "./types";
7
-
8
- import { parseConfig } from "./config";
9
- import { GatewayError } from "./errors/gateway";
10
- import { toOpenAIErrorResponse } from "./errors/openai";
11
- import { logger } from "./logger";
12
- import { getBaggageAttributes } from "./telemetry/baggage";
13
- import { instrumentFetch } from "./telemetry/fetch";
14
- import { recordRequestDuration } from "./telemetry/gen-ai";
15
- import { getRequestAttributes, getResponseAttributes } from "./telemetry/http";
16
- import { recordV8jsMemory } from "./telemetry/memory";
17
- import { addSpanEvent, setSpanEventsEnabled, setSpanTracer, startSpan } from "./telemetry/span";
18
- import { wrapStream } from "./telemetry/stream";
19
- import { resolveOrCreateRequestId } from "./utils/request";
20
- import { prepareResponseInit, toResponse } from "./utils/response";
21
-
22
- export const winterCgHandler = (
23
- run: (ctx: GatewayContext) => Promise<object | ReadableStream<object>>,
24
- config: GatewayConfig,
25
- ) => {
26
- const parsedConfig = parseConfig(config);
27
-
28
- if (parsedConfig.telemetry?.enabled) {
29
- setSpanTracer(parsedConfig.telemetry?.tracer);
30
- setSpanEventsEnabled(parsedConfig.telemetry?.signals?.hebo);
31
- instrumentFetch(parsedConfig.telemetry?.signals?.hebo);
32
- }
33
-
34
- return async (request: Request, state?: Record<string, unknown>): Promise<Response> => {
35
- const start = performance.now();
36
- const ctx: GatewayContext = {
37
- request,
38
- state: state ?? {},
39
- providers: parsedConfig.providers,
40
- models: parsedConfig.models,
41
- requestId: resolveOrCreateRequestId(request),
42
- };
43
-
44
- const span = startSpan(ctx.request.url);
45
- span.setAttributes(getBaggageAttributes(ctx.request));
46
- if (!span.isExisting) {
47
- span.setAttributes(getRequestAttributes(ctx.request, parsedConfig.telemetry?.signals?.http));
48
- span.setAttributes({ "http.request.id": ctx.requestId });
49
- }
50
-
51
- const finalize = (status: number, reason?: unknown) => {
52
- if (ctx.operation) {
53
- span.updateName(`${ctx.operation}${ctx.modelId ? ` ${ctx.modelId}` : ""}`);
54
- }
55
-
56
- if (!span.isExisting) {
57
- // FUTURE add http.server.request.duration
58
- span.setAttributes(
59
- getResponseAttributes(ctx.response!, parsedConfig.telemetry?.signals?.http),
60
- );
61
- }
62
-
63
- let realStatus = status;
64
- if (ctx.request.signal.aborted) realStatus = 499;
65
- else if (status === 200 && ctx.response?.status) realStatus = ctx.response.status;
66
-
67
- if (realStatus !== 200) {
68
- logger[realStatus >= 500 ? "error" : "warn"]({
69
- requestId: ctx.requestId,
70
- err: reason ?? ctx.request.signal.reason,
71
- });
72
-
73
- span.recordError(reason);
74
- }
75
- span.setAttributes({ "http.response.status_code_effective": realStatus });
76
-
77
- if (ctx.operation === "chat" || ctx.operation === "embeddings") {
78
- recordRequestDuration(
79
- performance.now() - start,
80
- realStatus,
81
- ctx,
82
- parsedConfig.telemetry?.signals?.gen_ai,
83
- );
84
- }
85
-
86
- recordV8jsMemory(parsedConfig.telemetry?.signals?.hebo);
87
-
88
- span.finish();
89
- };
90
-
91
- try {
92
- if (parsedConfig.hooks?.onRequest) {
93
- const onRequest = await parsedConfig.hooks.onRequest(ctx as OnRequestHookContext);
94
- addSpanEvent("hebo.hooks.on_request.completed");
95
-
96
- if (onRequest instanceof Response) {
97
- ctx.response = onRequest;
98
- }
99
- }
100
-
101
- if (!ctx.response) {
102
- ctx.result = (await span.runWithContext(() => run(ctx))) as typeof ctx.result;
103
-
104
- if (ctx.result instanceof ReadableStream) {
105
- ctx.result = wrapStream(ctx.result, { onDone: finalize });
106
- }
107
-
108
- ctx.response = toResponse(ctx.result!, prepareResponseInit(ctx.requestId));
109
- }
110
-
111
- if (parsedConfig.hooks?.onResponse) {
112
- const onResponse = await parsedConfig.hooks.onResponse(ctx as OnResponseHookContext);
113
- addSpanEvent("hebo.hooks.on_response.completed");
114
- if (onResponse) {
115
- ctx.response = onResponse;
116
- }
117
- }
118
-
119
- // FUTURE: this can leak if onResponse removed wrapper from response.body
120
- if (!(ctx.result instanceof ReadableStream)) {
121
- finalize(ctx.response.status);
122
- }
123
- } catch (error) {
124
- ctx.response = toOpenAIErrorResponse(
125
- ctx.request.signal.aborted
126
- ? new GatewayError(error ?? ctx.request.signal.reason, 499)
127
- : error,
128
- prepareResponseInit(ctx.requestId),
129
- );
130
- finalize(ctx.response.status, error);
131
- }
132
-
133
- return ctx.response ?? new Response("Internal Server Error", { status: 500 });
134
- };
135
- };
@@ -1,105 +0,0 @@
1
- import type { LogFn, LogLevel, Logger } from "./index";
2
-
3
- import { isProduction, isTest } from "../utils/env";
4
-
5
- const getDefaultLogLevel = (): LogLevel =>
6
- isTest() ? "silent" : isProduction() ? "info" : "debug";
7
-
8
- const noop: LogFn = () => {};
9
-
10
- const LEVEL = {
11
- trace: 5,
12
- debug: 10,
13
- info: 20,
14
- warn: 30,
15
- error: 40,
16
- silent: 50,
17
- };
18
- const LEVELS = Object.keys(LEVEL) as (keyof typeof LEVEL)[];
19
-
20
- const isRecord = (value: unknown): value is Record<string, unknown> =>
21
- typeof value === "object" && value !== null && !(value instanceof Error);
22
-
23
- function serializeError(err: unknown, _seen?: WeakSet<object>): Record<string, unknown> {
24
- if (!(err instanceof Error)) return { message: String(err) };
25
-
26
- const seen = _seen ?? new WeakSet();
27
- if (seen.has(err)) return { name: err.name, message: err.message, circular: true };
28
- seen.add(err);
29
-
30
- const out: Record<string, unknown> = {};
31
-
32
- for (const k of Object.getOwnPropertyNames(err)) {
33
- if (k.startsWith("_")) continue;
34
-
35
- let val: unknown;
36
- try {
37
- val = (err as any)[k];
38
- } catch {
39
- val = "[Unreadable]";
40
- }
41
-
42
- if (typeof val === "bigint") val = `${val}n`;
43
-
44
- // FUTURE: check for circular references within val
45
- out[String(k)] = val instanceof Error ? serializeError(val, seen) : val;
46
- }
47
-
48
- return out;
49
- }
50
-
51
- const buildLogObject = (level: LogLevel, args: unknown[]): Record<string, unknown> => {
52
- if (args.length === 0) return {};
53
-
54
- const [first, second] = args;
55
-
56
- let obj: Record<string, unknown> | undefined;
57
- let err: Record<string, unknown> | undefined;
58
- let msg: string | undefined;
59
-
60
- if (first instanceof Error) {
61
- err = serializeError(first);
62
- } else if (isRecord(first)) {
63
- if (first["err"] !== undefined) {
64
- err = serializeError(first["err"]);
65
- delete first["err"];
66
- }
67
- obj = first;
68
- } else {
69
- msg = String(first);
70
- }
71
-
72
- if (second !== undefined) {
73
- msg = String(second);
74
- }
75
-
76
- if (err && msg === undefined) {
77
- msg = err["message"] as string;
78
- }
79
-
80
- return {
81
- level,
82
- time: Date.now(),
83
- ...(msg ? { msg } : {}),
84
- ...(err ? { err } : {}),
85
- ...obj,
86
- };
87
- };
88
-
89
- const makeLogFn =
90
- (level: LogLevel, write: (line: string) => void): LogFn =>
91
- (...args: unknown[]) =>
92
- write(JSON.stringify(buildLogObject(level, args)));
93
-
94
- export const createDefaultLogger = (config: { level?: LogLevel }): Logger => {
95
- if (config.level === "silent" || getDefaultLogLevel() === "silent") {
96
- return { trace: noop, debug: noop, info: noop, warn: noop, error: noop };
97
- }
98
-
99
- const threshold = LEVEL[config.level ?? getDefaultLogLevel()];
100
- const enabled = (lvl: keyof typeof LEVEL) => LEVEL[lvl] >= threshold;
101
-
102
- return Object.fromEntries(
103
- LEVELS.map((lvl) => [lvl, enabled(lvl) ? makeLogFn(lvl, console.log) : noop]),
104
- ) as Logger;
105
- };
@@ -1,42 +0,0 @@
1
- import { isTest } from "../utils/env";
2
-
3
- export type LogFn = {
4
- (msg: string): void;
5
- (obj: Record<string, unknown>, msg?: string): void;
6
- (err: Error, msg?: string): void;
7
- };
8
-
9
- export type Logger = Record<"trace" | "debug" | "info" | "warn" | "error", LogFn>;
10
- export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "silent";
11
- export type LoggerConfig = { level?: LogLevel };
12
-
13
- const KEY = Symbol.for("@hebo/logger");
14
- type GlobalWithLogger = typeof globalThis & {
15
- [KEY]?: Logger;
16
- };
17
- const g = globalThis as GlobalWithLogger;
18
-
19
- g[KEY] ??= {
20
- trace: () => {},
21
- debug: () => {},
22
- info: () => {},
23
- warn: () => {},
24
- error: () => {},
25
- };
26
-
27
- export let logger: Logger = g[KEY];
28
-
29
- export const isLogger = (input: Logger | LoggerConfig): input is Logger =>
30
- typeof input === "object" && input !== null && "info" in input;
31
-
32
- export function isLoggerDisabled(input?: Logger | LoggerConfig | null): boolean {
33
- if (isTest()) return true;
34
- if (input === null) return true;
35
- if (!input || typeof input !== "object" || "info" in input) return false;
36
- return input.level === "silent";
37
- }
38
-
39
- export function setLoggerInstance(next: Logger) {
40
- g[KEY] = next;
41
- logger = g[KEY];
42
- }
@@ -1,215 +0,0 @@
1
- import { MockLanguageModelV3 } from "ai/test";
2
- import { describe, expect, test } from "bun:test";
3
-
4
- import { extractProviderNamespace, forwardParamsMiddleware } from "./common";
5
-
6
- describe("extractProviderNamespace", () => {
7
- test("should handle Google Vertex AI (google.vertex -> vertex)", () => {
8
- expect(extractProviderNamespace("google.vertex.chat")).toBe("vertex");
9
- expect(extractProviderNamespace("google.vertex.embedding")).toBe("vertex");
10
- expect(extractProviderNamespace("google.vertex.image")).toBe("vertex");
11
- expect(extractProviderNamespace("google.vertex.video")).toBe("vertex");
12
- });
13
-
14
- test("should handle Google Generative AI (google.others -> google)", () => {
15
- expect(extractProviderNamespace("google.generative-ai.chat")).toBe("google");
16
- expect(extractProviderNamespace("google.generative-ai.embedding")).toBe("google");
17
- expect(extractProviderNamespace("google.generative-ai.image")).toBe("google");
18
- expect(extractProviderNamespace("google.generative-ai.video")).toBe("google");
19
- });
20
-
21
- test("should handle Amazon Bedrock special case", () => {
22
- expect(extractProviderNamespace("amazon-bedrock")).toBe("bedrock");
23
- });
24
-
25
- test("should handle OpenAI (default to first component)", () => {
26
- expect(extractProviderNamespace("openai.chat")).toBe("openai");
27
- expect(extractProviderNamespace("openai.embedding")).toBe("openai");
28
- });
29
-
30
- test("should handle Anthropic and its infrastructure variants", () => {
31
- expect(extractProviderNamespace("anthropic.messages")).toBe("anthropic");
32
- expect(extractProviderNamespace("vertex.anthropic.messages")).toBe("vertex");
33
- expect(extractProviderNamespace("bedrock.anthropic.messages")).toBe("bedrock");
34
- });
35
-
36
- test("should handle Azure (default to first component)", () => {
37
- expect(extractProviderNamespace("azure.chat")).toBe("azure");
38
- expect(extractProviderNamespace("azure.embedding")).toBe("azure");
39
- });
40
- });
41
-
42
- describe("forwardParamsMiddleware", () => {
43
- test("should snakize providerMetadata in generate output", async () => {
44
- const middleware = forwardParamsMiddleware("google.vertex.chat");
45
- const model = new MockLanguageModelV3({
46
- modelId: "google/gemini-2.5-flash",
47
- // oxlint-disable-next-line require-await
48
- doGenerate: async () => ({
49
- content: [{ type: "text", text: "hi" }],
50
- finishReason: "stop",
51
- usage: { promptTokens: 1, completionTokens: 1 },
52
- providerMetadata: {
53
- vertex: {
54
- thoughtSignature: "encrypted-signature",
55
- },
56
- },
57
- warnings: [],
58
- }),
59
- });
60
-
61
- const result = await middleware.wrapGenerate!({
62
- model,
63
- params: { prompt: [] },
64
- doGenerate: () => model.doGenerate({ prompt: [] }),
65
- doStream: () => model.doStream({ prompt: [] }),
66
- });
67
-
68
- expect(result.providerMetadata).toEqual({
69
- vertex: {
70
- thought_signature: "encrypted-signature",
71
- },
72
- });
73
- });
74
-
75
- test("should snakize providerMetadata in generate output content parts", async () => {
76
- const middleware = forwardParamsMiddleware("google.vertex.chat");
77
- const model = new MockLanguageModelV3({
78
- modelId: "google/gemini-2.5-flash",
79
- // oxlint-disable-next-line require-await
80
- doGenerate: async () => ({
81
- content: [
82
- {
83
- type: "text",
84
- text: "hi",
85
- providerMetadata: { vertex: { thoughtSignature: "part-sig" } },
86
- },
87
- ],
88
- finishReason: "stop",
89
- usage: { promptTokens: 1, completionTokens: 1 },
90
- warnings: [],
91
- }),
92
- });
93
-
94
- const result = await middleware.wrapGenerate!({
95
- model,
96
- params: { prompt: [] },
97
- doGenerate: () => model.doGenerate({ prompt: [] }),
98
- doStream: () => model.doStream({ prompt: [] }),
99
- });
100
-
101
- expect(result.content[0].providerMetadata).toEqual({
102
- vertex: {
103
- thought_signature: "part-sig",
104
- },
105
- });
106
- });
107
-
108
- test("should snakize providerMetadata in stream parts", async () => {
109
- const middleware = forwardParamsMiddleware("google.vertex.chat");
110
- const model = new MockLanguageModelV3({
111
- modelId: "google/gemini-2.5-flash",
112
- // oxlint-disable-next-line require-await
113
- doStream: async () => ({
114
- stream: new ReadableStream({
115
- start(controller) {
116
- controller.enqueue({
117
- type: "text-delta",
118
- id: "1",
119
- delta: "hi",
120
- providerMetadata: { vertex: { thoughtSignature: "part-signature" } },
121
- });
122
- controller.enqueue({
123
- type: "finish",
124
- finishReason: "stop",
125
- usage: { promptTokens: 1, completionTokens: 1 },
126
- });
127
- controller.close();
128
- },
129
- }),
130
- }),
131
- });
132
-
133
- const result = await middleware.wrapStream!({
134
- model,
135
- params: { prompt: [] },
136
- doGenerate: () => model.doGenerate({ prompt: [] }),
137
- doStream: () => model.doStream({ prompt: [] }),
138
- });
139
-
140
- const reader = result.stream.getReader();
141
- const part = await reader.read();
142
- expect(part.value.providerMetadata).toEqual({
143
- vertex: { thought_signature: "part-signature" },
144
- });
145
- });
146
-
147
- test("should camelize providerOptions on the way in", async () => {
148
- const middleware = forwardParamsMiddleware("google.vertex.chat");
149
- const params = {
150
- prompt: [],
151
- providerOptions: {
152
- vertex: { thought_signature: "in-signature" },
153
- },
154
- };
155
-
156
- const result = await middleware.transformParams!({
157
- type: "generate",
158
- params,
159
- model: new MockLanguageModelV3({ modelId: "google/gemini-2.5-flash" }),
160
- });
161
-
162
- expect(result.providerOptions).toEqual({
163
- vertex: { thoughtSignature: "in-signature" },
164
- });
165
- });
166
-
167
- test("should merge unknown.reasoning_effort into openai.reasoningEffort for gpt-5", async () => {
168
- const middleware = forwardParamsMiddleware("openai.chat");
169
- const params = {
170
- prompt: [],
171
- providerOptions: {
172
- unknown: { reasoning_effort: "low" },
173
- },
174
- };
175
-
176
- const result = await middleware.transformParams!({
177
- type: "generate",
178
- params,
179
- model: new MockLanguageModelV3({ modelId: "openai/gpt-5" }),
180
- });
181
-
182
- expect(result.providerOptions).toEqual({
183
- openai: { reasoningEffort: "low" },
184
- });
185
- expect(result.providerOptions!.unknown).toBeUndefined();
186
- });
187
-
188
- test("should merge all providerOptions into target providerName and reserve others", async () => {
189
- const middleware = forwardParamsMiddleware("anthropic.messages");
190
- const params = {
191
- prompt: [],
192
- providerOptions: {
193
- unknown: { some_option: "value1" },
194
- openai: { other_option: "value2" },
195
- anthropic: { existing_option: "value3" },
196
- },
197
- };
198
-
199
- const result = await middleware.transformParams!({
200
- type: "generate",
201
- params,
202
- model: new MockLanguageModelV3({ modelId: "anthropic/claude-3-5-sonnet" }),
203
- });
204
-
205
- expect(result.providerOptions).toEqual({
206
- anthropic: {
207
- someOption: "value1",
208
- otherOption: "value2",
209
- existingOption: "value3",
210
- },
211
- openai: { other_option: "value2" },
212
- });
213
- expect(result.providerOptions!.unknown).toBeUndefined();
214
- });
215
- });
@@ -1,163 +0,0 @@
1
- import type { JSONObject } from "@ai-sdk/provider";
2
- import type { EmbeddingModelMiddleware, LanguageModelMiddleware } from "ai";
3
-
4
- import type { ProviderId } from "../providers/types";
5
-
6
- function snakeToCamel(key: string): string {
7
- if (key.indexOf("_") === -1) return key;
8
-
9
- let out = "";
10
- for (let i = 0; i < key.length; i++) {
11
- const c = key[i]!;
12
- if (c === "_" && i + 1 < key.length) {
13
- const next = key[i + 1]!;
14
- if (next >= "a" && next <= "z") {
15
- out += next.toUpperCase();
16
- i++;
17
- continue;
18
- }
19
- }
20
- out += c;
21
- }
22
-
23
- return out;
24
- }
25
-
26
- function hasUppercase(s: string): boolean {
27
- for (let i = 0; i < s.length; i++) {
28
- const c = s[i]!;
29
- if (c >= "A" && c <= "Z") return true;
30
- }
31
- return false;
32
- }
33
-
34
- function camelToSnake(key: string): string {
35
- if (!hasUppercase(key)) return key;
36
-
37
- let out = "";
38
- for (let i = 0; i < key.length; i++) {
39
- const c = key[i]!;
40
- out += c >= "A" && c <= "Z" ? "_" + c.toLowerCase() : c;
41
- }
42
- return out;
43
- }
44
-
45
- function remapDeep(value: unknown, mapKey: (k: string) => string): unknown {
46
- if (value === null || typeof value !== "object") return value;
47
-
48
- if (Array.isArray(value)) {
49
- return value.map((v) => remapDeep(v, mapKey));
50
- }
51
-
52
- const out: Record<string, unknown> = {};
53
- for (const key of Object.keys(value)) {
54
- out[mapKey(key)] = remapDeep((value as Record<string, unknown>)[key], mapKey);
55
- }
56
- return out;
57
- }
58
-
59
- function processOptions(options: Record<string, JSONObject>, providerName: ProviderId) {
60
- const target = (options[providerName] = remapDeep(
61
- options[providerName] ?? {},
62
- snakeToCamel,
63
- ) as JSONObject) as Record<string, JSONObject>;
64
-
65
- for (const namespace in options) {
66
- if (namespace === providerName) continue;
67
- Object.assign(
68
- target,
69
- remapDeep(options[namespace], snakeToCamel) as Record<string, JSONObject>,
70
- );
71
- if (namespace === "unknown") delete options[namespace];
72
- }
73
- }
74
-
75
- function processMetadata(metadata: Record<string, JSONObject>) {
76
- for (const namespace in metadata) {
77
- metadata[namespace] = remapDeep(metadata[namespace], camelToSnake) as JSONObject;
78
- }
79
- }
80
-
81
- /**
82
- * Converts snake_case params in providerOptions to camelCase
83
- * and moves all of them into providerOptions[providerName].
84
- * Also snakizes values in providerMetadata for OpenAI compatibility.
85
- */
86
- export function forwardLanguageParams(providerName: ProviderId): LanguageModelMiddleware {
87
- return {
88
- specificationVersion: "v3",
89
- // oxlint-disable-next-line require-await
90
- transformParams: async ({ params }) => {
91
- if (params.providerOptions) processOptions(params.providerOptions, providerName);
92
-
93
- for (const message of params.prompt) {
94
- if (message.providerOptions) {
95
- processOptions(message.providerOptions, providerName);
96
- }
97
- if (message.content && Array.isArray(message.content)) {
98
- for (const part of message.content) {
99
- if ("providerOptions" in part && part.providerOptions) {
100
- processOptions(part.providerOptions as Record<string, JSONObject>, providerName);
101
- }
102
- }
103
- }
104
- }
105
-
106
- return params;
107
- },
108
- wrapGenerate: async ({ doGenerate }) => {
109
- const result = await doGenerate();
110
- if (result.providerMetadata) processMetadata(result.providerMetadata);
111
- result.content?.forEach((part) => {
112
- if (part.providerMetadata) processMetadata(part.providerMetadata);
113
- });
114
- return result;
115
- },
116
- wrapStream: async ({ doStream }) => {
117
- const result = await doStream();
118
- result.stream = result.stream.pipeThrough(
119
- new TransformStream({
120
- transform(part, controller) {
121
- if ("providerMetadata" in part && part.providerMetadata) {
122
- processMetadata(part.providerMetadata);
123
- }
124
- controller.enqueue(part);
125
- },
126
- }),
127
- );
128
- return result;
129
- },
130
- };
131
- }
132
-
133
- export function forwardEmbeddingParams(providerName: ProviderId): EmbeddingModelMiddleware {
134
- return {
135
- specificationVersion: "v3",
136
- // oxlint-disable-next-line require-await
137
- transformParams: async ({ params }) => {
138
- if (params.providerOptions) processOptions(params.providerOptions, providerName);
139
- return params;
140
- },
141
- wrapEmbed: async ({ doEmbed }) => {
142
- const result = await doEmbed();
143
- if (result.providerMetadata) processMetadata(result.providerMetadata);
144
- return result;
145
- },
146
- };
147
- }
148
-
149
- export function extractProviderNamespace(id: string): string {
150
- if (id === "amazon-bedrock") return "bedrock";
151
- const [first, second] = id.split(".");
152
- // FUTURE: map vertex to google once AI SDK support per-message level provider options
153
- if (first === "vertex" || second === "vertex") return "vertex";
154
- return first!;
155
- }
156
-
157
- export function forwardParamsMiddleware(provider: string): LanguageModelMiddleware {
158
- return forwardLanguageParams(extractProviderNamespace(provider));
159
- }
160
-
161
- export function forwardParamsEmbeddingMiddleware(provider: string): EmbeddingModelMiddleware {
162
- return forwardEmbeddingParams(extractProviderNamespace(provider));
163
- }
@@ -1,37 +0,0 @@
1
- import type { EmbeddingModelMiddleware, LanguageModelMiddleware } from "ai";
2
-
3
- import { logger } from "../logger";
4
-
5
- export const debugFinalParamsMiddleware: LanguageModelMiddleware = {
6
- specificationVersion: "v3",
7
- // oxlint-disable-next-line require-await
8
- transformParams: async ({ params, model }) => {
9
- logger.trace(
10
- {
11
- kind: "text",
12
- modelId: model.modelId,
13
- providerId: model.provider,
14
- params,
15
- },
16
- "[middleware] final params",
17
- );
18
- return params;
19
- },
20
- };
21
-
22
- export const debugEmbeddingFinalParamsMiddleware: EmbeddingModelMiddleware = {
23
- specificationVersion: "v3",
24
- // oxlint-disable-next-line require-await
25
- transformParams: async ({ params, model }) => {
26
- logger.trace(
27
- {
28
- kind: "embedding",
29
- modelId: model.modelId,
30
- providerId: model.provider,
31
- params,
32
- },
33
- "[middleware] final params",
34
- );
35
- return params;
36
- },
37
- };