@velum-labs/routekit-gateway 0.17.4 → 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.
- package/dist/adapters/openai-responses-wire.d.ts +6 -0
- package/dist/adapters/openai-responses-wire.js +34 -0
- package/dist/adapters/responses-stream.js +1 -1
- package/dist/adapters/responses.js +3 -3
- package/dist/backend.js +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/server.js +3 -3
- package/dist/test/bedrock-source.test.js +98 -2
- package/dist/test/codex-model-selection.test.js +1 -1
- package/dist/test/openai-responses-wire.test.js +44 -1
- package/dist/test/responses.test.js +162 -3
- package/dist/test/router.test.js +54 -0
- package/package.json +5 -5
|
@@ -20,6 +20,12 @@ export type PreparedResponsesReasoningInput = {
|
|
|
20
20
|
body: unknown;
|
|
21
21
|
dropped: number;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Repair RouteKit's legacy tool-search item prefix before replaying history to
|
|
25
|
+
* a native OpenAI Responses destination. Tool outputs correlate through
|
|
26
|
+
* `call_id`, so changing only the item `id` preserves the execution pair.
|
|
27
|
+
*/
|
|
28
|
+
export declare function repairLegacyToolSearchItemIds(body: unknown): unknown;
|
|
23
29
|
/** Wrap provider-owned opaque reasoning without expanding the ciphertext itself. */
|
|
24
30
|
export declare function wrapResponsesEncryptedContent(ciphertext: string, owner: ResponsesReasoningOwner): string;
|
|
25
31
|
/** Parse a RouteKit-owned encrypted-reasoning envelope. Raw provider ciphertext is ownerless. */
|
|
@@ -2,9 +2,43 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
const OPENAI_RESPONSES_CALL_ID_MAX_LENGTH = 64;
|
|
3
3
|
const NORMALIZED_CALL_ID_PREFIX = "rk_";
|
|
4
4
|
const ENCRYPTED_REASONING_PREFIX = "rk1.";
|
|
5
|
+
const LEGACY_TOOL_SEARCH_ITEM_ID_PREFIX = "ttc_";
|
|
6
|
+
const TOOL_SEARCH_ITEM_ID_PREFIX = "tsc_";
|
|
5
7
|
function sameOwner(left, right) {
|
|
6
8
|
return left.provider === right.provider && left.nativeModel === right.nativeModel;
|
|
7
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Repair RouteKit's legacy tool-search item prefix before replaying history to
|
|
12
|
+
* a native OpenAI Responses destination. Tool outputs correlate through
|
|
13
|
+
* `call_id`, so changing only the item `id` preserves the execution pair.
|
|
14
|
+
*/
|
|
15
|
+
export function repairLegacyToolSearchItemIds(body) {
|
|
16
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
17
|
+
return body;
|
|
18
|
+
const record = body;
|
|
19
|
+
if (!Array.isArray(record.input))
|
|
20
|
+
return body;
|
|
21
|
+
let changed = false;
|
|
22
|
+
const input = record.input.map((candidate) => {
|
|
23
|
+
if (typeof candidate !== "object" ||
|
|
24
|
+
candidate === null ||
|
|
25
|
+
Array.isArray(candidate)) {
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
const item = candidate;
|
|
29
|
+
if (item.type !== "tool_search_call" ||
|
|
30
|
+
typeof item.id !== "string" ||
|
|
31
|
+
!item.id.startsWith(LEGACY_TOOL_SEARCH_ITEM_ID_PREFIX)) {
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
changed = true;
|
|
35
|
+
return {
|
|
36
|
+
...item,
|
|
37
|
+
id: `${TOOL_SEARCH_ITEM_ID_PREFIX}${item.id.slice(LEGACY_TOOL_SEARCH_ITEM_ID_PREFIX.length)}`
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return changed ? { ...record, input } : body;
|
|
41
|
+
}
|
|
8
42
|
/** Wrap provider-owned opaque reasoning without expanding the ciphertext itself. */
|
|
9
43
|
export function wrapResponsesEncryptedContent(ciphertext, owner) {
|
|
10
44
|
if (parseResponsesEncryptedContent(ciphertext) !== undefined)
|
|
@@ -475,7 +475,7 @@ export function openAiSseToResponses(upstream, model, toolRegistry = new Map())
|
|
|
475
475
|
itemId: kind === "custom"
|
|
476
476
|
? `ctc_${randomId()}`
|
|
477
477
|
: kind === "typed"
|
|
478
|
-
? `
|
|
478
|
+
? `tsc_${randomId()}`
|
|
479
479
|
: kind === "server"
|
|
480
480
|
? `ws_${randomId()}`
|
|
481
481
|
: `fc_${randomId()}`,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { randomId } from "@velum-labs/routekit-runtime";
|
|
16
16
|
import { attachReasoningSelection, attachReasoningSelectionError, attachResponsesReasoningMetadata, reasoningSelectionErrorOf, hasExplicitReasoningSelection, reasoningSelectionOf, routeKitRequestValidationErrorOf, responsesReasoningMetadataOf } from "./openai-chat-wire.js";
|
|
17
17
|
import { droppedField } from "./dropped.js";
|
|
18
|
-
import { prepareResponsesReasoningInput, wrapResponsesReasoningResponse } from "./openai-responses-wire.js";
|
|
18
|
+
import { prepareResponsesReasoningInput, repairLegacyToolSearchItemIds, wrapResponsesReasoningResponse } from "./openai-responses-wire.js";
|
|
19
19
|
import { unwrapUpstreamError } from "./upstream-error.js";
|
|
20
20
|
import { openAiSseToResponses } from "./responses-stream.js";
|
|
21
21
|
import { chatUsageToResponses } from "./responses-usage.js";
|
|
@@ -723,7 +723,7 @@ function buildOutput(message, toolRegistry) {
|
|
|
723
723
|
if (entry.kind === "typed") {
|
|
724
724
|
output.push(typedToolCallItem({
|
|
725
725
|
name,
|
|
726
|
-
itemId: `
|
|
726
|
+
itemId: `tsc_${randomId()}`,
|
|
727
727
|
callId: call.id ?? `call_${randomId()}`,
|
|
728
728
|
args
|
|
729
729
|
}));
|
|
@@ -864,7 +864,7 @@ export async function handleResponses(backend, body, modelCallId, signal, backen
|
|
|
864
864
|
destinationWireShape === "openai-responses" ||
|
|
865
865
|
destinationWireShape === "routekit-envelope";
|
|
866
866
|
if (supportsNativeResponses && nativeResponses !== undefined) {
|
|
867
|
-
const prepared = prepareResponsesReasoningInput(body, {
|
|
867
|
+
const prepared = prepareResponsesReasoningInput(repairLegacyToolSearchItemIds(body), {
|
|
868
868
|
mode: "forward",
|
|
869
869
|
owner: reasoningOwner
|
|
870
870
|
});
|
package/dist/backend.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REASONING_SELECTION, reasoningSelectionOf, routeKitRequestValidationErrorOf, withoutRouteKitExtensions } from "./adapters/openai-chat-wire.js";
|
|
2
|
-
import { normalizeOpenAiResponsesCallIds } from "./adapters/openai-responses-wire.js";
|
|
2
|
+
import { normalizeOpenAiResponsesCallIds, repairLegacyToolSearchItemIds } from "./adapters/openai-responses-wire.js";
|
|
3
3
|
/** Join a base URL (which may end in `/`) with a route path. */
|
|
4
4
|
export function joinPath(baseUrl, path) {
|
|
5
5
|
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
@@ -105,7 +105,7 @@ export class OpenAiBackend {
|
|
|
105
105
|
return fetch(joinPath(this.#baseUrl, "/responses"), {
|
|
106
106
|
method: "POST",
|
|
107
107
|
headers: this.#headers(options),
|
|
108
|
-
body: JSON.stringify(normalizeOpenAiResponsesCallIds(providerPayload)),
|
|
108
|
+
body: JSON.stringify(normalizeOpenAiResponsesCallIds(repairLegacyToolSearchItemIds(providerPayload))),
|
|
109
109
|
...(signal ? { signal } : {})
|
|
110
110
|
});
|
|
111
111
|
}
|
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");
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { effectiveModel, isStream, withDefaultModel } from "./adapters/chat.js";
|
|
|
5
5
|
import { cursorModelVariants, isCursorChatBody, resolveCursorModelSelection, translateCursorRequest } from "./adapters/cursor.js";
|
|
6
6
|
import { droppedField } from "./adapters/dropped.js";
|
|
7
7
|
import { routeKitRequestValidationErrorOf, withReasoningSelection } from "./adapters/openai-chat-wire.js";
|
|
8
|
-
import { prepareResponsesReasoningInput, wrapResponsesReasoningResponse } from "./adapters/openai-responses-wire.js";
|
|
8
|
+
import { prepareResponsesReasoningInput, repairLegacyToolSearchItemIds, wrapResponsesReasoningResponse } from "./adapters/openai-responses-wire.js";
|
|
9
9
|
import { handleResponses } from "./adapters/responses.js";
|
|
10
10
|
import { validateAnthropicRequest, validateChatRequest, validateCountTokensRequest, validateResponsesRequest } from "./adapters/validate.js";
|
|
11
11
|
import { authorizedRequest, parsePrincipalHeader, ROUTEKIT_PRINCIPAL_HEADER } from "./auth.js";
|
|
@@ -656,7 +656,7 @@ export async function startGateway(options) {
|
|
|
656
656
|
: withModel(routedBody, route.publicId);
|
|
657
657
|
if (codexProviderRelay !== undefined && route?.provider === "codex") {
|
|
658
658
|
const owner = { provider: "codex", nativeModel: route.nativeId };
|
|
659
|
-
const prepared = prepareResponsesReasoningInput(withModel(body, route.nativeId), {
|
|
659
|
+
const prepared = prepareResponsesReasoningInput(repairLegacyToolSearchItemIds(withModel(body, route.nativeId)), {
|
|
660
660
|
mode: "forward",
|
|
661
661
|
owner
|
|
662
662
|
});
|
|
@@ -692,7 +692,7 @@ export async function startGateway(options) {
|
|
|
692
692
|
provider: "codex",
|
|
693
693
|
nativeModel: requestedModel ?? "codex/default"
|
|
694
694
|
};
|
|
695
|
-
const prepared = prepareResponsesReasoningInput(body, {
|
|
695
|
+
const prepared = prepareResponsesReasoningInput(repairLegacyToolSearchItemIds(body), {
|
|
696
696
|
mode: "forward",
|
|
697
697
|
owner
|
|
698
698
|
});
|
|
@@ -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"));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
-
import { normalizeOpenAiResponsesCallIds, parseResponsesEncryptedContent, prepareResponsesReasoningInput, wrapResponsesEncryptedContent, wrapResponsesReasoningResponse } from "../adapters/openai-responses-wire.js";
|
|
4
|
+
import { normalizeOpenAiResponsesCallIds, parseResponsesEncryptedContent, prepareResponsesReasoningInput, repairLegacyToolSearchItemIds, wrapResponsesEncryptedContent, wrapResponsesReasoningResponse } from "../adapters/openai-responses-wire.js";
|
|
5
5
|
const OWNER_A = {
|
|
6
6
|
provider: "codex",
|
|
7
7
|
nativeModel: "gpt-5.6"
|
|
@@ -91,6 +91,49 @@ test("Responses call id replacement assignment is independent of occurrence orde
|
|
|
91
91
|
assert.equal(forward.get(firstLongId), reversed.get(firstLongId));
|
|
92
92
|
assert.equal(forward.get(secondLongId), reversed.get(secondLongId));
|
|
93
93
|
});
|
|
94
|
+
test("legacy tool-search item ids are repaired narrowly without mutating input", () => {
|
|
95
|
+
const legacy = {
|
|
96
|
+
type: "tool_search_call",
|
|
97
|
+
id: "ttc_legacy",
|
|
98
|
+
call_id: "call_search",
|
|
99
|
+
status: "completed"
|
|
100
|
+
};
|
|
101
|
+
const valid = {
|
|
102
|
+
type: "tool_search_call",
|
|
103
|
+
id: "tsc_valid",
|
|
104
|
+
call_id: "call_valid"
|
|
105
|
+
};
|
|
106
|
+
const unrelated = {
|
|
107
|
+
type: "function_call",
|
|
108
|
+
id: "ttc_function",
|
|
109
|
+
call_id: "call_function"
|
|
110
|
+
};
|
|
111
|
+
const output = {
|
|
112
|
+
type: "tool_search_output",
|
|
113
|
+
call_id: "call_search",
|
|
114
|
+
tools: []
|
|
115
|
+
};
|
|
116
|
+
const body = {
|
|
117
|
+
model: "gpt-5.6-sol",
|
|
118
|
+
input: [legacy, valid, unrelated, output]
|
|
119
|
+
};
|
|
120
|
+
const repaired = repairLegacyToolSearchItemIds(body);
|
|
121
|
+
assert.notEqual(repaired, body);
|
|
122
|
+
assert.notEqual(repaired.input, body.input);
|
|
123
|
+
assert.deepEqual(repaired.input[0], {
|
|
124
|
+
...legacy,
|
|
125
|
+
id: "tsc_legacy"
|
|
126
|
+
});
|
|
127
|
+
assert.equal(repaired.input[0]?.call_id, "call_search");
|
|
128
|
+
assert.equal(repaired.input[1], valid);
|
|
129
|
+
assert.equal(repaired.input[2], unrelated);
|
|
130
|
+
assert.equal(repaired.input[3], output);
|
|
131
|
+
assert.equal(legacy.id, "ttc_legacy");
|
|
132
|
+
const alreadyCompatible = { input: [valid, output] };
|
|
133
|
+
assert.equal(repairLegacyToolSearchItemIds(alreadyCompatible), alreadyCompatible);
|
|
134
|
+
const stringInput = { input: "hello" };
|
|
135
|
+
assert.equal(repairLegacyToolSearchItemIds(stringInput), stringInput);
|
|
136
|
+
});
|
|
94
137
|
test("encrypted reasoning ownership round-trips exact opaque content", () => {
|
|
95
138
|
const ciphertext = "opaque.with.separators/and unicode \u{1f9e0}\nbytes";
|
|
96
139
|
const wrapped = wrapResponsesEncryptedContent(ciphertext, OWNER_A);
|
|
@@ -1586,6 +1586,8 @@ test("chatToResponses emits a native typed item for a call resolved as typed", (
|
|
|
1586
1586
|
const output = response.output;
|
|
1587
1587
|
assert.equal(output.length, 1);
|
|
1588
1588
|
assert.equal(output[0]?.type, "tool_search_call");
|
|
1589
|
+
assert.match(String(output[0]?.id), /^tsc_/);
|
|
1590
|
+
assert.ok(!String(output[0]?.id).startsWith("ttc_"));
|
|
1589
1591
|
assert.equal(output[0]?.call_id, "call_ts");
|
|
1590
1592
|
assert.equal(output[0]?.execution, "client");
|
|
1591
1593
|
assert.equal(output[0]?.status, "completed");
|
|
@@ -1598,16 +1600,143 @@ test("openAiSseToResponses streams a typed tool call as its native item", async
|
|
|
1598
1600
|
const upstream = sseStream(chatChunk({ tool_calls: [{ index: 0, id: "call_ts", function: { name: "tool_search", arguments: args.slice(0, 10) } }] }), chatChunk({ tool_calls: [{ index: 0, function: { arguments: args.slice(10) } }] }), chatChunk({}, "tool_calls"), "data: [DONE]\n\n");
|
|
1599
1601
|
const text = await new Response(openAiSseToResponses(upstream, "route-primary", registry)).text();
|
|
1600
1602
|
assert.ok(text.includes('"type":"tool_search_call"'));
|
|
1603
|
+
assert.ok(!text.includes('"id":"ttc_'));
|
|
1601
1604
|
assert.ok(!text.includes('"type":"function_call"'), "typed calls never surface as function_call items");
|
|
1602
1605
|
assert.ok(!text.includes("response.function_call_arguments"), "typed calls emit no argument delta events");
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
+
const events = text
|
|
1607
|
+
.split("\n\n")
|
|
1608
|
+
.filter((event) => event.includes("\ndata: "))
|
|
1609
|
+
.map((event) => JSON.parse(event.slice(event.indexOf("data:") + 5)));
|
|
1610
|
+
const added = events.find((event) => event.type === "response.output_item.added");
|
|
1611
|
+
const done = events.find((event) => event.type === "response.output_item.done");
|
|
1612
|
+
const completed = events.find((event) => event.type === "response.completed");
|
|
1613
|
+
assert.equal(added?.item?.type, "tool_search_call");
|
|
1614
|
+
assert.equal(done?.item?.type, "tool_search_call");
|
|
1615
|
+
assert.match(added?.item?.id ?? "", /^tsc_/);
|
|
1616
|
+
assert.equal(done?.item?.id, added?.item?.id);
|
|
1617
|
+
const payload = completed;
|
|
1606
1618
|
const item = payload.response.output.find((entry) => entry.type === "tool_search_call");
|
|
1619
|
+
assert.equal(item?.id, added?.item?.id);
|
|
1607
1620
|
assert.equal(item?.call_id, "call_ts");
|
|
1608
1621
|
assert.equal(item?.execution, "client");
|
|
1609
1622
|
assert.deepEqual(item?.arguments, { query: "spawn sub-agent", limit: 8 });
|
|
1610
1623
|
});
|
|
1624
|
+
test("translated tool_search history keeps a valid item id when switching to native Codex", async () => {
|
|
1625
|
+
const source = (sourceId) => ({
|
|
1626
|
+
sourceId,
|
|
1627
|
+
discoverModels: async () => [{
|
|
1628
|
+
id: sourceId === "codex" ? "gpt-5.6-sol" : "claude-sonnet-4-6",
|
|
1629
|
+
metadata: {
|
|
1630
|
+
architecture: {
|
|
1631
|
+
inputModalities: ["text"],
|
|
1632
|
+
outputModalities: ["text"]
|
|
1633
|
+
},
|
|
1634
|
+
supportedParameters: ["tools", "tool_choice"],
|
|
1635
|
+
provenance: "route"
|
|
1636
|
+
}
|
|
1637
|
+
}],
|
|
1638
|
+
chat: async () => Response.json({
|
|
1639
|
+
id: "chatcmpl_tool_search",
|
|
1640
|
+
choices: [
|
|
1641
|
+
{
|
|
1642
|
+
index: 0,
|
|
1643
|
+
message: {
|
|
1644
|
+
role: "assistant",
|
|
1645
|
+
content: null,
|
|
1646
|
+
tool_calls: [
|
|
1647
|
+
{
|
|
1648
|
+
id: "call_ts",
|
|
1649
|
+
function: {
|
|
1650
|
+
name: "tool_search",
|
|
1651
|
+
arguments: '{"query":"spawn sub-agent","limit":8}'
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
]
|
|
1655
|
+
},
|
|
1656
|
+
finish_reason: "tool_calls"
|
|
1657
|
+
}
|
|
1658
|
+
],
|
|
1659
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 }
|
|
1660
|
+
}),
|
|
1661
|
+
embeddings: async () => Response.json({})
|
|
1662
|
+
});
|
|
1663
|
+
const backend = await CatalogBackend.create({
|
|
1664
|
+
config: {
|
|
1665
|
+
providers: { codex: {}, "claude-code": {} },
|
|
1666
|
+
defaultModel: "claude-code/claude-sonnet-4-6"
|
|
1667
|
+
},
|
|
1668
|
+
sources: {
|
|
1669
|
+
codex: source("codex"),
|
|
1670
|
+
"claude-code": source("claude-code")
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
let relayedBody;
|
|
1674
|
+
const relay = {
|
|
1675
|
+
dialect: "codex",
|
|
1676
|
+
shouldRelay: () => false,
|
|
1677
|
+
relay: async (_headers, body) => {
|
|
1678
|
+
relayedBody = body;
|
|
1679
|
+
return Response.json({
|
|
1680
|
+
id: "resp_native_after_switch",
|
|
1681
|
+
object: "response",
|
|
1682
|
+
status: "completed",
|
|
1683
|
+
model: body.model,
|
|
1684
|
+
output: [],
|
|
1685
|
+
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
const gateway = await startGateway({
|
|
1690
|
+
backend,
|
|
1691
|
+
providerRelays: { codex: relay }
|
|
1692
|
+
});
|
|
1693
|
+
try {
|
|
1694
|
+
const translated = await fetch(`${gateway.url()}/v1/responses`, {
|
|
1695
|
+
method: "POST",
|
|
1696
|
+
headers: { "content-type": "application/json" },
|
|
1697
|
+
body: JSON.stringify({
|
|
1698
|
+
model: "claude-code/claude-sonnet-4-6",
|
|
1699
|
+
input: "find a sub-agent tool",
|
|
1700
|
+
tools: [TOOL_SEARCH_DECL]
|
|
1701
|
+
})
|
|
1702
|
+
});
|
|
1703
|
+
assert.equal(translated.status, 200);
|
|
1704
|
+
const translatedPayload = await translated.json();
|
|
1705
|
+
const toolSearchCall = translatedPayload.output.find((item) => item.type === "tool_search_call");
|
|
1706
|
+
assert.ok(toolSearchCall !== undefined);
|
|
1707
|
+
assert.match(String(toolSearchCall.id), /^tsc_/);
|
|
1708
|
+
assert.equal(toolSearchCall.call_id, "call_ts");
|
|
1709
|
+
const legacyToolSearchCall = {
|
|
1710
|
+
...toolSearchCall,
|
|
1711
|
+
id: String(toolSearchCall.id).replace(/^tsc_/, "ttc_")
|
|
1712
|
+
};
|
|
1713
|
+
assert.match(String(legacyToolSearchCall.id), /^ttc_/);
|
|
1714
|
+
const switched = await fetch(`${gateway.url()}/v1/responses`, {
|
|
1715
|
+
method: "POST",
|
|
1716
|
+
headers: { "content-type": "application/json" },
|
|
1717
|
+
body: JSON.stringify({
|
|
1718
|
+
model: "gpt-5.6-sol",
|
|
1719
|
+
input: [
|
|
1720
|
+
legacyToolSearchCall,
|
|
1721
|
+
{
|
|
1722
|
+
type: "tool_search_output",
|
|
1723
|
+
call_id: "call_ts",
|
|
1724
|
+
status: "completed",
|
|
1725
|
+
execution: "client",
|
|
1726
|
+
tools: []
|
|
1727
|
+
}
|
|
1728
|
+
]
|
|
1729
|
+
})
|
|
1730
|
+
});
|
|
1731
|
+
assert.equal(switched.status, 200);
|
|
1732
|
+
const relayedInput = relayedBody?.input;
|
|
1733
|
+
assert.equal(relayedInput?.[0]?.id, toolSearchCall.id);
|
|
1734
|
+
assert.equal(relayedInput?.[0]?.call_id, "call_ts");
|
|
1735
|
+
}
|
|
1736
|
+
finally {
|
|
1737
|
+
await gateway.close();
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1611
1740
|
test("openAiSseToResponses keeps function tools on the incremental function_call path", async () => {
|
|
1612
1741
|
const upstream = sseStream(chatChunk({ tool_calls: [{ index: 0, id: "call_s", function: { name: "shell", arguments: '{"cmd":"ls"}' } }] }), chatChunk({}, "tool_calls"), "data: [DONE]\n\n");
|
|
1613
1742
|
const text = await new Response(openAiSseToResponses(upstream, "route-primary", new Map([["apply_patch", { kind: "custom" }]]))).text();
|
|
@@ -2012,6 +2141,21 @@ test("Codex client relay still receives unknown native models after alias resolu
|
|
|
2012
2141
|
{ type: "reasoning", encrypted_content: matching },
|
|
2013
2142
|
{ type: "reasoning", encrypted_content: foreign },
|
|
2014
2143
|
{ type: "reasoning", encrypted_content: "legacy-raw-request" },
|
|
2144
|
+
{
|
|
2145
|
+
type: "tool_search_call",
|
|
2146
|
+
id: "ttc_persisted",
|
|
2147
|
+
call_id: "call_search",
|
|
2148
|
+
status: "completed",
|
|
2149
|
+
execution: "client",
|
|
2150
|
+
arguments: { query: "verification" }
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
type: "tool_search_output",
|
|
2154
|
+
call_id: "call_search",
|
|
2155
|
+
status: "completed",
|
|
2156
|
+
execution: "client",
|
|
2157
|
+
tools: []
|
|
2158
|
+
},
|
|
2015
2159
|
{ type: "message", role: "user", content: "hi" }
|
|
2016
2160
|
]
|
|
2017
2161
|
})
|
|
@@ -2026,6 +2170,21 @@ test("Codex client relay still receives unknown native models after alias resolu
|
|
|
2026
2170
|
assert.equal(relayCalls[0]?.model, "upstream-only");
|
|
2027
2171
|
assert.deepEqual(relayCalls[0]?.input, [
|
|
2028
2172
|
{ type: "reasoning", encrypted_content: "raw-client-relay-request" },
|
|
2173
|
+
{
|
|
2174
|
+
type: "tool_search_call",
|
|
2175
|
+
id: "tsc_persisted",
|
|
2176
|
+
call_id: "call_search",
|
|
2177
|
+
status: "completed",
|
|
2178
|
+
execution: "client",
|
|
2179
|
+
arguments: { query: "verification" }
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
type: "tool_search_output",
|
|
2183
|
+
call_id: "call_search",
|
|
2184
|
+
status: "completed",
|
|
2185
|
+
execution: "client",
|
|
2186
|
+
tools: []
|
|
2187
|
+
},
|
|
2029
2188
|
{ type: "message", role: "user", content: "hi" }
|
|
2030
2189
|
]);
|
|
2031
2190
|
assert.ok(!JSON.stringify(relayCalls[0]).includes("rk1."));
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-gateway",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "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.
|
|
33
|
-
"@velum-labs/routekit-registry": "0.
|
|
34
|
-
"@velum-labs/routekit-runtime": "0.
|
|
35
|
-
"@velum-labs/routekit-tracing": "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",
|