@velum-labs/routekit-gateway 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/bedrock-source.d.ts +22 -0
- package/dist/bedrock-source.js +429 -0
- package/dist/endpoint-health.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/provider-source.d.ts +2 -2
- package/dist/provider-source.js +10 -0
- package/dist/router.d.ts +44 -3
- package/dist/router.js +134 -33
- package/dist/test/bedrock-source.test.d.ts +1 -0
- package/dist/test/bedrock-source.test.js +298 -0
- package/dist/test/boundary.test.js +13 -3
- package/dist/test/provenance.test.js +1 -1
- package/dist/test/router.test.js +180 -27
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -24,5 +24,4 @@ import {
|
|
|
24
24
|
API-key providers use registry-defined credentials and URLs. Multi-account
|
|
25
25
|
subscription providers and relays are in `@velum-labs/routekit-accounts`; they expose the
|
|
26
26
|
same source interface with per-model account eligibility and quota-aware
|
|
27
|
-
selection.
|
|
28
|
-
Product-specific orchestration is in `@fusionkit/gateway`.
|
|
27
|
+
selection. RouteKit hosts wire this package into the singleton daemon gateway.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BedrockClient } from "@aws-sdk/client-bedrock";
|
|
2
|
+
import { BedrockRuntimeClient, type ConverseCommandInput, type ConverseCommandOutput } from "@aws-sdk/client-bedrock-runtime";
|
|
3
|
+
import type { BackendRequestOptions } from "./backend.js";
|
|
4
|
+
import type { DiscoveredModel, ProviderSource } from "./provider-source.js";
|
|
5
|
+
export type BedrockControlClient = Pick<BedrockClient, "send">;
|
|
6
|
+
export type BedrockRuntime = Pick<BedrockRuntimeClient, "send">;
|
|
7
|
+
export type BedrockProviderSourceOptions = {
|
|
8
|
+
controlClient?: BedrockControlClient;
|
|
9
|
+
runtimeClient?: BedrockRuntime;
|
|
10
|
+
};
|
|
11
|
+
export declare function toBedrockConverseInput(body: unknown): ConverseCommandInput;
|
|
12
|
+
export declare function fromBedrockConverseOutput(output: ConverseCommandOutput, model: string): Record<string, unknown>;
|
|
13
|
+
export declare class BedrockProviderSource implements ProviderSource {
|
|
14
|
+
#private;
|
|
15
|
+
readonly sourceId: "bedrock";
|
|
16
|
+
constructor(options?: BedrockProviderSourceOptions);
|
|
17
|
+
discoverModels(signal?: AbortSignal): Promise<readonly DiscoveredModel[]>;
|
|
18
|
+
chat(body: unknown, signal?: AbortSignal, _options?: BackendRequestOptions): Promise<Response>;
|
|
19
|
+
embeddings(): Promise<Response>;
|
|
20
|
+
reasoningCapabilities(): DiscoveredModel["reasoning"];
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from "@aws-sdk/client-bedrock";
|
|
2
|
+
import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
3
|
+
import { randomId } from "@velum-labs/routekit-runtime";
|
|
4
|
+
import { anthropicReasoningDetailsOf, reasoningSelectionOf, withoutRouteKitExtensions } from "./adapters/openai-chat-wire.js";
|
|
5
|
+
function record(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
function anthropicFoundationModel(model) {
|
|
11
|
+
return model.providerName?.toLowerCase() === "anthropic" &&
|
|
12
|
+
model.modelLifecycle?.status === "ACTIVE" &&
|
|
13
|
+
typeof model.modelId === "string" && model.modelId.length > 0;
|
|
14
|
+
}
|
|
15
|
+
function foundationIdFromArn(arn) {
|
|
16
|
+
if (arn === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const marker = ":foundation-model/";
|
|
19
|
+
const index = arn.indexOf(marker);
|
|
20
|
+
return index < 0 ? undefined : arn.slice(index + marker.length);
|
|
21
|
+
}
|
|
22
|
+
function activeAnthropicProfile(profile, anthropicModels) {
|
|
23
|
+
return profile.status === "ACTIVE" &&
|
|
24
|
+
typeof profile.inferenceProfileId === "string" && profile.inferenceProfileId.length > 0 &&
|
|
25
|
+
(profile.models ?? []).some((model) => {
|
|
26
|
+
const id = foundationIdFromArn(model.modelArn);
|
|
27
|
+
return id !== undefined && anthropicModels.has(id);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function textParts(content) {
|
|
31
|
+
if (typeof content === "string")
|
|
32
|
+
return content;
|
|
33
|
+
if (!Array.isArray(content))
|
|
34
|
+
return "";
|
|
35
|
+
return content.flatMap((part) => {
|
|
36
|
+
const item = record(part);
|
|
37
|
+
return typeof item?.text === "string" ? [item.text] : [];
|
|
38
|
+
}).join("");
|
|
39
|
+
}
|
|
40
|
+
function imageBlock(part) {
|
|
41
|
+
const imageUrl = typeof part.image_url === "string" ? part.image_url : record(part.image_url)?.url;
|
|
42
|
+
if (typeof imageUrl !== "string")
|
|
43
|
+
return undefined;
|
|
44
|
+
const match = /^data:image\/(png|jpeg|gif|webp);base64,(.+)$/s.exec(imageUrl);
|
|
45
|
+
if (match === null)
|
|
46
|
+
return undefined;
|
|
47
|
+
return {
|
|
48
|
+
image: {
|
|
49
|
+
format: match[1],
|
|
50
|
+
source: { bytes: Uint8Array.from(Buffer.from(match[2], "base64")) }
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function bedrockReasoningBlocks(details) {
|
|
55
|
+
return anthropicReasoningDetailsOf(details, "message")
|
|
56
|
+
.sort((left, right) => left.index - right.index)
|
|
57
|
+
.map((detail) => detail.type === "redacted_thinking"
|
|
58
|
+
? { reasoningContent: { redactedContent: Uint8Array.from(Buffer.from(detail.data, "base64")) } }
|
|
59
|
+
: { reasoningContent: { reasoningText: {
|
|
60
|
+
text: detail.thinking ?? "",
|
|
61
|
+
...(detail.signature !== undefined ? { signature: detail.signature } : {})
|
|
62
|
+
} } });
|
|
63
|
+
}
|
|
64
|
+
function messageContent(message) {
|
|
65
|
+
const blocks = message.role === "assistant"
|
|
66
|
+
? bedrockReasoningBlocks(message.reasoning_details)
|
|
67
|
+
: [];
|
|
68
|
+
if (typeof message.content === "string") {
|
|
69
|
+
if (message.content.length > 0)
|
|
70
|
+
blocks.push({ text: message.content });
|
|
71
|
+
}
|
|
72
|
+
else if (Array.isArray(message.content)) {
|
|
73
|
+
for (const value of message.content) {
|
|
74
|
+
const part = record(value);
|
|
75
|
+
if (part === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
if (part.type === "image_url") {
|
|
78
|
+
const image = imageBlock(part);
|
|
79
|
+
if (image !== undefined)
|
|
80
|
+
blocks.push(image);
|
|
81
|
+
}
|
|
82
|
+
else if (typeof part.text === "string" && part.text.length > 0) {
|
|
83
|
+
blocks.push({ text: part.text });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const call of message.tool_calls ?? []) {
|
|
88
|
+
let input = {};
|
|
89
|
+
try {
|
|
90
|
+
input = JSON.parse(call.function?.arguments ?? "{}");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
input = { raw: call.function?.arguments ?? "" };
|
|
94
|
+
}
|
|
95
|
+
blocks.push({ toolUse: {
|
|
96
|
+
toolUseId: call.id ?? randomId(12, "toolu_"),
|
|
97
|
+
name: call.function?.name ?? "tool",
|
|
98
|
+
input: input
|
|
99
|
+
} });
|
|
100
|
+
}
|
|
101
|
+
return blocks;
|
|
102
|
+
}
|
|
103
|
+
function bedrockMessages(body) {
|
|
104
|
+
const systemText = (body.messages ?? []).filter((message) => message.role === "system")
|
|
105
|
+
.map((message) => textParts(message.content)).filter((text) => text.length > 0);
|
|
106
|
+
const messages = [];
|
|
107
|
+
const sourceMessages = body.messages ?? [];
|
|
108
|
+
for (let index = 0; index < sourceMessages.length; index += 1) {
|
|
109
|
+
const message = sourceMessages[index];
|
|
110
|
+
if (message.role === "system")
|
|
111
|
+
continue;
|
|
112
|
+
if (message.role === "tool") {
|
|
113
|
+
const content = [];
|
|
114
|
+
let cursor = index;
|
|
115
|
+
while (cursor < sourceMessages.length && sourceMessages[cursor]?.role === "tool") {
|
|
116
|
+
const toolMessage = sourceMessages[cursor];
|
|
117
|
+
content.push({ toolResult: {
|
|
118
|
+
toolUseId: toolMessage.tool_call_id ?? "",
|
|
119
|
+
content: [{ text: textParts(toolMessage.content) }]
|
|
120
|
+
} });
|
|
121
|
+
cursor += 1;
|
|
122
|
+
}
|
|
123
|
+
messages.push({ role: "user", content });
|
|
124
|
+
index = cursor - 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
128
|
+
continue;
|
|
129
|
+
const content = messageContent(message);
|
|
130
|
+
if (content.length > 0)
|
|
131
|
+
messages.push({ role: message.role, content });
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...(systemText.length > 0 ? { system: systemText.map((text) => ({ text })) } : {}),
|
|
135
|
+
messages
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function bedrockTools(body) {
|
|
139
|
+
const tools = (body.tools ?? []).flatMap((tool) => {
|
|
140
|
+
const fn = tool.function;
|
|
141
|
+
if (tool.type !== "function" || typeof fn?.name !== "string")
|
|
142
|
+
return [];
|
|
143
|
+
return [{ toolSpec: {
|
|
144
|
+
name: fn.name,
|
|
145
|
+
...(typeof fn.description === "string" ? { description: fn.description } : {}),
|
|
146
|
+
inputSchema: { json: (record(fn.parameters) ?? {}) }
|
|
147
|
+
} }];
|
|
148
|
+
});
|
|
149
|
+
return tools.length > 0 ? tools : undefined;
|
|
150
|
+
}
|
|
151
|
+
function toolChoice(value) {
|
|
152
|
+
if (value === "required")
|
|
153
|
+
return { any: {} };
|
|
154
|
+
const name = record(record(value)?.function)?.name;
|
|
155
|
+
if (typeof name === "string")
|
|
156
|
+
return { tool: { name } };
|
|
157
|
+
return { auto: {} };
|
|
158
|
+
}
|
|
159
|
+
export function toBedrockConverseInput(body) {
|
|
160
|
+
const canonical = withoutRouteKitExtensions(body);
|
|
161
|
+
if (typeof canonical.model !== "string" || canonical.model.length === 0)
|
|
162
|
+
throw new Error("Bedrock chat requires a model");
|
|
163
|
+
const translated = bedrockMessages(canonical);
|
|
164
|
+
const tools = canonical.tool_choice === "none" ? undefined : bedrockTools(canonical);
|
|
165
|
+
const maxTokens = typeof canonical.max_completion_tokens === "number" ? canonical.max_completion_tokens : canonical.max_tokens;
|
|
166
|
+
const stops = typeof canonical.stop === "string" ? [canonical.stop] : Array.isArray(canonical.stop)
|
|
167
|
+
? canonical.stop.filter((value) => typeof value === "string") : undefined;
|
|
168
|
+
const selection = reasoningSelectionOf(body);
|
|
169
|
+
const thinking = selection.mode === "budget"
|
|
170
|
+
? { thinking: { type: "enabled", budget_tokens: selection.budgetTokens } }
|
|
171
|
+
: undefined;
|
|
172
|
+
const additionalModelRequestFields = thinking !== undefined || typeof canonical.top_k === "number"
|
|
173
|
+
? {
|
|
174
|
+
...thinking,
|
|
175
|
+
...(typeof canonical.top_k === "number" ? { top_k: canonical.top_k } : {})
|
|
176
|
+
}
|
|
177
|
+
: undefined;
|
|
178
|
+
return {
|
|
179
|
+
modelId: canonical.model,
|
|
180
|
+
...translated,
|
|
181
|
+
...(tools !== undefined ? { toolConfig: { tools, toolChoice: toolChoice(canonical.tool_choice) } } : {}),
|
|
182
|
+
...(typeof maxTokens === "number" || typeof canonical.temperature === "number" ||
|
|
183
|
+
typeof canonical.top_p === "number" || stops !== undefined ? { inferenceConfig: {
|
|
184
|
+
...(typeof maxTokens === "number" ? { maxTokens } : {}),
|
|
185
|
+
...(typeof canonical.temperature === "number" ? { temperature: canonical.temperature } : {}),
|
|
186
|
+
...(typeof canonical.top_p === "number" ? { topP: canonical.top_p } : {}),
|
|
187
|
+
...(stops !== undefined ? { stopSequences: stops } : {})
|
|
188
|
+
} } : {}),
|
|
189
|
+
...(additionalModelRequestFields !== undefined ? { additionalModelRequestFields } : {})
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function finishReason(reason) {
|
|
193
|
+
switch (reason) {
|
|
194
|
+
case "max_tokens": return "length";
|
|
195
|
+
case "tool_use": return "tool_calls";
|
|
196
|
+
case "content_filtered":
|
|
197
|
+
case "guardrail_intervened": return "content_filter";
|
|
198
|
+
default: return "stop";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function usage(value) {
|
|
202
|
+
if (value === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
const prompt = value.inputTokens ?? 0;
|
|
205
|
+
const completion = value.outputTokens ?? 0;
|
|
206
|
+
return { prompt_tokens: prompt, completion_tokens: completion, total_tokens: value.totalTokens ?? prompt + completion };
|
|
207
|
+
}
|
|
208
|
+
function outputMessage(output) {
|
|
209
|
+
const content = output.output?.message?.content ?? [];
|
|
210
|
+
let text = "";
|
|
211
|
+
let reasoning = "";
|
|
212
|
+
const reasoningDetails = [];
|
|
213
|
+
const toolCalls = [];
|
|
214
|
+
for (const [index, block] of content.entries()) {
|
|
215
|
+
if ("text" in block && typeof block.text === "string")
|
|
216
|
+
text += block.text;
|
|
217
|
+
if ("toolUse" in block && block.toolUse !== undefined)
|
|
218
|
+
toolCalls.push({
|
|
219
|
+
id: block.toolUse.toolUseId, type: "function",
|
|
220
|
+
function: { name: block.toolUse.name, arguments: JSON.stringify(block.toolUse.input ?? {}) }
|
|
221
|
+
});
|
|
222
|
+
if ("reasoningContent" in block && block.reasoningContent !== undefined) {
|
|
223
|
+
if ("reasoningText" in block.reasoningContent && block.reasoningContent.reasoningText !== undefined) {
|
|
224
|
+
const reasoningText = block.reasoningContent.reasoningText;
|
|
225
|
+
reasoning += reasoningText.text ?? "";
|
|
226
|
+
reasoningDetails.push({
|
|
227
|
+
type: "thinking",
|
|
228
|
+
index,
|
|
229
|
+
thinking: reasoningText.text ?? "",
|
|
230
|
+
signature: reasoningText.signature ?? ""
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else if ("redactedContent" in block.reasoningContent && block.reasoningContent.redactedContent !== undefined) {
|
|
234
|
+
reasoningDetails.push({
|
|
235
|
+
type: "redacted_thinking",
|
|
236
|
+
index,
|
|
237
|
+
data: Buffer.from(block.reasoningContent.redactedContent).toString("base64")
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { role: "assistant", content: text,
|
|
243
|
+
...(reasoning.length > 0 ? { reasoning } : {}),
|
|
244
|
+
...(reasoningDetails.length > 0 ? { reasoning_details: reasoningDetails } : {}),
|
|
245
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}) };
|
|
246
|
+
}
|
|
247
|
+
export function fromBedrockConverseOutput(output, model) {
|
|
248
|
+
return {
|
|
249
|
+
id: output.$metadata.requestId ?? randomId(18, "chatcmpl_"), object: "chat.completion",
|
|
250
|
+
created: Math.floor(Date.now() / 1000), model,
|
|
251
|
+
choices: [{ index: 0, message: outputMessage(output), finish_reason: finishReason(output.stopReason) }],
|
|
252
|
+
...(usage(output.usage) !== undefined ? { usage: usage(output.usage) } : {})
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function streamError(event) {
|
|
256
|
+
for (const key of ["internalServerException", "modelStreamErrorException", "validationException", "throttlingException", "serviceUnavailableException"]) {
|
|
257
|
+
const value = event[key];
|
|
258
|
+
if (value !== undefined)
|
|
259
|
+
return new Error(value.message ?? `Bedrock stream ${key}`);
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
function sse(data) {
|
|
264
|
+
return new TextEncoder().encode(`data: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`);
|
|
265
|
+
}
|
|
266
|
+
function streamResponse(stream, model, signal) {
|
|
267
|
+
const id = randomId(18, "chatcmpl_");
|
|
268
|
+
const created = Math.floor(Date.now() / 1000);
|
|
269
|
+
const chunk = (delta, finishReasonValue = null, extra = {}) => ({
|
|
270
|
+
id, object: "chat.completion.chunk", created, model,
|
|
271
|
+
choices: [{ index: 0, delta, finish_reason: finishReasonValue }], ...extra
|
|
272
|
+
});
|
|
273
|
+
const body = new ReadableStream({
|
|
274
|
+
async start(controller) {
|
|
275
|
+
const indexes = new Map();
|
|
276
|
+
const reasoningIndexes = new Set();
|
|
277
|
+
let nextTool = 0;
|
|
278
|
+
try {
|
|
279
|
+
controller.enqueue(sse(chunk({ role: "assistant" })));
|
|
280
|
+
for await (const event of stream) {
|
|
281
|
+
if (signal?.aborted === true)
|
|
282
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
283
|
+
const error = streamError(event);
|
|
284
|
+
if (error !== undefined)
|
|
285
|
+
throw error;
|
|
286
|
+
if (event.contentBlockStart?.start?.toolUse !== undefined) {
|
|
287
|
+
const sourceIndex = event.contentBlockStart.contentBlockIndex;
|
|
288
|
+
const index = nextTool++;
|
|
289
|
+
if (sourceIndex !== undefined)
|
|
290
|
+
indexes.set(sourceIndex, index);
|
|
291
|
+
const tool = event.contentBlockStart.start.toolUse;
|
|
292
|
+
controller.enqueue(sse(chunk({ tool_calls: [{ index, id: tool.toolUseId, type: "function", function: { name: tool.name, arguments: "" } }] })));
|
|
293
|
+
}
|
|
294
|
+
const delta = event.contentBlockDelta?.delta;
|
|
295
|
+
if (delta !== undefined && "text" in delta && typeof delta.text === "string")
|
|
296
|
+
controller.enqueue(sse(chunk({ content: delta.text })));
|
|
297
|
+
if (delta !== undefined && "toolUse" in delta && delta.toolUse !== undefined) {
|
|
298
|
+
const sourceIndex = event.contentBlockDelta?.contentBlockIndex;
|
|
299
|
+
const index = sourceIndex === undefined ? 0 : indexes.get(sourceIndex) ?? 0;
|
|
300
|
+
controller.enqueue(sse(chunk({ tool_calls: [{ index, function: { arguments: delta.toolUse.input ?? "" } }] })));
|
|
301
|
+
}
|
|
302
|
+
if (delta !== undefined && "reasoningContent" in delta && delta.reasoningContent !== undefined) {
|
|
303
|
+
const reasoningIndex = event.contentBlockDelta?.contentBlockIndex ?? 0;
|
|
304
|
+
const isThinkingDelta = ("text" in delta.reasoningContent && typeof delta.reasoningContent.text === "string") ||
|
|
305
|
+
("signature" in delta.reasoningContent && typeof delta.reasoningContent.signature === "string");
|
|
306
|
+
if (isThinkingDelta && !reasoningIndexes.has(reasoningIndex)) {
|
|
307
|
+
reasoningIndexes.add(reasoningIndex);
|
|
308
|
+
controller.enqueue(sse(chunk({ reasoning_details: [{
|
|
309
|
+
type: "thinking", index: reasoningIndex, phase: "start"
|
|
310
|
+
}] })));
|
|
311
|
+
}
|
|
312
|
+
if ("text" in delta.reasoningContent && typeof delta.reasoningContent.text === "string") {
|
|
313
|
+
controller.enqueue(sse(chunk({
|
|
314
|
+
reasoning: delta.reasoningContent.text,
|
|
315
|
+
reasoning_details: [{
|
|
316
|
+
type: "thinking", index: reasoningIndex, phase: "delta",
|
|
317
|
+
thinking: delta.reasoningContent.text
|
|
318
|
+
}]
|
|
319
|
+
})));
|
|
320
|
+
}
|
|
321
|
+
else if ("signature" in delta.reasoningContent && typeof delta.reasoningContent.signature === "string") {
|
|
322
|
+
controller.enqueue(sse(chunk({ reasoning_details: [{
|
|
323
|
+
type: "thinking", index: reasoningIndex, phase: "signature",
|
|
324
|
+
signature: delta.reasoningContent.signature
|
|
325
|
+
}] })));
|
|
326
|
+
}
|
|
327
|
+
else if ("redactedContent" in delta.reasoningContent && delta.reasoningContent.redactedContent !== undefined) {
|
|
328
|
+
controller.enqueue(sse(chunk({ reasoning_details: [{
|
|
329
|
+
type: "redacted_thinking", index: reasoningIndex, phase: "block",
|
|
330
|
+
data: Buffer.from(delta.reasoningContent.redactedContent).toString("base64")
|
|
331
|
+
}] })));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const stoppedIndex = event.contentBlockStop?.contentBlockIndex;
|
|
335
|
+
if (stoppedIndex !== undefined && reasoningIndexes.has(stoppedIndex)) {
|
|
336
|
+
controller.enqueue(sse(chunk({ reasoning_details: [{
|
|
337
|
+
type: "thinking", index: stoppedIndex, phase: "stop"
|
|
338
|
+
}] })));
|
|
339
|
+
}
|
|
340
|
+
if (event.messageStop !== undefined)
|
|
341
|
+
controller.enqueue(sse(chunk({}, finishReason(event.messageStop.stopReason))));
|
|
342
|
+
if (event.metadata?.usage !== undefined)
|
|
343
|
+
controller.enqueue(sse(chunk({}, null, { usage: usage(event.metadata.usage) })));
|
|
344
|
+
}
|
|
345
|
+
controller.enqueue(sse("[DONE]"));
|
|
346
|
+
controller.close();
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (signal?.aborted !== true) {
|
|
350
|
+
controller.enqueue(sse({ error: { type: "provider_error", message: error instanceof Error ? error.message : String(error) } }));
|
|
351
|
+
controller.enqueue(sse("[DONE]"));
|
|
352
|
+
}
|
|
353
|
+
controller.close();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
return new Response(body, { headers: { "content-type": "text/event-stream; charset=utf-8" } });
|
|
358
|
+
}
|
|
359
|
+
function errorResponse(error) {
|
|
360
|
+
const metadata = record(record(error)?.$metadata);
|
|
361
|
+
const status = typeof metadata?.httpStatusCode === "number" ? metadata.httpStatusCode : 502;
|
|
362
|
+
return Response.json({ error: { type: "provider_error", message: error instanceof Error ? error.message : String(error) } }, { status });
|
|
363
|
+
}
|
|
364
|
+
export class BedrockProviderSource {
|
|
365
|
+
sourceId = "bedrock";
|
|
366
|
+
#control;
|
|
367
|
+
#runtime;
|
|
368
|
+
constructor(options = {}) {
|
|
369
|
+
this.#control = options.controlClient ?? new BedrockClient({});
|
|
370
|
+
this.#runtime = options.runtimeClient ?? new BedrockRuntimeClient({});
|
|
371
|
+
}
|
|
372
|
+
async discoverModels(signal) {
|
|
373
|
+
const foundation = await this.#control.send(new ListFoundationModelsCommand({ byProvider: "Anthropic" }), signal === undefined ? undefined : { abortSignal: signal });
|
|
374
|
+
const foundations = (foundation.modelSummaries ?? []).filter(anthropicFoundationModel);
|
|
375
|
+
const ids = new Set(foundations.map((model) => model.modelId));
|
|
376
|
+
const discovered = new Set(ids);
|
|
377
|
+
let nextToken;
|
|
378
|
+
do {
|
|
379
|
+
const profiles = await this.#control.send(new ListInferenceProfilesCommand({ ...(nextToken !== undefined ? { nextToken } : {}) }), signal === undefined ? undefined : { abortSignal: signal });
|
|
380
|
+
for (const profile of profiles.inferenceProfileSummaries ?? [])
|
|
381
|
+
if (activeAnthropicProfile(profile, ids))
|
|
382
|
+
discovered.add(profile.inferenceProfileId);
|
|
383
|
+
nextToken = profiles.nextToken;
|
|
384
|
+
} while (nextToken !== undefined && nextToken.length > 0);
|
|
385
|
+
if (discovered.size === 0)
|
|
386
|
+
throw new Error("model discovery returned no active Anthropic Bedrock models");
|
|
387
|
+
return [...discovered].map((id) => ({ id }));
|
|
388
|
+
}
|
|
389
|
+
async chat(body, signal, _options) {
|
|
390
|
+
let input;
|
|
391
|
+
try {
|
|
392
|
+
input = toBedrockConverseInput(body);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
return Response.json({ error: { type: "invalid_request_error", message: error instanceof Error ? error.message : String(error) } }, { status: 400 });
|
|
396
|
+
}
|
|
397
|
+
const stream = record(body)?.stream === true;
|
|
398
|
+
const modelId = input.modelId;
|
|
399
|
+
if (modelId === undefined)
|
|
400
|
+
return errorResponse(new Error("Bedrock chat requires a model"));
|
|
401
|
+
try {
|
|
402
|
+
if (stream) {
|
|
403
|
+
const output = await this.#runtime.send(new ConverseStreamCommand(input), signal === undefined ? undefined : { abortSignal: signal });
|
|
404
|
+
if (output.stream === undefined)
|
|
405
|
+
throw new Error("Bedrock returned no response stream");
|
|
406
|
+
return streamResponse(output.stream, modelId, signal);
|
|
407
|
+
}
|
|
408
|
+
const output = await this.#runtime.send(new ConverseCommand(input), signal === undefined ? undefined : { abortSignal: signal });
|
|
409
|
+
return Response.json(fromBedrockConverseOutput(output, modelId));
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
return errorResponse(error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
embeddings() {
|
|
416
|
+
return Promise.resolve(Response.json({ error: { type: "not_implemented", message: "Bedrock embeddings are not supported" } }, { status: 501 }));
|
|
417
|
+
}
|
|
418
|
+
reasoningCapabilities() {
|
|
419
|
+
return {
|
|
420
|
+
status: "unknown",
|
|
421
|
+
wireShape: "bedrock-converse",
|
|
422
|
+
provenance: "provider"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
close() {
|
|
426
|
+
this.#control.destroy?.();
|
|
427
|
+
this.#runtime.destroy?.();
|
|
428
|
+
}
|
|
429
|
+
}
|
package/dist/endpoint-health.js
CHANGED
|
@@ -38,6 +38,8 @@ export function providerAuthHeaders(auth, credential) {
|
|
|
38
38
|
return { "x-api-key": credential };
|
|
39
39
|
case "x-goog-api-key":
|
|
40
40
|
return { "x-goog-api-key": credential };
|
|
41
|
+
case "aws-sdk":
|
|
42
|
+
return {};
|
|
41
43
|
default: {
|
|
42
44
|
const exhaustive = auth;
|
|
43
45
|
throw new Error(`unknown provider authentication style: ${String(exhaustive)}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ export { joinPath, ModelRoutedBackend, OpenAiBackend } from "./backend.js";
|
|
|
7
7
|
export type { Backend, BackendModelRoute, BackendRequestOptions, RequestAttributionUpdate, ModelRoutedBackendOptions, OpenAiBackendOptions } from "./backend.js";
|
|
8
8
|
export { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "./provider-backends.js";
|
|
9
9
|
export type { ProviderBackendOptions, ProviderTransport } from "./provider-backends.js";
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
10
|
+
export { BedrockProviderSource, fromBedrockConverseOutput, toBedrockConverseInput } from "./bedrock-source.js";
|
|
11
|
+
export type { BedrockControlClient, BedrockProviderSourceOptions, BedrockRuntime } from "./bedrock-source.js";
|
|
12
|
+
export { CatalogBackend, DEFAULT_LEADERBOARD_DURABLE_RETENTION_DAYS, DEFAULT_LEADERBOARD_LIVE_LIMIT, DEFAULT_LEADERBOARD_LIVE_TTL_HOURS, isSubscriptionProvider, leaderboardConfigSchema, NoModelAvailableError, modelPolicyAllowsModel, modelPolicyRuleMatches, normalizeRouterConfigAliases, parseRouterConfig, resolveLeaderboardConfig, routerConfigSchema, splitNamespacedModel, UnknownModelError } from "./router.js";
|
|
13
|
+
export type { CatalogBackendOptions, CatalogModelInfo, LeaderboardConfig, ModelPolicy, ProviderPolicy, RouterConfig, } from "./router.js";
|
|
12
14
|
export { API_PROVIDER_IDS, ApiProviderSource, parseDiscoveredModels, parseReasoningCapabilities, PROVIDER_IDS, SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
|
13
15
|
export type { ApiProviderId, ApiProviderSourceOptions, DiscoveredModel, ProviderId, ProviderSource, ProviderSourceTransport, SubscriptionProviderId } from "./provider-source.js";
|
|
14
16
|
export { endpointHealthProbe, probeEndpointHealth, providerAuthHeaders } from "./endpoint-health.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@ export { startGateway } from "./server.js";
|
|
|
3
3
|
export { startSwitchingGatewayProxy } from "./switching-proxy.js";
|
|
4
4
|
export { joinPath, ModelRoutedBackend, OpenAiBackend } from "./backend.js";
|
|
5
5
|
export { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "./provider-backends.js";
|
|
6
|
-
export {
|
|
6
|
+
export { BedrockProviderSource, fromBedrockConverseOutput, toBedrockConverseInput } from "./bedrock-source.js";
|
|
7
|
+
export { CatalogBackend, DEFAULT_LEADERBOARD_DURABLE_RETENTION_DAYS, DEFAULT_LEADERBOARD_LIVE_LIMIT, DEFAULT_LEADERBOARD_LIVE_TTL_HOURS, isSubscriptionProvider, leaderboardConfigSchema, NoModelAvailableError, modelPolicyAllowsModel, modelPolicyRuleMatches, normalizeRouterConfigAliases, parseRouterConfig, resolveLeaderboardConfig, routerConfigSchema, splitNamespacedModel, UnknownModelError } from "./router.js";
|
|
7
8
|
export { API_PROVIDER_IDS, ApiProviderSource, parseDiscoveredModels, parseReasoningCapabilities, PROVIDER_IDS, SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
|
8
9
|
export { endpointHealthProbe, probeEndpointHealth, providerAuthHeaders } from "./endpoint-health.js";
|
|
9
10
|
export { CapacityPool } from "./capacity-pool.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type ProviderDiscoveryResponseShape } from "@velum-labs/routekit-registry";
|
|
2
2
|
import type { ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
|
|
3
3
|
import type { BackendRequestOptions } from "./backend.js";
|
|
4
|
-
export declare const API_PROVIDER_IDS: readonly ["openai", "anthropic", "google", "openrouter", "cliproxy"];
|
|
4
|
+
export declare const API_PROVIDER_IDS: readonly ["openai", "anthropic", "bedrock", "google", "openrouter", "cliproxy"];
|
|
5
5
|
export declare const SUBSCRIPTION_PROVIDER_IDS: readonly ["codex", "claude-code"];
|
|
6
|
-
export declare const PROVIDER_IDS: readonly ["openai", "anthropic", "google", "openrouter", "cliproxy", "codex", "claude-code"];
|
|
6
|
+
export declare const PROVIDER_IDS: readonly ["openai", "anthropic", "bedrock", "google", "openrouter", "cliproxy", "codex", "claude-code"];
|
|
7
7
|
export type ApiProviderId = (typeof API_PROVIDER_IDS)[number];
|
|
8
8
|
export type SubscriptionProviderId = (typeof SUBSCRIPTION_PROVIDER_IDS)[number];
|
|
9
9
|
export type ProviderId = (typeof PROVIDER_IDS)[number];
|
package/dist/provider-source.js
CHANGED
|
@@ -4,6 +4,7 @@ import { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "./p
|
|
|
4
4
|
export const API_PROVIDER_IDS = [
|
|
5
5
|
"openai",
|
|
6
6
|
"anthropic",
|
|
7
|
+
"bedrock",
|
|
7
8
|
"google",
|
|
8
9
|
"openrouter",
|
|
9
10
|
"cliproxy"
|
|
@@ -52,6 +53,8 @@ function reasoningWireShape(provider) {
|
|
|
52
53
|
case "anthropic":
|
|
53
54
|
case "claude-code":
|
|
54
55
|
return "anthropic";
|
|
56
|
+
case "bedrock":
|
|
57
|
+
return "bedrock-converse";
|
|
55
58
|
case "google":
|
|
56
59
|
return "google";
|
|
57
60
|
case "openrouter":
|
|
@@ -195,6 +198,9 @@ export function parseDiscoveredModels(shape, payload, provider) {
|
|
|
195
198
|
case "codex":
|
|
196
199
|
entries = Array.isArray(payload.models) ? payload.models : [];
|
|
197
200
|
break;
|
|
201
|
+
case "bedrock":
|
|
202
|
+
entries = Array.isArray(payload.models) ? payload.models : [];
|
|
203
|
+
break;
|
|
198
204
|
default: {
|
|
199
205
|
const unreachable = shape;
|
|
200
206
|
throw new Error(`unsupported discovery response shape: ${String(unreachable)}`);
|
|
@@ -233,6 +239,8 @@ function authHeaders(style, credential) {
|
|
|
233
239
|
return { "x-api-key": credential };
|
|
234
240
|
case "x-goog-api-key":
|
|
235
241
|
return { "x-goog-api-key": credential };
|
|
242
|
+
case "aws-sdk":
|
|
243
|
+
return {};
|
|
236
244
|
default: {
|
|
237
245
|
const unreachable = style;
|
|
238
246
|
throw new Error(`unsupported provider auth style: ${String(unreachable)}`);
|
|
@@ -269,6 +277,8 @@ function providerBackend(protocol, baseUrl, apiKey, headers) {
|
|
|
269
277
|
return new GoogleGenAiBackend(options);
|
|
270
278
|
case "codex":
|
|
271
279
|
return new CodexResponsesBackend(options);
|
|
280
|
+
case "bedrock":
|
|
281
|
+
throw new Error("Bedrock uses its dedicated AWS SDK provider source");
|
|
272
282
|
default: {
|
|
273
283
|
const unreachable = protocol;
|
|
274
284
|
throw new Error(`unsupported provider wire protocol: ${String(unreachable)}`);
|
package/dist/router.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import type { ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
|
|
2
|
+
import { z } from "zod";
|
|
3
3
|
import type { Backend, BackendModelRoute, BackendRequestOptions } from "./backend.js";
|
|
4
|
-
import { SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
|
5
4
|
import type { ApiProviderId, ProviderId, ProviderSource } from "./provider-source.js";
|
|
5
|
+
import { SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
|
6
6
|
export declare class UnknownModelError extends Error {
|
|
7
7
|
readonly model: string;
|
|
8
8
|
constructor(model: string);
|
|
@@ -10,6 +10,10 @@ export declare class UnknownModelError extends Error {
|
|
|
10
10
|
export declare class NoModelAvailableError extends Error {
|
|
11
11
|
constructor();
|
|
12
12
|
}
|
|
13
|
+
declare const modelPolicySchema: z.ZodObject<{
|
|
14
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
+
deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
16
|
+
}, z.core.$strict>;
|
|
13
17
|
declare const providerPolicySchema: z.ZodObject<{
|
|
14
18
|
strategy: z.ZodDefault<z.ZodEnum<{
|
|
15
19
|
sticky: "sticky";
|
|
@@ -20,6 +24,15 @@ declare const providerPolicySchema: z.ZodObject<{
|
|
|
20
24
|
probeIntervalMs: z.ZodOptional<z.ZodNumber>;
|
|
21
25
|
fallbackCooldownSeconds: z.ZodOptional<z.ZodNumber>;
|
|
22
26
|
}, z.core.$strict>;
|
|
27
|
+
export declare const DEFAULT_LEADERBOARD_LIVE_LIMIT = 1000;
|
|
28
|
+
export declare const DEFAULT_LEADERBOARD_LIVE_TTL_HOURS = 24;
|
|
29
|
+
export declare const DEFAULT_LEADERBOARD_DURABLE_RETENTION_DAYS = 14;
|
|
30
|
+
export declare const leaderboardConfigSchema: z.ZodObject<{
|
|
31
|
+
liveLimit: z.ZodDefault<z.ZodNumber>;
|
|
32
|
+
liveTtlHours: z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
durable: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
+
durableRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
}, z.core.$strict>;
|
|
23
36
|
export declare const routerConfigSchema: z.ZodObject<{
|
|
24
37
|
providers: z.ZodObject<{
|
|
25
38
|
openai: z.ZodOptional<z.ZodObject<{
|
|
@@ -42,6 +55,16 @@ export declare const routerConfigSchema: z.ZodObject<{
|
|
|
42
55
|
probeIntervalMs: z.ZodOptional<z.ZodNumber>;
|
|
43
56
|
fallbackCooldownSeconds: z.ZodOptional<z.ZodNumber>;
|
|
44
57
|
}, z.core.$strict>>;
|
|
58
|
+
bedrock: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
strategy: z.ZodDefault<z.ZodEnum<{
|
|
60
|
+
sticky: "sticky";
|
|
61
|
+
round_robin: "round_robin";
|
|
62
|
+
capacity_weighted: "capacity_weighted";
|
|
63
|
+
}>>;
|
|
64
|
+
switchThreshold: z.ZodDefault<z.ZodNumber>;
|
|
65
|
+
probeIntervalMs: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
fallbackCooldownSeconds: z.ZodOptional<z.ZodNumber>;
|
|
67
|
+
}, z.core.$strict>>;
|
|
45
68
|
google: z.ZodOptional<z.ZodObject<{
|
|
46
69
|
strategy: z.ZodDefault<z.ZodEnum<{
|
|
47
70
|
sticky: "sticky";
|
|
@@ -94,12 +117,16 @@ export declare const routerConfigSchema: z.ZodObject<{
|
|
|
94
117
|
}, z.core.$strict>>;
|
|
95
118
|
}, z.core.$strict>;
|
|
96
119
|
defaultModel: z.ZodOptional<z.ZodString>;
|
|
120
|
+
modelPolicy: z.ZodOptional<z.ZodObject<{
|
|
121
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
122
|
+
deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
123
|
+
}, z.core.$strict>>;
|
|
97
124
|
modelAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
98
125
|
reasoningCapabilities: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
99
126
|
status: z.ZodDefault<z.ZodEnum<{
|
|
100
127
|
unknown: "unknown";
|
|
101
|
-
unsupported: "unsupported";
|
|
102
128
|
supported: "supported";
|
|
129
|
+
unsupported: "unsupported";
|
|
103
130
|
}>>;
|
|
104
131
|
efforts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
105
132
|
id: z.ZodString;
|
|
@@ -116,9 +143,19 @@ export declare const routerConfigSchema: z.ZodObject<{
|
|
|
116
143
|
adaptive: z.ZodOptional<z.ZodBoolean>;
|
|
117
144
|
wireShape: z.ZodOptional<z.ZodString>;
|
|
118
145
|
}, z.core.$strict>>>;
|
|
146
|
+
leaderboard: z.ZodOptional<z.ZodObject<{
|
|
147
|
+
liveLimit: z.ZodDefault<z.ZodNumber>;
|
|
148
|
+
liveTtlHours: z.ZodDefault<z.ZodNumber>;
|
|
149
|
+
durable: z.ZodDefault<z.ZodBoolean>;
|
|
150
|
+
durableRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
151
|
+
}, z.core.$strict>>;
|
|
119
152
|
}, z.core.$strict>;
|
|
153
|
+
export type ModelPolicy = z.infer<typeof modelPolicySchema>;
|
|
120
154
|
export type ProviderPolicy = z.infer<typeof providerPolicySchema>;
|
|
121
155
|
export type RouterConfig = z.infer<typeof routerConfigSchema>;
|
|
156
|
+
export type LeaderboardConfig = z.infer<typeof leaderboardConfigSchema>;
|
|
157
|
+
/** Resolve leaderboard settings with schema defaults when the block is omitted. */
|
|
158
|
+
export declare function resolveLeaderboardConfig(config: Pick<RouterConfig, "leaderboard">): LeaderboardConfig;
|
|
122
159
|
export declare function normalizeRouterConfigAliases(value: unknown): unknown;
|
|
123
160
|
export declare function splitNamespacedModel(model: string): {
|
|
124
161
|
provider: ProviderId;
|
|
@@ -142,6 +179,10 @@ export type CatalogBackendOptions = {
|
|
|
142
179
|
createApiSource?: (provider: ApiProviderId, env: Readonly<Record<string, string | undefined>>) => ProviderSource;
|
|
143
180
|
signal?: AbortSignal;
|
|
144
181
|
};
|
|
182
|
+
/** Match an anchored model-policy glob where only `*` has special meaning. */
|
|
183
|
+
export declare function modelPolicyRuleMatches(rule: string, canonicalModel: string): boolean;
|
|
184
|
+
/** Apply inclusive allowlist then denylist; a matching deny rule always wins. */
|
|
185
|
+
export declare function modelPolicyAllowsModel(policy: RouterConfig["modelPolicy"], canonicalModel: string): boolean;
|
|
145
186
|
/**
|
|
146
187
|
* Conservative fallback for models whose discovery API omits reasoning metadata.
|
|
147
188
|
* Keep this allowlist tied to model families whose exact controls have been
|