@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.
@@ -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 {
@@ -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.3",
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.3",
33
- "@velum-labs/routekit-registry": "0.18.3",
34
- "@velum-labs/routekit-runtime": "0.18.3",
35
- "@velum-labs/routekit-tracing": "0.18.3"
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",