@jaypie/llm 1.2.28 → 1.2.31
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/cjs/constants.d.ts +5 -5
- package/dist/cjs/index.cjs +668 -126
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +29 -2
- package/dist/cjs/operate/adapters/GeminiAdapter.d.ts +11 -1
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +32 -4
- package/dist/cjs/operate/adapters/XaiAdapter.d.ts +5 -2
- package/dist/esm/constants.d.ts +5 -5
- package/dist/esm/index.js +668 -126
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +29 -2
- package/dist/esm/operate/adapters/GeminiAdapter.d.ts +11 -1
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +32 -4
- package/dist/esm/operate/adapters/XaiAdapter.d.ts +5 -2
- package/package.json +2 -1
package/dist/esm/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const FIRST_CLASS_PROVIDER = {
|
|
|
29
29
|
// https://developers.openai.com/api/docs/models
|
|
30
30
|
OPENAI: {
|
|
31
31
|
DEFAULT: "gpt-5.4",
|
|
32
|
-
LARGE: "gpt-5.
|
|
32
|
+
LARGE: "gpt-5.5",
|
|
33
33
|
SMALL: "gpt-5.4-mini",
|
|
34
34
|
TINY: "gpt-5.4-nano",
|
|
35
35
|
},
|
|
@@ -584,9 +584,7 @@ function jsonSchemaToOpenApi3(schema) {
|
|
|
584
584
|
}
|
|
585
585
|
result[key] = convertedProps;
|
|
586
586
|
}
|
|
587
|
-
else if (key === "items" &&
|
|
588
|
-
typeof value === "object" &&
|
|
589
|
-
value !== null) {
|
|
587
|
+
else if (key === "items" && typeof value === "object" && value !== null) {
|
|
590
588
|
result[key] = jsonSchemaToOpenApi3(value);
|
|
591
589
|
}
|
|
592
590
|
else {
|
|
@@ -984,8 +982,140 @@ function isTransientNetworkError(error) {
|
|
|
984
982
|
// Constants
|
|
985
983
|
//
|
|
986
984
|
const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
|
|
985
|
+
const STRUCTURED_OUTPUT_NON_PARSE_STOP_REASONS = new Set([
|
|
986
|
+
"refusal",
|
|
987
|
+
"max_tokens",
|
|
988
|
+
]);
|
|
987
989
|
// Regular expression to parse data URLs: data:mime/type;base64,data
|
|
988
990
|
const DATA_URL_REGEX = /^data:([^;]+);base64,(.+)$/;
|
|
991
|
+
// String formats accepted by Anthropic's structured-output grammar compiler.
|
|
992
|
+
// Other formats are stripped (move to description) so the API does not 400.
|
|
993
|
+
const SUPPORTED_STRING_FORMATS = new Set([
|
|
994
|
+
"date",
|
|
995
|
+
"date-time",
|
|
996
|
+
"duration",
|
|
997
|
+
"email",
|
|
998
|
+
"hostname",
|
|
999
|
+
"ipv4",
|
|
1000
|
+
"ipv6",
|
|
1001
|
+
"time",
|
|
1002
|
+
"uri",
|
|
1003
|
+
"uuid",
|
|
1004
|
+
]);
|
|
1005
|
+
// Top-level keywords stripped wholesale before sending: not part of the spec
|
|
1006
|
+
// the API enforces and the validator can reject them.
|
|
1007
|
+
const STRIPPED_TOP_LEVEL_KEYWORDS = new Set(["$schema", "$id"]);
|
|
1008
|
+
// Keywords Anthropic's structured-output grammar does not support. They are
|
|
1009
|
+
// removed from the schema and appended to `description` so the model still
|
|
1010
|
+
// sees the intent.
|
|
1011
|
+
const UNSUPPORTED_CONSTRAINT_KEYWORDS = new Set([
|
|
1012
|
+
"exclusiveMaximum",
|
|
1013
|
+
"exclusiveMinimum",
|
|
1014
|
+
"maxItems",
|
|
1015
|
+
"maxLength",
|
|
1016
|
+
"maxProperties",
|
|
1017
|
+
"maximum",
|
|
1018
|
+
"minLength",
|
|
1019
|
+
"minProperties",
|
|
1020
|
+
"minimum",
|
|
1021
|
+
"multipleOf",
|
|
1022
|
+
"pattern",
|
|
1023
|
+
"patternProperties",
|
|
1024
|
+
"uniqueItems",
|
|
1025
|
+
]);
|
|
1026
|
+
/**
|
|
1027
|
+
* Recursively transform a JSON Schema into the strict shape Anthropic's
|
|
1028
|
+
* structured-output grammar accepts: object types must have
|
|
1029
|
+
* `additionalProperties: false`, unsupported numeric/string/array
|
|
1030
|
+
* constraints are appended to `description`, and unsupported string
|
|
1031
|
+
* formats are stripped. Mirrors @anthropic-ai/sdk's `transformJSONSchema`
|
|
1032
|
+
* but inline so we do not take a runtime dependency on the optional
|
|
1033
|
+
* peer SDK.
|
|
1034
|
+
*/
|
|
1035
|
+
function sanitizeJsonSchemaForAnthropic(schema, isRoot = true) {
|
|
1036
|
+
const result = {};
|
|
1037
|
+
const carriedConstraints = [];
|
|
1038
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1039
|
+
if (isRoot && STRIPPED_TOP_LEVEL_KEYWORDS.has(key)) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (UNSUPPORTED_CONSTRAINT_KEYWORDS.has(key)) {
|
|
1043
|
+
carriedConstraints.push(`${key}: ${JSON.stringify(value)}`);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (key === "format" && typeof value === "string") {
|
|
1047
|
+
if (SUPPORTED_STRING_FORMATS.has(value)) {
|
|
1048
|
+
result[key] = value;
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
carriedConstraints.push(`format: ${JSON.stringify(value)}`);
|
|
1052
|
+
}
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (key === "minItems" && typeof value === "number") {
|
|
1056
|
+
if (value === 0 || value === 1) {
|
|
1057
|
+
result[key] = value;
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
carriedConstraints.push(`minItems: ${value}`);
|
|
1061
|
+
}
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (key === "properties" &&
|
|
1065
|
+
value &&
|
|
1066
|
+
typeof value === "object" &&
|
|
1067
|
+
!Array.isArray(value)) {
|
|
1068
|
+
const transformedProps = {};
|
|
1069
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
1070
|
+
transformedProps[propName] = isJsonSchema(propSchema)
|
|
1071
|
+
? sanitizeJsonSchemaForAnthropic(propSchema, false)
|
|
1072
|
+
: propSchema;
|
|
1073
|
+
}
|
|
1074
|
+
result[key] = transformedProps;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if ((key === "items" || key === "additionalItems" || key === "contains") &&
|
|
1078
|
+
isJsonSchema(value)) {
|
|
1079
|
+
result[key] = sanitizeJsonSchemaForAnthropic(value, false);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if ((key === "anyOf" || key === "oneOf" || key === "allOf") &&
|
|
1083
|
+
Array.isArray(value)) {
|
|
1084
|
+
const targetKey = key === "oneOf" ? "anyOf" : key;
|
|
1085
|
+
result[targetKey] = value.map((entry) => isJsonSchema(entry)
|
|
1086
|
+
? sanitizeJsonSchemaForAnthropic(entry, false)
|
|
1087
|
+
: entry);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (key === "$defs" && value && typeof value === "object") {
|
|
1091
|
+
const transformedDefs = {};
|
|
1092
|
+
for (const [defName, defSchema] of Object.entries(value)) {
|
|
1093
|
+
transformedDefs[defName] = isJsonSchema(defSchema)
|
|
1094
|
+
? sanitizeJsonSchemaForAnthropic(defSchema, false)
|
|
1095
|
+
: defSchema;
|
|
1096
|
+
}
|
|
1097
|
+
result[key] = transformedDefs;
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (key === "additionalProperties") {
|
|
1101
|
+
// Always force `false` on objects below; ignore caller-supplied value.
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
result[key] = value;
|
|
1105
|
+
}
|
|
1106
|
+
if (result.type === "object") {
|
|
1107
|
+
result.additionalProperties = false;
|
|
1108
|
+
}
|
|
1109
|
+
if (carriedConstraints.length > 0) {
|
|
1110
|
+
const existing = typeof result.description === "string" ? result.description : "";
|
|
1111
|
+
const suffix = `{${carriedConstraints.join(", ")}}`;
|
|
1112
|
+
result.description = existing ? `${existing}\n\n${suffix}` : suffix;
|
|
1113
|
+
}
|
|
1114
|
+
return result;
|
|
1115
|
+
}
|
|
1116
|
+
function isJsonSchema(value) {
|
|
1117
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1118
|
+
}
|
|
989
1119
|
/**
|
|
990
1120
|
* Parse a data URL into its components
|
|
991
1121
|
*/
|
|
@@ -1081,6 +1211,29 @@ function isTemperatureDeprecationError(error) {
|
|
|
1081
1211
|
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
1082
1212
|
return messages.some((m) => m.toLowerCase().includes("temperature"));
|
|
1083
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Detect 400 errors that indicate the model itself does not support native
|
|
1216
|
+
* structured outputs (`output_config.format`). Citations + structured output
|
|
1217
|
+
* is also a 400 case but is a caller error rather than a model-capability
|
|
1218
|
+
* gap, so we explicitly skip it to avoid masking the real problem under a
|
|
1219
|
+
* tool-emulation retry. The deprecated-`output_format` 400 (API renamed the
|
|
1220
|
+
* field) is also explicitly excluded — that's a code-path bug, not a model
|
|
1221
|
+
* gap; it should propagate so we notice and fix it.
|
|
1222
|
+
*/
|
|
1223
|
+
function isStructuredOutputUnsupportedError$1(error) {
|
|
1224
|
+
if (!error || typeof error !== "object")
|
|
1225
|
+
return false;
|
|
1226
|
+
const err = error;
|
|
1227
|
+
const name = error?.constructor?.name;
|
|
1228
|
+
if (name !== "BadRequestError" && err.status !== 400)
|
|
1229
|
+
return false;
|
|
1230
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
1231
|
+
if (messages.some((m) => /citation/i.test(m)))
|
|
1232
|
+
return false;
|
|
1233
|
+
if (messages.some((m) => /deprecated/i.test(m)))
|
|
1234
|
+
return false;
|
|
1235
|
+
return messages.some((m) => /output_config|output_format|json[_ ]schema|structured/i.test(m));
|
|
1236
|
+
}
|
|
1084
1237
|
//
|
|
1085
1238
|
//
|
|
1086
1239
|
// Main
|
|
@@ -1098,6 +1251,10 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1098
1251
|
// Session-level cache of models observed to reject `temperature` at runtime.
|
|
1099
1252
|
// Populated by executeRequest on 400 errors so repeat calls skip the param.
|
|
1100
1253
|
this.runtimeNoTemperatureModels = new Set();
|
|
1254
|
+
// Session-level cache of models observed to reject `output_format`. When a
|
|
1255
|
+
// model is in this set, buildRequest engages the legacy fake-tool path
|
|
1256
|
+
// instead of native structured output.
|
|
1257
|
+
this.runtimeNoStructuredOutputModels = new Set();
|
|
1101
1258
|
}
|
|
1102
1259
|
rememberModelRejectsTemperature(model) {
|
|
1103
1260
|
this.runtimeNoTemperatureModels.add(model);
|
|
@@ -1105,11 +1262,20 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1105
1262
|
clearRuntimeNoTemperatureModels() {
|
|
1106
1263
|
this.runtimeNoTemperatureModels.clear();
|
|
1107
1264
|
}
|
|
1265
|
+
rememberModelRejectsStructuredOutput(model) {
|
|
1266
|
+
this.runtimeNoStructuredOutputModels.add(model);
|
|
1267
|
+
}
|
|
1268
|
+
clearRuntimeNoStructuredOutputModels() {
|
|
1269
|
+
this.runtimeNoStructuredOutputModels.clear();
|
|
1270
|
+
}
|
|
1108
1271
|
supportsTemperature(model) {
|
|
1109
1272
|
if (this.runtimeNoTemperatureModels.has(model))
|
|
1110
1273
|
return false;
|
|
1111
1274
|
return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
|
|
1112
1275
|
}
|
|
1276
|
+
supportsStructuredOutput(model) {
|
|
1277
|
+
return !this.runtimeNoStructuredOutputModels.has(model);
|
|
1278
|
+
}
|
|
1113
1279
|
//
|
|
1114
1280
|
// Request Building
|
|
1115
1281
|
//
|
|
@@ -1177,8 +1343,22 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1177
1343
|
if (request.system) {
|
|
1178
1344
|
anthropicRequest.system = request.system;
|
|
1179
1345
|
}
|
|
1180
|
-
|
|
1181
|
-
anthropicRequest.
|
|
1346
|
+
const useFallbackStructuredOutput = Boolean(request.format) &&
|
|
1347
|
+
!this.supportsStructuredOutput(anthropicRequest.model);
|
|
1348
|
+
const allTools = request.tools
|
|
1349
|
+
? [...request.tools]
|
|
1350
|
+
: [];
|
|
1351
|
+
if (useFallbackStructuredOutput && request.format) {
|
|
1352
|
+
log$2.warn(`[AnthropicAdapter] Using legacy structured_output tool fallback for model ${anthropicRequest.model}; native output_config previously rejected for this model.`);
|
|
1353
|
+
allTools.push({
|
|
1354
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
1355
|
+
description: "Output a structured JSON object, " +
|
|
1356
|
+
"use this before your final response to give structured outputs to the user",
|
|
1357
|
+
parameters: request.format,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
if (allTools.length > 0) {
|
|
1361
|
+
anthropicRequest.tools = allTools.map((tool) => ({
|
|
1182
1362
|
name: tool.name,
|
|
1183
1363
|
description: tool.description,
|
|
1184
1364
|
input_schema: {
|
|
@@ -1187,10 +1367,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1187
1367
|
},
|
|
1188
1368
|
type: "custom",
|
|
1189
1369
|
}));
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1370
|
+
anthropicRequest.tool_choice = useFallbackStructuredOutput
|
|
1371
|
+
? { type: "any" }
|
|
1372
|
+
: { type: "auto" };
|
|
1373
|
+
}
|
|
1374
|
+
// Native structured output: send schema as `output_config.format`. The
|
|
1375
|
+
// legacy tool-emulation path is engaged only as a runtime fallback for
|
|
1376
|
+
// models the API has flagged as not supporting native structured output.
|
|
1377
|
+
if (request.format && !useFallbackStructuredOutput) {
|
|
1378
|
+
anthropicRequest.output_config = {
|
|
1379
|
+
format: {
|
|
1380
|
+
type: "json_schema",
|
|
1381
|
+
schema: request.format,
|
|
1382
|
+
},
|
|
1194
1383
|
};
|
|
1195
1384
|
}
|
|
1196
1385
|
if (request.providerOptions) {
|
|
@@ -1207,8 +1396,13 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1207
1396
|
}
|
|
1208
1397
|
return anthropicRequest;
|
|
1209
1398
|
}
|
|
1210
|
-
formatTools(toolkit,
|
|
1211
|
-
|
|
1399
|
+
formatTools(toolkit,
|
|
1400
|
+
// outputSchema is part of the interface contract but Anthropic now uses
|
|
1401
|
+
// native `output_format` (set in buildRequest), so we no longer inject a
|
|
1402
|
+
// synthetic structured-output tool here.
|
|
1403
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1404
|
+
_outputSchema) {
|
|
1405
|
+
return toolkit.tools.map((tool) => ({
|
|
1212
1406
|
name: tool.name,
|
|
1213
1407
|
description: tool.description,
|
|
1214
1408
|
parameters: {
|
|
@@ -1216,16 +1410,6 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1216
1410
|
type: "object",
|
|
1217
1411
|
},
|
|
1218
1412
|
}));
|
|
1219
|
-
// Add structured output tool if schema is provided
|
|
1220
|
-
if (outputSchema) {
|
|
1221
|
-
tools.push({
|
|
1222
|
-
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
1223
|
-
description: "Output a structured JSON object, " +
|
|
1224
|
-
"use this before your final response to give structured outputs to the user",
|
|
1225
|
-
parameters: outputSchema,
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
return tools;
|
|
1229
1413
|
}
|
|
1230
1414
|
formatOutputSchema(schema) {
|
|
1231
1415
|
let jsonSchema;
|
|
@@ -1244,11 +1428,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1244
1428
|
: naturalZodSchema(schema);
|
|
1245
1429
|
jsonSchema = z.toJSONSchema(zodSchema);
|
|
1246
1430
|
}
|
|
1247
|
-
|
|
1248
|
-
if (jsonSchema.$schema) {
|
|
1249
|
-
delete jsonSchema.$schema;
|
|
1250
|
-
}
|
|
1251
|
-
return jsonSchema;
|
|
1431
|
+
return sanitizeJsonSchemaForAnthropic(jsonSchema);
|
|
1252
1432
|
}
|
|
1253
1433
|
//
|
|
1254
1434
|
// API Execution
|
|
@@ -1256,8 +1436,13 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1256
1436
|
async executeRequest(client, request, signal) {
|
|
1257
1437
|
const anthropic = client;
|
|
1258
1438
|
const anthropicRequest = request;
|
|
1439
|
+
const wantsStructuredOutput = Boolean(anthropicRequest.output_config);
|
|
1259
1440
|
try {
|
|
1260
|
-
|
|
1441
|
+
const response = (await anthropic.messages.create(anthropicRequest, signal ? { signal } : undefined));
|
|
1442
|
+
if (wantsStructuredOutput) {
|
|
1443
|
+
response.__jaypieStructuredOutput = true;
|
|
1444
|
+
}
|
|
1445
|
+
return response;
|
|
1261
1446
|
}
|
|
1262
1447
|
catch (error) {
|
|
1263
1448
|
if (signal?.aborted)
|
|
@@ -1268,13 +1453,53 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1268
1453
|
this.rememberModelRejectsTemperature(anthropicRequest.model);
|
|
1269
1454
|
const retryRequest = { ...anthropicRequest };
|
|
1270
1455
|
delete retryRequest.temperature;
|
|
1271
|
-
|
|
1456
|
+
const response = (await anthropic.messages.create(retryRequest, signal ? { signal } : undefined));
|
|
1457
|
+
if (wantsStructuredOutput) {
|
|
1458
|
+
response.__jaypieStructuredOutput = true;
|
|
1459
|
+
}
|
|
1460
|
+
return response;
|
|
1461
|
+
}
|
|
1462
|
+
// If the model rejected native structured output, cache it and retry
|
|
1463
|
+
// via the legacy fake-tool emulation path.
|
|
1464
|
+
if (wantsStructuredOutput && isStructuredOutputUnsupportedError$1(error)) {
|
|
1465
|
+
const model = anthropicRequest.model;
|
|
1466
|
+
this.rememberModelRejectsStructuredOutput(model);
|
|
1467
|
+
log$2.warn(`[AnthropicAdapter] Model ${model} rejected native output_config; falling back to legacy structured_output tool emulation.`);
|
|
1468
|
+
const fallbackRequest = this.toFallbackStructuredOutputRequest(anthropicRequest);
|
|
1469
|
+
return (await anthropic.messages.create(fallbackRequest, signal ? { signal } : undefined));
|
|
1272
1470
|
}
|
|
1273
1471
|
throw error;
|
|
1274
1472
|
}
|
|
1275
1473
|
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Rebuild a structured-output request without `output_format`, swapping in
|
|
1476
|
+
* the legacy fake-tool emulation. Used as a runtime fallback when a model
|
|
1477
|
+
* rejects native `output_config.format`.
|
|
1478
|
+
*/
|
|
1479
|
+
toFallbackStructuredOutputRequest(request) {
|
|
1480
|
+
const { output_config, ...rest } = request;
|
|
1481
|
+
if (!output_config)
|
|
1482
|
+
return request;
|
|
1483
|
+
const fallbackRequest = { ...rest };
|
|
1484
|
+
const fakeTool = {
|
|
1485
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
1486
|
+
description: "Output a structured JSON object, " +
|
|
1487
|
+
"use this before your final response to give structured outputs to the user",
|
|
1488
|
+
input_schema: {
|
|
1489
|
+
...output_config.format.schema,
|
|
1490
|
+
type: "object",
|
|
1491
|
+
},
|
|
1492
|
+
type: "custom",
|
|
1493
|
+
};
|
|
1494
|
+
fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
|
|
1495
|
+
fallbackRequest.tool_choice = { type: "any" };
|
|
1496
|
+
return fallbackRequest;
|
|
1497
|
+
}
|
|
1276
1498
|
async *executeStreamRequest(client, request, signal) {
|
|
1277
1499
|
const anthropic = client;
|
|
1500
|
+
// Preserve `output_config` when passing through to the SDK by typing
|
|
1501
|
+
// through the local extension instead of the upstream
|
|
1502
|
+
// MessageCreateParams shape.
|
|
1278
1503
|
let streamRequest = {
|
|
1279
1504
|
...request,
|
|
1280
1505
|
stream: true,
|
|
@@ -1534,13 +1759,39 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1534
1759
|
}
|
|
1535
1760
|
hasStructuredOutput(response) {
|
|
1536
1761
|
const anthropicResponse = response;
|
|
1537
|
-
//
|
|
1762
|
+
// Native path: executeRequest annotates the response when we sent
|
|
1763
|
+
// `output_format`, so we can detect intent statelessly.
|
|
1764
|
+
if (anthropicResponse.__jaypieStructuredOutput) {
|
|
1765
|
+
return this.extractStructuredOutput(response) !== undefined;
|
|
1766
|
+
}
|
|
1767
|
+
// Fallback path: legacy fake-tool emulation, kept for models that the
|
|
1768
|
+
// runtime has cached as not supporting `output_format`.
|
|
1538
1769
|
const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
|
|
1539
1770
|
return (lastBlock?.type === "tool_use" &&
|
|
1540
1771
|
lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
|
|
1541
1772
|
}
|
|
1542
1773
|
extractStructuredOutput(response) {
|
|
1543
1774
|
const anthropicResponse = response;
|
|
1775
|
+
if (anthropicResponse.__jaypieStructuredOutput) {
|
|
1776
|
+
// Refusal and truncation are explicit non-JSON outcomes per Anthropic
|
|
1777
|
+
// structured-outputs docs — surface the text upstream instead of
|
|
1778
|
+
// forcing a JSON.parse on what is not JSON.
|
|
1779
|
+
if (anthropicResponse.stop_reason &&
|
|
1780
|
+
STRUCTURED_OUTPUT_NON_PARSE_STOP_REASONS.has(anthropicResponse.stop_reason)) {
|
|
1781
|
+
return undefined;
|
|
1782
|
+
}
|
|
1783
|
+
const textBlock = anthropicResponse.content.find((block) => block.type === "text");
|
|
1784
|
+
if (!textBlock)
|
|
1785
|
+
return undefined;
|
|
1786
|
+
try {
|
|
1787
|
+
const parsed = JSON.parse(textBlock.text);
|
|
1788
|
+
return parsed;
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
return undefined;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
// Fallback path: legacy fake-tool emulation
|
|
1544
1795
|
const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
|
|
1545
1796
|
if (lastBlock?.type === "tool_use" &&
|
|
1546
1797
|
lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2) {
|
|
@@ -1569,6 +1820,26 @@ const anthropicAdapter = new AnthropicAdapter();
|
|
|
1569
1820
|
// Constants
|
|
1570
1821
|
//
|
|
1571
1822
|
const STRUCTURED_OUTPUT_TOOL_NAME$1 = "structured_output";
|
|
1823
|
+
// Gemini 3 family supports combining tools (function calling) with native
|
|
1824
|
+
// structured output via `responseJsonSchema`. Earlier Gemini families
|
|
1825
|
+
// (including 2.5 thinking) do not support the combo and fall back to the
|
|
1826
|
+
// legacy `structured_output` fake-tool emulation.
|
|
1827
|
+
const GEMINI_3_PATTERN = /^gemini-3/;
|
|
1828
|
+
/**
|
|
1829
|
+
* Detect 4xx errors that indicate the model itself does not support the
|
|
1830
|
+
* `responseJsonSchema` + tools combo. Triggers the runtime fallback to the
|
|
1831
|
+
* fake-tool emulation path. Other 400s propagate.
|
|
1832
|
+
*/
|
|
1833
|
+
function isStructuredOutputComboUnsupportedError(error) {
|
|
1834
|
+
if (!error || typeof error !== "object")
|
|
1835
|
+
return false;
|
|
1836
|
+
const err = error;
|
|
1837
|
+
const status = err.status ?? err.code;
|
|
1838
|
+
if (status !== 400)
|
|
1839
|
+
return false;
|
|
1840
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
1841
|
+
return messages.some((m) => /response[_ ]?json[_ ]?schema|response[_ ]?schema|response[_ ]?mime|function[_ ]?call|tools/i.test(m));
|
|
1842
|
+
}
|
|
1572
1843
|
// Gemini uses HTTP status codes for error classification
|
|
1573
1844
|
// Documented at: https://ai.google.dev/api/rest/v1beta/Status
|
|
1574
1845
|
const RETRYABLE_STATUS_CODES$1 = [
|
|
@@ -1601,6 +1872,21 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1601
1872
|
super(...arguments);
|
|
1602
1873
|
this.name = PROVIDER.GEMINI.NAME;
|
|
1603
1874
|
this.defaultModel = PROVIDER.GEMINI.MODEL.DEFAULT;
|
|
1875
|
+
// Session-level cache of Gemini 3 models observed to reject the native
|
|
1876
|
+
// `responseJsonSchema` + tools combo. When a model is in this set,
|
|
1877
|
+
// buildRequest engages the legacy fake-tool path instead.
|
|
1878
|
+
this.runtimeNoStructuredOutputComboModels = new Set();
|
|
1879
|
+
}
|
|
1880
|
+
rememberModelRejectsStructuredOutputCombo(model) {
|
|
1881
|
+
this.runtimeNoStructuredOutputComboModels.add(model);
|
|
1882
|
+
}
|
|
1883
|
+
clearRuntimeNoStructuredOutputComboModels() {
|
|
1884
|
+
this.runtimeNoStructuredOutputComboModels.clear();
|
|
1885
|
+
}
|
|
1886
|
+
supportsStructuredOutputCombo(model) {
|
|
1887
|
+
if (this.runtimeNoStructuredOutputComboModels.has(model))
|
|
1888
|
+
return false;
|
|
1889
|
+
return GEMINI_3_PATTERN.test(model);
|
|
1604
1890
|
}
|
|
1605
1891
|
//
|
|
1606
1892
|
// Request Building
|
|
@@ -1629,9 +1915,27 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1629
1915
|
}
|
|
1630
1916
|
}
|
|
1631
1917
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1918
|
+
const hasUserTools = !!(request.tools && request.tools.length > 0);
|
|
1919
|
+
const useNativeCombo = Boolean(request.format) &&
|
|
1920
|
+
hasUserTools &&
|
|
1921
|
+
this.supportsStructuredOutputCombo(geminiRequest.model);
|
|
1922
|
+
// When tools+format are combined and the model does not support the native
|
|
1923
|
+
// combo, inject the legacy `structured_output` fake tool here so the model
|
|
1924
|
+
// is forced to call it before its final answer.
|
|
1925
|
+
const allTools = request.tools
|
|
1926
|
+
? [...request.tools]
|
|
1927
|
+
: [];
|
|
1928
|
+
if (request.format && hasUserTools && !useNativeCombo) {
|
|
1929
|
+
log$2.warn(`[GeminiAdapter] Using legacy structured_output tool fallback for model ${geminiRequest.model}; native responseJsonSchema + tools combo is only available on Gemini 3.`);
|
|
1930
|
+
allTools.push({
|
|
1931
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
1932
|
+
description: "Output a structured JSON object, " +
|
|
1933
|
+
"use this before your final response to give structured outputs to the user",
|
|
1934
|
+
parameters: request.format,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
if (allTools.length > 0) {
|
|
1938
|
+
const functionDeclarations = allTools.map((tool) => ({
|
|
1635
1939
|
name: tool.name,
|
|
1636
1940
|
description: tool.description,
|
|
1637
1941
|
parameters: tool.parameters,
|
|
@@ -1641,11 +1945,13 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1641
1945
|
tools: [{ functionDeclarations }],
|
|
1642
1946
|
};
|
|
1643
1947
|
}
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
|
|
1648
|
-
|
|
1948
|
+
// Native structured output: send schema as `responseJsonSchema`
|
|
1949
|
+
// (or `responseSchema` for Gemini 2.5+ no-tools path). The legacy
|
|
1950
|
+
// fake-tool emulation only runs when format+tools is combined on a model
|
|
1951
|
+
// that doesn't support the native combo.
|
|
1952
|
+
const wantsNativeStructured = Boolean(request.format) && (!hasUserTools || useNativeCombo);
|
|
1953
|
+
if (wantsNativeStructured) {
|
|
1954
|
+
const useJsonSchema = useNativeCombo || request.providerOptions?.useJsonSchema === true;
|
|
1649
1955
|
if (useJsonSchema) {
|
|
1650
1956
|
geminiRequest.config = {
|
|
1651
1957
|
...geminiRequest.config,
|
|
@@ -1661,11 +1967,11 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1661
1967
|
};
|
|
1662
1968
|
}
|
|
1663
1969
|
}
|
|
1664
|
-
//
|
|
1665
|
-
|
|
1970
|
+
// Legacy fake-tool path needs a system-prompt nudge so the model actually
|
|
1971
|
+
// calls the synthetic tool before its final answer.
|
|
1972
|
+
if (request.format && hasUserTools && !useNativeCombo) {
|
|
1666
1973
|
const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
|
|
1667
1974
|
"to output your answer in the required JSON format.";
|
|
1668
|
-
// Add to system instruction if it exists, otherwise create one
|
|
1669
1975
|
const existingSystem = geminiRequest.config?.systemInstruction || "";
|
|
1670
1976
|
geminiRequest.config = {
|
|
1671
1977
|
...geminiRequest.config,
|
|
@@ -1690,8 +1996,14 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1690
1996
|
}
|
|
1691
1997
|
return geminiRequest;
|
|
1692
1998
|
}
|
|
1693
|
-
formatTools(toolkit,
|
|
1694
|
-
|
|
1999
|
+
formatTools(toolkit,
|
|
2000
|
+
// outputSchema is part of the interface contract but Gemini now handles
|
|
2001
|
+
// structured output via `responseJsonSchema`/`responseSchema` (or the
|
|
2002
|
+
// legacy fake-tool injected in buildRequest as a fallback). We no longer
|
|
2003
|
+
// inject a synthetic structured-output tool here.
|
|
2004
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2005
|
+
_outputSchema) {
|
|
2006
|
+
return toolkit.tools.map((tool) => ({
|
|
1695
2007
|
name: tool.name,
|
|
1696
2008
|
description: tool.description,
|
|
1697
2009
|
parameters: {
|
|
@@ -1699,16 +2011,6 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1699
2011
|
type: "object",
|
|
1700
2012
|
},
|
|
1701
2013
|
}));
|
|
1702
|
-
// Add structured output tool if schema is provided
|
|
1703
|
-
if (outputSchema) {
|
|
1704
|
-
tools.push({
|
|
1705
|
-
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
1706
|
-
description: "Output a structured JSON object, " +
|
|
1707
|
-
"use this before your final response to give structured outputs to the user",
|
|
1708
|
-
parameters: outputSchema,
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
return tools;
|
|
1712
2014
|
}
|
|
1713
2015
|
formatOutputSchema(schema) {
|
|
1714
2016
|
let jsonSchema;
|
|
@@ -1735,6 +2037,8 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1735
2037
|
async executeRequest(client, request, signal) {
|
|
1736
2038
|
const genAI = client;
|
|
1737
2039
|
const geminiRequest = request;
|
|
2040
|
+
const wantsNativeCombo = !!geminiRequest.config?.responseJsonSchema &&
|
|
2041
|
+
!!geminiRequest.config?.tools;
|
|
1738
2042
|
try {
|
|
1739
2043
|
// Cast config to any to bypass strict type checking between our internal types
|
|
1740
2044
|
// and the SDK's types. The SDK will validate at runtime.
|
|
@@ -1748,9 +2052,56 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1748
2052
|
catch (error) {
|
|
1749
2053
|
if (signal?.aborted)
|
|
1750
2054
|
return undefined;
|
|
2055
|
+
// If the model rejected the native responseJsonSchema + tools combo,
|
|
2056
|
+
// cache it and retry with the legacy fake-tool emulation path.
|
|
2057
|
+
if (wantsNativeCombo && isStructuredOutputComboUnsupportedError(error)) {
|
|
2058
|
+
const model = geminiRequest.model;
|
|
2059
|
+
this.rememberModelRejectsStructuredOutputCombo(model);
|
|
2060
|
+
log$2.warn(`[GeminiAdapter] Model ${model} rejected native responseJsonSchema + tools combo; falling back to legacy structured_output tool emulation.`);
|
|
2061
|
+
const fallbackRequest = this.toFallbackStructuredOutputRequest(geminiRequest);
|
|
2062
|
+
const response = await genAI.models.generateContent({
|
|
2063
|
+
model: fallbackRequest.model,
|
|
2064
|
+
contents: fallbackRequest.contents,
|
|
2065
|
+
config: fallbackRequest.config,
|
|
2066
|
+
});
|
|
2067
|
+
return response;
|
|
2068
|
+
}
|
|
1751
2069
|
throw error;
|
|
1752
2070
|
}
|
|
1753
2071
|
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Rebuild a Gemini 3 native-combo request without `responseJsonSchema`/
|
|
2074
|
+
* `responseMimeType`, swapping in the legacy fake-tool emulation. Used as
|
|
2075
|
+
* a runtime fallback when a Gemini 3 model rejects the combo.
|
|
2076
|
+
*/
|
|
2077
|
+
toFallbackStructuredOutputRequest(request) {
|
|
2078
|
+
if (!request.config?.responseJsonSchema)
|
|
2079
|
+
return request;
|
|
2080
|
+
const schema = request.config.responseJsonSchema;
|
|
2081
|
+
const newConfig = { ...request.config };
|
|
2082
|
+
delete newConfig.responseJsonSchema;
|
|
2083
|
+
delete newConfig.responseMimeType;
|
|
2084
|
+
const fakeTool = {
|
|
2085
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
2086
|
+
description: "Output a structured JSON object, " +
|
|
2087
|
+
"use this before your final response to give structured outputs to the user",
|
|
2088
|
+
parameters: schema,
|
|
2089
|
+
};
|
|
2090
|
+
const existingDeclarations = newConfig.tools?.[0]?.functionDeclarations ?? [];
|
|
2091
|
+
newConfig.tools = [
|
|
2092
|
+
{ functionDeclarations: [...existingDeclarations, fakeTool] },
|
|
2093
|
+
];
|
|
2094
|
+
const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
|
|
2095
|
+
"to output your answer in the required JSON format.";
|
|
2096
|
+
const existingSystem = newConfig.systemInstruction || "";
|
|
2097
|
+
newConfig.systemInstruction = existingSystem
|
|
2098
|
+
? `${existingSystem}\n\n${structuredOutputInstruction}`
|
|
2099
|
+
: structuredOutputInstruction;
|
|
2100
|
+
return {
|
|
2101
|
+
...request,
|
|
2102
|
+
config: newConfig,
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
1754
2105
|
async *executeStreamRequest(client, request, signal) {
|
|
1755
2106
|
const genAI = client;
|
|
1756
2107
|
const geminiRequest = request;
|
|
@@ -2751,6 +3102,23 @@ const openAiAdapter = new OpenAiAdapter();
|
|
|
2751
3102
|
// Constants
|
|
2752
3103
|
//
|
|
2753
3104
|
const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
|
|
3105
|
+
const STRUCTURED_OUTPUT_SCHEMA_NAME = "response";
|
|
3106
|
+
/**
|
|
3107
|
+
* Detect 4xx errors that indicate the model itself does not support
|
|
3108
|
+
* `response_format: json_schema`. Mirrors the Anthropic pattern: we only
|
|
3109
|
+
* trigger the fake-tool fallback when the failure is plausibly a capability
|
|
3110
|
+
* gap, not a generic 400.
|
|
3111
|
+
*/
|
|
3112
|
+
function isStructuredOutputUnsupportedError(error) {
|
|
3113
|
+
if (!error || typeof error !== "object")
|
|
3114
|
+
return false;
|
|
3115
|
+
const err = error;
|
|
3116
|
+
const status = err.status ?? err.statusCode;
|
|
3117
|
+
if (status !== 400 && status !== 422)
|
|
3118
|
+
return false;
|
|
3119
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
3120
|
+
return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
|
|
3121
|
+
}
|
|
2754
3122
|
/**
|
|
2755
3123
|
* Convert standardized content items to OpenRouter format
|
|
2756
3124
|
* Note: OpenRouter does not support native file/image uploads.
|
|
@@ -2789,6 +3157,32 @@ function convertContentToOpenRouter(content) {
|
|
|
2789
3157
|
// OpenRouter SDK error types based on HTTP status codes
|
|
2790
3158
|
const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
|
|
2791
3159
|
const RATE_LIMIT_STATUS_CODE = 429;
|
|
3160
|
+
/**
|
|
3161
|
+
* Walk the JSON schema and force `additionalProperties: false` on every
|
|
3162
|
+
* object node. Required by the OpenAI-style json_schema response_format
|
|
3163
|
+
* (which OpenRouter accepts) when `strict: true`.
|
|
3164
|
+
*/
|
|
3165
|
+
function enforceAdditionalPropertiesFalse(schema) {
|
|
3166
|
+
const stack = [schema];
|
|
3167
|
+
while (stack.length > 0) {
|
|
3168
|
+
const node = stack.pop();
|
|
3169
|
+
if (node.type === "object") {
|
|
3170
|
+
node.additionalProperties = false;
|
|
3171
|
+
}
|
|
3172
|
+
for (const value of Object.values(node)) {
|
|
3173
|
+
if (Array.isArray(value)) {
|
|
3174
|
+
for (const entry of value) {
|
|
3175
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
3176
|
+
stack.push(entry);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
else if (value && typeof value === "object") {
|
|
3181
|
+
stack.push(value);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
2792
3186
|
//
|
|
2793
3187
|
//
|
|
2794
3188
|
// Main
|
|
@@ -2804,6 +3198,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2804
3198
|
super(...arguments);
|
|
2805
3199
|
this.name = PROVIDER.OPENROUTER.NAME;
|
|
2806
3200
|
this.defaultModel = PROVIDER.OPENROUTER.MODEL.DEFAULT;
|
|
3201
|
+
// Session-level cache of models observed to reject native
|
|
3202
|
+
// `response_format: json_schema`. When a model is in this set, buildRequest
|
|
3203
|
+
// engages the legacy fake-tool path instead of native structured output.
|
|
3204
|
+
this.runtimeNoStructuredOutputModels = new Set();
|
|
3205
|
+
}
|
|
3206
|
+
rememberModelRejectsStructuredOutput(model) {
|
|
3207
|
+
this.runtimeNoStructuredOutputModels.add(model);
|
|
3208
|
+
}
|
|
3209
|
+
clearRuntimeNoStructuredOutputModels() {
|
|
3210
|
+
this.runtimeNoStructuredOutputModels.clear();
|
|
3211
|
+
}
|
|
3212
|
+
supportsStructuredOutput(model) {
|
|
3213
|
+
return !this.runtimeNoStructuredOutputModels.has(model);
|
|
2807
3214
|
}
|
|
2808
3215
|
//
|
|
2809
3216
|
// Request Building
|
|
@@ -2825,8 +3232,23 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2825
3232
|
if (request.user) {
|
|
2826
3233
|
openRouterRequest.user = request.user;
|
|
2827
3234
|
}
|
|
2828
|
-
|
|
2829
|
-
openRouterRequest.
|
|
3235
|
+
const useFallbackStructuredOutput = Boolean(request.format) &&
|
|
3236
|
+
!this.supportsStructuredOutput(openRouterRequest.model);
|
|
3237
|
+
const allTools = request.tools
|
|
3238
|
+
? [...request.tools]
|
|
3239
|
+
: [];
|
|
3240
|
+
if (useFallbackStructuredOutput && request.format) {
|
|
3241
|
+
log$1.warn(`[OpenRouterAdapter] Using legacy structured_output tool fallback for model ${openRouterRequest.model}; native response_format previously rejected for this model.`);
|
|
3242
|
+
allTools.push({
|
|
3243
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
3244
|
+
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
3245
|
+
"After gathering all necessary information (including results from other tools), " +
|
|
3246
|
+
"call this tool with the structured data to complete the request.",
|
|
3247
|
+
parameters: request.format,
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
if (allTools.length > 0) {
|
|
3251
|
+
openRouterRequest.tools = allTools.map((tool) => ({
|
|
2830
3252
|
type: "function",
|
|
2831
3253
|
function: {
|
|
2832
3254
|
name: tool.name,
|
|
@@ -2838,6 +3260,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2838
3260
|
// The structured_output tool prompt already emphasizes it must be called
|
|
2839
3261
|
openRouterRequest.tool_choice = "auto";
|
|
2840
3262
|
}
|
|
3263
|
+
// Native structured output: send schema as `response_format`. The legacy
|
|
3264
|
+
// tool-emulation path is engaged only as a runtime fallback for models the
|
|
3265
|
+
// API has flagged as not supporting native json_schema.
|
|
3266
|
+
if (request.format && !useFallbackStructuredOutput) {
|
|
3267
|
+
openRouterRequest.response_format = {
|
|
3268
|
+
type: "json_schema",
|
|
3269
|
+
json_schema: {
|
|
3270
|
+
name: STRUCTURED_OUTPUT_SCHEMA_NAME,
|
|
3271
|
+
schema: request.format,
|
|
3272
|
+
strict: true,
|
|
3273
|
+
},
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
2841
3276
|
if (request.providerOptions) {
|
|
2842
3277
|
Object.assign(openRouterRequest, request.providerOptions);
|
|
2843
3278
|
}
|
|
@@ -2848,24 +3283,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2848
3283
|
}
|
|
2849
3284
|
return openRouterRequest;
|
|
2850
3285
|
}
|
|
2851
|
-
formatTools(toolkit,
|
|
2852
|
-
|
|
3286
|
+
formatTools(toolkit,
|
|
3287
|
+
// outputSchema is part of the interface contract but OpenRouter now uses
|
|
3288
|
+
// native `response_format` (set in buildRequest), so we no longer inject a
|
|
3289
|
+
// synthetic structured-output tool here. The legacy fake-tool injection
|
|
3290
|
+
// happens in buildRequest only as a runtime fallback.
|
|
3291
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3292
|
+
_outputSchema) {
|
|
3293
|
+
return toolkit.tools.map((tool) => ({
|
|
2853
3294
|
name: tool.name,
|
|
2854
3295
|
description: tool.description,
|
|
2855
3296
|
parameters: tool.parameters,
|
|
2856
3297
|
}));
|
|
2857
|
-
// Add structured output tool if schema is provided
|
|
2858
|
-
// (OpenRouter doesn't have native structured output like OpenAI, so use tool approach)
|
|
2859
|
-
if (outputSchema) {
|
|
2860
|
-
tools.push({
|
|
2861
|
-
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
2862
|
-
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
2863
|
-
"After gathering all necessary information (including results from other tools), " +
|
|
2864
|
-
"call this tool with the structured data to complete the request.",
|
|
2865
|
-
parameters: outputSchema,
|
|
2866
|
-
});
|
|
2867
|
-
}
|
|
2868
|
-
return tools;
|
|
2869
3298
|
}
|
|
2870
3299
|
formatOutputSchema(schema) {
|
|
2871
3300
|
let jsonSchema;
|
|
@@ -2888,6 +3317,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2888
3317
|
if (jsonSchema.$schema) {
|
|
2889
3318
|
delete jsonSchema.$schema;
|
|
2890
3319
|
}
|
|
3320
|
+
// OpenRouter (and most backends behind it) require additionalProperties:
|
|
3321
|
+
// false on every object when using strict json_schema response_format.
|
|
3322
|
+
enforceAdditionalPropertiesFalse(jsonSchema);
|
|
2891
3323
|
return jsonSchema;
|
|
2892
3324
|
}
|
|
2893
3325
|
//
|
|
@@ -2896,34 +3328,99 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2896
3328
|
async executeRequest(client, request, signal) {
|
|
2897
3329
|
const openRouter = client;
|
|
2898
3330
|
const openRouterRequest = request;
|
|
3331
|
+
const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
|
|
2899
3332
|
try {
|
|
2900
|
-
const response = await openRouter.chat.send({
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
toolChoice: openRouterRequest.tool_choice,
|
|
2905
|
-
user: openRouterRequest.user,
|
|
2906
|
-
}, signal ? { signal } : undefined);
|
|
3333
|
+
const response = (await openRouter.chat.send(this.toSdkChatParams(openRouterRequest), signal ? { signal } : undefined));
|
|
3334
|
+
if (wantsStructuredOutput) {
|
|
3335
|
+
response.__jaypieStructuredOutput = true;
|
|
3336
|
+
}
|
|
2907
3337
|
return response;
|
|
2908
3338
|
}
|
|
2909
3339
|
catch (error) {
|
|
2910
3340
|
if (signal?.aborted)
|
|
2911
3341
|
return undefined;
|
|
3342
|
+
// If the model rejected `response_format`, cache it and retry with the
|
|
3343
|
+
// legacy fake-tool emulation path.
|
|
3344
|
+
if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {
|
|
3345
|
+
const model = openRouterRequest.model;
|
|
3346
|
+
this.rememberModelRejectsStructuredOutput(model);
|
|
3347
|
+
log$1.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
|
|
3348
|
+
const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
|
|
3349
|
+
return (await openRouter.chat.send(this.toSdkChatParams(fallbackRequest), signal ? { signal } : undefined));
|
|
3350
|
+
}
|
|
2912
3351
|
throw error;
|
|
2913
3352
|
}
|
|
2914
3353
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
3354
|
+
/**
|
|
3355
|
+
* Translate our internal snake_case `OpenRouterRequest` into the SDK's
|
|
3356
|
+
* camelCase shape, forwarding only the fields we care about (the SDK
|
|
3357
|
+
* silently strips unknown fields).
|
|
3358
|
+
*/
|
|
3359
|
+
toSdkChatParams(openRouterRequest) {
|
|
3360
|
+
const params = {
|
|
2920
3361
|
model: openRouterRequest.model,
|
|
2921
3362
|
messages: openRouterRequest.messages,
|
|
2922
3363
|
tools: openRouterRequest.tools,
|
|
2923
3364
|
toolChoice: openRouterRequest.tool_choice,
|
|
2924
3365
|
user: openRouterRequest.user,
|
|
3366
|
+
};
|
|
3367
|
+
if (openRouterRequest.response_format) {
|
|
3368
|
+
const format = openRouterRequest.response_format;
|
|
3369
|
+
if (format.type === "json_schema") {
|
|
3370
|
+
params.responseFormat = {
|
|
3371
|
+
type: "json_schema",
|
|
3372
|
+
jsonSchema: format.json_schema,
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
else {
|
|
3376
|
+
params.responseFormat = format;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
const temperature = openRouterRequest.temperature;
|
|
3380
|
+
if (temperature !== undefined) {
|
|
3381
|
+
params.temperature = temperature;
|
|
3382
|
+
}
|
|
3383
|
+
return params;
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Rebuild a structured-output request without `response_format`, swapping in
|
|
3387
|
+
* the legacy fake-tool emulation. Used as a runtime fallback when a model
|
|
3388
|
+
* rejects native json_schema.
|
|
3389
|
+
*/
|
|
3390
|
+
toFallbackStructuredOutputRequest(request) {
|
|
3391
|
+
if (!request.response_format ||
|
|
3392
|
+
request.response_format.type !== "json_schema") {
|
|
3393
|
+
return request;
|
|
3394
|
+
}
|
|
3395
|
+
const { response_format, ...rest } = request;
|
|
3396
|
+
const fallbackRequest = { ...rest };
|
|
3397
|
+
const schema = response_format.json_schema.schema;
|
|
3398
|
+
const fakeTool = {
|
|
3399
|
+
type: "function",
|
|
3400
|
+
function: {
|
|
3401
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
3402
|
+
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
3403
|
+
"After gathering all necessary information (including results from other tools), " +
|
|
3404
|
+
"call this tool with the structured data to complete the request.",
|
|
3405
|
+
parameters: schema,
|
|
3406
|
+
},
|
|
3407
|
+
};
|
|
3408
|
+
fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
|
|
3409
|
+
fallbackRequest.tool_choice = "auto";
|
|
3410
|
+
return fallbackRequest;
|
|
3411
|
+
}
|
|
3412
|
+
async *executeStreamRequest(client, request, signal) {
|
|
3413
|
+
const openRouter = client;
|
|
3414
|
+
const openRouterRequest = request;
|
|
3415
|
+
// Use chat.send with stream: true for streaming responses.
|
|
3416
|
+
// Cast the result to AsyncIterable: when stream: true, the SDK returns a
|
|
3417
|
+
// stream we can iterate, but the typed result is the union with the
|
|
3418
|
+
// non-stream response.
|
|
3419
|
+
const streamParams = {
|
|
3420
|
+
...this.toSdkChatParams(openRouterRequest),
|
|
2925
3421
|
stream: true,
|
|
2926
|
-
}
|
|
3422
|
+
};
|
|
3423
|
+
const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
|
|
2927
3424
|
// Track current tool call being built
|
|
2928
3425
|
let currentToolCall = null;
|
|
2929
3426
|
// Track usage for final chunk
|
|
@@ -3202,6 +3699,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3202
3699
|
}
|
|
3203
3700
|
hasStructuredOutput(response) {
|
|
3204
3701
|
const openRouterResponse = response;
|
|
3702
|
+
// Native path: executeRequest annotates the response when we sent
|
|
3703
|
+
// `response_format`, so we can detect intent statelessly.
|
|
3704
|
+
if (openRouterResponse.__jaypieStructuredOutput) {
|
|
3705
|
+
return this.extractStructuredOutput(response) !== undefined;
|
|
3706
|
+
}
|
|
3707
|
+
// Fallback path: legacy fake-tool emulation, kept for models that the
|
|
3708
|
+
// runtime has cached as not supporting native `response_format`.
|
|
3205
3709
|
const choice = openRouterResponse.choices[0];
|
|
3206
3710
|
// SDK returns camelCase (toolCalls)
|
|
3207
3711
|
if (!choice?.message?.toolCalls?.length) {
|
|
@@ -3213,6 +3717,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3213
3717
|
}
|
|
3214
3718
|
extractStructuredOutput(response) {
|
|
3215
3719
|
const openRouterResponse = response;
|
|
3720
|
+
if (openRouterResponse.__jaypieStructuredOutput) {
|
|
3721
|
+
const choice = openRouterResponse.choices[0];
|
|
3722
|
+
const content = choice?.message?.content;
|
|
3723
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
3724
|
+
return undefined;
|
|
3725
|
+
}
|
|
3726
|
+
try {
|
|
3727
|
+
return JSON.parse(content);
|
|
3728
|
+
}
|
|
3729
|
+
catch {
|
|
3730
|
+
return undefined;
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
// Fallback path: legacy fake-tool emulation
|
|
3216
3734
|
const choice = openRouterResponse.choices[0];
|
|
3217
3735
|
// SDK returns camelCase (toolCalls)
|
|
3218
3736
|
if (!choice?.message?.toolCalls?.length) {
|
|
@@ -3338,10 +3856,29 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3338
3856
|
// Export singleton instance
|
|
3339
3857
|
const openRouterAdapter = new OpenRouterAdapter();
|
|
3340
3858
|
|
|
3859
|
+
/**
|
|
3860
|
+
* Error-message substrings that indicate a transient xAI media-ingest flake.
|
|
3861
|
+
* The xAI ingest service enters a bad state after ~12 consecutive file-bearing
|
|
3862
|
+
* calls and rejects subsequent requests with HTTP 400. The condition is
|
|
3863
|
+
* self-clearing, so these errors should be retried with backoff rather than
|
|
3864
|
+
* surfaced as unrecoverable.
|
|
3865
|
+
*
|
|
3866
|
+
* See: github.com/finlaysonstudio/jaypie issue #301
|
|
3867
|
+
*/
|
|
3868
|
+
const TRANSIENT_INGEST_MESSAGE_PATTERNS = [
|
|
3869
|
+
"failed to ingest inline file bytes",
|
|
3870
|
+
];
|
|
3871
|
+
function isTransientIngestError(error) {
|
|
3872
|
+
if (!(error instanceof BadRequestError))
|
|
3873
|
+
return false;
|
|
3874
|
+
const message = (error.message ?? "").toLowerCase();
|
|
3875
|
+
return TRANSIENT_INGEST_MESSAGE_PATTERNS.some((pattern) => message.includes(pattern));
|
|
3876
|
+
}
|
|
3341
3877
|
/**
|
|
3342
3878
|
* XaiAdapter extends OpenAiAdapter since xAI (Grok) uses an OpenAI-compatible API.
|
|
3343
|
-
*
|
|
3344
|
-
*
|
|
3879
|
+
* Name, default model, and transient-ingest-error detection are overridden;
|
|
3880
|
+
* all request building, response parsing, tool handling, and streaming are
|
|
3881
|
+
* inherited.
|
|
3345
3882
|
*/
|
|
3346
3883
|
class XaiAdapter extends OpenAiAdapter {
|
|
3347
3884
|
constructor() {
|
|
@@ -3351,6 +3888,16 @@ class XaiAdapter extends OpenAiAdapter {
|
|
|
3351
3888
|
// @ts-expect-error Narrowing override: xAI default model differs from parent's literal
|
|
3352
3889
|
this.defaultModel = PROVIDER.XAI.MODEL.DEFAULT;
|
|
3353
3890
|
}
|
|
3891
|
+
classifyError(error) {
|
|
3892
|
+
if (isTransientIngestError(error)) {
|
|
3893
|
+
return {
|
|
3894
|
+
error,
|
|
3895
|
+
category: ErrorCategory.Retryable,
|
|
3896
|
+
shouldRetry: true,
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
3899
|
+
return super.classifyError(error);
|
|
3900
|
+
}
|
|
3354
3901
|
}
|
|
3355
3902
|
// Export singleton instance
|
|
3356
3903
|
const xaiAdapter = new XaiAdapter();
|
|
@@ -5204,55 +5751,50 @@ async function createTextCompletion$1(client, messages, model, systemMessage) {
|
|
|
5204
5751
|
log$1.trace(`Assistant reply: ${text.length} characters`);
|
|
5205
5752
|
return text;
|
|
5206
5753
|
}
|
|
5207
|
-
// Structured output completion
|
|
5754
|
+
// Structured output completion using Anthropic's native `output_config.format`
|
|
5755
|
+
// field. Returns a JsonObject parsed and validated against the caller's
|
|
5756
|
+
// Zod schema.
|
|
5208
5757
|
async function createStructuredCompletion$1(client, messages, model, responseSchema, systemMessage) {
|
|
5209
|
-
log$1.trace("Using structured output");
|
|
5210
|
-
// Get the JSON schema for the response
|
|
5758
|
+
log$1.trace("Using native structured output");
|
|
5211
5759
|
const schema = responseSchema instanceof z.ZodType
|
|
5212
5760
|
? responseSchema
|
|
5213
5761
|
: naturalZodSchema(responseSchema);
|
|
5214
|
-
|
|
5215
|
-
const
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
try {
|
|
5236
|
-
// Parse the JSON response
|
|
5237
|
-
const jsonStr = jsonMatch[1] || jsonMatch[0];
|
|
5238
|
-
const result = JSON.parse(jsonStr);
|
|
5239
|
-
if (!schema.parse(result)) {
|
|
5240
|
-
throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
|
|
5241
|
-
}
|
|
5242
|
-
log$1.trace("Received structured response", { result });
|
|
5243
|
-
return result;
|
|
5244
|
-
}
|
|
5245
|
-
catch {
|
|
5246
|
-
throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
|
|
5247
|
-
}
|
|
5248
|
-
}
|
|
5249
|
-
// If we can't extract JSON
|
|
5762
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
5763
|
+
const params = {
|
|
5764
|
+
model,
|
|
5765
|
+
messages,
|
|
5766
|
+
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
5767
|
+
output_config: {
|
|
5768
|
+
format: { type: "json_schema", schema: jsonSchema },
|
|
5769
|
+
},
|
|
5770
|
+
};
|
|
5771
|
+
if (systemMessage) {
|
|
5772
|
+
params.system = systemMessage;
|
|
5773
|
+
}
|
|
5774
|
+
const response = (await client.messages.create(params));
|
|
5775
|
+
if (response.stop_reason === "refusal") {
|
|
5776
|
+
throw new Error("Anthropic refused the structured-output request (stop_reason=refusal)");
|
|
5777
|
+
}
|
|
5778
|
+
if (response.stop_reason === "max_tokens") {
|
|
5779
|
+
throw new Error("Anthropic structured-output response was truncated (stop_reason=max_tokens); increase max_tokens");
|
|
5780
|
+
}
|
|
5781
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
5782
|
+
if (!textBlock) {
|
|
5250
5783
|
throw new Error("Failed to parse structured response from Anthropic");
|
|
5251
5784
|
}
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5785
|
+
let parsed;
|
|
5786
|
+
try {
|
|
5787
|
+
parsed = JSON.parse(textBlock.text);
|
|
5788
|
+
}
|
|
5789
|
+
catch {
|
|
5790
|
+
throw new Error("Failed to parse structured response from Anthropic: " + textBlock.text);
|
|
5791
|
+
}
|
|
5792
|
+
const validation = schema.safeParse(parsed);
|
|
5793
|
+
if (!validation.success) {
|
|
5794
|
+
throw new Error(`JSON response from Anthropic does not match schema: ${textBlock.text}`);
|
|
5255
5795
|
}
|
|
5796
|
+
log$1.trace("Received structured response", { result: validation.data });
|
|
5797
|
+
return validation.data;
|
|
5256
5798
|
}
|
|
5257
5799
|
|
|
5258
5800
|
// Maps Jaypie roles to Anthropic roles
|