@neutrome/open-ai-router 0.4.2 → 0.5.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/.dev.vars.example +2 -0
- package/README.md +5 -1
- package/package.json +5 -4
- package/src/admission/access.ts +0 -1
- package/src/admission/types.ts +0 -1
- package/src/app/factory.test.ts +40 -34
- package/src/app/factory.ts +120 -173
- package/src/app/google-genai-route.ts +48 -0
- package/src/app/http-utils.ts +14 -0
- package/src/app/model-listing.ts +29 -0
- package/src/app/response-lifecycle.ts +66 -0
- package/src/app/types.ts +38 -0
- package/src/index.ts +11 -43
- package/src/otlp.ts +148 -0
- package/src/router/audit.ts +5 -12
- package/src/router/config.ts +4 -4
- package/src/router/error-codec.ts +170 -0
- package/src/router/errors.ts +38 -1
- package/src/router/execute.test.ts +177 -107
- package/src/router/execute.ts +68 -924
- package/src/router/execution-events.ts +39 -0
- package/src/router/execution-invocation.ts +144 -0
- package/src/router/execution-observation.ts +161 -0
- package/src/router/execution-resolution.ts +120 -0
- package/src/router/execution-runtime.test.ts +156 -83
- package/src/router/execution-runtime.ts +144 -542
- package/src/router/execution-transforms.ts +82 -0
- package/src/router/execution-types.ts +8 -19
- package/src/router/index.ts +6 -5
- package/src/router/mcp.ts +12 -6
- package/src/router/provider-invoker.ts +134 -0
- package/src/router/provider-stream.ts +192 -0
- package/src/router/provider-telemetry.ts +59 -0
- package/src/router/remote-mcp-execution.ts +61 -0
- package/src/router/request-program.ts +16 -0
- package/src/router/request-target.ts +56 -0
- package/src/router/resolve.test.ts +43 -10
- package/src/router/resolve.ts +50 -28
- package/src/router/response-delivery.ts +141 -0
- package/src/router/runtime.test.ts +56 -0
- package/src/router/runtime.ts +79 -9
- package/src/router/targets.ts +3 -1
- package/src/router/upstream-client.ts +137 -0
- package/src/telemetry.test.ts +50 -0
- package/src/telemetry.ts +316 -0
- package/src/trace-labels.ts +29 -0
- package/src/upstream-auth/env.ts +15 -3
- package/src/upstream-auth/proxy.ts +6 -1
- package/src/upstream-auth/registry.ts +3 -1
- package/src/util/glob.ts +1 -1
- package/src/util/model-syntax.ts +3 -3
- package/src/worker.ts +12 -6
- package/worker-configuration.d.ts +195 -164
- package/pnpm-workspace.yaml +0 -4
- package/src/admission/jwt.test.ts +0 -96
- package/src/admission/jwt.ts +0 -221
- package/src/app/handlers.ts +0 -198
- package/src/example.test.ts +0 -377
- package/src/example.ts +0 -73
- package/src/observer.test.ts +0 -54
- package/src/observer.ts +0 -315
- package/src/timing.ts +0 -36
package/src/admission/jwt.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import type { AuthPrincipal, RuntimeAuthenticator } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
export const DEFAULT_RUNTIME_AUTH_ISSUER = "neutrome-platform";
|
|
4
|
-
export const DEFAULT_RUNTIME_AUTH_AUDIENCE = "workspace-runtime";
|
|
5
|
-
|
|
6
|
-
export type RuntimeJwtAuthConfig = {
|
|
7
|
-
jwksJson?: string;
|
|
8
|
-
issuer?: string;
|
|
9
|
-
audience?: string;
|
|
10
|
-
revokedKeyIdsJson?: string;
|
|
11
|
-
workspaceId?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type JsonWebKeySet = {
|
|
15
|
-
keys?: JsonWebKey[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type JwtHeader = {
|
|
19
|
-
alg?: unknown;
|
|
20
|
-
kid?: unknown;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type RuntimeJwtClaims = {
|
|
24
|
-
iss?: unknown;
|
|
25
|
-
aud?: unknown;
|
|
26
|
-
sub?: unknown;
|
|
27
|
-
jti?: unknown;
|
|
28
|
-
models?: unknown;
|
|
29
|
-
namespaces?: unknown;
|
|
30
|
-
exp?: unknown;
|
|
31
|
-
nbf?: unknown;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type RuntimeJwtEnv = Record<string, unknown> & {
|
|
35
|
-
LIL_RUNTIME_AUTH_JWKS_JSON?: unknown;
|
|
36
|
-
LIL_RUNTIME_AUTH_ISSUER?: unknown;
|
|
37
|
-
LIL_RUNTIME_AUTH_AUDIENCE?: unknown;
|
|
38
|
-
LIL_RUNTIME_REVOKED_KEY_IDS_JSON?: unknown;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const encoder = new TextEncoder();
|
|
42
|
-
const importedKeys = new Map<string, Promise<CryptoKey>>();
|
|
43
|
-
|
|
44
|
-
export function createRuntimeJwtAuthenticator(
|
|
45
|
-
config: RuntimeJwtAuthConfig = {},
|
|
46
|
-
): RuntimeAuthenticator {
|
|
47
|
-
return {
|
|
48
|
-
async authenticate(request, env) {
|
|
49
|
-
const token = readRuntimeToken(request);
|
|
50
|
-
if (!token) return null;
|
|
51
|
-
return verifyRuntimeJwt(token, readConfig(config, env));
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function verifyRuntimeJwt(
|
|
57
|
-
token: string,
|
|
58
|
-
config: Required<RuntimeJwtAuthConfig>,
|
|
59
|
-
): Promise<AuthPrincipal | null> {
|
|
60
|
-
const parts = token.split(".");
|
|
61
|
-
if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) return null;
|
|
62
|
-
|
|
63
|
-
const header = parseJsonPart<JwtHeader>(parts[0]);
|
|
64
|
-
const claims = parseJsonPart<RuntimeJwtClaims>(parts[1]);
|
|
65
|
-
if (!header || !claims || header.alg !== "ES256" || typeof header.kid !== "string") return null;
|
|
66
|
-
|
|
67
|
-
const key = findJwk(config.jwksJson, header.kid);
|
|
68
|
-
if (!key) return null;
|
|
69
|
-
|
|
70
|
-
const signature = base64UrlToBytes(parts[2]);
|
|
71
|
-
const signingInput = `${parts[0]}.${parts[1]}`;
|
|
72
|
-
const verified = await crypto.subtle.verify(
|
|
73
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
74
|
-
await importVerifyKey(key),
|
|
75
|
-
toArrayBuffer(derToJose(signature)),
|
|
76
|
-
toArrayBuffer(encoder.encode(signingInput)),
|
|
77
|
-
).catch(() => false);
|
|
78
|
-
if (!verified) return null;
|
|
79
|
-
|
|
80
|
-
const now = Math.floor(Date.now() / 1000);
|
|
81
|
-
if (claims.iss !== config.issuer) return null;
|
|
82
|
-
if (!audienceMatches(claims.aud, config.audience)) return null;
|
|
83
|
-
if (typeof claims.sub !== "string" || !claims.sub) return null;
|
|
84
|
-
if (typeof claims.jti !== "string" || !claims.jti) return null;
|
|
85
|
-
if (typeof claims.exp !== "number" || claims.exp <= now) return null;
|
|
86
|
-
if (typeof claims.nbf === "number" && claims.nbf > now) return null;
|
|
87
|
-
if (config.workspaceId && claims.sub !== config.workspaceId) return null;
|
|
88
|
-
if (revokedKeyIds(config.revokedKeyIdsJson).has(claims.jti)) return null;
|
|
89
|
-
|
|
90
|
-
const allowedModels = readStringArrayClaim(claims.models);
|
|
91
|
-
const allowedNamespaces = readStringArrayClaim(claims.namespaces);
|
|
92
|
-
if (allowedModels === undefined || allowedNamespaces === undefined) return null;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
workspaceId: claims.sub,
|
|
96
|
-
keyId: claims.jti,
|
|
97
|
-
allowedModels,
|
|
98
|
-
allowedNamespaces,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function readRuntimeToken(request: Request): string | null {
|
|
103
|
-
const authHeader = request.headers.get("authorization");
|
|
104
|
-
const bearerMatch = authHeader?.match(/^Bearer\s+(.+)$/i);
|
|
105
|
-
if (bearerMatch?.[1]) return bearerMatch[1];
|
|
106
|
-
|
|
107
|
-
const headerToken = request.headers.get("x-api-key")
|
|
108
|
-
?? request.headers.get("x-goog-api-key");
|
|
109
|
-
if (headerToken) return headerToken;
|
|
110
|
-
|
|
111
|
-
const url = new URL(request.url);
|
|
112
|
-
return url.searchParams.get("key") ?? url.searchParams.get("api_key");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function readConfig(
|
|
116
|
-
config: RuntimeJwtAuthConfig,
|
|
117
|
-
env: Record<string, unknown>,
|
|
118
|
-
): Required<RuntimeJwtAuthConfig> {
|
|
119
|
-
const runtimeEnv = env as RuntimeJwtEnv;
|
|
120
|
-
return {
|
|
121
|
-
jwksJson: config.jwksJson ?? stringBinding(runtimeEnv.LIL_RUNTIME_AUTH_JWKS_JSON) ?? "",
|
|
122
|
-
issuer: config.issuer ?? stringBinding(runtimeEnv.LIL_RUNTIME_AUTH_ISSUER) ?? DEFAULT_RUNTIME_AUTH_ISSUER,
|
|
123
|
-
audience: config.audience ?? stringBinding(runtimeEnv.LIL_RUNTIME_AUTH_AUDIENCE) ?? DEFAULT_RUNTIME_AUTH_AUDIENCE,
|
|
124
|
-
revokedKeyIdsJson: config.revokedKeyIdsJson ?? stringBinding(runtimeEnv.LIL_RUNTIME_REVOKED_KEY_IDS_JSON) ?? "",
|
|
125
|
-
workspaceId: config.workspaceId ?? "",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function stringBinding(value: unknown): string | undefined {
|
|
130
|
-
return typeof value === "string" && value ? value : undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function findJwk(jwksJson: string, kid: string): JsonWebKey | null {
|
|
134
|
-
try {
|
|
135
|
-
const parsed = JSON.parse(jwksJson) as JsonWebKeySet;
|
|
136
|
-
const key = parsed.keys?.find((item) => (item as { kid?: unknown }).kid === kid);
|
|
137
|
-
return key ?? null;
|
|
138
|
-
} catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function revokedKeyIds(value: string): Set<string> {
|
|
144
|
-
try {
|
|
145
|
-
const parsed = JSON.parse(value) as unknown;
|
|
146
|
-
return new Set(Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : []);
|
|
147
|
-
} catch {
|
|
148
|
-
return new Set();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function audienceMatches(value: unknown, expected: string): boolean {
|
|
153
|
-
return value === expected || (Array.isArray(value) && value.includes(expected));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function readStringArrayClaim(value: unknown): readonly string[] | null | undefined {
|
|
157
|
-
if (value === undefined || value === null) return null;
|
|
158
|
-
if (!Array.isArray(value)) return undefined;
|
|
159
|
-
return value.every((item) => typeof item === "string") ? value : undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function parseJsonPart<T>(part: string): T | null {
|
|
163
|
-
try {
|
|
164
|
-
return JSON.parse(new TextDecoder().decode(base64UrlToBytes(part))) as T;
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function importVerifyKey(jwk: JsonWebKey): Promise<CryptoKey> {
|
|
171
|
-
const cacheKey = JSON.stringify(jwk);
|
|
172
|
-
let imported = importedKeys.get(cacheKey);
|
|
173
|
-
if (!imported) {
|
|
174
|
-
imported = crypto.subtle.importKey(
|
|
175
|
-
"jwk",
|
|
176
|
-
jwk,
|
|
177
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
178
|
-
false,
|
|
179
|
-
["verify"],
|
|
180
|
-
);
|
|
181
|
-
importedKeys.set(cacheKey, imported);
|
|
182
|
-
}
|
|
183
|
-
return imported;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function base64UrlToBytes(value: string): Uint8Array {
|
|
187
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
188
|
-
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
189
|
-
const binary = atob(padded);
|
|
190
|
-
const bytes = new Uint8Array(binary.length);
|
|
191
|
-
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
192
|
-
return bytes;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
196
|
-
return bytes.buffer.slice(
|
|
197
|
-
bytes.byteOffset,
|
|
198
|
-
bytes.byteOffset + bytes.byteLength,
|
|
199
|
-
) as ArrayBuffer;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function derToJose(signature: Uint8Array): Uint8Array {
|
|
203
|
-
if (signature.byteLength === 64) return signature;
|
|
204
|
-
const bytes = [...signature];
|
|
205
|
-
if (bytes[0] !== 0x30) return signature;
|
|
206
|
-
let offset = 2;
|
|
207
|
-
if (bytes[1] === 0x81) offset = 3;
|
|
208
|
-
if (bytes[offset] !== 0x02) return signature;
|
|
209
|
-
const rLength = bytes[offset + 1] ?? 0;
|
|
210
|
-
const r = bytes.slice(offset + 2, offset + 2 + rLength);
|
|
211
|
-
offset += 2 + rLength;
|
|
212
|
-
if (bytes[offset] !== 0x02) return signature;
|
|
213
|
-
const sLength = bytes[offset + 1] ?? 0;
|
|
214
|
-
const s = bytes.slice(offset + 2, offset + 2 + sLength);
|
|
215
|
-
return new Uint8Array([...normalizeInteger(r), ...normalizeInteger(s)]);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function normalizeInteger(value: number[]): number[] {
|
|
219
|
-
const trimmed = value[0] === 0 ? value.slice(1) : value;
|
|
220
|
-
return [...new Array(Math.max(0, 32 - trimmed.length)).fill(0), ...trimmed].slice(-32);
|
|
221
|
-
}
|
package/src/app/handlers.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import type { Context } from "hono";
|
|
2
|
-
import type { RequestContext } from "../upstream-auth/types.ts";
|
|
3
|
-
import {
|
|
4
|
-
handleAnthropicMessages,
|
|
5
|
-
handleChatCompletions,
|
|
6
|
-
handleGoogleGenAI,
|
|
7
|
-
handleResponses,
|
|
8
|
-
listConfiguredModels,
|
|
9
|
-
type RouterExecutionOptions,
|
|
10
|
-
type RouterRuntime,
|
|
11
|
-
} from "../router/index.ts";
|
|
12
|
-
|
|
13
|
-
type OpenAiModel = {
|
|
14
|
-
id: string;
|
|
15
|
-
object: "model";
|
|
16
|
-
created: number;
|
|
17
|
-
owned_by: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type ChatCompletionsHandlerOptions = RouterExecutionOptions;
|
|
21
|
-
|
|
22
|
-
export type ResponsesHandlerOptions = ChatCompletionsHandlerOptions;
|
|
23
|
-
export type AnthropicMessagesHandlerOptions = ChatCompletionsHandlerOptions;
|
|
24
|
-
export type GoogleGenAIHandlerOptions = ChatCompletionsHandlerOptions;
|
|
25
|
-
|
|
26
|
-
export function listModelsHandler(runtime: RouterRuntime) {
|
|
27
|
-
return (c: Context) => {
|
|
28
|
-
const modelIds = listAvailableModels(runtime);
|
|
29
|
-
const models: OpenAiModel[] = modelIds.map((id) => ({
|
|
30
|
-
id,
|
|
31
|
-
object: "model",
|
|
32
|
-
created: 0,
|
|
33
|
-
owned_by: ownerFromModelId(id),
|
|
34
|
-
}));
|
|
35
|
-
return c.json({ object: "list", data: models });
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function listAvailableModels(runtime: RouterRuntime): string[] {
|
|
40
|
-
return listConfiguredModels(runtime);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function chatCompletionsHandler(
|
|
44
|
-
runtime: RouterRuntime,
|
|
45
|
-
options: ChatCompletionsHandlerOptions = {},
|
|
46
|
-
) {
|
|
47
|
-
return async (c: Context) => {
|
|
48
|
-
const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
|
|
49
|
-
return handleChatCompletions(handlerOptions);
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function responsesHandler(
|
|
54
|
-
runtime: RouterRuntime,
|
|
55
|
-
options: ResponsesHandlerOptions = {},
|
|
56
|
-
) {
|
|
57
|
-
return async (c: Context) => {
|
|
58
|
-
const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
|
|
59
|
-
return handleResponses(handlerOptions);
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function anthropicMessagesHandler(
|
|
64
|
-
runtime: RouterRuntime,
|
|
65
|
-
options: AnthropicMessagesHandlerOptions = {},
|
|
66
|
-
) {
|
|
67
|
-
return async (c: Context) => {
|
|
68
|
-
const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
|
|
69
|
-
return handleAnthropicMessages(handlerOptions);
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function googleGenAIHandler(
|
|
74
|
-
runtime: RouterRuntime,
|
|
75
|
-
options: GoogleGenAIHandlerOptions = {},
|
|
76
|
-
) {
|
|
77
|
-
return async (c: Context) => {
|
|
78
|
-
const route = parseGoogleGenAIRoute(c.req.raw);
|
|
79
|
-
if (!route) {
|
|
80
|
-
return c.text("Not Found", 404);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const request = await rewriteGoogleGenAIRequest(c.req.raw, route);
|
|
84
|
-
const handlerOptions = createExecutionHandlerOptions(
|
|
85
|
-
c,
|
|
86
|
-
runtime,
|
|
87
|
-
options,
|
|
88
|
-
request,
|
|
89
|
-
) as Parameters<typeof handleGoogleGenAI>[0];
|
|
90
|
-
if (route.stream) {
|
|
91
|
-
handlerOptions.forceStreaming = true;
|
|
92
|
-
}
|
|
93
|
-
return handleGoogleGenAI(handlerOptions);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function createExecutionHandlerOptions(
|
|
98
|
-
c: Context,
|
|
99
|
-
runtime: RouterRuntime,
|
|
100
|
-
options: ChatCompletionsHandlerOptions,
|
|
101
|
-
request = c.req.raw,
|
|
102
|
-
) {
|
|
103
|
-
const ctx = createRequestContext(c, request);
|
|
104
|
-
collectIncomingAuth(ctx, runtime);
|
|
105
|
-
const handlerOptions = {
|
|
106
|
-
runtime,
|
|
107
|
-
ctx,
|
|
108
|
-
} as Parameters<typeof handleChatCompletions>[0];
|
|
109
|
-
|
|
110
|
-
if (options.executorImplementations) {
|
|
111
|
-
handlerOptions.executorImplementations = options.executorImplementations;
|
|
112
|
-
}
|
|
113
|
-
if (options.resolveExecutor) {
|
|
114
|
-
handlerOptions.resolveExecutor = options.resolveExecutor;
|
|
115
|
-
}
|
|
116
|
-
if (options.transforms) {
|
|
117
|
-
handlerOptions.transforms = options.transforms;
|
|
118
|
-
}
|
|
119
|
-
if (options.observe) {
|
|
120
|
-
handlerOptions.observe = options.observe;
|
|
121
|
-
}
|
|
122
|
-
if (options.requestIdFactory) {
|
|
123
|
-
handlerOptions.requestIdFactory = options.requestIdFactory;
|
|
124
|
-
}
|
|
125
|
-
if (options.executionIdFactory) {
|
|
126
|
-
handlerOptions.executionIdFactory = options.executionIdFactory;
|
|
127
|
-
}
|
|
128
|
-
if (options.upstreamTimeoutMs !== undefined) {
|
|
129
|
-
handlerOptions.upstreamTimeoutMs = options.upstreamTimeoutMs;
|
|
130
|
-
}
|
|
131
|
-
if (options.remoteMcpClientFactory) {
|
|
132
|
-
handlerOptions.remoteMcpClientFactory = options.remoteMcpClientFactory;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return handlerOptions;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function createRequestContext(c: Context, request = c.req.raw): RequestContext {
|
|
139
|
-
return {
|
|
140
|
-
request,
|
|
141
|
-
incomingAuth: new Map(),
|
|
142
|
-
env: (c.env ?? {}) as Record<string, unknown>,
|
|
143
|
-
state: new Map(),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function collectIncomingAuth(ctx: RequestContext, runtime: RouterRuntime): void {
|
|
148
|
-
for (const driver of runtime.upstreamAuthChain) {
|
|
149
|
-
driver.collectIncoming?.(ctx);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function ownerFromModelId(id: string): string {
|
|
154
|
-
const slash = id.indexOf("/");
|
|
155
|
-
if (slash <= 0) {
|
|
156
|
-
return "unknown";
|
|
157
|
-
}
|
|
158
|
-
return id.slice(0, slash);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function parseGoogleGenAIRoute(request: Request): { model: string; stream: boolean } | null {
|
|
162
|
-
const pathname = new URL(request.url).pathname;
|
|
163
|
-
const match = pathname.match(/^\/v1\/models\/(.+):(generateContent|streamGenerateContent)$/);
|
|
164
|
-
if (!match?.[1] || !match[2]) {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
return {
|
|
170
|
-
model: decodeURIComponent(match[1]),
|
|
171
|
-
stream: match[2] === "streamGenerateContent",
|
|
172
|
-
};
|
|
173
|
-
} catch {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function rewriteGoogleGenAIRequest(
|
|
179
|
-
request: Request,
|
|
180
|
-
route: { model: string; stream: boolean },
|
|
181
|
-
): Promise<Request> {
|
|
182
|
-
const bodyText = await request.text();
|
|
183
|
-
const headers = new Headers(request.headers);
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const raw = JSON.parse(bodyText) as Record<string, unknown>;
|
|
187
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
188
|
-
const body = JSON.stringify({
|
|
189
|
-
...raw,
|
|
190
|
-
model: route.model,
|
|
191
|
-
...(route.stream ? { stream: true } : {}),
|
|
192
|
-
});
|
|
193
|
-
return new Request(request.url, { method: request.method, headers, body });
|
|
194
|
-
}
|
|
195
|
-
} catch {}
|
|
196
|
-
|
|
197
|
-
return new Request(request.url, { method: request.method, headers, body: bodyText });
|
|
198
|
-
}
|