@velum-labs/routekit-gateway 1.0.7 → 1.0.8
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.
|
@@ -135,6 +135,114 @@ function anthropicToolChoice(choice, parallelToolCalls) {
|
|
|
135
135
|
? { type: "tool", name: fn.name, ...disableParallel }
|
|
136
136
|
: undefined;
|
|
137
137
|
}
|
|
138
|
+
const ANTHROPIC_SUPPORTED_SCHEMA_KEYWORDS = new Set([
|
|
139
|
+
"$defs",
|
|
140
|
+
"$ref",
|
|
141
|
+
"additionalProperties",
|
|
142
|
+
"allOf",
|
|
143
|
+
"anyOf",
|
|
144
|
+
"const",
|
|
145
|
+
"description",
|
|
146
|
+
"enum",
|
|
147
|
+
"format",
|
|
148
|
+
"items",
|
|
149
|
+
"minItems",
|
|
150
|
+
"oneOf",
|
|
151
|
+
"pattern",
|
|
152
|
+
"properties",
|
|
153
|
+
"required",
|
|
154
|
+
"title",
|
|
155
|
+
"type"
|
|
156
|
+
]);
|
|
157
|
+
const ANTHROPIC_SUPPORTED_STRING_FORMATS = new Set([
|
|
158
|
+
"date",
|
|
159
|
+
"date-time",
|
|
160
|
+
"duration",
|
|
161
|
+
"email",
|
|
162
|
+
"hostname",
|
|
163
|
+
"ipv4",
|
|
164
|
+
"ipv6",
|
|
165
|
+
"time",
|
|
166
|
+
"uri",
|
|
167
|
+
"uuid"
|
|
168
|
+
]);
|
|
169
|
+
function isRecord(value) {
|
|
170
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
171
|
+
}
|
|
172
|
+
function cloneSchemaValue(value) {
|
|
173
|
+
if (Array.isArray(value))
|
|
174
|
+
return value.map(cloneSchemaValue);
|
|
175
|
+
if (!isRecord(value))
|
|
176
|
+
return value;
|
|
177
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneSchemaValue(child)]));
|
|
178
|
+
}
|
|
179
|
+
function sanitizedSchemaMap(value) {
|
|
180
|
+
if (!isRecord(value))
|
|
181
|
+
return cloneSchemaValue(value);
|
|
182
|
+
return Object.fromEntries(Object.entries(value).map(([key, schema]) => [key, anthropicStructuredOutputSchema(schema)]));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Anthropic structured outputs accept only a subset of JSON Schema. Match the
|
|
186
|
+
* documented SDK behavior by retaining that subset, converting `oneOf` to
|
|
187
|
+
* `anyOf`, and moving omitted constraints into descriptions for model guidance.
|
|
188
|
+
* Callers remain responsible for validating the parsed response against the
|
|
189
|
+
* original schema.
|
|
190
|
+
*/
|
|
191
|
+
function anthropicStructuredOutputSchema(schema) {
|
|
192
|
+
if (!isRecord(schema))
|
|
193
|
+
return cloneSchemaValue(schema);
|
|
194
|
+
if (typeof schema.$ref === "string")
|
|
195
|
+
return { $ref: schema.$ref };
|
|
196
|
+
const sanitized = {};
|
|
197
|
+
const deferredConstraints = [];
|
|
198
|
+
let oneOf;
|
|
199
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
200
|
+
if (!ANTHROPIC_SUPPORTED_SCHEMA_KEYWORDS.has(key) ||
|
|
201
|
+
(key === "minItems" && value !== 0 && value !== 1) ||
|
|
202
|
+
(key === "format" &&
|
|
203
|
+
(typeof value !== "string" || !ANTHROPIC_SUPPORTED_STRING_FORMATS.has(value)))) {
|
|
204
|
+
deferredConstraints.push([key, cloneSchemaValue(value)]);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (key === "$defs" || key === "properties") {
|
|
208
|
+
sanitized[key] = sanitizedSchemaMap(value);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if ((key === "allOf" || key === "anyOf") && Array.isArray(value)) {
|
|
212
|
+
sanitized[key] = value.map(anthropicStructuredOutputSchema);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (key === "oneOf") {
|
|
216
|
+
oneOf = value;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (key === "additionalProperties" || key === "items") {
|
|
220
|
+
sanitized[key] = Array.isArray(value)
|
|
221
|
+
? value.map(anthropicStructuredOutputSchema)
|
|
222
|
+
: anthropicStructuredOutputSchema(value);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
sanitized[key] = cloneSchemaValue(value);
|
|
226
|
+
}
|
|
227
|
+
if (oneOf !== undefined) {
|
|
228
|
+
if (!Object.hasOwn(sanitized, "anyOf") && Array.isArray(oneOf)) {
|
|
229
|
+
sanitized.anyOf = oneOf.map(anthropicStructuredOutputSchema);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
deferredConstraints.push(["oneOf", cloneSchemaValue(oneOf)]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (deferredConstraints.length > 0) {
|
|
236
|
+
const constraintDescription = `{${deferredConstraints
|
|
237
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
|
238
|
+
.join(", ")}}`;
|
|
239
|
+
sanitized.description =
|
|
240
|
+
typeof sanitized.description === "string" && sanitized.description.length > 0
|
|
241
|
+
? `${sanitized.description}\n\n${constraintDescription}`
|
|
242
|
+
: constraintDescription;
|
|
243
|
+
}
|
|
244
|
+
return sanitized;
|
|
245
|
+
}
|
|
138
246
|
function anthropicStructuredOutputFormat(responseFormat) {
|
|
139
247
|
if (responseFormat?.type !== "json_schema" ||
|
|
140
248
|
typeof responseFormat.json_schema?.schema !== "object" ||
|
|
@@ -143,7 +251,22 @@ function anthropicStructuredOutputFormat(responseFormat) {
|
|
|
143
251
|
}
|
|
144
252
|
return {
|
|
145
253
|
type: "json_schema",
|
|
146
|
-
schema: responseFormat.json_schema.schema
|
|
254
|
+
schema: anthropicStructuredOutputSchema(responseFormat.json_schema.schema)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function sanitizedAnthropicOutputConfig(outputConfig) {
|
|
258
|
+
if (outputConfig === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
const format = outputConfig.format;
|
|
261
|
+
if (!isRecord(format) || format.type !== "json_schema" || !isRecord(format.schema)) {
|
|
262
|
+
return outputConfig;
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
...outputConfig,
|
|
266
|
+
format: {
|
|
267
|
+
...format,
|
|
268
|
+
schema: anthropicStructuredOutputSchema(format.schema)
|
|
269
|
+
}
|
|
147
270
|
};
|
|
148
271
|
}
|
|
149
272
|
export function anthropicMessages(body, model) {
|
|
@@ -211,7 +334,7 @@ export function anthropicMessages(body, model) {
|
|
|
211
334
|
const translatedOutput = selection.mode === "effort" ? { effort: selection.effort } : undefined;
|
|
212
335
|
const thinking = metadata?.thinking ?? translatedThinking;
|
|
213
336
|
const structuredOutputFormat = anthropicStructuredOutputFormat(body.response_format);
|
|
214
|
-
const outputConfig = metadata?.output_config != null
|
|
337
|
+
const outputConfig = sanitizedAnthropicOutputConfig(metadata?.output_config != null
|
|
215
338
|
? {
|
|
216
339
|
...metadata.output_config,
|
|
217
340
|
...(structuredOutputFormat !== undefined &&
|
|
@@ -224,7 +347,7 @@ export function anthropicMessages(body, model) {
|
|
|
224
347
|
...translatedOutput,
|
|
225
348
|
...(structuredOutputFormat !== undefined ? { format: structuredOutputFormat } : {})
|
|
226
349
|
}
|
|
227
|
-
: undefined;
|
|
350
|
+
: undefined);
|
|
228
351
|
const toolChoice = anthropicToolChoice(body.tool_choice, body.parallel_tool_calls);
|
|
229
352
|
return {
|
|
230
353
|
model,
|
|
@@ -246,7 +369,7 @@ export function anthropicMessages(body, model) {
|
|
|
246
369
|
{
|
|
247
370
|
name: tool.function.name,
|
|
248
371
|
description: tool.function.description,
|
|
249
|
-
input_schema: tool.function.parameters ?? { type: "object" }
|
|
372
|
+
input_schema: anthropicStructuredOutputSchema(tool.function.parameters ?? { type: "object" })
|
|
250
373
|
}
|
|
251
374
|
])
|
|
252
375
|
}
|
|
@@ -35,6 +35,177 @@ test("Responses JSON schemas reach Anthropic as native structured output formats
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
test("Anthropic structured outputs defer unsupported JSON Schema constraints", () => {
|
|
39
|
+
const schema = {
|
|
40
|
+
type: "object",
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
required: ["minimum", "scores", "labels"],
|
|
43
|
+
properties: {
|
|
44
|
+
minimum: { type: "integer", minimum: 1, maximum: 16_384 },
|
|
45
|
+
scores: {
|
|
46
|
+
type: "array",
|
|
47
|
+
minItems: 20,
|
|
48
|
+
maxItems: 20,
|
|
49
|
+
items: { type: "number", minimum: 0, maximum: 1 }
|
|
50
|
+
},
|
|
51
|
+
labels: {
|
|
52
|
+
type: "array",
|
|
53
|
+
minItems: 1,
|
|
54
|
+
items: { type: "string", minLength: 1, maxLength: 128 }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const original = structuredClone(schema);
|
|
59
|
+
const outbound = anthropicMessages({
|
|
60
|
+
model: "claude-opus-5",
|
|
61
|
+
messages: [{ role: "user", content: "author evaluations" }],
|
|
62
|
+
response_format: {
|
|
63
|
+
type: "json_schema",
|
|
64
|
+
json_schema: { name: "routekit_evaluations", schema, strict: true }
|
|
65
|
+
}
|
|
66
|
+
}, "claude-opus-5");
|
|
67
|
+
assert.deepEqual(schema, original);
|
|
68
|
+
assert.deepEqual(outbound.output_config, {
|
|
69
|
+
format: {
|
|
70
|
+
type: "json_schema",
|
|
71
|
+
schema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ["minimum", "scores", "labels"],
|
|
75
|
+
properties: {
|
|
76
|
+
minimum: {
|
|
77
|
+
type: "integer",
|
|
78
|
+
description: "{minimum: 1, maximum: 16384}"
|
|
79
|
+
},
|
|
80
|
+
scores: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: {
|
|
83
|
+
type: "number",
|
|
84
|
+
description: "{minimum: 0, maximum: 1}"
|
|
85
|
+
},
|
|
86
|
+
description: "{minItems: 20, maxItems: 20}"
|
|
87
|
+
},
|
|
88
|
+
labels: {
|
|
89
|
+
type: "array",
|
|
90
|
+
minItems: 1,
|
|
91
|
+
items: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "{minLength: 1, maxLength: 128}"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const nativeOutbound = anthropicMessages(anthropicToChat({
|
|
101
|
+
model: "claude-opus-5",
|
|
102
|
+
messages: [{ role: "user", content: "author evaluations" }],
|
|
103
|
+
output_config: {
|
|
104
|
+
format: { type: "json_schema", schema }
|
|
105
|
+
}
|
|
106
|
+
}, "claude-opus-5"), "claude-opus-5");
|
|
107
|
+
assert.deepEqual(nativeOutbound.output_config, outbound.output_config);
|
|
108
|
+
});
|
|
109
|
+
test("Anthropic tool input schemas defer unsupported JSON Schema constraints", () => {
|
|
110
|
+
const parameters = {
|
|
111
|
+
type: "object",
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
required: ["limit", "scores"],
|
|
114
|
+
properties: {
|
|
115
|
+
limit: { type: "integer", minimum: 1, maximum: 16_384 },
|
|
116
|
+
scores: {
|
|
117
|
+
type: "array",
|
|
118
|
+
minItems: 20,
|
|
119
|
+
maxItems: 20,
|
|
120
|
+
items: { type: "number", minimum: 0, maximum: 1 }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const original = structuredClone(parameters);
|
|
125
|
+
const outbound = anthropicMessages({
|
|
126
|
+
model: "claude-opus-5",
|
|
127
|
+
messages: [{ role: "user", content: "score the inputs" }],
|
|
128
|
+
tools: [
|
|
129
|
+
{
|
|
130
|
+
type: "function",
|
|
131
|
+
function: {
|
|
132
|
+
name: "score_inputs",
|
|
133
|
+
description: "score structured inputs",
|
|
134
|
+
parameters
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}, "claude-opus-5");
|
|
139
|
+
assert.deepEqual(parameters, original);
|
|
140
|
+
assert.deepEqual(outbound.tools, [
|
|
141
|
+
{
|
|
142
|
+
name: "score_inputs",
|
|
143
|
+
description: "score structured inputs",
|
|
144
|
+
input_schema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
additionalProperties: false,
|
|
147
|
+
required: ["limit", "scores"],
|
|
148
|
+
properties: {
|
|
149
|
+
limit: {
|
|
150
|
+
type: "integer",
|
|
151
|
+
description: "{minimum: 1, maximum: 16384}"
|
|
152
|
+
},
|
|
153
|
+
scores: {
|
|
154
|
+
type: "array",
|
|
155
|
+
items: {
|
|
156
|
+
type: "number",
|
|
157
|
+
description: "{minimum: 0, maximum: 1}"
|
|
158
|
+
},
|
|
159
|
+
description: "{minItems: 20, maxItems: 20}"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
test("Anthropic structured outputs keep only supported schema keywords and formats", () => {
|
|
167
|
+
const outbound = anthropicMessages({
|
|
168
|
+
model: "claude-opus-5",
|
|
169
|
+
messages: [{ role: "user", content: "return a structured value" }],
|
|
170
|
+
response_format: {
|
|
171
|
+
type: "json_schema",
|
|
172
|
+
json_schema: {
|
|
173
|
+
name: "routekit_supported_schema",
|
|
174
|
+
strict: true,
|
|
175
|
+
schema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
minProperties: 1,
|
|
178
|
+
properties: {
|
|
179
|
+
identifier: {
|
|
180
|
+
type: "string",
|
|
181
|
+
pattern: "^[a-z]+$",
|
|
182
|
+
format: "regex"
|
|
183
|
+
},
|
|
184
|
+
createdAt: {
|
|
185
|
+
type: "string",
|
|
186
|
+
format: "date-time"
|
|
187
|
+
},
|
|
188
|
+
choice: {
|
|
189
|
+
oneOf: [{ const: "one" }, { const: "two" }]
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
required: ["identifier", "createdAt", "choice"],
|
|
193
|
+
additionalProperties: false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}, "claude-opus-5");
|
|
198
|
+
const schema = outbound.output_config.format.schema;
|
|
199
|
+
const properties = schema.properties;
|
|
200
|
+
assert.equal(schema.minProperties, undefined);
|
|
201
|
+
assert.match(schema.description, /minProperties/u);
|
|
202
|
+
assert.equal(properties.identifier.format, undefined);
|
|
203
|
+
assert.equal(properties.identifier.pattern, "^[a-z]+$");
|
|
204
|
+
assert.match(properties.identifier.description, /format/u);
|
|
205
|
+
assert.equal(properties.createdAt.format, "date-time");
|
|
206
|
+
assert.equal(properties.choice.oneOf, undefined);
|
|
207
|
+
assert.deepEqual(properties.choice.anyOf, [{ const: "one" }, { const: "two" }]);
|
|
208
|
+
});
|
|
38
209
|
test("direct provider backends reject malformed reasoning controls before transport", async () => {
|
|
39
210
|
const cases = [
|
|
40
211
|
{
|
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.8",
|
|
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-contracts": "1.0.
|
|
51
|
-
"@velum-labs/routekit-eval-core": "1.0.
|
|
52
|
-
"@velum-labs/routekit-registry": "1.0.
|
|
53
|
-
"@velum-labs/routekit-runtime": "1.0.
|
|
48
|
+
"@velum-labs/routekit-config-core": "1.0.8",
|
|
49
|
+
"@velum-labs/routekit-contracts": "1.0.8",
|
|
50
|
+
"@velum-labs/routekit-eval-contracts": "1.0.8",
|
|
51
|
+
"@velum-labs/routekit-eval-core": "1.0.8",
|
|
52
|
+
"@velum-labs/routekit-registry": "1.0.8",
|
|
53
|
+
"@velum-labs/routekit-runtime": "1.0.8"
|
|
54
54
|
},
|
|
55
55
|
"keywords": [
|
|
56
56
|
"routekit",
|