@velum-labs/routekit-gateway 0.18.0 → 0.18.1

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.
@@ -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
  }
@@ -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
- : undefined;
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(input), signal === undefined ? undefined : { abortSignal: signal });
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(input), signal === undefined ? undefined : { abortSignal: signal });
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 timeout = AbortSignal.timeout(this.#timeoutMs);
124
- const settled = await Promise.allSettled(TASK_CATALOGS.map(async (catalog) => {
125
- const response = await this.#fetch(`${this.#baseUrl}${catalog.path}`, {
126
- headers: { accept: "application/json" },
127
- signal: timeout
128
- });
129
- if (!response.ok) {
130
- throw new Error(`OpenRouter ${catalog.path} returned HTTP ${response.status}`);
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
- const payload = record(await response.json());
133
- if (!Array.isArray(payload?.data)) {
134
- throw new Error(`OpenRouter ${catalog.path} returned an invalid model catalog`);
150
+ finally {
151
+ clearTimeout(timeout);
135
152
  }
136
- return {
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");
@@ -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", "us.anthropic.claude-3", "eu.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[1]?.metadata, discovered[0]?.metadata);
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: 1_000 });
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"));
@@ -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
+ });
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.0",
4
+ "version": "0.18.1",
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.0",
33
- "@velum-labs/routekit-registry": "0.18.0",
34
- "@velum-labs/routekit-runtime": "0.18.0",
35
- "@velum-labs/routekit-tracing": "0.18.0"
32
+ "@velum-labs/routekit-contracts": "0.18.1",
33
+ "@velum-labs/routekit-registry": "0.18.1",
34
+ "@velum-labs/routekit-runtime": "0.18.1",
35
+ "@velum-labs/routekit-tracing": "0.18.1"
36
36
  },
37
37
  "keywords": [
38
38
  "routekit",