@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
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
|
-
};
|
package/src/logger/default.ts
DELETED
|
@@ -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
|
-
};
|
package/src/logger/index.ts
DELETED
|
@@ -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
|
-
});
|
package/src/middleware/common.ts
DELETED
|
@@ -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
|
-
}
|
package/src/middleware/debug.ts
DELETED
|
@@ -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
|
-
};
|