@velum-labs/routekit-gateway 0.18.3 → 0.18.5
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 +10 -0
- package/dist/bedrock-source.js +91 -2
- package/dist/test/bedrock-source.test.js +127 -1
- package/package.json +5 -5
package/dist/bedrock-source.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ export type BedrockProviderSourceOptions = {
|
|
|
13
13
|
};
|
|
14
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
15
|
export declare function isBedrockOpenAiModel(modelId: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Codex talks to RouteKit as OpenAI-compatible, so it sends OpenAI/Codex
|
|
18
|
+
* protocol items that Bedrock mantle rejects. Native OpenAI is unchanged.
|
|
19
|
+
*
|
|
20
|
+
* - `search_content_types` on `web_search*`
|
|
21
|
+
* - `agent_message` (Codex 0.147 subagent handoff)
|
|
22
|
+
* - `encrypted_content` content parts (same 400 as `agent_message`)
|
|
23
|
+
* - `compaction` / `reasoning` encrypted blobs without `rsn_` / `smry_`
|
|
24
|
+
*/
|
|
25
|
+
export declare function sanitizeBedrockMantleRequestBody(body: unknown): unknown;
|
|
16
26
|
export declare function toBedrockConverseInput(body: unknown): ConverseCommandInput;
|
|
17
27
|
export declare function fromBedrockConverseOutput(output: ConverseCommandOutput, model: string): Record<string, unknown>;
|
|
18
28
|
export declare class BedrockProviderSource implements ProviderSource {
|
package/dist/bedrock-source.js
CHANGED
|
@@ -14,6 +14,95 @@ const BEDROCK_OPENAI_MODEL = /^(?:(?:us|eu|global)\.)?openai\.gpt-/;
|
|
|
14
14
|
export function isBedrockOpenAiModel(modelId) {
|
|
15
15
|
return BEDROCK_OPENAI_MODEL.test(modelId);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Codex talks to RouteKit as OpenAI-compatible, so it sends OpenAI/Codex
|
|
19
|
+
* protocol items that Bedrock mantle rejects. Native OpenAI is unchanged.
|
|
20
|
+
*
|
|
21
|
+
* - `search_content_types` on `web_search*`
|
|
22
|
+
* - `agent_message` (Codex 0.147 subagent handoff)
|
|
23
|
+
* - `encrypted_content` content parts (same 400 as `agent_message`)
|
|
24
|
+
* - `compaction` / `reasoning` encrypted blobs without `rsn_` / `smry_`
|
|
25
|
+
*/
|
|
26
|
+
export function sanitizeBedrockMantleRequestBody(body) {
|
|
27
|
+
const payload = record(body);
|
|
28
|
+
if (payload === undefined)
|
|
29
|
+
return body;
|
|
30
|
+
let next = payload;
|
|
31
|
+
let changed = false;
|
|
32
|
+
if (Array.isArray(payload.tools)) {
|
|
33
|
+
const tools = payload.tools.map((tool) => {
|
|
34
|
+
const entry = record(tool);
|
|
35
|
+
if (entry === undefined)
|
|
36
|
+
return tool;
|
|
37
|
+
const type = typeof entry.type === "string" ? entry.type : "";
|
|
38
|
+
if (!type.startsWith("web_search") || !("search_content_types" in entry))
|
|
39
|
+
return tool;
|
|
40
|
+
changed = true;
|
|
41
|
+
const { search_content_types: _ignored, ...rest } = entry;
|
|
42
|
+
return rest;
|
|
43
|
+
});
|
|
44
|
+
if (changed)
|
|
45
|
+
next = { ...next, tools };
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(payload.input)) {
|
|
48
|
+
const input = [];
|
|
49
|
+
let inputChanged = false;
|
|
50
|
+
for (const item of payload.input) {
|
|
51
|
+
const sanitized = sanitizeBedrockMantleInputItem(item);
|
|
52
|
+
if (sanitized === undefined) {
|
|
53
|
+
inputChanged = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (sanitized !== item)
|
|
57
|
+
inputChanged = true;
|
|
58
|
+
input.push(sanitized);
|
|
59
|
+
}
|
|
60
|
+
if (inputChanged) {
|
|
61
|
+
changed = true;
|
|
62
|
+
next = { ...next, input };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return changed ? next : body;
|
|
66
|
+
}
|
|
67
|
+
function recognizedEncryptedPrefix(value) {
|
|
68
|
+
return typeof value === "string" && (value.startsWith("rsn_") || value.startsWith("smry_"));
|
|
69
|
+
}
|
|
70
|
+
function sanitizeBedrockMantleContent(content) {
|
|
71
|
+
if (!Array.isArray(content))
|
|
72
|
+
return { content, changed: false };
|
|
73
|
+
const parts = content.filter((part) => record(part)?.type !== "encrypted_content");
|
|
74
|
+
return { content: parts, changed: parts.length !== content.length };
|
|
75
|
+
}
|
|
76
|
+
function sanitizeBedrockMantleInputItem(item) {
|
|
77
|
+
const entry = record(item);
|
|
78
|
+
if (entry === undefined)
|
|
79
|
+
return item;
|
|
80
|
+
const type = typeof entry.type === "string" ? entry.type : "";
|
|
81
|
+
if (type === "compaction" && "encrypted_content" in entry && !recognizedEncryptedPrefix(entry.encrypted_content)) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (type === "agent_message") {
|
|
85
|
+
const { content } = sanitizeBedrockMantleContent(entry.content);
|
|
86
|
+
return { type: "message", role: "user", content };
|
|
87
|
+
}
|
|
88
|
+
let next = entry;
|
|
89
|
+
let changed = false;
|
|
90
|
+
if (Array.isArray(entry.content)) {
|
|
91
|
+
const sanitized = sanitizeBedrockMantleContent(entry.content);
|
|
92
|
+
if (sanitized.changed) {
|
|
93
|
+
next = { ...next, content: sanitized.content };
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (type === "reasoning" &&
|
|
98
|
+
"encrypted_content" in next &&
|
|
99
|
+
!recognizedEncryptedPrefix(next.encrypted_content)) {
|
|
100
|
+
const { encrypted_content: _ignored, ...rest } = next;
|
|
101
|
+
next = rest;
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
return changed ? next : item;
|
|
105
|
+
}
|
|
17
106
|
function mantleApiKey(env) {
|
|
18
107
|
const key = env.AWS_BEARER_TOKEN_BEDROCK ?? env.BEDROCK_API_KEY;
|
|
19
108
|
return typeof key === "string" && key.length > 0 ? key : undefined;
|
|
@@ -588,7 +677,7 @@ export class BedrockProviderSource {
|
|
|
588
677
|
const backend = this.#mantleBackend();
|
|
589
678
|
if (backend === undefined)
|
|
590
679
|
return this.#missingMantleResponse();
|
|
591
|
-
return backend.responses(body, signal, options);
|
|
680
|
+
return backend.responses(sanitizeBedrockMantleRequestBody(body), signal, options);
|
|
592
681
|
}
|
|
593
682
|
async chat(body, signal, options) {
|
|
594
683
|
const requestedModel = record(body)?.model;
|
|
@@ -596,7 +685,7 @@ export class BedrockProviderSource {
|
|
|
596
685
|
const backend = this.#mantleBackend();
|
|
597
686
|
if (backend === undefined)
|
|
598
687
|
return this.#missingMantleResponse();
|
|
599
|
-
return backend.chat(body, signal, options);
|
|
688
|
+
return backend.chat(sanitizeBedrockMantleRequestBody(body), signal, options);
|
|
600
689
|
}
|
|
601
690
|
let input;
|
|
602
691
|
try {
|
|
@@ -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 { BEDROCK_OPENAI_ALLOWLIST, BedrockProviderSource, isBedrockOpenAiModel, 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({
|
|
@@ -501,6 +501,132 @@ test("Bedrock routes OpenAI models through mantle and leaves Anthropic on Conver
|
|
|
501
501
|
});
|
|
502
502
|
assert.equal(rejected.status, 501);
|
|
503
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("sanitizeBedrockMantleRequestBody rewrites Codex agent_message and drops encrypted_content", () => {
|
|
527
|
+
const body = {
|
|
528
|
+
model: "openai.gpt-5.6-sol",
|
|
529
|
+
input: [
|
|
530
|
+
{
|
|
531
|
+
type: "agent_message",
|
|
532
|
+
author: "/root",
|
|
533
|
+
recipient: "/root/package_architecture",
|
|
534
|
+
content: [
|
|
535
|
+
{ type: "input_text", text: "Reply with exactly PONG" },
|
|
536
|
+
{ type: "encrypted_content", encrypted_content: " " }
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
type: "message",
|
|
541
|
+
role: "user",
|
|
542
|
+
content: [
|
|
543
|
+
{ type: "input_text", text: "keep" },
|
|
544
|
+
{ type: "encrypted_content", encrypted_content: " " }
|
|
545
|
+
]
|
|
546
|
+
},
|
|
547
|
+
{ type: "compaction", encrypted_content: "not-a-prefix" },
|
|
548
|
+
{ type: "reasoning", encrypted_content: "rsn_ok", summary: [] }
|
|
549
|
+
]
|
|
550
|
+
};
|
|
551
|
+
assert.deepEqual(sanitizeBedrockMantleRequestBody(body), {
|
|
552
|
+
model: "openai.gpt-5.6-sol",
|
|
553
|
+
input: [
|
|
554
|
+
{
|
|
555
|
+
type: "message",
|
|
556
|
+
role: "user",
|
|
557
|
+
content: [{ type: "input_text", text: "Reply with exactly PONG" }]
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
type: "message",
|
|
561
|
+
role: "user",
|
|
562
|
+
content: [{ type: "input_text", text: "keep" }]
|
|
563
|
+
},
|
|
564
|
+
{ type: "reasoning", encrypted_content: "rsn_ok", summary: [] }
|
|
565
|
+
]
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
test("Bedrock mantle Responses drops Codex search_content_types before egress", async () => {
|
|
569
|
+
const forwarded = [];
|
|
570
|
+
const source = new BedrockProviderSource({
|
|
571
|
+
env: { AWS_BEARER_TOKEN_BEDROCK: "bedrock-key", AWS_REGION: "us-east-1" },
|
|
572
|
+
controlClient: { send: async () => ({}) },
|
|
573
|
+
runtimeClient: { send: async () => ({}) },
|
|
574
|
+
mantleBackend: {
|
|
575
|
+
chat: async () => Response.json({}),
|
|
576
|
+
responses: async (body) => {
|
|
577
|
+
forwarded.push(body);
|
|
578
|
+
return Response.json({ id: "resp" });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
const response = await source.responses({
|
|
583
|
+
model: "openai.gpt-5.6-sol",
|
|
584
|
+
input: "hi",
|
|
585
|
+
tools: [{ type: "web_search", search_content_types: ["text", "image"] }]
|
|
586
|
+
});
|
|
587
|
+
assert.equal(response.status, 200);
|
|
588
|
+
assert.deepEqual(forwarded, [
|
|
589
|
+
{
|
|
590
|
+
model: "openai.gpt-5.6-sol",
|
|
591
|
+
input: "hi",
|
|
592
|
+
tools: [{ type: "web_search" }]
|
|
593
|
+
}
|
|
594
|
+
]);
|
|
595
|
+
});
|
|
596
|
+
test("Bedrock mantle Responses rewrites Codex agent_message before egress", async () => {
|
|
597
|
+
const forwarded = [];
|
|
598
|
+
const source = new BedrockProviderSource({
|
|
599
|
+
env: { AWS_BEARER_TOKEN_BEDROCK: "bedrock-key", AWS_REGION: "us-east-1" },
|
|
600
|
+
controlClient: { send: async () => ({}) },
|
|
601
|
+
runtimeClient: { send: async () => ({}) },
|
|
602
|
+
mantleBackend: {
|
|
603
|
+
chat: async () => Response.json({}),
|
|
604
|
+
responses: async (body) => {
|
|
605
|
+
forwarded.push(body);
|
|
606
|
+
return Response.json({ id: "resp" });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
const response = await source.responses({
|
|
611
|
+
model: "openai.gpt-5.6-sol",
|
|
612
|
+
input: [{
|
|
613
|
+
type: "agent_message",
|
|
614
|
+
author: "/root",
|
|
615
|
+
recipient: "/root/async_lifecycle",
|
|
616
|
+
content: [
|
|
617
|
+
{ type: "input_text", text: "hi" },
|
|
618
|
+
{ type: "encrypted_content", encrypted_content: " " }
|
|
619
|
+
]
|
|
620
|
+
}]
|
|
621
|
+
});
|
|
622
|
+
assert.equal(response.status, 200);
|
|
623
|
+
assert.deepEqual(forwarded, [
|
|
624
|
+
{
|
|
625
|
+
model: "openai.gpt-5.6-sol",
|
|
626
|
+
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }]
|
|
627
|
+
}
|
|
628
|
+
]);
|
|
629
|
+
});
|
|
504
630
|
test("Bedrock OpenAI chat without a mantle key returns 400 and does not call Converse", async () => {
|
|
505
631
|
const runtimeCalls = [];
|
|
506
632
|
const source = new BedrockProviderSource({
|
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.5",
|
|
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.5",
|
|
33
|
+
"@velum-labs/routekit-registry": "0.18.5",
|
|
34
|
+
"@velum-labs/routekit-runtime": "0.18.5",
|
|
35
|
+
"@velum-labs/routekit-tracing": "0.18.5"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"routekit",
|