@velum-labs/routekit-gateway 0.17.3 → 0.17.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/adapters/openai-responses-wire.d.ts +37 -0
- package/dist/adapters/openai-responses-wire.js +242 -0
- package/dist/adapters/responses.js +36 -16
- package/dist/backend.d.ts +4 -1
- package/dist/bedrock-source.js +45 -6
- package/dist/codex-model-selection.d.ts +26 -0
- package/dist/codex-model-selection.js +236 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/provider-backends.js +27 -7
- package/dist/provider-source.d.ts +3 -2
- package/dist/provider-source.js +100 -3
- package/dist/router.d.ts +4 -2
- package/dist/router.js +42 -7
- package/dist/server.js +131 -74
- package/dist/test/bedrock-source.test.js +19 -3
- package/dist/test/codex-model-selection.test.d.ts +1 -0
- package/dist/test/codex-model-selection.test.js +227 -0
- package/dist/test/cursor.test.js +9 -2
- package/dist/test/openai-responses-wire.test.js +144 -1
- package/dist/test/provider-backends.test.js +126 -2
- package/dist/test/provider-source.test.js +91 -0
- package/dist/test/responses.test.js +372 -26
- package/dist/test/router.test.js +41 -1
- package/dist/test/server-resilience.test.js +42 -0
- package/dist/test/web-search-loop.test.js +8 -3
- package/package.json +5 -5
|
@@ -1,2 +1,39 @@
|
|
|
1
|
+
export type ResponsesReasoningOwner = {
|
|
2
|
+
provider: string;
|
|
3
|
+
nativeModel: string;
|
|
4
|
+
};
|
|
5
|
+
export type ResponsesReasoningInputPolicy = {
|
|
6
|
+
mode: "drop";
|
|
7
|
+
} | {
|
|
8
|
+
mode: "relay";
|
|
9
|
+
} | {
|
|
10
|
+
mode: "forward";
|
|
11
|
+
owner: ResponsesReasoningOwner;
|
|
12
|
+
/** Keep the RouteKit wrapper for a later Chat-to-Responses provider bridge. */
|
|
13
|
+
unwrap?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ParsedResponsesEncryptedContent = {
|
|
16
|
+
owner: ResponsesReasoningOwner;
|
|
17
|
+
ciphertext: string;
|
|
18
|
+
};
|
|
19
|
+
export type PreparedResponsesReasoningInput = {
|
|
20
|
+
body: unknown;
|
|
21
|
+
dropped: number;
|
|
22
|
+
};
|
|
23
|
+
/** Wrap provider-owned opaque reasoning without expanding the ciphertext itself. */
|
|
24
|
+
export declare function wrapResponsesEncryptedContent(ciphertext: string, owner: ResponsesReasoningOwner): string;
|
|
25
|
+
/** Parse a RouteKit-owned encrypted-reasoning envelope. Raw provider ciphertext is ownerless. */
|
|
26
|
+
export declare function parseResponsesEncryptedContent(value: unknown): ParsedResponsesEncryptedContent | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Apply the destination policy to Responses input without mutating the request.
|
|
29
|
+
* Incompatible reasoning items are removed while their assistant/tool carriers
|
|
30
|
+
* remain in the portable transcript.
|
|
31
|
+
*/
|
|
32
|
+
export declare function prepareResponsesReasoningInput(body: unknown, policy: ResponsesReasoningInputPolicy): PreparedResponsesReasoningInput;
|
|
33
|
+
/**
|
|
34
|
+
* Add ownership to encrypted reasoning emitted by a successful native
|
|
35
|
+
* Responses call. Provider failures and unrelated response formats pass through.
|
|
36
|
+
*/
|
|
37
|
+
export declare function wrapResponsesReasoningResponse(response: Response, owner: ResponsesReasoningOwner): Promise<Response>;
|
|
1
38
|
/** Normalize OpenAI Responses call ids without mutating the caller's request. */
|
|
2
39
|
export declare function normalizeOpenAiResponsesCallIds(body: unknown): unknown;
|
|
@@ -1,6 +1,248 @@
|
|
|
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 ENCRYPTED_REASONING_PREFIX = "rk1.";
|
|
5
|
+
function sameOwner(left, right) {
|
|
6
|
+
return left.provider === right.provider && left.nativeModel === right.nativeModel;
|
|
7
|
+
}
|
|
8
|
+
/** Wrap provider-owned opaque reasoning without expanding the ciphertext itself. */
|
|
9
|
+
export function wrapResponsesEncryptedContent(ciphertext, owner) {
|
|
10
|
+
if (parseResponsesEncryptedContent(ciphertext) !== undefined)
|
|
11
|
+
return ciphertext;
|
|
12
|
+
const encodedOwner = Buffer.from(JSON.stringify({ p: owner.provider, m: owner.nativeModel }), "utf8").toString("base64url");
|
|
13
|
+
return `${ENCRYPTED_REASONING_PREFIX}${encodedOwner}.${ciphertext}`;
|
|
14
|
+
}
|
|
15
|
+
/** Parse a RouteKit-owned encrypted-reasoning envelope. Raw provider ciphertext is ownerless. */
|
|
16
|
+
export function parseResponsesEncryptedContent(value) {
|
|
17
|
+
if (typeof value !== "string" || !value.startsWith(ENCRYPTED_REASONING_PREFIX)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const separator = value.indexOf(".", ENCRYPTED_REASONING_PREFIX.length);
|
|
21
|
+
if (separator === -1)
|
|
22
|
+
return undefined;
|
|
23
|
+
const encodedOwner = value.slice(ENCRYPTED_REASONING_PREFIX.length, separator);
|
|
24
|
+
const ciphertext = value.slice(separator + 1);
|
|
25
|
+
if (encodedOwner.length === 0 || ciphertext.length === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encodedOwner))
|
|
28
|
+
return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const ownerBytes = Buffer.from(encodedOwner, "base64url");
|
|
31
|
+
if (ownerBytes.toString("base64url") !== encodedOwner)
|
|
32
|
+
return undefined;
|
|
33
|
+
const parsed = JSON.parse(ownerBytes.toString("utf8"));
|
|
34
|
+
if (parsed === null ||
|
|
35
|
+
typeof parsed !== "object" ||
|
|
36
|
+
typeof parsed.p !== "string" ||
|
|
37
|
+
parsed.p.length === 0 ||
|
|
38
|
+
typeof parsed.m !== "string" ||
|
|
39
|
+
parsed.m.length === 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
owner: { provider: parsed.p, nativeModel: parsed.m },
|
|
44
|
+
ciphertext
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply the destination policy to Responses input without mutating the request.
|
|
53
|
+
* Incompatible reasoning items are removed while their assistant/tool carriers
|
|
54
|
+
* remain in the portable transcript.
|
|
55
|
+
*/
|
|
56
|
+
export function prepareResponsesReasoningInput(body, policy) {
|
|
57
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
58
|
+
return { body, dropped: 0 };
|
|
59
|
+
}
|
|
60
|
+
const record = body;
|
|
61
|
+
if (!Array.isArray(record.input))
|
|
62
|
+
return { body, dropped: 0 };
|
|
63
|
+
let dropped = 0;
|
|
64
|
+
let changed = false;
|
|
65
|
+
const input = [];
|
|
66
|
+
for (const candidate of record.input) {
|
|
67
|
+
if (typeof candidate !== "object" ||
|
|
68
|
+
candidate === null ||
|
|
69
|
+
Array.isArray(candidate)) {
|
|
70
|
+
input.push(candidate);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const item = candidate;
|
|
74
|
+
if (item.type !== "reasoning" ||
|
|
75
|
+
typeof item.encrypted_content !== "string" ||
|
|
76
|
+
item.encrypted_content.length === 0) {
|
|
77
|
+
input.push(candidate);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const parsed = parseResponsesEncryptedContent(item.encrypted_content);
|
|
81
|
+
if (policy.mode === "drop") {
|
|
82
|
+
dropped += 1;
|
|
83
|
+
changed = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (policy.mode === "relay") {
|
|
87
|
+
if (parsed === undefined) {
|
|
88
|
+
dropped += 1;
|
|
89
|
+
changed = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
input.push(candidate);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (parsed === undefined || !sameOwner(parsed.owner, policy.owner)) {
|
|
96
|
+
dropped += 1;
|
|
97
|
+
changed = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (policy.unwrap === false) {
|
|
101
|
+
input.push(candidate);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
input.push({ ...item, encrypted_content: parsed.ciphertext });
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
body: changed ? { ...record, input } : body,
|
|
109
|
+
dropped
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function wrapEncryptedReasoningValue(value, owner) {
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
let changed = false;
|
|
115
|
+
const items = value.map((item) => {
|
|
116
|
+
const transformed = wrapEncryptedReasoningValue(item, owner);
|
|
117
|
+
changed ||= transformed.changed;
|
|
118
|
+
return transformed.value;
|
|
119
|
+
});
|
|
120
|
+
return { value: changed ? items : value, changed };
|
|
121
|
+
}
|
|
122
|
+
if (typeof value !== "object" || value === null) {
|
|
123
|
+
return { value, changed: false };
|
|
124
|
+
}
|
|
125
|
+
const record = value;
|
|
126
|
+
let changed = false;
|
|
127
|
+
const output = {};
|
|
128
|
+
for (const [key, field] of Object.entries(record)) {
|
|
129
|
+
if (key === "encrypted_content" &&
|
|
130
|
+
record.type === "reasoning" &&
|
|
131
|
+
typeof field === "string" &&
|
|
132
|
+
field.length > 0) {
|
|
133
|
+
const wrapped = wrapResponsesEncryptedContent(field, owner);
|
|
134
|
+
output[key] = wrapped;
|
|
135
|
+
changed ||= wrapped !== field;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const transformed = wrapEncryptedReasoningValue(field, owner);
|
|
139
|
+
output[key] = transformed.value;
|
|
140
|
+
changed ||= transformed.changed;
|
|
141
|
+
}
|
|
142
|
+
return { value: changed ? output : value, changed };
|
|
143
|
+
}
|
|
144
|
+
function sseDataValue(line) {
|
|
145
|
+
if (line === "data")
|
|
146
|
+
return "";
|
|
147
|
+
if (!line.startsWith("data:"))
|
|
148
|
+
return undefined;
|
|
149
|
+
const value = line.slice("data:".length);
|
|
150
|
+
return value.startsWith(" ") ? value.slice(1) : value;
|
|
151
|
+
}
|
|
152
|
+
function wrapSseFrame(frame, delimiter, owner) {
|
|
153
|
+
const lineEnding = frame.includes("\r\n") ? "\r\n" : "\n";
|
|
154
|
+
const lines = frame.split(/\r?\n/);
|
|
155
|
+
const data = lines.flatMap((line) => {
|
|
156
|
+
const value = sseDataValue(line);
|
|
157
|
+
return value === undefined ? [] : [value];
|
|
158
|
+
});
|
|
159
|
+
if (data.length === 0 || data.join("\n") === "[DONE]") {
|
|
160
|
+
return `${frame}${delimiter}`;
|
|
161
|
+
}
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(data.join("\n"));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return `${frame}${delimiter}`;
|
|
168
|
+
}
|
|
169
|
+
const transformed = wrapEncryptedReasoningValue(parsed, owner);
|
|
170
|
+
if (!transformed.changed)
|
|
171
|
+
return `${frame}${delimiter}`;
|
|
172
|
+
let emittedData = false;
|
|
173
|
+
const rewritten = lines.flatMap((line) => {
|
|
174
|
+
if (sseDataValue(line) === undefined)
|
|
175
|
+
return [line];
|
|
176
|
+
if (emittedData)
|
|
177
|
+
return [];
|
|
178
|
+
emittedData = true;
|
|
179
|
+
return [`data: ${JSON.stringify(transformed.value)}`];
|
|
180
|
+
});
|
|
181
|
+
return `${rewritten.join(lineEnding)}${delimiter}`;
|
|
182
|
+
}
|
|
183
|
+
function wrapResponsesReasoningSse(source, owner) {
|
|
184
|
+
const decoder = new TextDecoder();
|
|
185
|
+
const encoder = new TextEncoder();
|
|
186
|
+
let buffer = "";
|
|
187
|
+
return source.pipeThrough(new TransformStream({
|
|
188
|
+
transform(chunk, controller) {
|
|
189
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
190
|
+
for (;;) {
|
|
191
|
+
const match = /\r?\n\r?\n/.exec(buffer);
|
|
192
|
+
if (match === null || match.index === undefined)
|
|
193
|
+
break;
|
|
194
|
+
const frame = buffer.slice(0, match.index);
|
|
195
|
+
const delimiter = match[0];
|
|
196
|
+
buffer = buffer.slice(match.index + delimiter.length);
|
|
197
|
+
controller.enqueue(encoder.encode(wrapSseFrame(frame, delimiter, owner)));
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
flush(controller) {
|
|
201
|
+
buffer += decoder.decode();
|
|
202
|
+
if (buffer.length > 0)
|
|
203
|
+
controller.enqueue(encoder.encode(buffer));
|
|
204
|
+
}
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Add ownership to encrypted reasoning emitted by a successful native
|
|
209
|
+
* Responses call. Provider failures and unrelated response formats pass through.
|
|
210
|
+
*/
|
|
211
|
+
export async function wrapResponsesReasoningResponse(response, owner) {
|
|
212
|
+
if (!response.ok || response.body === null)
|
|
213
|
+
return response;
|
|
214
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
215
|
+
if (contentType.includes("text/event-stream")) {
|
|
216
|
+
const headers = new Headers(response.headers);
|
|
217
|
+
headers.delete("content-length");
|
|
218
|
+
headers.delete("content-encoding");
|
|
219
|
+
return new Response(wrapResponsesReasoningSse(response.body, owner), {
|
|
220
|
+
status: response.status,
|
|
221
|
+
statusText: response.statusText,
|
|
222
|
+
headers
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (!contentType.includes("json"))
|
|
226
|
+
return response;
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = await response.clone().json();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return response;
|
|
233
|
+
}
|
|
234
|
+
const transformed = wrapEncryptedReasoningValue(parsed, owner);
|
|
235
|
+
if (!transformed.changed)
|
|
236
|
+
return response;
|
|
237
|
+
const headers = new Headers(response.headers);
|
|
238
|
+
headers.delete("content-length");
|
|
239
|
+
headers.delete("content-encoding");
|
|
240
|
+
return new Response(JSON.stringify(transformed.value), {
|
|
241
|
+
status: response.status,
|
|
242
|
+
statusText: response.statusText,
|
|
243
|
+
headers
|
|
244
|
+
});
|
|
245
|
+
}
|
|
4
246
|
function normalizedCallId(callId, attempt = 0) {
|
|
5
247
|
const hash = createHash("sha256").update(callId, "utf8");
|
|
6
248
|
if (attempt > 0)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { randomId } from "@velum-labs/routekit-runtime";
|
|
16
16
|
import { attachReasoningSelection, attachReasoningSelectionError, attachResponsesReasoningMetadata, reasoningSelectionErrorOf, hasExplicitReasoningSelection, reasoningSelectionOf, routeKitRequestValidationErrorOf, responsesReasoningMetadataOf } from "./openai-chat-wire.js";
|
|
17
17
|
import { droppedField } from "./dropped.js";
|
|
18
|
+
import { prepareResponsesReasoningInput, wrapResponsesReasoningResponse } from "./openai-responses-wire.js";
|
|
18
19
|
import { unwrapUpstreamError } from "./upstream-error.js";
|
|
19
20
|
import { openAiSseToResponses } from "./responses-stream.js";
|
|
20
21
|
import { chatUsageToResponses } from "./responses-usage.js";
|
|
@@ -775,6 +776,24 @@ export function chatToResponses(openai, model, toolRegistry = EMPTY_TOOL_REGISTR
|
|
|
775
776
|
function jsonResponse(status, value) {
|
|
776
777
|
return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
|
|
777
778
|
}
|
|
779
|
+
function responsesReasoningOwner(backend, upstreamModel, destinationWireShape, supportsNativeResponses) {
|
|
780
|
+
const route = backend.resolveModelRoute?.(upstreamModel);
|
|
781
|
+
return route === undefined
|
|
782
|
+
? {
|
|
783
|
+
// The only Chat backend that reconstructs OpenAI Responses is the
|
|
784
|
+
// Codex bridge. Native Responses fall back to their protocol identity.
|
|
785
|
+
provider: !supportsNativeResponses && destinationWireShape === "openai-responses"
|
|
786
|
+
? "codex"
|
|
787
|
+
: destinationWireShape ?? "openai-responses",
|
|
788
|
+
nativeModel: upstreamModel
|
|
789
|
+
}
|
|
790
|
+
: { provider: route.provider, nativeModel: route.nativeId };
|
|
791
|
+
}
|
|
792
|
+
function recordDroppedEncryptedReasoning(count) {
|
|
793
|
+
if (count > 0) {
|
|
794
|
+
droppedField("responses", "encrypted_content", "input.reasoning");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
778
797
|
export async function handleResponses(backend, body, modelCallId, signal, backendOptions = {}) {
|
|
779
798
|
const requestedModel = body.model ?? backend.defaultModel ?? "";
|
|
780
799
|
const validationError = routeKitRequestValidationErrorOf(body);
|
|
@@ -829,6 +848,7 @@ export async function handleResponses(backend, body, modelCallId, signal, backen
|
|
|
829
848
|
const nativeResponses = backend.responses?.bind(backend);
|
|
830
849
|
const supportsNativeResponses = nativeResponses !== undefined &&
|
|
831
850
|
(backend.supportsResponses?.(upstreamModel) ?? true);
|
|
851
|
+
const reasoningOwner = responsesReasoningOwner(backend, upstreamModel, destinationWireShape, supportsNativeResponses);
|
|
832
852
|
if (body.previous_response_id != null && !supportsNativeResponses) {
|
|
833
853
|
return jsonResponse(400, {
|
|
834
854
|
error: {
|
|
@@ -839,29 +859,21 @@ export async function handleResponses(backend, body, modelCallId, signal, backen
|
|
|
839
859
|
}
|
|
840
860
|
});
|
|
841
861
|
}
|
|
842
|
-
const hasEncryptedReasoning = Array.isArray(body.input) &&
|
|
843
|
-
body.input.some((item) => item.type === "reasoning" &&
|
|
844
|
-
typeof item.encrypted_content === "string" &&
|
|
845
|
-
item.encrypted_content.length > 0);
|
|
846
862
|
const requestsEncryptedReasoning = Array.isArray(body.include) && body.include.includes("reasoning.encrypted_content");
|
|
847
863
|
const preservesOpaqueReasoning = supportsNativeResponses ||
|
|
848
864
|
destinationWireShape === "openai-responses" ||
|
|
849
865
|
destinationWireShape === "routekit-envelope";
|
|
850
|
-
if (hasEncryptedReasoning && !preservesOpaqueReasoning) {
|
|
851
|
-
return jsonResponse(400, {
|
|
852
|
-
error: {
|
|
853
|
-
type: "invalid_request_error",
|
|
854
|
-
code: "unsupported_encrypted_reasoning",
|
|
855
|
-
param: "input",
|
|
856
|
-
message: `model "${requestedModel}" cannot consume OpenAI Responses encrypted reasoning`
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
866
|
if (supportsNativeResponses && nativeResponses !== undefined) {
|
|
861
|
-
|
|
867
|
+
const prepared = prepareResponsesReasoningInput(body, {
|
|
868
|
+
mode: "forward",
|
|
869
|
+
owner: reasoningOwner
|
|
870
|
+
});
|
|
871
|
+
recordDroppedEncryptedReasoning(prepared.dropped);
|
|
872
|
+
const response = await nativeResponses(prepared.body, signal, {
|
|
862
873
|
...backendOptions,
|
|
863
874
|
modelCallId
|
|
864
875
|
});
|
|
876
|
+
return wrapResponsesReasoningResponse(response, reasoningOwner);
|
|
865
877
|
}
|
|
866
878
|
// Server-executed web search is honored when the caller declared the tool,
|
|
867
879
|
// an executor is available (a provider key exists), and no *client* tool
|
|
@@ -872,9 +884,17 @@ export async function handleResponses(backend, body, modelCallId, signal, backen
|
|
|
872
884
|
const executor = declaresWebSearch && !clientNameCollision ? resolveWebSearchExecutor("responses") : undefined;
|
|
873
885
|
const serverTools = executor !== undefined;
|
|
874
886
|
const toolRegistry = responsesToolRegistry(body, { serverTools });
|
|
875
|
-
const
|
|
887
|
+
const includeCompatibleBody = requestsEncryptedReasoning && !preservesOpaqueReasoning
|
|
876
888
|
? { ...body, include: body.include?.filter((value) => value !== "reasoning.encrypted_content") }
|
|
877
889
|
: body;
|
|
890
|
+
const reasoningPolicy = destinationWireShape === "routekit-envelope"
|
|
891
|
+
? { mode: "relay" }
|
|
892
|
+
: destinationWireShape === "openai-responses"
|
|
893
|
+
? { mode: "forward", owner: reasoningOwner, unwrap: false }
|
|
894
|
+
: { mode: "drop" };
|
|
895
|
+
const prepared = prepareResponsesReasoningInput(includeCompatibleBody, reasoningPolicy);
|
|
896
|
+
recordDroppedEncryptedReasoning(prepared.dropped);
|
|
897
|
+
const translatedBody = prepared.body;
|
|
878
898
|
let chat;
|
|
879
899
|
try {
|
|
880
900
|
chat = responsesToChat(translatedBody, upstreamModel, { serverTools, destinationWireShape });
|
package/dist/backend.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* surface can stream straight through and the dialect adapters can consume the
|
|
7
7
|
* same core without a second abstraction.
|
|
8
8
|
*/
|
|
9
|
-
import type { ModelReasoningCapabilities, RequestAttribution } from "@velum-labs/routekit-contracts";
|
|
9
|
+
import type { ModelCapabilityMetadata, ModelReasoningCapabilities, RequestAttribution } from "@velum-labs/routekit-contracts";
|
|
10
10
|
export type BackendModelRoute = {
|
|
11
11
|
/** Stable RouteKit catalog id (`provider/model`). */
|
|
12
12
|
publicId: string;
|
|
@@ -14,6 +14,7 @@ export type BackendModelRoute = {
|
|
|
14
14
|
nativeId: string;
|
|
15
15
|
/** Configured provider that owns the model. */
|
|
16
16
|
provider: string;
|
|
17
|
+
metadata?: ModelCapabilityMetadata;
|
|
17
18
|
reasoning?: ModelReasoningCapabilities;
|
|
18
19
|
};
|
|
19
20
|
export type Backend = {
|
|
@@ -51,6 +52,8 @@ export type Backend = {
|
|
|
51
52
|
servesModel?(model: string): boolean;
|
|
52
53
|
/** Capabilities advertised for a model id. */
|
|
53
54
|
capabilities?(model: string): Readonly<Record<string, string>>;
|
|
55
|
+
/** OpenRouter-compatible architecture and parameter metadata. */
|
|
56
|
+
modelMetadata?(model: string): ModelCapabilityMetadata | undefined;
|
|
54
57
|
/** Structured reasoning controls advertised for a model id. */
|
|
55
58
|
reasoningCapabilities?(model: string): ModelReasoningCapabilities | undefined;
|
|
56
59
|
/**
|
package/dist/bedrock-source.js
CHANGED
|
@@ -27,6 +27,34 @@ function activeAnthropicProfile(profile, anthropicModels) {
|
|
|
27
27
|
return id !== undefined && anthropicModels.has(id);
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
function bedrockMetadata(model) {
|
|
31
|
+
const inputModalities = (model.inputModalities ?? []).map((value) => value.toLowerCase());
|
|
32
|
+
const outputModalities = (model.outputModalities ?? []).map((value) => value.toLowerCase());
|
|
33
|
+
const inputs = inputModalities.length > 0 ? inputModalities : ["text"];
|
|
34
|
+
const outputs = outputModalities.length > 0 ? outputModalities : ["text"];
|
|
35
|
+
return {
|
|
36
|
+
architecture: {
|
|
37
|
+
modality: `${inputs.join("+")}->${outputs.join("+")}`,
|
|
38
|
+
inputModalities: inputs,
|
|
39
|
+
outputModalities: outputs
|
|
40
|
+
},
|
|
41
|
+
supportedParameters: ["tools", "tool_choice"],
|
|
42
|
+
provenance: inputModalities.length > 0 || outputModalities.length > 0 ? "provider" : "route"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function bedrockDiscoveredModel(id, model) {
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
metadata: bedrockMetadata(model),
|
|
49
|
+
...(model.responseStreamingSupported !== undefined
|
|
50
|
+
? {
|
|
51
|
+
capabilities: {
|
|
52
|
+
streaming: model.responseStreamingSupported ? "supported" : "unsupported"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
: {})
|
|
56
|
+
};
|
|
57
|
+
}
|
|
30
58
|
function textParts(content) {
|
|
31
59
|
if (typeof content === "string")
|
|
32
60
|
return content;
|
|
@@ -372,19 +400,30 @@ export class BedrockProviderSource {
|
|
|
372
400
|
async discoverModels(signal) {
|
|
373
401
|
const foundation = await this.#control.send(new ListFoundationModelsCommand({ byProvider: "Anthropic" }), signal === undefined ? undefined : { abortSignal: signal });
|
|
374
402
|
const foundations = (foundation.modelSummaries ?? []).filter(anthropicFoundationModel);
|
|
375
|
-
const
|
|
376
|
-
const
|
|
403
|
+
const byId = new Map(foundations.map((model) => [model.modelId, model]));
|
|
404
|
+
const ids = new Set(byId.keys());
|
|
405
|
+
const discovered = new Map(foundations.map((model) => [
|
|
406
|
+
model.modelId,
|
|
407
|
+
bedrockDiscoveredModel(model.modelId, model)
|
|
408
|
+
]));
|
|
377
409
|
let nextToken;
|
|
378
410
|
do {
|
|
379
411
|
const profiles = await this.#control.send(new ListInferenceProfilesCommand({ ...(nextToken !== undefined ? { nextToken } : {}) }), signal === undefined ? undefined : { abortSignal: signal });
|
|
380
|
-
for (const profile of profiles.inferenceProfileSummaries ?? [])
|
|
381
|
-
if (activeAnthropicProfile(profile, ids))
|
|
382
|
-
|
|
412
|
+
for (const profile of profiles.inferenceProfileSummaries ?? []) {
|
|
413
|
+
if (!activeAnthropicProfile(profile, ids))
|
|
414
|
+
continue;
|
|
415
|
+
const backingId = (profile.models ?? [])
|
|
416
|
+
.map((model) => foundationIdFromArn(model.modelArn))
|
|
417
|
+
.find((id) => id !== undefined && ids.has(id));
|
|
418
|
+
if (backingId !== undefined) {
|
|
419
|
+
discovered.set(profile.inferenceProfileId, bedrockDiscoveredModel(profile.inferenceProfileId, byId.get(backingId)));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
383
422
|
nextToken = profiles.nextToken;
|
|
384
423
|
} while (nextToken !== undefined && nextToken.length > 0);
|
|
385
424
|
if (discovered.size === 0)
|
|
386
425
|
throw new Error("model discovery returned no active Anthropic Bedrock models");
|
|
387
|
-
return [...discovered
|
|
426
|
+
return [...discovered.values()];
|
|
388
427
|
}
|
|
389
428
|
async chat(body, signal, _options) {
|
|
390
429
|
let input;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CodexModelCandidate, type CodexStartupSelection, type ModelCapabilityMetadata, type ModelSelectionSignals } from "@velum-labs/routekit-contracts";
|
|
2
|
+
export type OpenRouterModelMetadata = ModelCapabilityMetadata & Pick<ModelSelectionSignals, "createdAt">;
|
|
3
|
+
export type OpenRouterModelMetadataClientOptions = {
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
freshMs?: number;
|
|
7
|
+
staleMs?: number;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare class OpenRouterModelMetadataClient {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(options?: OpenRouterModelMetadataClientOptions);
|
|
14
|
+
models(signal?: AbortSignal): Promise<ReadonlyMap<string, OpenRouterModelMetadata>>;
|
|
15
|
+
}
|
|
16
|
+
export type ResolvedCodexStartupSelection = CodexStartupSelection & {
|
|
17
|
+
models: readonly CodexModelCandidate[];
|
|
18
|
+
};
|
|
19
|
+
export declare function resolveCodexStartupModel(input: {
|
|
20
|
+
models: readonly CodexModelCandidate[];
|
|
21
|
+
preferredModel?: string;
|
|
22
|
+
requestedModel?: string;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}, dependencies?: {
|
|
25
|
+
openRouter?: OpenRouterModelMetadataClient;
|
|
26
|
+
}): Promise<ResolvedCodexStartupSelection>;
|