@velum-labs/routekit-gateway 0.18.0 → 0.18.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/dist/backend.d.ts +2 -2
- package/dist/bedrock-source.d.ts +1 -1
- package/dist/bedrock-source.js +52 -4
- package/dist/codex-model-selection.js +29 -19
- package/dist/switching-proxy.d.ts +1 -0
- package/dist/switching-proxy.js +35 -6
- package/dist/test/bedrock-source.test.js +98 -2
- package/dist/test/codex-model-selection.test.js +1 -1
- package/dist/test/router.test.js +54 -0
- package/dist/test/switching-proxy-retire.test.d.ts +1 -0
- package/dist/test/switching-proxy-retire.test.js +88 -0
- package/package.json +5 -5
package/dist/backend.d.ts
CHANGED
|
@@ -44,8 +44,8 @@ export type Backend = {
|
|
|
44
44
|
resolveModelRoute?(requested: string | undefined, nativeProvider?: string): BackendModelRoute | undefined;
|
|
45
45
|
/**
|
|
46
46
|
* Whether the backend serves this exact model id itself. Unlike
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* The defaulting resolver folds unknown ids into the default; this
|
|
48
|
+
* distinguishes "mine" from "unknown", so the
|
|
49
49
|
* gateway can hand unknown ids to a relay (e.g. the Codex backend relay)
|
|
50
50
|
* instead of silently routing them to the default.
|
|
51
51
|
*/
|
package/dist/bedrock-source.d.ts
CHANGED
|
@@ -17,6 +17,6 @@ export declare class BedrockProviderSource implements ProviderSource {
|
|
|
17
17
|
discoverModels(signal?: AbortSignal): Promise<readonly DiscoveredModel[]>;
|
|
18
18
|
chat(body: unknown, signal?: AbortSignal, _options?: BackendRequestOptions): Promise<Response>;
|
|
19
19
|
embeddings(): Promise<Response>;
|
|
20
|
-
reasoningCapabilities(): DiscoveredModel["reasoning"];
|
|
20
|
+
reasoningCapabilities(model?: string): DiscoveredModel["reasoning"];
|
|
21
21
|
close(): void;
|
|
22
22
|
}
|
package/dist/bedrock-source.js
CHANGED
|
@@ -27,6 +27,34 @@ function activeAnthropicProfile(profile, anthropicModels) {
|
|
|
27
27
|
return id !== undefined && anthropicModels.has(id);
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
function isOpusFiveModel(modelId) {
|
|
31
|
+
return /(?:^|\.)anthropic\.claude-opus-5$/.test(modelId);
|
|
32
|
+
}
|
|
33
|
+
function inferenceProfilePriority(profileId) {
|
|
34
|
+
if (profileId.startsWith("us."))
|
|
35
|
+
return 0;
|
|
36
|
+
if (profileId.startsWith("global."))
|
|
37
|
+
return 1;
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
function preferredInferenceProfile(current, candidate) {
|
|
41
|
+
if (current === undefined)
|
|
42
|
+
return candidate;
|
|
43
|
+
return inferenceProfilePriority(candidate) < inferenceProfilePriority(current)
|
|
44
|
+
? candidate
|
|
45
|
+
: current;
|
|
46
|
+
}
|
|
47
|
+
function bedrockReasoningCapabilities(modelId) {
|
|
48
|
+
if (modelId === undefined || !isOpusFiveModel(modelId))
|
|
49
|
+
return undefined;
|
|
50
|
+
return {
|
|
51
|
+
status: "supported",
|
|
52
|
+
efforts: ["low", "medium", "high", "max"].map((id) => ({ id })),
|
|
53
|
+
adaptive: true,
|
|
54
|
+
wireShape: "bedrock-converse",
|
|
55
|
+
provenance: "builtin"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
30
58
|
function bedrockMetadata(model) {
|
|
31
59
|
const inputModalities = (model.inputModalities ?? []).map((value) => value.toLowerCase());
|
|
32
60
|
const outputModalities = (model.outputModalities ?? []).map((value) => value.toLowerCase());
|
|
@@ -43,9 +71,11 @@ function bedrockMetadata(model) {
|
|
|
43
71
|
};
|
|
44
72
|
}
|
|
45
73
|
function bedrockDiscoveredModel(id, model) {
|
|
74
|
+
const reasoning = bedrockReasoningCapabilities(model.modelId);
|
|
46
75
|
return {
|
|
47
76
|
id,
|
|
48
77
|
metadata: bedrockMetadata(model),
|
|
78
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
49
79
|
...(model.responseStreamingSupported !== undefined
|
|
50
80
|
? {
|
|
51
81
|
capabilities: {
|
|
@@ -196,7 +226,15 @@ export function toBedrockConverseInput(body) {
|
|
|
196
226
|
const selection = reasoningSelectionOf(body);
|
|
197
227
|
const thinking = selection.mode === "budget"
|
|
198
228
|
? { thinking: { type: "enabled", budget_tokens: selection.budgetTokens } }
|
|
199
|
-
:
|
|
229
|
+
: isOpusFiveModel(canonical.model)
|
|
230
|
+
? selection.mode === "effort"
|
|
231
|
+
? { thinking: { type: "adaptive" }, output_config: { effort: selection.effort } }
|
|
232
|
+
: selection.mode === "adaptive"
|
|
233
|
+
? { thinking: { type: "adaptive" } }
|
|
234
|
+
: selection.mode === "disabled"
|
|
235
|
+
? { thinking: { type: "disabled" } }
|
|
236
|
+
: undefined
|
|
237
|
+
: undefined;
|
|
200
238
|
const additionalModelRequestFields = thinking !== undefined || typeof canonical.top_k === "number"
|
|
201
239
|
? {
|
|
202
240
|
...thinking,
|
|
@@ -393,11 +431,13 @@ export class BedrockProviderSource {
|
|
|
393
431
|
sourceId = "bedrock";
|
|
394
432
|
#control;
|
|
395
433
|
#runtime;
|
|
434
|
+
#inferenceProfilesByFoundation = new Map();
|
|
396
435
|
constructor(options = {}) {
|
|
397
436
|
this.#control = options.controlClient ?? new BedrockClient({});
|
|
398
437
|
this.#runtime = options.runtimeClient ?? new BedrockRuntimeClient({});
|
|
399
438
|
}
|
|
400
439
|
async discoverModels(signal) {
|
|
440
|
+
this.#inferenceProfilesByFoundation.clear();
|
|
401
441
|
const foundation = await this.#control.send(new ListFoundationModelsCommand({ byProvider: "Anthropic" }), signal === undefined ? undefined : { abortSignal: signal });
|
|
402
442
|
const foundations = (foundation.modelSummaries ?? []).filter(anthropicFoundationModel);
|
|
403
443
|
const byId = new Map(foundations.map((model) => [model.modelId, model]));
|
|
@@ -416,6 +456,7 @@ export class BedrockProviderSource {
|
|
|
416
456
|
.map((model) => foundationIdFromArn(model.modelArn))
|
|
417
457
|
.find((id) => id !== undefined && ids.has(id));
|
|
418
458
|
if (backingId !== undefined) {
|
|
459
|
+
this.#inferenceProfilesByFoundation.set(backingId, preferredInferenceProfile(this.#inferenceProfilesByFoundation.get(backingId), profile.inferenceProfileId));
|
|
419
460
|
discovered.set(profile.inferenceProfileId, bedrockDiscoveredModel(profile.inferenceProfileId, byId.get(backingId)));
|
|
420
461
|
}
|
|
421
462
|
}
|
|
@@ -437,14 +478,18 @@ export class BedrockProviderSource {
|
|
|
437
478
|
const modelId = input.modelId;
|
|
438
479
|
if (modelId === undefined)
|
|
439
480
|
return errorResponse(new Error("Bedrock chat requires a model"));
|
|
481
|
+
const runtimeModelId = modelId === "anthropic.claude-opus-5"
|
|
482
|
+
? this.#inferenceProfilesByFoundation.get(modelId) ?? modelId
|
|
483
|
+
: modelId;
|
|
484
|
+
const runtimeInput = runtimeModelId === modelId ? input : { ...input, modelId: runtimeModelId };
|
|
440
485
|
try {
|
|
441
486
|
if (stream) {
|
|
442
|
-
const output = await this.#runtime.send(new ConverseStreamCommand(
|
|
487
|
+
const output = await this.#runtime.send(new ConverseStreamCommand(runtimeInput), signal === undefined ? undefined : { abortSignal: signal });
|
|
443
488
|
if (output.stream === undefined)
|
|
444
489
|
throw new Error("Bedrock returned no response stream");
|
|
445
490
|
return streamResponse(output.stream, modelId, signal);
|
|
446
491
|
}
|
|
447
|
-
const output = await this.#runtime.send(new ConverseCommand(
|
|
492
|
+
const output = await this.#runtime.send(new ConverseCommand(runtimeInput), signal === undefined ? undefined : { abortSignal: signal });
|
|
448
493
|
return Response.json(fromBedrockConverseOutput(output, modelId));
|
|
449
494
|
}
|
|
450
495
|
catch (error) {
|
|
@@ -454,7 +499,10 @@ export class BedrockProviderSource {
|
|
|
454
499
|
embeddings() {
|
|
455
500
|
return Promise.resolve(Response.json({ error: { type: "not_implemented", message: "Bedrock embeddings are not supported" } }, { status: 501 }));
|
|
456
501
|
}
|
|
457
|
-
reasoningCapabilities() {
|
|
502
|
+
reasoningCapabilities(model) {
|
|
503
|
+
const known = bedrockReasoningCapabilities(model);
|
|
504
|
+
if (known !== undefined)
|
|
505
|
+
return known;
|
|
458
506
|
return {
|
|
459
507
|
status: "unknown",
|
|
460
508
|
wireShape: "bedrock-converse",
|
|
@@ -120,27 +120,37 @@ export class OpenRouterModelMetadataClient {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
async #refresh() {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
controller.abort(new DOMException("OpenRouter model metadata timed out", "TimeoutError"));
|
|
126
|
+
}, this.#timeoutMs);
|
|
127
|
+
const settled = await (async () => {
|
|
128
|
+
try {
|
|
129
|
+
return await Promise.allSettled(TASK_CATALOGS.map(async (catalog) => {
|
|
130
|
+
const response = await this.#fetch(`${this.#baseUrl}${catalog.path}`, {
|
|
131
|
+
headers: { accept: "application/json" },
|
|
132
|
+
signal: controller.signal
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(`OpenRouter ${catalog.path} returned HTTP ${response.status}`);
|
|
136
|
+
}
|
|
137
|
+
const payload = record(await response.json());
|
|
138
|
+
if (!Array.isArray(payload?.data)) {
|
|
139
|
+
throw new Error(`OpenRouter ${catalog.path} returned an invalid model catalog`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
kind: catalog.kind,
|
|
143
|
+
entries: payload.data.flatMap((entry) => {
|
|
144
|
+
const parsed = metadataFromEntry(entry, catalog);
|
|
145
|
+
return parsed === undefined ? [] : [parsed];
|
|
146
|
+
})
|
|
147
|
+
};
|
|
148
|
+
}));
|
|
131
149
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
throw new Error(`OpenRouter ${catalog.path} returned an invalid model catalog`);
|
|
150
|
+
finally {
|
|
151
|
+
clearTimeout(timeout);
|
|
135
152
|
}
|
|
136
|
-
|
|
137
|
-
kind: catalog.kind,
|
|
138
|
-
entries: payload.data.flatMap((entry) => {
|
|
139
|
-
const parsed = metadataFromEntry(entry, catalog);
|
|
140
|
-
return parsed === undefined ? [] : [parsed];
|
|
141
|
-
})
|
|
142
|
-
};
|
|
143
|
-
}));
|
|
153
|
+
})();
|
|
144
154
|
const responses = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
145
155
|
if (responses.length === 0) {
|
|
146
156
|
const firstFailure = settled.find((result) => result.status === "rejected");
|
|
@@ -5,6 +5,7 @@ export type SwitchingGatewayProxy = {
|
|
|
5
5
|
target(): string;
|
|
6
6
|
swapTarget(target: string): string;
|
|
7
7
|
waitForTargetIdle(target: string, graceMs: number): Promise<boolean>;
|
|
8
|
+
retire(graceMs?: number): Promise<void>;
|
|
8
9
|
drain(graceMs?: number): Promise<void>;
|
|
9
10
|
close(): Promise<void>;
|
|
10
11
|
};
|
package/dist/switching-proxy.js
CHANGED
|
@@ -68,13 +68,17 @@ function writeJson(res, status, value) {
|
|
|
68
68
|
res.setHeader("content-length", String(payload.length));
|
|
69
69
|
res.end(payload);
|
|
70
70
|
}
|
|
71
|
-
async function pipe(res, upstream) {
|
|
71
|
+
async function pipe(res, upstream, closeConnection) {
|
|
72
72
|
res.statusCode = upstream.status;
|
|
73
73
|
for (const [name, value] of upstream.headers) {
|
|
74
74
|
if (!HOP_BY_HOP.has(name.toLowerCase()) && name.toLowerCase() !== "content-length") {
|
|
75
75
|
res.setHeader(name, value);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
if (closeConnection()) {
|
|
79
|
+
res.shouldKeepAlive = false;
|
|
80
|
+
res.setHeader("connection", "close");
|
|
81
|
+
}
|
|
78
82
|
if (upstream.body === null) {
|
|
79
83
|
res.end();
|
|
80
84
|
return;
|
|
@@ -108,8 +112,13 @@ export async function startSwitchingGatewayProxy(input) {
|
|
|
108
112
|
};
|
|
109
113
|
const generations = new Map([[active.url, active]]);
|
|
110
114
|
let draining = false;
|
|
115
|
+
let retiring = false;
|
|
111
116
|
let inflight = 0;
|
|
112
117
|
const server = createServer((req, res) => {
|
|
118
|
+
if (retiring) {
|
|
119
|
+
res.shouldKeepAlive = false;
|
|
120
|
+
res.setHeader("connection", "close");
|
|
121
|
+
}
|
|
113
122
|
inflight += 1;
|
|
114
123
|
res.once("close", () => {
|
|
115
124
|
inflight -= 1;
|
|
@@ -170,7 +179,7 @@ export async function startSwitchingGatewayProxy(input) {
|
|
|
170
179
|
AbortSignal.timeout(10 * 60 * 1000)
|
|
171
180
|
])
|
|
172
181
|
});
|
|
173
|
-
await pipe(res, upstream);
|
|
182
|
+
await pipe(res, upstream, () => retiring);
|
|
174
183
|
}
|
|
175
184
|
catch {
|
|
176
185
|
if (!res.destroyed && !res.headersSent) {
|
|
@@ -205,14 +214,33 @@ export async function startSwitchingGatewayProxy(input) {
|
|
|
205
214
|
const address = server.address();
|
|
206
215
|
const port = typeof address === "object" && address !== null ? address.port : input.port ?? 0;
|
|
207
216
|
let drainRun;
|
|
217
|
+
let retireRun;
|
|
218
|
+
const waitForInflight = async (graceMs) => {
|
|
219
|
+
const deadline = Date.now() + graceMs;
|
|
220
|
+
while (inflight > 0 && Date.now() < deadline) {
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const closeRetiringListener = async (graceMs) => {
|
|
225
|
+
server.closeIdleConnections();
|
|
226
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
227
|
+
await waitForInflight(graceMs);
|
|
228
|
+
if (inflight > 0)
|
|
229
|
+
server.closeAllConnections();
|
|
230
|
+
await closed;
|
|
231
|
+
};
|
|
232
|
+
const retire = (graceMs = 0) => {
|
|
233
|
+
retireRun ??= (async () => {
|
|
234
|
+
retiring = true;
|
|
235
|
+
await closeRetiringListener(graceMs);
|
|
236
|
+
})();
|
|
237
|
+
return retireRun;
|
|
238
|
+
};
|
|
208
239
|
const drain = (graceMs = 0) => {
|
|
209
240
|
drainRun ??= (async () => {
|
|
210
241
|
draining = true;
|
|
211
242
|
server.closeIdleConnections();
|
|
212
|
-
|
|
213
|
-
while (inflight > 0 && Date.now() < deadline) {
|
|
214
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
215
|
-
}
|
|
243
|
+
await waitForInflight(graceMs);
|
|
216
244
|
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
217
245
|
server.closeAllConnections();
|
|
218
246
|
await closed;
|
|
@@ -252,6 +280,7 @@ export async function startSwitchingGatewayProxy(input) {
|
|
|
252
280
|
generations.delete(generation.url);
|
|
253
281
|
return result;
|
|
254
282
|
},
|
|
283
|
+
retire,
|
|
255
284
|
drain,
|
|
256
285
|
close: async () => await drain(0)
|
|
257
286
|
};
|
|
@@ -20,6 +20,14 @@ test("Bedrock discovery includes active Anthropic foundations and paginated back
|
|
|
20
20
|
outputModalities: ["TEXT"],
|
|
21
21
|
responseStreamingSupported: true
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
modelId: "anthropic.claude-opus-5",
|
|
25
|
+
providerName: "Anthropic",
|
|
26
|
+
modelLifecycle: { status: "ACTIVE" },
|
|
27
|
+
inputModalities: ["TEXT"],
|
|
28
|
+
outputModalities: ["TEXT"],
|
|
29
|
+
responseStreamingSupported: true
|
|
30
|
+
},
|
|
23
31
|
{ modelId: "anthropic.old", providerName: "Anthropic", modelLifecycle: { status: "LEGACY" } },
|
|
24
32
|
{ modelId: "amazon.titan", providerName: "Amazon", modelLifecycle: { status: "ACTIVE" } }
|
|
25
33
|
]
|
|
@@ -28,6 +36,7 @@ test("Bedrock discovery includes active Anthropic foundations and paginated back
|
|
|
28
36
|
return token === undefined ? {
|
|
29
37
|
inferenceProfileSummaries: [
|
|
30
38
|
{ inferenceProfileId: "us.anthropic.claude-3", inferenceProfileName: "Claude", inferenceProfileArn: "arn:profile", status: "ACTIVE", type: "SYSTEM_DEFINED", createdAt: new Date("2026-07-09T00:00:00Z"), models: [{ modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3" }] },
|
|
39
|
+
{ inferenceProfileId: "us.anthropic.claude-opus-5", inferenceProfileName: "Opus 5", inferenceProfileArn: "arn:profile-opus-5", status: "ACTIVE", type: "SYSTEM_DEFINED", models: [{ modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-5" }] },
|
|
31
40
|
{ inferenceProfileId: "us.amazon.titan", inferenceProfileName: "Titan", inferenceProfileArn: "arn:profile2", status: "ACTIVE", type: "SYSTEM_DEFINED", models: [{ modelArn: "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan" }] }
|
|
32
41
|
],
|
|
33
42
|
nextToken: "page-2"
|
|
@@ -42,16 +51,29 @@ test("Bedrock discovery includes active Anthropic foundations and paginated back
|
|
|
42
51
|
});
|
|
43
52
|
const discovered = await source.discoverModels();
|
|
44
53
|
assert.deepEqual(discovered.map((model) => model.id), [
|
|
45
|
-
"anthropic.claude-3",
|
|
54
|
+
"anthropic.claude-3",
|
|
55
|
+
"anthropic.claude-opus-5",
|
|
56
|
+
"us.anthropic.claude-3",
|
|
57
|
+
"us.anthropic.claude-opus-5",
|
|
58
|
+
"eu.anthropic.claude-3"
|
|
46
59
|
]);
|
|
47
60
|
assert.deepEqual(discovered[0]?.metadata?.architecture, {
|
|
48
61
|
modality: "text+image->text",
|
|
49
62
|
inputModalities: ["text", "image"],
|
|
50
63
|
outputModalities: ["text"]
|
|
51
64
|
});
|
|
52
|
-
assert.deepEqual(discovered[
|
|
65
|
+
assert.deepEqual(discovered[2]?.metadata, discovered[0]?.metadata);
|
|
53
66
|
assert.ok(discovered.every((model) => model.createdAt === undefined && model.providerPriority === undefined), "profile creation time is not model recency");
|
|
54
67
|
assert.equal(discovered[0]?.capabilities?.streaming, "supported");
|
|
68
|
+
assert.equal(discovered[0]?.reasoning, undefined);
|
|
69
|
+
assert.deepEqual(discovered[1]?.reasoning, {
|
|
70
|
+
status: "supported",
|
|
71
|
+
efforts: [{ id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
|
|
72
|
+
adaptive: true,
|
|
73
|
+
wireShape: "bedrock-converse",
|
|
74
|
+
provenance: "builtin"
|
|
75
|
+
});
|
|
76
|
+
assert.deepEqual(discovered[3]?.reasoning, discovered[1]?.reasoning);
|
|
55
77
|
assert.equal(commands.length, 3);
|
|
56
78
|
});
|
|
57
79
|
test("Bedrock request translation covers system, image, tools, results, and inference config", () => {
|
|
@@ -80,6 +102,79 @@ test("Bedrock request translation covers system, image, tools, results, and infe
|
|
|
80
102
|
assert.deepEqual(input.inferenceConfig, { maxTokens: 256, temperature: 0.2, topP: 0.9, stopSequences: ["END"] });
|
|
81
103
|
assert.deepEqual(input.additionalModelRequestFields, { top_k: 40 });
|
|
82
104
|
});
|
|
105
|
+
test("Bedrock Opus 5 translates effort and adaptive reasoning controls", () => {
|
|
106
|
+
assert.deepEqual(toBedrockConverseInput({
|
|
107
|
+
model: "anthropic.claude-opus-5",
|
|
108
|
+
messages: [],
|
|
109
|
+
reasoning_effort: "high"
|
|
110
|
+
}).additionalModelRequestFields, { thinking: { type: "adaptive" }, output_config: { effort: "high" } });
|
|
111
|
+
assert.deepEqual(toBedrockConverseInput({
|
|
112
|
+
model: "us.anthropic.claude-opus-5",
|
|
113
|
+
messages: [],
|
|
114
|
+
x_routekit: { version: 1, selection: { mode: "adaptive" } }
|
|
115
|
+
}).additionalModelRequestFields, { thinking: { type: "adaptive" } });
|
|
116
|
+
assert.deepEqual(toBedrockConverseInput({
|
|
117
|
+
model: "anthropic.claude-opus-5",
|
|
118
|
+
messages: [],
|
|
119
|
+
x_routekit: { version: 1, selection: { mode: "budget", budgetTokens: 2048 } }
|
|
120
|
+
}).additionalModelRequestFields, { thinking: { type: "enabled", budget_tokens: 2048 } });
|
|
121
|
+
assert.deepEqual(toBedrockConverseInput({
|
|
122
|
+
model: "anthropic.claude-opus-5",
|
|
123
|
+
messages: [],
|
|
124
|
+
x_routekit: { version: 1, selection: { mode: "disabled" } }
|
|
125
|
+
}).additionalModelRequestFields, { thinking: { type: "disabled" } });
|
|
126
|
+
assert.equal(toBedrockConverseInput({
|
|
127
|
+
model: "anthropic.claude-opus-5",
|
|
128
|
+
messages: []
|
|
129
|
+
}).additionalModelRequestFields, undefined);
|
|
130
|
+
});
|
|
131
|
+
test("Bedrock maps profile-required Opus 5 foundations to a discovered inference profile", async () => {
|
|
132
|
+
let command;
|
|
133
|
+
const source = new BedrockProviderSource({
|
|
134
|
+
controlClient: {
|
|
135
|
+
send: async (value) => {
|
|
136
|
+
if (value instanceof ListFoundationModelsCommand) {
|
|
137
|
+
return {
|
|
138
|
+
modelSummaries: [{
|
|
139
|
+
modelId: "anthropic.claude-opus-5",
|
|
140
|
+
providerName: "Anthropic",
|
|
141
|
+
modelLifecycle: { status: "ACTIVE" }
|
|
142
|
+
}]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
inferenceProfileSummaries: [{
|
|
147
|
+
inferenceProfileId: "us.anthropic.claude-opus-5",
|
|
148
|
+
status: "ACTIVE",
|
|
149
|
+
models: [{
|
|
150
|
+
modelArn: "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-opus-5"
|
|
151
|
+
}]
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
runtimeClient: {
|
|
157
|
+
send: async (value) => {
|
|
158
|
+
command = value;
|
|
159
|
+
return {
|
|
160
|
+
$metadata: { requestId: "req-opus-5" },
|
|
161
|
+
output: { message: { role: "assistant", content: [{ text: "OK" }] } },
|
|
162
|
+
stopReason: "end_turn"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
await source.discoverModels();
|
|
168
|
+
const response = await source.chat({
|
|
169
|
+
model: "anthropic.claude-opus-5",
|
|
170
|
+
messages: [{ role: "user", content: "Reply with exactly OK." }],
|
|
171
|
+
reasoning_effort: "low"
|
|
172
|
+
});
|
|
173
|
+
assert.equal(response.status, 200);
|
|
174
|
+
assert.equal(command instanceof ConverseCommand, true);
|
|
175
|
+
assert.equal(command?.input.modelId, "us.anthropic.claude-opus-5");
|
|
176
|
+
assert.equal((await response.json()).model, "anthropic.claude-opus-5");
|
|
177
|
+
});
|
|
83
178
|
test("Bedrock Converse maps text, reasoning, tools, stop, and usage", async () => {
|
|
84
179
|
let command;
|
|
85
180
|
const source = new BedrockProviderSource({
|
|
@@ -302,6 +397,7 @@ test("Bedrock defaults reasoning to unknown and ordinary requests omit thinking"
|
|
|
302
397
|
controlClient: { send: async () => ({}) },
|
|
303
398
|
runtimeClient: { send: async () => ({}) }
|
|
304
399
|
});
|
|
400
|
+
assert.equal(source.reasoningCapabilities("anthropic.claude-opus-5")?.status, "supported");
|
|
305
401
|
assert.deepEqual(source.reasoningCapabilities(), {
|
|
306
402
|
status: "unknown", wireShape: "bedrock-converse", provenance: "provider"
|
|
307
403
|
});
|
|
@@ -215,7 +215,7 @@ test("OpenRouter metadata propagates caller cancellation and enforces its timeou
|
|
|
215
215
|
const fetch = async (_input, init) => await new Promise((_resolve, reject) => {
|
|
216
216
|
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason ?? new Error("aborted")), { once: true });
|
|
217
217
|
});
|
|
218
|
-
const canceled = new OpenRouterModelMetadataClient({ fetch, timeoutMs:
|
|
218
|
+
const canceled = new OpenRouterModelMetadataClient({ fetch, timeoutMs: 25 });
|
|
219
219
|
const controller = new AbortController();
|
|
220
220
|
const pending = canceled.models(controller.signal);
|
|
221
221
|
controller.abort(new Error("caller canceled"));
|
package/dist/test/router.test.js
CHANGED
|
@@ -714,3 +714,57 @@ test("Bedrock models use canonical ids and API-key billing attribution", async (
|
|
|
714
714
|
}
|
|
715
715
|
]);
|
|
716
716
|
});
|
|
717
|
+
test("Bedrock Opus 5 exposes reasoning controls and accepts routed effort selections", async () => {
|
|
718
|
+
const bodies = [];
|
|
719
|
+
const backend = await CatalogBackend.create({
|
|
720
|
+
config: {
|
|
721
|
+
providers: { bedrock: {} },
|
|
722
|
+
defaultModel: "bedrock/anthropic.claude-opus-5"
|
|
723
|
+
},
|
|
724
|
+
sources: {
|
|
725
|
+
bedrock: {
|
|
726
|
+
...fakeSource("bedrock", [{
|
|
727
|
+
id: "anthropic.claude-opus-5",
|
|
728
|
+
reasoning: {
|
|
729
|
+
status: "supported",
|
|
730
|
+
efforts: [{ id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
|
|
731
|
+
adaptive: true,
|
|
732
|
+
wireShape: "bedrock-converse",
|
|
733
|
+
provenance: "builtin"
|
|
734
|
+
}
|
|
735
|
+
}]),
|
|
736
|
+
async chat(body) {
|
|
737
|
+
bodies.push(body);
|
|
738
|
+
return Response.json({ ok: true });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
const models = (await (await backend.models()).json());
|
|
744
|
+
assert.deepEqual(models.data[0]?.reasoning, {
|
|
745
|
+
status: "supported",
|
|
746
|
+
efforts: [{ id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
|
|
747
|
+
adaptive: true,
|
|
748
|
+
wireShape: "bedrock-converse",
|
|
749
|
+
provenance: "builtin"
|
|
750
|
+
});
|
|
751
|
+
const accepted = await backend.chat({
|
|
752
|
+
model: "bedrock/anthropic.claude-opus-5",
|
|
753
|
+
messages: [],
|
|
754
|
+
reasoning_effort: "high"
|
|
755
|
+
});
|
|
756
|
+
assert.equal(accepted.status, 200);
|
|
757
|
+
assert.equal(bodies[0]?.model, "anthropic.claude-opus-5");
|
|
758
|
+
assert.deepEqual(bodies[0]?.x_routekit, {
|
|
759
|
+
version: 1,
|
|
760
|
+
selection: { mode: "effort", effort: "high" }
|
|
761
|
+
});
|
|
762
|
+
const rejected = await backend.chat({
|
|
763
|
+
model: "bedrock/anthropic.claude-opus-5",
|
|
764
|
+
messages: [],
|
|
765
|
+
reasoning_effort: "minimal"
|
|
766
|
+
});
|
|
767
|
+
assert.equal(rejected.status, 400);
|
|
768
|
+
const error = (await rejected.json());
|
|
769
|
+
assert.equal(error.error.code, "unsupported_reasoning_control");
|
|
770
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { startSwitchingGatewayProxy } from "../switching-proxy.js";
|
|
5
|
+
async function heldTarget() {
|
|
6
|
+
let markStarted;
|
|
7
|
+
const started = new Promise((resolve) => {
|
|
8
|
+
markStarted = resolve;
|
|
9
|
+
});
|
|
10
|
+
let release;
|
|
11
|
+
const released = new Promise((resolve) => {
|
|
12
|
+
release = resolve;
|
|
13
|
+
});
|
|
14
|
+
const server = createServer((request, response) => {
|
|
15
|
+
if (request.url === "/stream") {
|
|
16
|
+
response.setHeader("content-type", "text/plain");
|
|
17
|
+
response.write("first\n");
|
|
18
|
+
markStarted();
|
|
19
|
+
void released.then(() => response.end("last\n"));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
response.end("ok");
|
|
23
|
+
});
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
server.once("error", reject);
|
|
26
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
27
|
+
});
|
|
28
|
+
const port = server.address().port;
|
|
29
|
+
return {
|
|
30
|
+
url: `http://127.0.0.1:${port}`,
|
|
31
|
+
started,
|
|
32
|
+
release,
|
|
33
|
+
close: async () => await new Promise((resolve, reject) => server.close((error) => (error === undefined ? resolve() : reject(error))))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
test("switching proxy retirement preserves an admitted streaming response", async () => {
|
|
37
|
+
const target = await heldTarget();
|
|
38
|
+
const proxy = await startSwitchingGatewayProxy({ target: target.url });
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${proxy.url()}/stream`);
|
|
41
|
+
const reader = response.body?.getReader();
|
|
42
|
+
assert.ok(reader !== undefined);
|
|
43
|
+
const first = await reader.read();
|
|
44
|
+
assert.match(Buffer.from(first.value ?? []).toString("utf8"), /first/);
|
|
45
|
+
await target.started;
|
|
46
|
+
const retiring = proxy.retire(2_000);
|
|
47
|
+
target.release();
|
|
48
|
+
let remainder = "";
|
|
49
|
+
for (;;) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done)
|
|
52
|
+
break;
|
|
53
|
+
remainder += Buffer.from(value ?? []).toString("utf8");
|
|
54
|
+
}
|
|
55
|
+
assert.match(remainder, /last/);
|
|
56
|
+
await retiring;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await proxy.close();
|
|
60
|
+
await target.close();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
test("switching proxy retirement deadline terminates a stream that never finishes", async () => {
|
|
64
|
+
const target = await heldTarget();
|
|
65
|
+
const proxy = await startSwitchingGatewayProxy({ target: target.url });
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`${proxy.url()}/stream`);
|
|
68
|
+
const reader = response.body?.getReader();
|
|
69
|
+
assert.ok(reader !== undefined);
|
|
70
|
+
await reader.read();
|
|
71
|
+
await target.started;
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
await proxy.retire(250);
|
|
74
|
+
assert.ok(Date.now() - startedAt >= 200);
|
|
75
|
+
await assert.rejects(async () => {
|
|
76
|
+
for (;;) {
|
|
77
|
+
const { done } = await reader.read();
|
|
78
|
+
if (done)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
target.release();
|
|
85
|
+
await proxy.close();
|
|
86
|
+
await target.close();
|
|
87
|
+
}
|
|
88
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-gateway",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.2",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@aws-sdk/client-bedrock": "3.1095.0",
|
|
30
30
|
"@aws-sdk/client-bedrock-runtime": "3.1095.0",
|
|
31
31
|
"zod": "4.4.3",
|
|
32
|
-
"@velum-labs/routekit-contracts": "0.18.
|
|
33
|
-
"@velum-labs/routekit-registry": "0.18.
|
|
34
|
-
"@velum-labs/routekit-runtime": "0.18.
|
|
35
|
-
"@velum-labs/routekit-tracing": "0.18.
|
|
32
|
+
"@velum-labs/routekit-contracts": "0.18.2",
|
|
33
|
+
"@velum-labs/routekit-registry": "0.18.2",
|
|
34
|
+
"@velum-labs/routekit-runtime": "0.18.2",
|
|
35
|
+
"@velum-labs/routekit-tracing": "0.18.2"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"routekit",
|