@velum-labs/routekit-gateway 1.0.1 → 1.0.3
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/responses.js +3 -1
- package/dist/adapters/upstream-error.d.ts +5 -1
- package/dist/adapters/upstream-error.js +12 -2
- package/dist/providers/anthropic-codec.js +27 -5
- package/dist/providers/backend-core.d.ts +8 -0
- package/dist/providers/backend-core.js +2 -2
- package/dist/routing/router.js +2 -0
- package/dist/test/provider-validation-anthropic.test.js +30 -1
- package/dist/test/responses-reasoning.test.js +3 -1
- package/package.json +7 -7
|
@@ -179,7 +179,9 @@ export function handleResponses(backend, body, modelCallId, signal, backendOptio
|
|
|
179
179
|
const upstream = yield* backend.chat(chat, signal, requestOptions);
|
|
180
180
|
if (!upstream.ok) {
|
|
181
181
|
const detail = yield* gatewayTryPromise(() => upstream.text());
|
|
182
|
-
return jsonResponse(upstream.status, {
|
|
182
|
+
return jsonResponse(upstream.status, {
|
|
183
|
+
error: unwrapUpstreamError(detail, { preserveMetadata: true })
|
|
184
|
+
});
|
|
183
185
|
}
|
|
184
186
|
if (executor !== undefined) {
|
|
185
187
|
const loopOptions = {
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* `invalid_request_error` from the backend must stay an
|
|
9
9
|
* `invalid_request_error` on the door.
|
|
10
10
|
*/
|
|
11
|
-
export declare function unwrapUpstreamError(detail: string
|
|
11
|
+
export declare function unwrapUpstreamError(detail: string, options?: {
|
|
12
|
+
readonly preserveMetadata?: boolean;
|
|
13
|
+
}): {
|
|
12
14
|
type: string;
|
|
13
15
|
message: string;
|
|
16
|
+
code?: string;
|
|
17
|
+
param?: string;
|
|
14
18
|
};
|
|
@@ -8,13 +8,23 @@
|
|
|
8
8
|
* `invalid_request_error` from the backend must stay an
|
|
9
9
|
* `invalid_request_error` on the door.
|
|
10
10
|
*/
|
|
11
|
-
export function unwrapUpstreamError(detail) {
|
|
11
|
+
export function unwrapUpstreamError(detail, options = {}) {
|
|
12
12
|
try {
|
|
13
13
|
const parsed = JSON.parse(detail);
|
|
14
14
|
if (typeof parsed.error?.message === "string") {
|
|
15
15
|
return {
|
|
16
16
|
type: typeof parsed.error.type === "string" ? parsed.error.type : "api_error",
|
|
17
|
-
message: parsed.error.message
|
|
17
|
+
message: parsed.error.message,
|
|
18
|
+
...(options.preserveMetadata === true &&
|
|
19
|
+
typeof parsed.error.code === "string" &&
|
|
20
|
+
parsed.error.code.length > 0
|
|
21
|
+
? { code: parsed.error.code }
|
|
22
|
+
: {}),
|
|
23
|
+
...(options.preserveMetadata === true &&
|
|
24
|
+
typeof parsed.error.param === "string" &&
|
|
25
|
+
parsed.error.param.length > 0
|
|
26
|
+
? { param: parsed.error.param }
|
|
27
|
+
: {})
|
|
18
28
|
};
|
|
19
29
|
}
|
|
20
30
|
}
|
|
@@ -135,6 +135,17 @@ function anthropicToolChoice(choice, parallelToolCalls) {
|
|
|
135
135
|
? { type: "tool", name: fn.name, ...disableParallel }
|
|
136
136
|
: undefined;
|
|
137
137
|
}
|
|
138
|
+
function anthropicStructuredOutputFormat(responseFormat) {
|
|
139
|
+
if (responseFormat?.type !== "json_schema" ||
|
|
140
|
+
typeof responseFormat.json_schema?.schema !== "object" ||
|
|
141
|
+
responseFormat.json_schema.schema === null) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
type: "json_schema",
|
|
146
|
+
schema: responseFormat.json_schema.schema
|
|
147
|
+
};
|
|
148
|
+
}
|
|
138
149
|
export function anthropicMessages(body, model) {
|
|
139
150
|
const system = (body.messages ?? [])
|
|
140
151
|
.filter((message) => message.role === "system")
|
|
@@ -199,6 +210,21 @@ export function anthropicMessages(body, model) {
|
|
|
199
210
|
: undefined;
|
|
200
211
|
const translatedOutput = selection.mode === "effort" ? { effort: selection.effort } : undefined;
|
|
201
212
|
const thinking = metadata?.thinking ?? translatedThinking;
|
|
213
|
+
const structuredOutputFormat = anthropicStructuredOutputFormat(body.response_format);
|
|
214
|
+
const outputConfig = metadata?.output_config != null
|
|
215
|
+
? {
|
|
216
|
+
...metadata.output_config,
|
|
217
|
+
...(structuredOutputFormat !== undefined &&
|
|
218
|
+
!Object.hasOwn(metadata.output_config, "format")
|
|
219
|
+
? { format: structuredOutputFormat }
|
|
220
|
+
: {})
|
|
221
|
+
}
|
|
222
|
+
: translatedOutput !== undefined || structuredOutputFormat !== undefined
|
|
223
|
+
? {
|
|
224
|
+
...translatedOutput,
|
|
225
|
+
...(structuredOutputFormat !== undefined ? { format: structuredOutputFormat } : {})
|
|
226
|
+
}
|
|
227
|
+
: undefined;
|
|
202
228
|
const toolChoice = anthropicToolChoice(body.tool_choice, body.parallel_tool_calls);
|
|
203
229
|
return {
|
|
204
230
|
model,
|
|
@@ -210,11 +236,7 @@ export function anthropicMessages(body, model) {
|
|
|
210
236
|
...(body.top_p !== undefined ? { top_p: body.top_p } : {}),
|
|
211
237
|
...(body.top_k !== undefined ? { top_k: body.top_k } : {}),
|
|
212
238
|
...(thinking !== undefined ? { thinking } : {}),
|
|
213
|
-
...(
|
|
214
|
-
? { output_config: metadata.output_config }
|
|
215
|
-
: translatedOutput !== undefined
|
|
216
|
-
? { output_config: translatedOutput }
|
|
217
|
-
: {}),
|
|
239
|
+
...(outputConfig !== undefined ? { output_config: outputConfig } : {}),
|
|
218
240
|
...(toolChoice !== undefined ? { tool_choice: toolChoice } : {}),
|
|
219
241
|
...(body.tools !== undefined
|
|
220
242
|
? {
|
|
@@ -36,6 +36,14 @@ export type ChatBody = {
|
|
|
36
36
|
top_p?: number;
|
|
37
37
|
top_k?: number;
|
|
38
38
|
reasoning_effort?: string;
|
|
39
|
+
response_format?: {
|
|
40
|
+
type?: string;
|
|
41
|
+
json_schema?: {
|
|
42
|
+
name?: string;
|
|
43
|
+
schema?: unknown;
|
|
44
|
+
strict?: boolean;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
39
47
|
};
|
|
40
48
|
export type ProviderBackendOptions = {
|
|
41
49
|
baseUrl: string;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { conversationFromOpenAiMessages, conversationText } from "@velum-labs/routekit-contracts/protocol-ir";
|
|
2
|
-
import { randomId } from "@velum-labs/routekit-runtime/timing";
|
|
3
2
|
import { executeWebRequest, routeKitError } from "@velum-labs/routekit-runtime/effect";
|
|
4
3
|
import { StreamPump } from "@velum-labs/routekit-runtime/sse";
|
|
4
|
+
import { randomId } from "@velum-labs/routekit-runtime/timing";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
|
-
import { staticBackendModelPort } from "./backend.js";
|
|
7
6
|
import { jsonResponse } from "../http/response.js";
|
|
8
7
|
import { SseParseError } from "../sse/parse.js";
|
|
8
|
+
import { staticBackendModelPort } from "./backend.js";
|
|
9
9
|
export function invalidReasoningControlResponse(message, metadata = false, path) {
|
|
10
10
|
return jsonResponse({
|
|
11
11
|
error: {
|
package/dist/routing/router.js
CHANGED
|
@@ -332,6 +332,7 @@ export class RoutingBackend {
|
|
|
332
332
|
error: {
|
|
333
333
|
type: "invalid_request_error",
|
|
334
334
|
code: "unsupported_reasoning_control",
|
|
335
|
+
param: requestedSelection.mode === "effort" ? "reasoning.effort" : "reasoning",
|
|
335
336
|
message: selection
|
|
336
337
|
}
|
|
337
338
|
}, { status: 400 }));
|
|
@@ -422,6 +423,7 @@ export class RoutingBackend {
|
|
|
422
423
|
error: {
|
|
423
424
|
type: "invalid_request_error",
|
|
424
425
|
code: "unsupported_reasoning_control",
|
|
426
|
+
param: requestedSelection.mode === "effort" ? "reasoning.effort" : "reasoning",
|
|
425
427
|
message: selection
|
|
426
428
|
}
|
|
427
429
|
}, { status: 400 }));
|
|
@@ -3,9 +3,38 @@ import { test } from "node:test";
|
|
|
3
3
|
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
4
4
|
import { anthropicToChat } from "../adapters/anthropic.js";
|
|
5
5
|
import { ANTHROPIC_MESSAGE_CONTENT, ANTHROPIC_REQUEST_METADATA, attachReasoningSelection, REASONING_SELECTION } from "../adapters/openai-chat-wire.js";
|
|
6
|
-
import {
|
|
6
|
+
import { responsesToChat } from "../adapters/responses.js";
|
|
7
|
+
import { anthropicMessages } from "../providers/anthropic-codec.js";
|
|
7
8
|
import { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "../providers/backends.js";
|
|
9
|
+
import { OpenAiBackend } from "../providers/openai-backend.js";
|
|
8
10
|
import { asTransport } from "./provider-backends-fixtures.js";
|
|
11
|
+
test("Responses JSON schemas reach Anthropic as native structured output formats", () => {
|
|
12
|
+
const schema = {
|
|
13
|
+
type: "object",
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
required: ["dimensions"],
|
|
16
|
+
properties: { dimensions: { type: "array" } }
|
|
17
|
+
};
|
|
18
|
+
const chat = responsesToChat({
|
|
19
|
+
model: "claude-code/claude-opus-5",
|
|
20
|
+
input: "propose dimensions",
|
|
21
|
+
text: {
|
|
22
|
+
format: {
|
|
23
|
+
type: "json_schema",
|
|
24
|
+
name: "routekit_routing_basis",
|
|
25
|
+
schema,
|
|
26
|
+
strict: true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}, "claude-opus-5");
|
|
30
|
+
const outbound = anthropicMessages(chat, "claude-opus-5");
|
|
31
|
+
assert.deepEqual(outbound.output_config, {
|
|
32
|
+
format: {
|
|
33
|
+
type: "json_schema",
|
|
34
|
+
schema
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
9
38
|
test("direct provider backends reject malformed reasoning controls before transport", async () => {
|
|
10
39
|
const cases = [
|
|
11
40
|
{
|
|
@@ -320,7 +320,9 @@ test("Responses routes discovered Claude efforts to adaptive Anthropic egress",
|
|
|
320
320
|
})
|
|
321
321
|
});
|
|
322
322
|
assert.equal(unsupported.status, 400);
|
|
323
|
-
|
|
323
|
+
const error = (await unsupported.json());
|
|
324
|
+
assert.equal(error.error?.param, "reasoning.effort");
|
|
325
|
+
assert.equal(error.error?.message, 'reasoning effort "max" is not supported by model "claude-code/claude-fable-5"');
|
|
324
326
|
assert.equal(requests.length, 1);
|
|
325
327
|
}
|
|
326
328
|
finally {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-gateway",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@aws-sdk/client-bedrock": "3.1095.0",
|
|
46
46
|
"@aws-sdk/client-bedrock-runtime": "3.1095.0",
|
|
47
47
|
"effect": "4.0.0-rc.108",
|
|
48
|
-
"@velum-labs/routekit-config-core": "1.0.
|
|
49
|
-
"@velum-labs/routekit-contracts": "1.0.
|
|
50
|
-
"@velum-labs/routekit-eval-
|
|
51
|
-
"@velum-labs/routekit-
|
|
52
|
-
"@velum-labs/routekit-
|
|
53
|
-
"@velum-labs/routekit-runtime": "1.0.
|
|
48
|
+
"@velum-labs/routekit-config-core": "1.0.3",
|
|
49
|
+
"@velum-labs/routekit-contracts": "1.0.3",
|
|
50
|
+
"@velum-labs/routekit-eval-contracts": "1.0.3",
|
|
51
|
+
"@velum-labs/routekit-eval-core": "1.0.3",
|
|
52
|
+
"@velum-labs/routekit-registry": "1.0.3",
|
|
53
|
+
"@velum-labs/routekit-runtime": "1.0.3"
|
|
54
54
|
},
|
|
55
55
|
"keywords": [
|
|
56
56
|
"routekit",
|