@velum-labs/routekit-gateway 0.18.4 → 0.18.6
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 +14 -14
- package/dist/adapters/openai-responses-wire.js +176 -54
- package/dist/adapters/responses.js +13 -13
- package/dist/bedrock-source.d.ts +7 -3
- package/dist/bedrock-source.js +81 -15
- package/dist/provider-backends.js +7 -7
- package/dist/server.js +7 -7
- package/dist/test/bedrock-source.test.js +76 -0
- package/dist/test/openai-responses-wire.test.js +202 -15
- package/dist/test/responses.test.js +60 -1
- package/package.json +5 -5
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type ResponsesEncryptedContentOwner = {
|
|
2
2
|
provider: string;
|
|
3
3
|
nativeModel: string;
|
|
4
4
|
};
|
|
5
|
-
export type
|
|
5
|
+
export type ResponsesEncryptedInputPolicy = {
|
|
6
6
|
mode: "drop";
|
|
7
7
|
} | {
|
|
8
8
|
mode: "relay";
|
|
9
9
|
} | {
|
|
10
10
|
mode: "forward";
|
|
11
|
-
owner:
|
|
11
|
+
owner: ResponsesEncryptedContentOwner;
|
|
12
12
|
/** Keep the RouteKit wrapper for a later Chat-to-Responses provider bridge. */
|
|
13
13
|
unwrap?: boolean;
|
|
14
14
|
};
|
|
15
15
|
export type ParsedResponsesEncryptedContent = {
|
|
16
|
-
owner:
|
|
16
|
+
owner: ResponsesEncryptedContentOwner;
|
|
17
17
|
ciphertext: string;
|
|
18
18
|
};
|
|
19
|
-
export type
|
|
19
|
+
export type PreparedResponsesEncryptedInput = {
|
|
20
20
|
body: unknown;
|
|
21
21
|
dropped: number;
|
|
22
22
|
};
|
|
@@ -26,20 +26,20 @@ export type PreparedResponsesReasoningInput = {
|
|
|
26
26
|
* `call_id`, so changing only the item `id` preserves the execution pair.
|
|
27
27
|
*/
|
|
28
28
|
export declare function repairLegacyToolSearchItemIds(body: unknown): unknown;
|
|
29
|
-
/** Wrap provider-owned opaque
|
|
30
|
-
export declare function wrapResponsesEncryptedContent(ciphertext: string, owner:
|
|
31
|
-
/** Parse a RouteKit-owned encrypted-
|
|
29
|
+
/** Wrap provider-owned opaque Responses content without expanding the ciphertext itself. */
|
|
30
|
+
export declare function wrapResponsesEncryptedContent(ciphertext: string, owner: ResponsesEncryptedContentOwner): string;
|
|
31
|
+
/** Parse a RouteKit-owned encrypted-content envelope. Raw provider ciphertext is ownerless. */
|
|
32
32
|
export declare function parseResponsesEncryptedContent(value: unknown): ParsedResponsesEncryptedContent | undefined;
|
|
33
33
|
/**
|
|
34
|
-
* Apply the destination policy to Responses input without
|
|
35
|
-
* Incompatible
|
|
36
|
-
* remain in the portable transcript.
|
|
34
|
+
* Apply the destination policy to provider-owned Responses input without
|
|
35
|
+
* mutating the request. Incompatible opaque state is removed while visible
|
|
36
|
+
* messages and tool-call/output carriers remain in the portable transcript.
|
|
37
37
|
*/
|
|
38
|
-
export declare function
|
|
38
|
+
export declare function prepareResponsesEncryptedInput(body: unknown, policy: ResponsesEncryptedInputPolicy): PreparedResponsesEncryptedInput;
|
|
39
39
|
/**
|
|
40
|
-
* Add ownership to encrypted
|
|
40
|
+
* Add ownership to encrypted Responses state emitted by a successful native
|
|
41
41
|
* Responses call. Provider failures and unrelated response formats pass through.
|
|
42
42
|
*/
|
|
43
|
-
export declare function
|
|
43
|
+
export declare function wrapResponsesEncryptedResponse(response: Response, owner: ResponsesEncryptedContentOwner): Promise<Response>;
|
|
44
44
|
/** Normalize OpenAI Responses call ids without mutating the caller's request. */
|
|
45
45
|
export declare function normalizeOpenAiResponsesCallIds(body: unknown): unknown;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
const OPENAI_RESPONSES_CALL_ID_MAX_LENGTH = 64;
|
|
3
3
|
const NORMALIZED_CALL_ID_PREFIX = "rk_";
|
|
4
|
-
const
|
|
4
|
+
const ENCRYPTED_CONTENT_PREFIX = "rk1.";
|
|
5
5
|
const LEGACY_TOOL_SEARCH_ITEM_ID_PREFIX = "ttc_";
|
|
6
6
|
const TOOL_SEARCH_ITEM_ID_PREFIX = "tsc_";
|
|
7
7
|
function sameOwner(left, right) {
|
|
@@ -39,22 +39,22 @@ export function repairLegacyToolSearchItemIds(body) {
|
|
|
39
39
|
});
|
|
40
40
|
return changed ? { ...record, input } : body;
|
|
41
41
|
}
|
|
42
|
-
/** Wrap provider-owned opaque
|
|
42
|
+
/** Wrap provider-owned opaque Responses content without expanding the ciphertext itself. */
|
|
43
43
|
export function wrapResponsesEncryptedContent(ciphertext, owner) {
|
|
44
44
|
if (parseResponsesEncryptedContent(ciphertext) !== undefined)
|
|
45
45
|
return ciphertext;
|
|
46
46
|
const encodedOwner = Buffer.from(JSON.stringify({ p: owner.provider, m: owner.nativeModel }), "utf8").toString("base64url");
|
|
47
|
-
return `${
|
|
47
|
+
return `${ENCRYPTED_CONTENT_PREFIX}${encodedOwner}.${ciphertext}`;
|
|
48
48
|
}
|
|
49
|
-
/** Parse a RouteKit-owned encrypted-
|
|
49
|
+
/** Parse a RouteKit-owned encrypted-content envelope. Raw provider ciphertext is ownerless. */
|
|
50
50
|
export function parseResponsesEncryptedContent(value) {
|
|
51
|
-
if (typeof value !== "string" || !value.startsWith(
|
|
51
|
+
if (typeof value !== "string" || !value.startsWith(ENCRYPTED_CONTENT_PREFIX)) {
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
|
-
const separator = value.indexOf(".",
|
|
54
|
+
const separator = value.indexOf(".", ENCRYPTED_CONTENT_PREFIX.length);
|
|
55
55
|
if (separator === -1)
|
|
56
56
|
return undefined;
|
|
57
|
-
const encodedOwner = value.slice(
|
|
57
|
+
const encodedOwner = value.slice(ENCRYPTED_CONTENT_PREFIX.length, separator);
|
|
58
58
|
const ciphertext = value.slice(separator + 1);
|
|
59
59
|
if (encodedOwner.length === 0 || ciphertext.length === 0)
|
|
60
60
|
return undefined;
|
|
@@ -82,72 +82,193 @@ export function parseResponsesEncryptedContent(value) {
|
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
function applyEncryptedContentPolicy(value, policy) {
|
|
86
|
+
const parsed = parseResponsesEncryptedContent(value);
|
|
87
|
+
if (policy.mode === "drop")
|
|
88
|
+
return { action: "drop" };
|
|
89
|
+
if (policy.mode === "relay") {
|
|
90
|
+
return parsed === undefined
|
|
91
|
+
? { action: "drop" }
|
|
92
|
+
: { action: "keep", value, changed: false };
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
if (parsed === undefined || !sameOwner(parsed.owner, policy.owner)) {
|
|
95
|
+
return { action: "drop" };
|
|
96
|
+
}
|
|
97
|
+
if (policy.unwrap === false) {
|
|
98
|
+
return { action: "keep", value, changed: false };
|
|
99
|
+
}
|
|
100
|
+
return { action: "keep", value: parsed.ciphertext, changed: true };
|
|
101
|
+
}
|
|
102
|
+
function prepareEncryptedContentParts(parts, policy) {
|
|
98
103
|
let changed = false;
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
let dropped = 0;
|
|
105
|
+
const value = [];
|
|
106
|
+
for (const candidate of parts) {
|
|
101
107
|
if (typeof candidate !== "object" ||
|
|
102
108
|
candidate === null ||
|
|
103
109
|
Array.isArray(candidate)) {
|
|
104
|
-
|
|
110
|
+
value.push(candidate);
|
|
105
111
|
continue;
|
|
106
112
|
}
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
typeof
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const part = candidate;
|
|
114
|
+
if (part.type !== "encrypted_content" ||
|
|
115
|
+
typeof part.encrypted_content !== "string" ||
|
|
116
|
+
part.encrypted_content.length === 0) {
|
|
117
|
+
value.push(candidate);
|
|
112
118
|
continue;
|
|
113
119
|
}
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
dropped += 1;
|
|
120
|
+
const prepared = applyEncryptedContentPolicy(part.encrypted_content, policy);
|
|
121
|
+
if (prepared.action === "drop") {
|
|
117
122
|
changed = true;
|
|
123
|
+
dropped += 1;
|
|
118
124
|
continue;
|
|
119
125
|
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
changed = true;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
input.push(candidate);
|
|
126
|
+
if (prepared.changed) {
|
|
127
|
+
value.push({ ...part, encrypted_content: prepared.value });
|
|
128
|
+
changed = true;
|
|
127
129
|
continue;
|
|
128
130
|
}
|
|
129
|
-
|
|
131
|
+
value.push(candidate);
|
|
132
|
+
}
|
|
133
|
+
return { value, changed, dropped };
|
|
134
|
+
}
|
|
135
|
+
function prepareEncryptedInputItem(candidate, policy) {
|
|
136
|
+
if (typeof candidate !== "object" ||
|
|
137
|
+
candidate === null ||
|
|
138
|
+
Array.isArray(candidate)) {
|
|
139
|
+
return { value: candidate, changed: false, dropped: 0, remove: false };
|
|
140
|
+
}
|
|
141
|
+
const item = candidate;
|
|
142
|
+
const type = typeof item.type === "string" ? item.type : "";
|
|
143
|
+
let next = item;
|
|
144
|
+
let changed = false;
|
|
145
|
+
let dropped = 0;
|
|
146
|
+
if (ENCRYPTED_CONTENT_RECORD_TYPES.has(type) &&
|
|
147
|
+
typeof item.encrypted_content === "string" &&
|
|
148
|
+
item.encrypted_content.length > 0) {
|
|
149
|
+
const prepared = applyEncryptedContentPolicy(item.encrypted_content, policy);
|
|
150
|
+
if (prepared.action === "drop") {
|
|
151
|
+
if (type === "reasoning" ||
|
|
152
|
+
type === "compaction" ||
|
|
153
|
+
type === "encrypted_content") {
|
|
154
|
+
return { value: candidate, changed: true, dropped: 1, remove: true };
|
|
155
|
+
}
|
|
156
|
+
const { encrypted_content: _encryptedContent, ...portable } = next;
|
|
157
|
+
next = portable;
|
|
158
|
+
changed = true;
|
|
130
159
|
dropped += 1;
|
|
160
|
+
if ((type === "message" || type === "agent_message") &&
|
|
161
|
+
!Array.isArray(item.content)) {
|
|
162
|
+
return { value: candidate, changed: true, dropped, remove: true };
|
|
163
|
+
}
|
|
164
|
+
if ((type === "function_call_output" ||
|
|
165
|
+
type === "custom_tool_call_output") &&
|
|
166
|
+
!Object.hasOwn(item, "output")) {
|
|
167
|
+
next = { ...next, output: "" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (prepared.action === "keep" && prepared.changed) {
|
|
171
|
+
next = { ...next, encrypted_content: prepared.value };
|
|
131
172
|
changed = true;
|
|
132
|
-
continue;
|
|
133
173
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
}
|
|
175
|
+
if ((type === "message" || type === "agent_message") &&
|
|
176
|
+
Array.isArray(item.content)) {
|
|
177
|
+
const prepared = prepareEncryptedContentParts(item.content, policy);
|
|
178
|
+
dropped += prepared.dropped;
|
|
179
|
+
if (prepared.changed) {
|
|
180
|
+
if (prepared.value.length === 0) {
|
|
181
|
+
return { value: candidate, changed: true, dropped, remove: true };
|
|
182
|
+
}
|
|
183
|
+
next = { ...next, content: prepared.value };
|
|
184
|
+
changed = true;
|
|
137
185
|
}
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
}
|
|
187
|
+
if ((type === "function_call_output" ||
|
|
188
|
+
type === "custom_tool_call_output") &&
|
|
189
|
+
Array.isArray(item.output)) {
|
|
190
|
+
const prepared = prepareEncryptedContentParts(item.output, policy);
|
|
191
|
+
dropped += prepared.dropped;
|
|
192
|
+
if (prepared.changed) {
|
|
193
|
+
next = {
|
|
194
|
+
...next,
|
|
195
|
+
output: prepared.value.length === 0 ? "" : prepared.value
|
|
196
|
+
};
|
|
197
|
+
changed = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if ((type === "function_call_output" ||
|
|
201
|
+
type === "custom_tool_call_output") &&
|
|
202
|
+
typeof item.output === "object" &&
|
|
203
|
+
item.output !== null &&
|
|
204
|
+
!Array.isArray(item.output)) {
|
|
205
|
+
const output = item.output;
|
|
206
|
+
if (output.type === "encrypted_content" &&
|
|
207
|
+
typeof output.encrypted_content === "string" &&
|
|
208
|
+
output.encrypted_content.length > 0) {
|
|
209
|
+
const prepared = applyEncryptedContentPolicy(output.encrypted_content, policy);
|
|
210
|
+
if (prepared.action === "drop") {
|
|
211
|
+
next = { ...next, output: "" };
|
|
212
|
+
changed = true;
|
|
213
|
+
dropped += 1;
|
|
214
|
+
}
|
|
215
|
+
else if (prepared.changed) {
|
|
216
|
+
next = {
|
|
217
|
+
...next,
|
|
218
|
+
output: { ...output, encrypted_content: prepared.value }
|
|
219
|
+
};
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
value: changed ? next : candidate,
|
|
226
|
+
changed,
|
|
227
|
+
dropped,
|
|
228
|
+
remove: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Apply the destination policy to provider-owned Responses input without
|
|
233
|
+
* mutating the request. Incompatible opaque state is removed while visible
|
|
234
|
+
* messages and tool-call/output carriers remain in the portable transcript.
|
|
235
|
+
*/
|
|
236
|
+
export function prepareResponsesEncryptedInput(body, policy) {
|
|
237
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
238
|
+
return { body, dropped: 0 };
|
|
239
|
+
}
|
|
240
|
+
const record = body;
|
|
241
|
+
if (!Array.isArray(record.input))
|
|
242
|
+
return { body, dropped: 0 };
|
|
243
|
+
let dropped = 0;
|
|
244
|
+
let changed = false;
|
|
245
|
+
const input = [];
|
|
246
|
+
for (const candidate of record.input) {
|
|
247
|
+
const prepared = prepareEncryptedInputItem(candidate, policy);
|
|
248
|
+
dropped += prepared.dropped;
|
|
249
|
+
changed ||= prepared.changed;
|
|
250
|
+
if (!prepared.remove)
|
|
251
|
+
input.push(prepared.value);
|
|
140
252
|
}
|
|
141
253
|
return {
|
|
142
254
|
body: changed ? { ...record, input } : body,
|
|
143
255
|
dropped
|
|
144
256
|
};
|
|
145
257
|
}
|
|
146
|
-
|
|
258
|
+
const ENCRYPTED_CONTENT_RECORD_TYPES = new Set([
|
|
259
|
+
"reasoning",
|
|
260
|
+
"compaction",
|
|
261
|
+
"encrypted_content",
|
|
262
|
+
"message",
|
|
263
|
+
"function_call_output",
|
|
264
|
+
"custom_tool_call_output",
|
|
265
|
+
"agent_message"
|
|
266
|
+
]);
|
|
267
|
+
function wrapEncryptedResponsesValue(value, owner) {
|
|
147
268
|
if (Array.isArray(value)) {
|
|
148
269
|
let changed = false;
|
|
149
270
|
const items = value.map((item) => {
|
|
150
|
-
const transformed =
|
|
271
|
+
const transformed = wrapEncryptedResponsesValue(item, owner);
|
|
151
272
|
changed ||= transformed.changed;
|
|
152
273
|
return transformed.value;
|
|
153
274
|
});
|
|
@@ -161,7 +282,8 @@ function wrapEncryptedReasoningValue(value, owner) {
|
|
|
161
282
|
const output = {};
|
|
162
283
|
for (const [key, field] of Object.entries(record)) {
|
|
163
284
|
if (key === "encrypted_content" &&
|
|
164
|
-
record.type === "
|
|
285
|
+
typeof record.type === "string" &&
|
|
286
|
+
ENCRYPTED_CONTENT_RECORD_TYPES.has(record.type) &&
|
|
165
287
|
typeof field === "string" &&
|
|
166
288
|
field.length > 0) {
|
|
167
289
|
const wrapped = wrapResponsesEncryptedContent(field, owner);
|
|
@@ -169,7 +291,7 @@ function wrapEncryptedReasoningValue(value, owner) {
|
|
|
169
291
|
changed ||= wrapped !== field;
|
|
170
292
|
continue;
|
|
171
293
|
}
|
|
172
|
-
const transformed =
|
|
294
|
+
const transformed = wrapEncryptedResponsesValue(field, owner);
|
|
173
295
|
output[key] = transformed.value;
|
|
174
296
|
changed ||= transformed.changed;
|
|
175
297
|
}
|
|
@@ -200,7 +322,7 @@ function wrapSseFrame(frame, delimiter, owner) {
|
|
|
200
322
|
catch {
|
|
201
323
|
return `${frame}${delimiter}`;
|
|
202
324
|
}
|
|
203
|
-
const transformed =
|
|
325
|
+
const transformed = wrapEncryptedResponsesValue(parsed, owner);
|
|
204
326
|
if (!transformed.changed)
|
|
205
327
|
return `${frame}${delimiter}`;
|
|
206
328
|
let emittedData = false;
|
|
@@ -214,7 +336,7 @@ function wrapSseFrame(frame, delimiter, owner) {
|
|
|
214
336
|
});
|
|
215
337
|
return `${rewritten.join(lineEnding)}${delimiter}`;
|
|
216
338
|
}
|
|
217
|
-
function
|
|
339
|
+
function wrapResponsesEncryptedSse(source, owner) {
|
|
218
340
|
const decoder = new TextDecoder();
|
|
219
341
|
const encoder = new TextEncoder();
|
|
220
342
|
let buffer = "";
|
|
@@ -239,10 +361,10 @@ function wrapResponsesReasoningSse(source, owner) {
|
|
|
239
361
|
}));
|
|
240
362
|
}
|
|
241
363
|
/**
|
|
242
|
-
* Add ownership to encrypted
|
|
364
|
+
* Add ownership to encrypted Responses state emitted by a successful native
|
|
243
365
|
* Responses call. Provider failures and unrelated response formats pass through.
|
|
244
366
|
*/
|
|
245
|
-
export async function
|
|
367
|
+
export async function wrapResponsesEncryptedResponse(response, owner) {
|
|
246
368
|
if (!response.ok || response.body === null)
|
|
247
369
|
return response;
|
|
248
370
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -250,7 +372,7 @@ export async function wrapResponsesReasoningResponse(response, owner) {
|
|
|
250
372
|
const headers = new Headers(response.headers);
|
|
251
373
|
headers.delete("content-length");
|
|
252
374
|
headers.delete("content-encoding");
|
|
253
|
-
return new Response(
|
|
375
|
+
return new Response(wrapResponsesEncryptedSse(response.body, owner), {
|
|
254
376
|
status: response.status,
|
|
255
377
|
statusText: response.statusText,
|
|
256
378
|
headers
|
|
@@ -265,7 +387,7 @@ export async function wrapResponsesReasoningResponse(response, owner) {
|
|
|
265
387
|
catch {
|
|
266
388
|
return response;
|
|
267
389
|
}
|
|
268
|
-
const transformed =
|
|
390
|
+
const transformed = wrapEncryptedResponsesValue(parsed, owner);
|
|
269
391
|
if (!transformed.changed)
|
|
270
392
|
return response;
|
|
271
393
|
const headers = new Headers(response.headers);
|
|
@@ -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 {
|
|
18
|
+
import { prepareResponsesEncryptedInput, repairLegacyToolSearchItemIds, wrapResponsesEncryptedResponse } 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";
|
|
@@ -776,7 +776,7 @@ export function chatToResponses(openai, model, toolRegistry = EMPTY_TOOL_REGISTR
|
|
|
776
776
|
function jsonResponse(status, value) {
|
|
777
777
|
return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
|
|
778
778
|
}
|
|
779
|
-
function
|
|
779
|
+
function responsesEncryptedContentOwner(backend, upstreamModel, destinationWireShape, supportsNativeResponses) {
|
|
780
780
|
const route = backend.resolveModelRoute?.(upstreamModel);
|
|
781
781
|
return route === undefined
|
|
782
782
|
? {
|
|
@@ -789,9 +789,9 @@ function responsesReasoningOwner(backend, upstreamModel, destinationWireShape, s
|
|
|
789
789
|
}
|
|
790
790
|
: { provider: route.provider, nativeModel: route.nativeId };
|
|
791
791
|
}
|
|
792
|
-
function
|
|
792
|
+
function recordDroppedEncryptedContent(count) {
|
|
793
793
|
if (count > 0) {
|
|
794
|
-
droppedField("responses", "encrypted_content", "input
|
|
794
|
+
droppedField("responses", "encrypted_content", "input");
|
|
795
795
|
}
|
|
796
796
|
}
|
|
797
797
|
export async function handleResponses(backend, body, modelCallId, signal, backendOptions = {}) {
|
|
@@ -848,7 +848,7 @@ export async function handleResponses(backend, body, modelCallId, signal, backen
|
|
|
848
848
|
const nativeResponses = backend.responses?.bind(backend);
|
|
849
849
|
const supportsNativeResponses = nativeResponses !== undefined &&
|
|
850
850
|
(backend.supportsResponses?.(upstreamModel) ?? true);
|
|
851
|
-
const
|
|
851
|
+
const encryptedContentOwner = responsesEncryptedContentOwner(backend, upstreamModel, destinationWireShape, supportsNativeResponses);
|
|
852
852
|
if (body.previous_response_id != null && !supportsNativeResponses) {
|
|
853
853
|
return jsonResponse(400, {
|
|
854
854
|
error: {
|
|
@@ -864,16 +864,16 @@ 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 =
|
|
867
|
+
const prepared = prepareResponsesEncryptedInput(repairLegacyToolSearchItemIds(body), {
|
|
868
868
|
mode: "forward",
|
|
869
|
-
owner:
|
|
869
|
+
owner: encryptedContentOwner
|
|
870
870
|
});
|
|
871
|
-
|
|
871
|
+
recordDroppedEncryptedContent(prepared.dropped);
|
|
872
872
|
const response = await nativeResponses(prepared.body, signal, {
|
|
873
873
|
...backendOptions,
|
|
874
874
|
modelCallId
|
|
875
875
|
});
|
|
876
|
-
return
|
|
876
|
+
return wrapResponsesEncryptedResponse(response, encryptedContentOwner);
|
|
877
877
|
}
|
|
878
878
|
// Server-executed web search is honored when the caller declared the tool,
|
|
879
879
|
// an executor is available (a provider key exists), and no *client* tool
|
|
@@ -887,13 +887,13 @@ export async function handleResponses(backend, body, modelCallId, signal, backen
|
|
|
887
887
|
const includeCompatibleBody = requestsEncryptedReasoning && !preservesOpaqueReasoning
|
|
888
888
|
? { ...body, include: body.include?.filter((value) => value !== "reasoning.encrypted_content") }
|
|
889
889
|
: body;
|
|
890
|
-
const
|
|
890
|
+
const encryptedInputPolicy = destinationWireShape === "routekit-envelope"
|
|
891
891
|
? { mode: "relay" }
|
|
892
892
|
: destinationWireShape === "openai-responses"
|
|
893
|
-
? { mode: "forward", owner:
|
|
893
|
+
? { mode: "forward", owner: encryptedContentOwner, unwrap: false }
|
|
894
894
|
: { mode: "drop" };
|
|
895
|
-
const prepared =
|
|
896
|
-
|
|
895
|
+
const prepared = prepareResponsesEncryptedInput(includeCompatibleBody, encryptedInputPolicy);
|
|
896
|
+
recordDroppedEncryptedContent(prepared.dropped);
|
|
897
897
|
const translatedBody = prepared.body;
|
|
898
898
|
let chat;
|
|
899
899
|
try {
|
package/dist/bedrock-source.d.ts
CHANGED
|
@@ -14,9 +14,13 @@ export type BedrockProviderSourceOptions = {
|
|
|
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
16
|
/**
|
|
17
|
-
* Codex
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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_`
|
|
20
24
|
*/
|
|
21
25
|
export declare function sanitizeBedrockMantleRequestBody(body: unknown): unknown;
|
|
22
26
|
export declare function toBedrockConverseInput(body: unknown): ConverseCommandInput;
|
package/dist/bedrock-source.js
CHANGED
|
@@ -15,27 +15,93 @@ export function isBedrockOpenAiModel(modelId) {
|
|
|
15
15
|
return BEDROCK_OPENAI_MODEL.test(modelId);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Codex
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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_`
|
|
21
25
|
*/
|
|
22
26
|
export function sanitizeBedrockMantleRequestBody(body) {
|
|
23
27
|
const payload = record(body);
|
|
24
|
-
if (payload === undefined
|
|
28
|
+
if (payload === undefined)
|
|
25
29
|
return body;
|
|
30
|
+
let next = payload;
|
|
26
31
|
let changed = false;
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
102
|
changed = true;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
return changed ? { ...payload, tools } : body;
|
|
103
|
+
}
|
|
104
|
+
return changed ? next : item;
|
|
39
105
|
}
|
|
40
106
|
function mantleApiKey(env) {
|
|
41
107
|
const key = env.AWS_BEARER_TOKEN_BEDROCK ?? env.BEDROCK_API_KEY;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomId } from "@velum-labs/routekit-runtime";
|
|
2
2
|
import { joinPath } from "./backend.js";
|
|
3
3
|
import { droppedField } from "./adapters/dropped.js";
|
|
4
|
-
import { normalizeOpenAiResponsesCallIds,
|
|
4
|
+
import { normalizeOpenAiResponsesCallIds, prepareResponsesEncryptedInput, wrapResponsesEncryptedContent } from "./adapters/openai-responses-wire.js";
|
|
5
5
|
import { SseDecoder, SseParseError } from "./sse/parse.js";
|
|
6
6
|
import { anthropicMessageContentOf, anthropicReasoningDetailsOf, anthropicRequestMetadataOf, attachGoogleToolCallIndexes, googleThoughtDetailsOf, googleToolCallIndexesOf, reasoningSelectionOf, routeKitRequestValidationErrorOf, attachResponsesReasoningMetadata, responsesReasoningMetadataOf } from "./adapters/openai-chat-wire.js";
|
|
7
7
|
function invalidReasoningControlResponse(message, metadata = false, path) {
|
|
@@ -1059,16 +1059,16 @@ function responsesRequest(body, model, options) {
|
|
|
1059
1059
|
}
|
|
1060
1060
|
: {})
|
|
1061
1061
|
});
|
|
1062
|
-
const prepared =
|
|
1062
|
+
const prepared = prepareResponsesEncryptedInput(request, {
|
|
1063
1063
|
mode: "forward",
|
|
1064
|
-
owner:
|
|
1064
|
+
owner: codexEncryptedContentOwner(model)
|
|
1065
1065
|
});
|
|
1066
1066
|
if (prepared.dropped > 0) {
|
|
1067
|
-
droppedField("responses", "encrypted_content", "input
|
|
1067
|
+
droppedField("responses", "encrypted_content", "input");
|
|
1068
1068
|
}
|
|
1069
1069
|
return prepared.body;
|
|
1070
1070
|
}
|
|
1071
|
-
function
|
|
1071
|
+
function codexEncryptedContentOwner(model) {
|
|
1072
1072
|
return { provider: "codex", nativeModel: model };
|
|
1073
1073
|
}
|
|
1074
1074
|
function responsesOutput(payload, model) {
|
|
@@ -1089,7 +1089,7 @@ function responsesOutput(payload, model) {
|
|
|
1089
1089
|
item.encrypted_content.length > 0
|
|
1090
1090
|
? [{
|
|
1091
1091
|
...item,
|
|
1092
|
-
encrypted_content: wrapResponsesEncryptedContent(item.encrypted_content,
|
|
1092
|
+
encrypted_content: wrapResponsesEncryptedContent(item.encrypted_content, codexEncryptedContentOwner(model))
|
|
1093
1093
|
}]
|
|
1094
1094
|
: []);
|
|
1095
1095
|
const toolCalls = (output ?? []).flatMap((item, index) => item.type === "function_call"
|
|
@@ -1315,7 +1315,7 @@ export class CodexResponsesBackend extends HttpProviderBackend {
|
|
|
1315
1315
|
const delta = {};
|
|
1316
1316
|
const wrappedOutput = {
|
|
1317
1317
|
...output,
|
|
1318
|
-
encrypted_content: wrapResponsesEncryptedContent(output.encrypted_content,
|
|
1318
|
+
encrypted_content: wrapResponsesEncryptedContent(output.encrypted_content, codexEncryptedContentOwner(model))
|
|
1319
1319
|
};
|
|
1320
1320
|
attachResponsesReasoningMetadata(delta, {
|
|
1321
1321
|
items: [wrappedOutput],
|
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 {
|
|
8
|
+
import { prepareResponsesEncryptedInput, repairLegacyToolSearchItemIds, wrapResponsesEncryptedResponse } 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 =
|
|
659
|
+
const prepared = prepareResponsesEncryptedInput(repairLegacyToolSearchItemIds(withModel(body, route.nativeId)), {
|
|
660
660
|
mode: "forward",
|
|
661
661
|
owner
|
|
662
662
|
});
|
|
@@ -673,13 +673,13 @@ export async function startGateway(options) {
|
|
|
673
673
|
},
|
|
674
674
|
invoke: async (_callId, signal, onAttribution) => {
|
|
675
675
|
if (prepared.dropped > 0) {
|
|
676
|
-
droppedField("responses", "encrypted_content", "input
|
|
676
|
+
droppedField("responses", "encrypted_content", "input");
|
|
677
677
|
}
|
|
678
678
|
const response = await codexProviderRelay.relay(req.headers, relayBody, signal, {
|
|
679
679
|
responseMode: isStream(body) ? "streaming" : "buffered",
|
|
680
680
|
onAttribution
|
|
681
681
|
});
|
|
682
|
-
return await
|
|
682
|
+
return await wrapResponsesEncryptedResponse(response, owner);
|
|
683
683
|
}
|
|
684
684
|
});
|
|
685
685
|
return;
|
|
@@ -692,7 +692,7 @@ export async function startGateway(options) {
|
|
|
692
692
|
provider: "codex",
|
|
693
693
|
nativeModel: requestedModel ?? "codex/default"
|
|
694
694
|
};
|
|
695
|
-
const prepared =
|
|
695
|
+
const prepared = prepareResponsesEncryptedInput(repairLegacyToolSearchItemIds(body), {
|
|
696
696
|
mode: "forward",
|
|
697
697
|
owner
|
|
698
698
|
});
|
|
@@ -708,13 +708,13 @@ export async function startGateway(options) {
|
|
|
708
708
|
},
|
|
709
709
|
invoke: async (_callId, signal, onAttribution) => {
|
|
710
710
|
if (prepared.dropped > 0) {
|
|
711
|
-
droppedField("responses", "encrypted_content", "input
|
|
711
|
+
droppedField("responses", "encrypted_content", "input");
|
|
712
712
|
}
|
|
713
713
|
const response = await codexRequestRelay.relay(req.headers, prepared.body, signal, {
|
|
714
714
|
responseMode: isStream(body) ? "streaming" : "buffered",
|
|
715
715
|
onAttribution
|
|
716
716
|
});
|
|
717
|
-
return await
|
|
717
|
+
return await wrapResponsesEncryptedResponse(response, owner);
|
|
718
718
|
}
|
|
719
719
|
});
|
|
720
720
|
return;
|
|
@@ -523,6 +523,48 @@ test("sanitizeBedrockMantleRequestBody strips search_content_types from web_sear
|
|
|
523
523
|
const untouched = { model: "openai.gpt-5.6-sol", tools: [{ type: "web_search" }] };
|
|
524
524
|
assert.equal(sanitizeBedrockMantleRequestBody(untouched), untouched);
|
|
525
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
|
+
});
|
|
526
568
|
test("Bedrock mantle Responses drops Codex search_content_types before egress", async () => {
|
|
527
569
|
const forwarded = [];
|
|
528
570
|
const source = new BedrockProviderSource({
|
|
@@ -551,6 +593,40 @@ test("Bedrock mantle Responses drops Codex search_content_types before egress",
|
|
|
551
593
|
}
|
|
552
594
|
]);
|
|
553
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
|
+
});
|
|
554
630
|
test("Bedrock OpenAI chat without a mantle key returns 400 and does not call Converse", async () => {
|
|
555
631
|
const runtimeCalls = [];
|
|
556
632
|
const source = new BedrockProviderSource({
|
|
@@ -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,
|
|
4
|
+
import { normalizeOpenAiResponsesCallIds, parseResponsesEncryptedContent, prepareResponsesEncryptedInput, repairLegacyToolSearchItemIds, wrapResponsesEncryptedContent, wrapResponsesEncryptedResponse } from "../adapters/openai-responses-wire.js";
|
|
5
5
|
const OWNER_A = {
|
|
6
6
|
provider: "codex",
|
|
7
7
|
nativeModel: "gpt-5.6"
|
|
@@ -134,7 +134,7 @@ test("legacy tool-search item ids are repaired narrowly without mutating input",
|
|
|
134
134
|
const stringInput = { input: "hello" };
|
|
135
135
|
assert.equal(repairLegacyToolSearchItemIds(stringInput), stringInput);
|
|
136
136
|
});
|
|
137
|
-
test("encrypted
|
|
137
|
+
test("encrypted Responses ownership round-trips exact opaque content", () => {
|
|
138
138
|
const ciphertext = "opaque.with.separators/and unicode \u{1f9e0}\nbytes";
|
|
139
139
|
const wrapped = wrapResponsesEncryptedContent(ciphertext, OWNER_A);
|
|
140
140
|
assert.match(wrapped, /^rk1\.[A-Za-z0-9_-]+\./);
|
|
@@ -144,7 +144,7 @@ test("encrypted reasoning ownership round-trips exact opaque content", () => {
|
|
|
144
144
|
});
|
|
145
145
|
assert.equal(wrapResponsesEncryptedContent(wrapped, OWNER_B), wrapped);
|
|
146
146
|
});
|
|
147
|
-
test("
|
|
147
|
+
test("encrypted input forward policy unwraps only the matching provider and native model", () => {
|
|
148
148
|
const matching = wrapResponsesEncryptedContent("cipher-a", OWNER_A);
|
|
149
149
|
const otherProvider = wrapResponsesEncryptedContent("cipher-b", OWNER_B);
|
|
150
150
|
const otherModel = wrapResponsesEncryptedContent("cipher-c", {
|
|
@@ -164,7 +164,7 @@ test("reasoning input forward policy unwraps only the matching provider and nati
|
|
|
164
164
|
{ type: "function_call", call_id: "call_1", name: "read", arguments: "{}" }
|
|
165
165
|
]
|
|
166
166
|
};
|
|
167
|
-
const prepared =
|
|
167
|
+
const prepared = prepareResponsesEncryptedInput(body, {
|
|
168
168
|
mode: "forward",
|
|
169
169
|
owner: OWNER_A
|
|
170
170
|
});
|
|
@@ -175,7 +175,7 @@ test("reasoning input forward policy unwraps only the matching provider and nati
|
|
|
175
175
|
assert.equal(input[4]?.encrypted_content, undefined);
|
|
176
176
|
assert.equal(body.input[1]?.encrypted_content, matching, "caller input is not mutated");
|
|
177
177
|
});
|
|
178
|
-
test("
|
|
178
|
+
test("encrypted input relay and drop policies retain the portable transcript", () => {
|
|
179
179
|
const ownedA = wrapResponsesEncryptedContent("cipher-a", OWNER_A);
|
|
180
180
|
const ownedB = wrapResponsesEncryptedContent("cipher-b", OWNER_B);
|
|
181
181
|
const malformedHeader = `rk1.${ownedA.split(".")[1]}!.cipher-malformed`;
|
|
@@ -191,10 +191,10 @@ test("reasoning input relay and drop policies retain the portable transcript", (
|
|
|
191
191
|
{ type: "function_call", call_id: "call_1", name: "read", arguments: "{}" }
|
|
192
192
|
]
|
|
193
193
|
};
|
|
194
|
-
const relayed =
|
|
194
|
+
const relayed = prepareResponsesEncryptedInput(body, { mode: "relay" });
|
|
195
195
|
assert.equal(relayed.dropped, 3);
|
|
196
196
|
assert.deepEqual(relayed.body.input.map((item) => item.encrypted_content), [ownedA, undefined, ownedB, undefined, undefined]);
|
|
197
|
-
const dropped =
|
|
197
|
+
const dropped = prepareResponsesEncryptedInput(body, { mode: "drop" });
|
|
198
198
|
assert.equal(dropped.dropped, 5);
|
|
199
199
|
assert.deepEqual(dropped.body.input.map((item) => item.type), ["message", "message", "function_call"]);
|
|
200
200
|
});
|
|
@@ -203,7 +203,7 @@ test("forward policy can retain a matching wrapper for a later provider bridge",
|
|
|
203
203
|
const body = {
|
|
204
204
|
input: [{ type: "reasoning", encrypted_content: wrapped }]
|
|
205
205
|
};
|
|
206
|
-
const prepared =
|
|
206
|
+
const prepared = prepareResponsesEncryptedInput(body, {
|
|
207
207
|
mode: "forward",
|
|
208
208
|
owner: OWNER_A,
|
|
209
209
|
unwrap: false
|
|
@@ -211,24 +211,208 @@ test("forward policy can retain a matching wrapper for a later provider bridge",
|
|
|
211
211
|
assert.equal(prepared.body, body);
|
|
212
212
|
assert.equal(prepared.dropped, 0);
|
|
213
213
|
});
|
|
214
|
-
test("
|
|
214
|
+
test("encrypted tool and message parts preserve portable history across provider switches", () => {
|
|
215
|
+
const matching = wrapResponsesEncryptedContent("matching", OWNER_A);
|
|
216
|
+
const foreign = wrapResponsesEncryptedContent("foreign", OWNER_B);
|
|
217
|
+
const otherModel = wrapResponsesEncryptedContent("other-model", {
|
|
218
|
+
provider: OWNER_A.provider,
|
|
219
|
+
nativeModel: "gpt-5.7"
|
|
220
|
+
});
|
|
221
|
+
const message = {
|
|
222
|
+
type: "agent_message",
|
|
223
|
+
id: "amsg_1",
|
|
224
|
+
content: [
|
|
225
|
+
{ type: "input_text", text: "visible handoff" },
|
|
226
|
+
{ type: "encrypted_content", encrypted_content: foreign }
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
const functionOutput = {
|
|
230
|
+
type: "function_call_output",
|
|
231
|
+
call_id: "call_1",
|
|
232
|
+
output: [
|
|
233
|
+
{ type: "input_text", text: "visible result" },
|
|
234
|
+
{ type: "encrypted_content", encrypted_content: matching },
|
|
235
|
+
{ type: "encrypted_content", encrypted_content: foreign },
|
|
236
|
+
{ type: "encrypted_content", encrypted_content: "legacy-raw" }
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
const customOutput = {
|
|
240
|
+
type: "custom_tool_call_output",
|
|
241
|
+
call_id: "call_2",
|
|
242
|
+
output: [
|
|
243
|
+
{ type: "encrypted_content", encrypted_content: otherModel }
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
const objectOutput = {
|
|
247
|
+
type: "function_call_output",
|
|
248
|
+
call_id: "call_3",
|
|
249
|
+
output: {
|
|
250
|
+
type: "encrypted_content",
|
|
251
|
+
encrypted_content: matching
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const compaction = {
|
|
255
|
+
type: "compaction",
|
|
256
|
+
encrypted_content: foreign
|
|
257
|
+
};
|
|
258
|
+
const body = {
|
|
259
|
+
input: [
|
|
260
|
+
message,
|
|
261
|
+
functionOutput,
|
|
262
|
+
customOutput,
|
|
263
|
+
objectOutput,
|
|
264
|
+
compaction
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
const prepared = prepareResponsesEncryptedInput(body, {
|
|
268
|
+
mode: "forward",
|
|
269
|
+
owner: OWNER_A
|
|
270
|
+
});
|
|
271
|
+
const input = prepared.body.input;
|
|
272
|
+
assert.equal(prepared.dropped, 5);
|
|
273
|
+
assert.deepEqual(input, [
|
|
274
|
+
{
|
|
275
|
+
type: "agent_message",
|
|
276
|
+
id: "amsg_1",
|
|
277
|
+
content: [{ type: "input_text", text: "visible handoff" }]
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
type: "function_call_output",
|
|
281
|
+
call_id: "call_1",
|
|
282
|
+
output: [
|
|
283
|
+
{ type: "input_text", text: "visible result" },
|
|
284
|
+
{ type: "encrypted_content", encrypted_content: "matching" }
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: "custom_tool_call_output",
|
|
289
|
+
call_id: "call_2",
|
|
290
|
+
output: ""
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: "function_call_output",
|
|
294
|
+
call_id: "call_3",
|
|
295
|
+
output: {
|
|
296
|
+
type: "encrypted_content",
|
|
297
|
+
encrypted_content: "matching"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
]);
|
|
301
|
+
assert.equal(message.content[1].encrypted_content, foreign, "caller message is not mutated");
|
|
302
|
+
assert.equal(functionOutput.output[1]
|
|
303
|
+
.encrypted_content, matching, "caller function output is not mutated");
|
|
304
|
+
});
|
|
305
|
+
test("encrypted-only message carriers are removed while tool outputs retain pairing", () => {
|
|
306
|
+
const foreign = wrapResponsesEncryptedContent("foreign", OWNER_B);
|
|
307
|
+
const body = {
|
|
308
|
+
input: [
|
|
309
|
+
{
|
|
310
|
+
type: "message",
|
|
311
|
+
role: "assistant",
|
|
312
|
+
content: [
|
|
313
|
+
{ type: "encrypted_content", encrypted_content: foreign }
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
type: "agent_message",
|
|
318
|
+
id: "amsg_1",
|
|
319
|
+
content: [
|
|
320
|
+
{ type: "encrypted_content", encrypted_content: "legacy-raw" }
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
type: "function_call_output",
|
|
325
|
+
call_id: "call_1",
|
|
326
|
+
output: [
|
|
327
|
+
{ type: "encrypted_content", encrypted_content: foreign }
|
|
328
|
+
]
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: "custom_tool_call_output",
|
|
332
|
+
call_id: "call_2",
|
|
333
|
+
output: {
|
|
334
|
+
type: "encrypted_content",
|
|
335
|
+
encrypted_content: "legacy-raw"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
};
|
|
340
|
+
const prepared = prepareResponsesEncryptedInput(body, {
|
|
341
|
+
mode: "forward",
|
|
342
|
+
owner: OWNER_A
|
|
343
|
+
});
|
|
344
|
+
assert.equal(prepared.dropped, 4);
|
|
345
|
+
assert.deepEqual(prepared.body.input, [
|
|
346
|
+
{ type: "function_call_output", call_id: "call_1", output: "" },
|
|
347
|
+
{ type: "custom_tool_call_output", call_id: "call_2", output: "" }
|
|
348
|
+
]);
|
|
349
|
+
});
|
|
350
|
+
test("relay policy retains owned nested content without unwrapping", () => {
|
|
351
|
+
const owned = wrapResponsesEncryptedContent("owned", OWNER_A);
|
|
352
|
+
const body = {
|
|
353
|
+
input: [
|
|
354
|
+
{
|
|
355
|
+
type: "function_call_output",
|
|
356
|
+
call_id: "call_1",
|
|
357
|
+
output: [
|
|
358
|
+
{ type: "encrypted_content", encrypted_content: owned },
|
|
359
|
+
{ type: "encrypted_content", encrypted_content: "legacy-raw" }
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
};
|
|
364
|
+
const prepared = prepareResponsesEncryptedInput(body, { mode: "relay" });
|
|
365
|
+
assert.equal(prepared.dropped, 1);
|
|
366
|
+
assert.deepEqual(prepared.body.input, [
|
|
367
|
+
{
|
|
368
|
+
type: "function_call_output",
|
|
369
|
+
call_id: "call_1",
|
|
370
|
+
output: [{ type: "encrypted_content", encrypted_content: owned }]
|
|
371
|
+
}
|
|
372
|
+
]);
|
|
373
|
+
});
|
|
374
|
+
test("buffered Responses output wraps all supported encrypted replay content", async () => {
|
|
215
375
|
const response = Response.json({
|
|
216
376
|
id: "resp_1",
|
|
217
377
|
output: [
|
|
218
378
|
{ type: "reasoning", encrypted_content: "cipher-a", summary: [] },
|
|
379
|
+
{ type: "compaction", encrypted_content: "compact-a" },
|
|
380
|
+
{
|
|
381
|
+
type: "agent_message",
|
|
382
|
+
content: [
|
|
383
|
+
{ type: "input_text", text: "handoff" },
|
|
384
|
+
{ type: "encrypted_content", encrypted_content: "agent-a" }
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
type: "function_call_output",
|
|
389
|
+
call_id: "call_1",
|
|
390
|
+
output: [
|
|
391
|
+
{ type: "input_text", text: "result" },
|
|
392
|
+
{ type: "encrypted_content", encrypted_content: "function-a" }
|
|
393
|
+
]
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
type: "custom_tool_call_output",
|
|
397
|
+
call_id: "call_2",
|
|
398
|
+
output: {
|
|
399
|
+
type: "encrypted_content",
|
|
400
|
+
encrypted_content: "custom-a"
|
|
401
|
+
}
|
|
402
|
+
},
|
|
219
403
|
{ type: "message", content: [{ type: "output_text", text: "done" }] }
|
|
220
404
|
]
|
|
221
405
|
}, {
|
|
222
406
|
headers: { "content-length": "1", "x-test": "kept" }
|
|
223
407
|
});
|
|
224
|
-
const wrapped = await
|
|
408
|
+
const wrapped = await wrapResponsesEncryptedResponse(response, OWNER_A);
|
|
225
409
|
const payload = await wrapped.json();
|
|
226
|
-
|
|
227
|
-
assert.
|
|
410
|
+
const encrypted = [...JSON.stringify(payload).matchAll(/"encrypted_content":"([^"]+)"/g)].map((match) => parseResponsesEncryptedContent(match[1]));
|
|
411
|
+
assert.deepEqual(encrypted, ["cipher-a", "compact-a", "agent-a", "function-a", "custom-a"].map((ciphertext) => ({ owner: OWNER_A, ciphertext })));
|
|
228
412
|
assert.equal(wrapped.headers.get("content-length"), null);
|
|
229
413
|
assert.equal(wrapped.headers.get("x-test"), "kept");
|
|
230
414
|
});
|
|
231
|
-
test("streaming Responses output wraps
|
|
415
|
+
test("streaming Responses output wraps encrypted replay content and preserves SSE framing", async () => {
|
|
232
416
|
const encoder = new TextEncoder();
|
|
233
417
|
const raw = [
|
|
234
418
|
": keepalive\r\n\r\n",
|
|
@@ -237,8 +421,10 @@ test("streaming Responses output wraps reasoning and preserves SSE framing", asy
|
|
|
237
421
|
"event: response.output_item.done\r\n",
|
|
238
422
|
'data: {"output_index":0,\r\n',
|
|
239
423
|
'data: "item":{"type":"reasoning","encrypted_content":"cipher-\u{1f9e0}"}}\r\n\r\n',
|
|
424
|
+
"event: response.output_item.done\r\n",
|
|
425
|
+
'data: {"output_index":1,"item":{"type":"function_call_output","call_id":"call_1","output":[{"type":"encrypted_content","encrypted_content":"function-stream"}]}}\r\n\r\n',
|
|
240
426
|
"event: response.completed\r\n",
|
|
241
|
-
'data: {"response":{"output":[{"type":"
|
|
427
|
+
'data: {"response":{"output":[{"type":"agent_message","content":[{"type":"encrypted_content","encrypted_content":"terminal"}]}]}}\r\n\r\n',
|
|
242
428
|
"event: custom\r\n",
|
|
243
429
|
"data: not-json\r\n\r\n",
|
|
244
430
|
"data: [DONE]\r\n\r\n"
|
|
@@ -255,7 +441,7 @@ test("streaming Responses output wraps reasoning and preserves SSE framing", asy
|
|
|
255
441
|
const response = new Response(source, {
|
|
256
442
|
headers: { "content-type": "text/event-stream", "content-length": "5" }
|
|
257
443
|
});
|
|
258
|
-
const wrapped = await
|
|
444
|
+
const wrapped = await wrapResponsesEncryptedResponse(response, OWNER_A);
|
|
259
445
|
const text = await wrapped.text();
|
|
260
446
|
assert.match(text, /^: keepalive\r\n\r\n/);
|
|
261
447
|
assert.match(text, /id: event-1\r\nretry: 1000\r\nevent: response\.output_item\.done/);
|
|
@@ -266,6 +452,7 @@ test("streaming Responses output wraps reasoning and preserves SSE framing", asy
|
|
|
266
452
|
.map((match) => parseResponsesEncryptedContent(match[1]));
|
|
267
453
|
assert.deepEqual(envelopes, [
|
|
268
454
|
{ owner: OWNER_A, ciphertext: "cipher-\u{1f9e0}" },
|
|
455
|
+
{ owner: OWNER_A, ciphertext: "function-stream" },
|
|
269
456
|
{ owner: OWNER_A, ciphertext: "terminal" }
|
|
270
457
|
]);
|
|
271
458
|
});
|
|
@@ -1011,7 +1011,7 @@ test("Responses follows ModelRoutedBackend reasoning wire capability", async ()
|
|
|
1011
1011
|
await gateway.close();
|
|
1012
1012
|
}
|
|
1013
1013
|
});
|
|
1014
|
-
test("native Responses swaps isolate
|
|
1014
|
+
test("native Responses swaps isolate encrypted replay state and restore it on A to B to A", async () => {
|
|
1015
1015
|
const requests = [];
|
|
1016
1016
|
const routes = {
|
|
1017
1017
|
"provider-a/model-a": {
|
|
@@ -1054,6 +1054,17 @@ test("native Responses swaps isolate provider reasoning and restore it on A to B
|
|
|
1054
1054
|
encrypted_content: `raw-${suffix}`,
|
|
1055
1055
|
summary: []
|
|
1056
1056
|
},
|
|
1057
|
+
{
|
|
1058
|
+
type: "function_call_output",
|
|
1059
|
+
call_id: `call_${suffix}`,
|
|
1060
|
+
output: [
|
|
1061
|
+
{ type: "input_text", text: `tool-${suffix}` },
|
|
1062
|
+
{
|
|
1063
|
+
type: "encrypted_content",
|
|
1064
|
+
encrypted_content: `raw-tool-${suffix}`
|
|
1065
|
+
}
|
|
1066
|
+
]
|
|
1067
|
+
},
|
|
1057
1068
|
{
|
|
1058
1069
|
type: "message",
|
|
1059
1070
|
role: "assistant",
|
|
@@ -1084,9 +1095,25 @@ test("native Responses swaps isolate provider reasoning and restore it on A to B
|
|
|
1084
1095
|
owner: { provider: "provider-a", nativeModel: "model-a" },
|
|
1085
1096
|
ciphertext: "raw-a"
|
|
1086
1097
|
});
|
|
1098
|
+
const encryptedToolA = String(firstPayload.output[1]?.output?.[1]?.encrypted_content);
|
|
1099
|
+
assert.deepEqual(parseResponsesEncryptedContent(encryptedToolA), {
|
|
1100
|
+
owner: { provider: "provider-a", nativeModel: "model-a" },
|
|
1101
|
+
ciphertext: "raw-tool-a"
|
|
1102
|
+
});
|
|
1087
1103
|
const secondInput = [
|
|
1088
1104
|
{ role: "user", content: "start" },
|
|
1089
1105
|
{ type: "reasoning", id: "rs_a", encrypted_content: encryptedA },
|
|
1106
|
+
{
|
|
1107
|
+
type: "function_call_output",
|
|
1108
|
+
call_id: "call_a",
|
|
1109
|
+
output: [
|
|
1110
|
+
{ type: "input_text", text: "tool-a" },
|
|
1111
|
+
{
|
|
1112
|
+
type: "encrypted_content",
|
|
1113
|
+
encrypted_content: encryptedToolA
|
|
1114
|
+
}
|
|
1115
|
+
]
|
|
1116
|
+
},
|
|
1090
1117
|
{ type: "message", role: "assistant", content: "visible-a" }
|
|
1091
1118
|
];
|
|
1092
1119
|
const second = await fetch(`${gateway.url()}/v1/responses`, {
|
|
@@ -1107,7 +1134,18 @@ test("native Responses swaps isolate provider reasoning and restore it on A to B
|
|
|
1107
1134
|
});
|
|
1108
1135
|
const sentToB = requests[1];
|
|
1109
1136
|
assert.equal(sentToB.input.some((item) => item.type === "reasoning"), false);
|
|
1137
|
+
assert.deepEqual(sentToB.input.find((item) => item.type === "function_call_output"), {
|
|
1138
|
+
type: "function_call_output",
|
|
1139
|
+
call_id: "call_a",
|
|
1140
|
+
output: [{ type: "input_text", text: "tool-a" }]
|
|
1141
|
+
});
|
|
1142
|
+
assert.equal(JSON.stringify(sentToB).includes("raw-tool-a"), false);
|
|
1110
1143
|
assert.deepEqual(sentToB.include, ["reasoning.encrypted_content"]);
|
|
1144
|
+
const encryptedToolB = String(secondPayload.output[1]?.output?.[1]?.encrypted_content);
|
|
1145
|
+
assert.deepEqual(parseResponsesEncryptedContent(encryptedToolB), {
|
|
1146
|
+
owner: { provider: "provider-b", nativeModel: "model-b" },
|
|
1147
|
+
ciphertext: "raw-tool-b"
|
|
1148
|
+
});
|
|
1111
1149
|
const third = await fetch(`${gateway.url()}/v1/responses`, {
|
|
1112
1150
|
method: "POST",
|
|
1113
1151
|
headers: { "content-type": "application/json" },
|
|
@@ -1116,6 +1154,17 @@ test("native Responses swaps isolate provider reasoning and restore it on A to B
|
|
|
1116
1154
|
input: [
|
|
1117
1155
|
...secondInput,
|
|
1118
1156
|
{ type: "reasoning", id: "rs_b", encrypted_content: encryptedB },
|
|
1157
|
+
{
|
|
1158
|
+
type: "function_call_output",
|
|
1159
|
+
call_id: "call_b",
|
|
1160
|
+
output: [
|
|
1161
|
+
{ type: "input_text", text: "tool-b" },
|
|
1162
|
+
{
|
|
1163
|
+
type: "encrypted_content",
|
|
1164
|
+
encrypted_content: encryptedToolB
|
|
1165
|
+
}
|
|
1166
|
+
]
|
|
1167
|
+
},
|
|
1119
1168
|
{ type: "message", role: "assistant", content: "visible-b" }
|
|
1120
1169
|
],
|
|
1121
1170
|
include: ["reasoning.encrypted_content"]
|
|
@@ -1130,7 +1179,17 @@ test("native Responses swaps isolate provider reasoning and restore it on A to B
|
|
|
1130
1179
|
encrypted_content: "raw-a"
|
|
1131
1180
|
}]);
|
|
1132
1181
|
assert.equal(JSON.stringify(sentBackToA).includes("raw-b"), false);
|
|
1182
|
+
assert.equal(JSON.stringify(sentBackToA).includes("raw-tool-b"), false);
|
|
1133
1183
|
assert.equal(JSON.stringify(sentBackToA).includes("rk1."), false);
|
|
1184
|
+
assert.deepEqual(sentBackToA.input
|
|
1185
|
+
.filter((item) => item.type === "function_call_output")
|
|
1186
|
+
.map((item) => item.output), [
|
|
1187
|
+
[
|
|
1188
|
+
{ type: "input_text", text: "tool-a" },
|
|
1189
|
+
{ type: "encrypted_content", encrypted_content: "raw-tool-a" }
|
|
1190
|
+
],
|
|
1191
|
+
[{ type: "input_text", text: "tool-b" }]
|
|
1192
|
+
]);
|
|
1134
1193
|
assert.deepEqual(sentBackToA.input
|
|
1135
1194
|
.filter((item) => item.type === "message")
|
|
1136
1195
|
.map((item) => item.content), ["visible-a", "visible-b"]);
|
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.6",
|
|
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-
|
|
34
|
-
"@velum-labs/routekit-runtime": "0.18.
|
|
35
|
-
"@velum-labs/routekit-
|
|
32
|
+
"@velum-labs/routekit-contracts": "0.18.6",
|
|
33
|
+
"@velum-labs/routekit-tracing": "0.18.6",
|
|
34
|
+
"@velum-labs/routekit-runtime": "0.18.6",
|
|
35
|
+
"@velum-labs/routekit-registry": "0.18.6"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"routekit",
|