@velum-labs/routekit-gateway 0.18.2 → 0.18.4
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/bedrock-source.d.ts +15 -2
- package/dist/bedrock-source.js +180 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/router.js +15 -2
- package/dist/test/bedrock-source.test.js +180 -1
- package/dist/test/router.test.js +33 -0
- package/package.json +5 -5
package/dist/bedrock-source.d.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { BedrockClient } from "@aws-sdk/client-bedrock";
|
|
2
2
|
import { BedrockRuntimeClient, type ConverseCommandInput, type ConverseCommandOutput } from "@aws-sdk/client-bedrock-runtime";
|
|
3
|
-
import type
|
|
3
|
+
import { OpenAiBackend, type BackendRequestOptions } from "./backend.js";
|
|
4
4
|
import type { DiscoveredModel, ProviderSource } from "./provider-source.js";
|
|
5
5
|
export type BedrockControlClient = Pick<BedrockClient, "send">;
|
|
6
6
|
export type BedrockRuntime = Pick<BedrockRuntimeClient, "send">;
|
|
7
|
+
export type BedrockMantleBackend = Pick<OpenAiBackend, "chat" | "responses">;
|
|
7
8
|
export type BedrockProviderSourceOptions = {
|
|
8
9
|
controlClient?: BedrockControlClient;
|
|
9
10
|
runtimeClient?: BedrockRuntime;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
mantleBackend?: BedrockMantleBackend;
|
|
10
13
|
};
|
|
14
|
+
export declare const BEDROCK_OPENAI_ALLOWLIST: readonly ["openai.gpt-5.4", "openai.gpt-5.5", "openai.gpt-5.6-sol", "openai.gpt-5.6-terra", "openai.gpt-5.6-luna"];
|
|
15
|
+
export declare function isBedrockOpenAiModel(modelId: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Codex advertises `web_search_tool_type: "text_and_image"` and sends
|
|
18
|
+
* `search_content_types` on `{ type: "web_search" }`. Bedrock mantle accepts
|
|
19
|
+
* the tool but rejects that field. Native OpenAI Responses is unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sanitizeBedrockMantleRequestBody(body: unknown): unknown;
|
|
11
22
|
export declare function toBedrockConverseInput(body: unknown): ConverseCommandInput;
|
|
12
23
|
export declare function fromBedrockConverseOutput(output: ConverseCommandOutput, model: string): Record<string, unknown>;
|
|
13
24
|
export declare class BedrockProviderSource implements ProviderSource {
|
|
@@ -15,7 +26,9 @@ export declare class BedrockProviderSource implements ProviderSource {
|
|
|
15
26
|
readonly sourceId: "bedrock";
|
|
16
27
|
constructor(options?: BedrockProviderSourceOptions);
|
|
17
28
|
discoverModels(signal?: AbortSignal): Promise<readonly DiscoveredModel[]>;
|
|
18
|
-
|
|
29
|
+
supportsResponses(model: string): boolean;
|
|
30
|
+
responses(body: unknown, signal?: AbortSignal, options?: BackendRequestOptions): Promise<Response>;
|
|
31
|
+
chat(body: unknown, signal?: AbortSignal, options?: BackendRequestOptions): Promise<Response>;
|
|
19
32
|
embeddings(): Promise<Response>;
|
|
20
33
|
reasoningCapabilities(model?: string): DiscoveredModel["reasoning"];
|
|
21
34
|
close(): void;
|
package/dist/bedrock-source.js
CHANGED
|
@@ -1,7 +1,97 @@
|
|
|
1
1
|
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from "@aws-sdk/client-bedrock";
|
|
2
2
|
import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
3
3
|
import { randomId } from "@velum-labs/routekit-runtime";
|
|
4
|
+
import { OpenAiBackend } from "./backend.js";
|
|
4
5
|
import { anthropicReasoningDetailsOf, reasoningSelectionOf, withoutRouteKitExtensions } from "./adapters/openai-chat-wire.js";
|
|
6
|
+
export const BEDROCK_OPENAI_ALLOWLIST = [
|
|
7
|
+
"openai.gpt-5.4",
|
|
8
|
+
"openai.gpt-5.5",
|
|
9
|
+
"openai.gpt-5.6-sol",
|
|
10
|
+
"openai.gpt-5.6-terra",
|
|
11
|
+
"openai.gpt-5.6-luna"
|
|
12
|
+
];
|
|
13
|
+
const BEDROCK_OPENAI_MODEL = /^(?:(?:us|eu|global)\.)?openai\.gpt-/;
|
|
14
|
+
export function isBedrockOpenAiModel(modelId) {
|
|
15
|
+
return BEDROCK_OPENAI_MODEL.test(modelId);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Codex advertises `web_search_tool_type: "text_and_image"` and sends
|
|
19
|
+
* `search_content_types` on `{ type: "web_search" }`. Bedrock mantle accepts
|
|
20
|
+
* the tool but rejects that field. Native OpenAI Responses is unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export function sanitizeBedrockMantleRequestBody(body) {
|
|
23
|
+
const payload = record(body);
|
|
24
|
+
if (payload === undefined || !Array.isArray(payload.tools))
|
|
25
|
+
return body;
|
|
26
|
+
let changed = false;
|
|
27
|
+
const tools = payload.tools.map((tool) => {
|
|
28
|
+
const entry = record(tool);
|
|
29
|
+
if (entry === undefined)
|
|
30
|
+
return tool;
|
|
31
|
+
const type = typeof entry.type === "string" ? entry.type : "";
|
|
32
|
+
if (!type.startsWith("web_search") || !("search_content_types" in entry))
|
|
33
|
+
return tool;
|
|
34
|
+
changed = true;
|
|
35
|
+
const { search_content_types: _ignored, ...rest } = entry;
|
|
36
|
+
return rest;
|
|
37
|
+
});
|
|
38
|
+
return changed ? { ...payload, tools } : body;
|
|
39
|
+
}
|
|
40
|
+
function mantleApiKey(env) {
|
|
41
|
+
const key = env.AWS_BEARER_TOKEN_BEDROCK ?? env.BEDROCK_API_KEY;
|
|
42
|
+
return typeof key === "string" && key.length > 0 ? key : undefined;
|
|
43
|
+
}
|
|
44
|
+
function mantleRegion(env) {
|
|
45
|
+
const region = env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
46
|
+
return typeof region === "string" && region.length > 0 ? region : undefined;
|
|
47
|
+
}
|
|
48
|
+
function mantleBaseUrl(region) {
|
|
49
|
+
return `https://bedrock-mantle.${region}.api.aws/openai/v1`;
|
|
50
|
+
}
|
|
51
|
+
function bedrockOpenAiNativeId(modelId) {
|
|
52
|
+
return modelId.replace(/^(?:us|eu|global)\./, "");
|
|
53
|
+
}
|
|
54
|
+
function bedrockOpenAiReasoning(modelId) {
|
|
55
|
+
const native = bedrockOpenAiNativeId(modelId).replace(/^openai\./, "");
|
|
56
|
+
if (/^gpt-5\.6(?:-(?:sol|terra|luna))?(?:-\d{4}-\d{2}-\d{2})?$/.test(native)) {
|
|
57
|
+
return {
|
|
58
|
+
status: "supported",
|
|
59
|
+
efforts: ["none", "low", "medium", "high", "xhigh", "max"].map((id) => ({ id })),
|
|
60
|
+
defaultEffort: "medium",
|
|
61
|
+
wireShape: "openai-responses",
|
|
62
|
+
provenance: "builtin"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (/^gpt-5\.(?:4|5)(?:-\d{4}-\d{2}-\d{2})?$/.test(native)) {
|
|
66
|
+
return {
|
|
67
|
+
status: "supported",
|
|
68
|
+
efforts: ["none", "low", "medium", "high", "xhigh"].map((id) => ({ id })),
|
|
69
|
+
wireShape: "openai-responses",
|
|
70
|
+
provenance: "builtin"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
status: "supported",
|
|
75
|
+
wireShape: "openai-responses",
|
|
76
|
+
provenance: "builtin"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function bedrockOpenAiDiscoveredModel(id) {
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
metadata: {
|
|
83
|
+
architecture: {
|
|
84
|
+
modality: "text+image->text",
|
|
85
|
+
inputModalities: ["text", "image"],
|
|
86
|
+
outputModalities: ["text"]
|
|
87
|
+
},
|
|
88
|
+
supportedParameters: ["tools", "tool_choice"],
|
|
89
|
+
provenance: "route"
|
|
90
|
+
},
|
|
91
|
+
reasoning: bedrockOpenAiReasoning(id),
|
|
92
|
+
capabilities: { streaming: "supported" }
|
|
93
|
+
};
|
|
94
|
+
}
|
|
5
95
|
function record(value) {
|
|
6
96
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
7
97
|
? value
|
|
@@ -431,42 +521,106 @@ export class BedrockProviderSource {
|
|
|
431
521
|
sourceId = "bedrock";
|
|
432
522
|
#control;
|
|
433
523
|
#runtime;
|
|
524
|
+
#env;
|
|
525
|
+
#injectedMantle;
|
|
526
|
+
#mantle;
|
|
434
527
|
#inferenceProfilesByFoundation = new Map();
|
|
435
528
|
constructor(options = {}) {
|
|
436
529
|
this.#control = options.controlClient ?? new BedrockClient({});
|
|
437
530
|
this.#runtime = options.runtimeClient ?? new BedrockRuntimeClient({});
|
|
531
|
+
this.#env = options.env ?? process.env;
|
|
532
|
+
this.#injectedMantle = options.mantleBackend;
|
|
533
|
+
}
|
|
534
|
+
#mantleBackend() {
|
|
535
|
+
if (this.#injectedMantle !== undefined)
|
|
536
|
+
return this.#injectedMantle;
|
|
537
|
+
if (this.#mantle !== undefined)
|
|
538
|
+
return this.#mantle;
|
|
539
|
+
const apiKey = mantleApiKey(this.#env);
|
|
540
|
+
const region = mantleRegion(this.#env);
|
|
541
|
+
if (apiKey === undefined || region === undefined)
|
|
542
|
+
return undefined;
|
|
543
|
+
this.#mantle = new OpenAiBackend({
|
|
544
|
+
baseUrl: mantleBaseUrl(region),
|
|
545
|
+
apiKey
|
|
546
|
+
});
|
|
547
|
+
return this.#mantle;
|
|
548
|
+
}
|
|
549
|
+
#missingMantleResponse() {
|
|
550
|
+
return Response.json({
|
|
551
|
+
error: {
|
|
552
|
+
type: "invalid_request_error",
|
|
553
|
+
message: "Bedrock OpenAI models require AWS_BEARER_TOKEN_BEDROCK and AWS_REGION"
|
|
554
|
+
}
|
|
555
|
+
}, { status: 400 });
|
|
438
556
|
}
|
|
439
557
|
async discoverModels(signal) {
|
|
440
558
|
this.#inferenceProfilesByFoundation.clear();
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
model.modelId,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
559
|
+
const abort = signal === undefined ? undefined : { abortSignal: signal };
|
|
560
|
+
let discovered = new Map();
|
|
561
|
+
try {
|
|
562
|
+
const foundation = await this.#control.send(new ListFoundationModelsCommand({ byProvider: "Anthropic" }), abort);
|
|
563
|
+
const foundations = (foundation.modelSummaries ?? []).filter(anthropicFoundationModel);
|
|
564
|
+
const byId = new Map(foundations.map((model) => [model.modelId, model]));
|
|
565
|
+
const ids = new Set(byId.keys());
|
|
566
|
+
discovered = new Map(foundations.map((model) => [
|
|
567
|
+
model.modelId,
|
|
568
|
+
bedrockDiscoveredModel(model.modelId, model)
|
|
569
|
+
]));
|
|
570
|
+
let nextToken;
|
|
571
|
+
do {
|
|
572
|
+
const profiles = await this.#control.send(new ListInferenceProfilesCommand({ ...(nextToken !== undefined ? { nextToken } : {}) }), abort);
|
|
573
|
+
for (const profile of profiles.inferenceProfileSummaries ?? []) {
|
|
574
|
+
if (!activeAnthropicProfile(profile, ids))
|
|
575
|
+
continue;
|
|
576
|
+
const backingId = (profile.models ?? [])
|
|
577
|
+
.map((model) => foundationIdFromArn(model.modelArn))
|
|
578
|
+
.find((id) => id !== undefined && ids.has(id));
|
|
579
|
+
if (backingId !== undefined) {
|
|
580
|
+
this.#inferenceProfilesByFoundation.set(backingId, preferredInferenceProfile(this.#inferenceProfilesByFoundation.get(backingId), profile.inferenceProfileId));
|
|
581
|
+
discovered.set(profile.inferenceProfileId, bedrockDiscoveredModel(profile.inferenceProfileId, byId.get(backingId)));
|
|
582
|
+
}
|
|
461
583
|
}
|
|
584
|
+
nextToken = profiles.nextToken;
|
|
585
|
+
} while (nextToken !== undefined && nextToken.length > 0);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (mantleApiKey(this.#env) === undefined)
|
|
589
|
+
throw error;
|
|
590
|
+
discovered = new Map();
|
|
591
|
+
this.#inferenceProfilesByFoundation.clear();
|
|
592
|
+
}
|
|
593
|
+
if (mantleApiKey(this.#env) !== undefined) {
|
|
594
|
+
for (const id of BEDROCK_OPENAI_ALLOWLIST) {
|
|
595
|
+
discovered.set(id, bedrockOpenAiDiscoveredModel(id));
|
|
462
596
|
}
|
|
463
|
-
|
|
464
|
-
} while (nextToken !== undefined && nextToken.length > 0);
|
|
597
|
+
}
|
|
465
598
|
if (discovered.size === 0)
|
|
466
599
|
throw new Error("model discovery returned no active Anthropic Bedrock models");
|
|
467
600
|
return [...discovered.values()];
|
|
468
601
|
}
|
|
469
|
-
|
|
602
|
+
supportsResponses(model) {
|
|
603
|
+
return isBedrockOpenAiModel(model);
|
|
604
|
+
}
|
|
605
|
+
async responses(body, signal, options) {
|
|
606
|
+
const requestedModel = record(body)?.model;
|
|
607
|
+
const model = typeof requestedModel === "string" ? requestedModel : "";
|
|
608
|
+
if (!isBedrockOpenAiModel(model)) {
|
|
609
|
+
return Response.json({ error: { type: "not_supported", message: "native Responses egress is not supported" } }, { status: 501 });
|
|
610
|
+
}
|
|
611
|
+
const backend = this.#mantleBackend();
|
|
612
|
+
if (backend === undefined)
|
|
613
|
+
return this.#missingMantleResponse();
|
|
614
|
+
return backend.responses(sanitizeBedrockMantleRequestBody(body), signal, options);
|
|
615
|
+
}
|
|
616
|
+
async chat(body, signal, options) {
|
|
617
|
+
const requestedModel = record(body)?.model;
|
|
618
|
+
if (typeof requestedModel === "string" && isBedrockOpenAiModel(requestedModel)) {
|
|
619
|
+
const backend = this.#mantleBackend();
|
|
620
|
+
if (backend === undefined)
|
|
621
|
+
return this.#missingMantleResponse();
|
|
622
|
+
return backend.chat(sanitizeBedrockMantleRequestBody(body), signal, options);
|
|
623
|
+
}
|
|
470
624
|
let input;
|
|
471
625
|
try {
|
|
472
626
|
input = toBedrockConverseInput(body);
|
|
@@ -500,6 +654,9 @@ export class BedrockProviderSource {
|
|
|
500
654
|
return Promise.resolve(Response.json({ error: { type: "not_implemented", message: "Bedrock embeddings are not supported" } }, { status: 501 }));
|
|
501
655
|
}
|
|
502
656
|
reasoningCapabilities(model) {
|
|
657
|
+
if (model !== undefined && isBedrockOpenAiModel(model)) {
|
|
658
|
+
return bedrockOpenAiReasoning(model);
|
|
659
|
+
}
|
|
503
660
|
const known = bedrockReasoningCapabilities(model);
|
|
504
661
|
if (known !== undefined)
|
|
505
662
|
return known;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export { joinPath, ModelRoutedBackend, OpenAiBackend } from "./backend.js";
|
|
|
7
7
|
export type { Backend, BackendModelRoute, BackendRequestOptions, BackendResponseMode, 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 { BedrockProviderSource, fromBedrockConverseOutput, toBedrockConverseInput } from "./bedrock-source.js";
|
|
11
|
-
export type { BedrockControlClient, BedrockProviderSourceOptions, BedrockRuntime } from "./bedrock-source.js";
|
|
10
|
+
export { BEDROCK_OPENAI_ALLOWLIST, BedrockProviderSource, fromBedrockConverseOutput, isBedrockOpenAiModel, toBedrockConverseInput } from "./bedrock-source.js";
|
|
11
|
+
export type { BedrockControlClient, BedrockMantleBackend, BedrockProviderSourceOptions, BedrockRuntime } from "./bedrock-source.js";
|
|
12
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
13
|
export type { CatalogBackendOptions, CatalogModelInfo, LeaderboardConfig, ModelPolicy, ProviderPolicy, RouterConfig } from "./router.js";
|
|
14
14
|
export { API_PROVIDER_IDS, ApiProviderSource, parseDiscoveredModels, parseReasoningCapabilities, PROVIDER_IDS, SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ 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 { BedrockProviderSource, fromBedrockConverseOutput, toBedrockConverseInput } from "./bedrock-source.js";
|
|
6
|
+
export { BEDROCK_OPENAI_ALLOWLIST, BedrockProviderSource, fromBedrockConverseOutput, isBedrockOpenAiModel, toBedrockConverseInput } from "./bedrock-source.js";
|
|
7
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";
|
|
8
8
|
export { API_PROVIDER_IDS, ApiProviderSource, parseDiscoveredModels, parseReasoningCapabilities, PROVIDER_IDS, SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
|
9
9
|
export { OpenRouterModelMetadataClient, resolveCodexStartupModel } from "./codex-model-selection.js";
|
package/dist/router.js
CHANGED
|
@@ -280,8 +280,12 @@ export function modelPolicyAllowsModel(policy, canonicalModel) {
|
|
|
280
280
|
* verified; provider discovery and explicit config always take precedence.
|
|
281
281
|
*/
|
|
282
282
|
export function inferKnownReasoningCapabilities(provider, model) {
|
|
283
|
-
|
|
284
|
-
/^
|
|
283
|
+
const bedrockOpenAi = provider === "bedrock" && /^(?:(?:us|eu|global)\.)?openai\./.test(model)
|
|
284
|
+
? model.replace(/^(?:(?:us|eu|global)\.)?openai\./, "")
|
|
285
|
+
: undefined;
|
|
286
|
+
const openaiModel = bedrockOpenAi ?? (provider === "openai" ? model : undefined);
|
|
287
|
+
if (openaiModel !== undefined &&
|
|
288
|
+
/^gpt-5\.6(?:-(?:sol|terra|luna))?(?:-\d{4}-\d{2}-\d{2})?$/.test(openaiModel)) {
|
|
285
289
|
return {
|
|
286
290
|
status: "supported",
|
|
287
291
|
efforts: ["none", "low", "medium", "high", "xhigh", "max"].map((id) => ({ id })),
|
|
@@ -290,6 +294,15 @@ export function inferKnownReasoningCapabilities(provider, model) {
|
|
|
290
294
|
provenance: "builtin"
|
|
291
295
|
};
|
|
292
296
|
}
|
|
297
|
+
if (bedrockOpenAi !== undefined &&
|
|
298
|
+
/^gpt-5\.(?:4|5)(?:-\d{4}-\d{2}-\d{2})?$/.test(bedrockOpenAi)) {
|
|
299
|
+
return {
|
|
300
|
+
status: "supported",
|
|
301
|
+
efforts: ["none", "low", "medium", "high", "xhigh"].map((id) => ({ id })),
|
|
302
|
+
wireShape: "openai-responses",
|
|
303
|
+
provenance: "builtin"
|
|
304
|
+
};
|
|
305
|
+
}
|
|
293
306
|
if (provider === "openai" && /^gpt-5\.5(?:-\d{4}-\d{2}-\d{2})?$/.test(model)) {
|
|
294
307
|
return {
|
|
295
308
|
status: "supported",
|
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import { ConverseCommand, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
4
4
|
import { ListFoundationModelsCommand } from "@aws-sdk/client-bedrock";
|
|
5
|
-
import { BedrockProviderSource, toBedrockConverseInput } from "../bedrock-source.js";
|
|
5
|
+
import { BEDROCK_OPENAI_ALLOWLIST, BedrockProviderSource, isBedrockOpenAiModel, sanitizeBedrockMantleRequestBody, toBedrockConverseInput } from "../bedrock-source.js";
|
|
6
6
|
test("Bedrock discovery includes active Anthropic foundations and paginated backed profiles", async () => {
|
|
7
7
|
const commands = [];
|
|
8
8
|
const source = new BedrockProviderSource({
|
|
@@ -392,6 +392,185 @@ test("Bedrock groups parallel tool results into one user message", () => {
|
|
|
392
392
|
]
|
|
393
393
|
});
|
|
394
394
|
});
|
|
395
|
+
test("Bedrock OpenAI model ids stay on mantle and never match Anthropic Converse", () => {
|
|
396
|
+
assert.equal(isBedrockOpenAiModel("openai.gpt-5.4"), true);
|
|
397
|
+
assert.equal(isBedrockOpenAiModel("openai.gpt-5.6-sol"), true);
|
|
398
|
+
assert.equal(isBedrockOpenAiModel("us.openai.gpt-5.6-terra"), true);
|
|
399
|
+
assert.equal(isBedrockOpenAiModel("anthropic.claude-3"), false);
|
|
400
|
+
assert.equal(isBedrockOpenAiModel("us.anthropic.claude-3"), false);
|
|
401
|
+
});
|
|
402
|
+
test("Bedrock discovery unions OpenAI mantle models when a Bedrock API key is present", async () => {
|
|
403
|
+
const source = new BedrockProviderSource({
|
|
404
|
+
env: { AWS_BEARER_TOKEN_BEDROCK: "bedrock-key", AWS_REGION: "us-east-1" },
|
|
405
|
+
controlClient: {
|
|
406
|
+
send: async (command) => {
|
|
407
|
+
if (command instanceof ListFoundationModelsCommand)
|
|
408
|
+
return {
|
|
409
|
+
modelSummaries: [{
|
|
410
|
+
modelId: "anthropic.claude-3",
|
|
411
|
+
providerName: "Anthropic",
|
|
412
|
+
modelLifecycle: { status: "ACTIVE" }
|
|
413
|
+
}]
|
|
414
|
+
};
|
|
415
|
+
return { inferenceProfileSummaries: [] };
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
runtimeClient: { send: async () => ({}) }
|
|
419
|
+
});
|
|
420
|
+
const discovered = await source.discoverModels();
|
|
421
|
+
assert.deepEqual(discovered.map((model) => model.id), ["anthropic.claude-3", ...BEDROCK_OPENAI_ALLOWLIST]);
|
|
422
|
+
const sol = discovered.find((model) => model.id === "openai.gpt-5.6-sol");
|
|
423
|
+
assert.equal(sol?.reasoning?.wireShape, "openai-responses");
|
|
424
|
+
assert.equal(sol?.reasoning?.defaultEffort, "medium");
|
|
425
|
+
});
|
|
426
|
+
test("Bedrock discovery keeps OpenAI models when Anthropic listing fails and a key is present", async () => {
|
|
427
|
+
const source = new BedrockProviderSource({
|
|
428
|
+
env: { AWS_BEARER_TOKEN_BEDROCK: "bedrock-key", AWS_REGION: "us-east-1" },
|
|
429
|
+
controlClient: {
|
|
430
|
+
send: async () => {
|
|
431
|
+
throw new Error("not authorized");
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
runtimeClient: { send: async () => ({}) }
|
|
435
|
+
});
|
|
436
|
+
const discovered = await source.discoverModels();
|
|
437
|
+
assert.deepEqual(discovered.map((model) => model.id), [...BEDROCK_OPENAI_ALLOWLIST]);
|
|
438
|
+
});
|
|
439
|
+
test("Bedrock routes OpenAI models through mantle and leaves Anthropic on Converse", async () => {
|
|
440
|
+
const runtimeCalls = [];
|
|
441
|
+
const mantleCalls = [];
|
|
442
|
+
const source = new BedrockProviderSource({
|
|
443
|
+
env: { AWS_BEARER_TOKEN_BEDROCK: "bedrock-key", AWS_REGION: "us-east-1" },
|
|
444
|
+
controlClient: { send: async () => ({}) },
|
|
445
|
+
runtimeClient: {
|
|
446
|
+
send: async (value) => {
|
|
447
|
+
runtimeCalls.push(value);
|
|
448
|
+
return {
|
|
449
|
+
$metadata: { requestId: "req-1" },
|
|
450
|
+
output: { message: { role: "assistant", content: [{ text: "claude" }] } },
|
|
451
|
+
stopReason: "end_turn"
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
mantleBackend: {
|
|
456
|
+
chat: async (body) => {
|
|
457
|
+
const model = typeof body.model === "string"
|
|
458
|
+
? body.model
|
|
459
|
+
: undefined;
|
|
460
|
+
mantleCalls.push({ kind: "chat", ...(model !== undefined ? { model } : {}) });
|
|
461
|
+
return Response.json({ id: "chat", model });
|
|
462
|
+
},
|
|
463
|
+
responses: async (body) => {
|
|
464
|
+
const model = typeof body.model === "string"
|
|
465
|
+
? body.model
|
|
466
|
+
: undefined;
|
|
467
|
+
mantleCalls.push({ kind: "responses", ...(model !== undefined ? { model } : {}) });
|
|
468
|
+
return Response.json({ id: "resp", model });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
assert.equal(source.supportsResponses("openai.gpt-5.4"), true);
|
|
473
|
+
assert.equal(source.supportsResponses("anthropic.claude-3"), false);
|
|
474
|
+
assert.equal(source.reasoningCapabilities("openai.gpt-5.6-sol")?.wireShape, "openai-responses");
|
|
475
|
+
const openaiChat = await source.chat({
|
|
476
|
+
model: "openai.gpt-5.4",
|
|
477
|
+
messages: [{ role: "user", content: "hi" }]
|
|
478
|
+
});
|
|
479
|
+
assert.equal(openaiChat.status, 200);
|
|
480
|
+
assert.deepEqual(await openaiChat.json(), { id: "chat", model: "openai.gpt-5.4" });
|
|
481
|
+
const openaiResponses = await source.responses({
|
|
482
|
+
model: "openai.gpt-5.6-terra",
|
|
483
|
+
input: "hi"
|
|
484
|
+
});
|
|
485
|
+
assert.equal(openaiResponses.status, 200);
|
|
486
|
+
assert.deepEqual(await openaiResponses.json(), { id: "resp", model: "openai.gpt-5.6-terra" });
|
|
487
|
+
const anthropic = await source.chat({
|
|
488
|
+
model: "anthropic.claude-3",
|
|
489
|
+
messages: [{ role: "user", content: "hi" }]
|
|
490
|
+
});
|
|
491
|
+
assert.equal(anthropic.status, 200);
|
|
492
|
+
assert.equal(runtimeCalls.length, 1);
|
|
493
|
+
assert.equal(runtimeCalls[0] instanceof ConverseCommand, true);
|
|
494
|
+
assert.deepEqual(mantleCalls, [
|
|
495
|
+
{ kind: "chat", model: "openai.gpt-5.4" },
|
|
496
|
+
{ kind: "responses", model: "openai.gpt-5.6-terra" }
|
|
497
|
+
]);
|
|
498
|
+
const rejected = await source.responses({
|
|
499
|
+
model: "anthropic.claude-3",
|
|
500
|
+
input: "hi"
|
|
501
|
+
});
|
|
502
|
+
assert.equal(rejected.status, 501);
|
|
503
|
+
});
|
|
504
|
+
test("sanitizeBedrockMantleRequestBody strips search_content_types from web_search only", () => {
|
|
505
|
+
const lookup = {
|
|
506
|
+
type: "function",
|
|
507
|
+
name: "lookup",
|
|
508
|
+
parameters: { type: "object" }
|
|
509
|
+
};
|
|
510
|
+
const body = {
|
|
511
|
+
model: "openai.gpt-5.6-sol",
|
|
512
|
+
tools: [
|
|
513
|
+
{ type: "web_search", search_content_types: ["text", "image"], external_web_access: true },
|
|
514
|
+
lookup
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
assert.deepEqual(sanitizeBedrockMantleRequestBody(body), {
|
|
518
|
+
model: "openai.gpt-5.6-sol",
|
|
519
|
+
tools: [{ type: "web_search", external_web_access: true }, lookup]
|
|
520
|
+
});
|
|
521
|
+
const noTools = { model: "openai.gpt-5.6-sol", input: "hi" };
|
|
522
|
+
assert.equal(sanitizeBedrockMantleRequestBody(noTools), noTools);
|
|
523
|
+
const untouched = { model: "openai.gpt-5.6-sol", tools: [{ type: "web_search" }] };
|
|
524
|
+
assert.equal(sanitizeBedrockMantleRequestBody(untouched), untouched);
|
|
525
|
+
});
|
|
526
|
+
test("Bedrock mantle Responses drops Codex search_content_types before egress", async () => {
|
|
527
|
+
const forwarded = [];
|
|
528
|
+
const source = new BedrockProviderSource({
|
|
529
|
+
env: { AWS_BEARER_TOKEN_BEDROCK: "bedrock-key", AWS_REGION: "us-east-1" },
|
|
530
|
+
controlClient: { send: async () => ({}) },
|
|
531
|
+
runtimeClient: { send: async () => ({}) },
|
|
532
|
+
mantleBackend: {
|
|
533
|
+
chat: async () => Response.json({}),
|
|
534
|
+
responses: async (body) => {
|
|
535
|
+
forwarded.push(body);
|
|
536
|
+
return Response.json({ id: "resp" });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
const response = await source.responses({
|
|
541
|
+
model: "openai.gpt-5.6-sol",
|
|
542
|
+
input: "hi",
|
|
543
|
+
tools: [{ type: "web_search", search_content_types: ["text", "image"] }]
|
|
544
|
+
});
|
|
545
|
+
assert.equal(response.status, 200);
|
|
546
|
+
assert.deepEqual(forwarded, [
|
|
547
|
+
{
|
|
548
|
+
model: "openai.gpt-5.6-sol",
|
|
549
|
+
input: "hi",
|
|
550
|
+
tools: [{ type: "web_search" }]
|
|
551
|
+
}
|
|
552
|
+
]);
|
|
553
|
+
});
|
|
554
|
+
test("Bedrock OpenAI chat without a mantle key returns 400 and does not call Converse", async () => {
|
|
555
|
+
const runtimeCalls = [];
|
|
556
|
+
const source = new BedrockProviderSource({
|
|
557
|
+
env: { AWS_REGION: "us-east-1" },
|
|
558
|
+
controlClient: { send: async () => ({}) },
|
|
559
|
+
runtimeClient: {
|
|
560
|
+
send: async (value) => {
|
|
561
|
+
runtimeCalls.push(value);
|
|
562
|
+
return {};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
const response = await source.chat({
|
|
567
|
+
model: "openai.gpt-5.5",
|
|
568
|
+
messages: [{ role: "user", content: "hi" }]
|
|
569
|
+
});
|
|
570
|
+
assert.equal(response.status, 400);
|
|
571
|
+
assert.match(await response.text(), /AWS_BEARER_TOKEN_BEDROCK/);
|
|
572
|
+
assert.equal(runtimeCalls.length, 0);
|
|
573
|
+
});
|
|
395
574
|
test("Bedrock defaults reasoning to unknown and ordinary requests omit thinking", () => {
|
|
396
575
|
const source = new BedrockProviderSource({
|
|
397
576
|
controlClient: { send: async () => ({}) },
|
package/dist/test/router.test.js
CHANGED
|
@@ -768,3 +768,36 @@ test("Bedrock Opus 5 exposes reasoning controls and accepts routed effort select
|
|
|
768
768
|
const error = (await rejected.json());
|
|
769
769
|
assert.equal(error.error.code, "unsupported_reasoning_control");
|
|
770
770
|
});
|
|
771
|
+
test("Bedrock OpenAI models keep native openai. ids and Responses reasoning", async () => {
|
|
772
|
+
const calls = [];
|
|
773
|
+
const backend = await CatalogBackend.create({
|
|
774
|
+
config: { providers: { bedrock: {} }, defaultModel: "bedrock/openai.gpt-5.6-sol" },
|
|
775
|
+
sources: {
|
|
776
|
+
bedrock: {
|
|
777
|
+
...fakeSource("bedrock", [{ id: "openai.gpt-5.6-sol" }], calls),
|
|
778
|
+
supportsResponses(model) {
|
|
779
|
+
return model.startsWith("openai.");
|
|
780
|
+
},
|
|
781
|
+
async responses(body) {
|
|
782
|
+
const model = typeof body === "object" &&
|
|
783
|
+
body !== null &&
|
|
784
|
+
"model" in body &&
|
|
785
|
+
typeof body.model === "string"
|
|
786
|
+
? body.model
|
|
787
|
+
: undefined;
|
|
788
|
+
calls.push({ source: "bedrock-responses", ...(model !== undefined ? { model } : {}) });
|
|
789
|
+
return Response.json({ ok: true, model });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
assert.equal(backend.supportsResponses("bedrock/openai.gpt-5.6-sol"), true);
|
|
795
|
+
assert.equal(backend.modelInfo("bedrock/openai.gpt-5.6-sol")?.reasoning?.wireShape, "openai-responses");
|
|
796
|
+
const response = await backend.responses({
|
|
797
|
+
model: "bedrock/openai.gpt-5.6-sol",
|
|
798
|
+
input: "hi"
|
|
799
|
+
});
|
|
800
|
+
assert.equal(response.status, 200);
|
|
801
|
+
assert.deepEqual(await response.json(), { ok: true, model: "openai.gpt-5.6-sol" });
|
|
802
|
+
assert.deepEqual(calls, [{ source: "bedrock-responses", model: "openai.gpt-5.6-sol" }]);
|
|
803
|
+
});
|
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.4",
|
|
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.4",
|
|
33
|
+
"@velum-labs/routekit-registry": "0.18.4",
|
|
34
|
+
"@velum-labs/routekit-runtime": "0.18.4",
|
|
35
|
+
"@velum-labs/routekit-tracing": "0.18.4"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"routekit",
|