@jaypie/llm 1.2.29 → 1.2.32
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 +692 -132
- 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/OpenAiAdapter.d.ts +4 -0
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +32 -4
- package/dist/esm/constants.d.ts +5 -5
- package/dist/esm/index.js +692 -132
- 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/OpenAiAdapter.d.ts +4 -0
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +32 -4
- package/package.json +2 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -31,7 +31,7 @@ const FIRST_CLASS_PROVIDER = {
|
|
|
31
31
|
// https://developers.openai.com/api/docs/models
|
|
32
32
|
OPENAI: {
|
|
33
33
|
DEFAULT: "gpt-5.4",
|
|
34
|
-
LARGE: "gpt-5.
|
|
34
|
+
LARGE: "gpt-5.5",
|
|
35
35
|
SMALL: "gpt-5.4-mini",
|
|
36
36
|
TINY: "gpt-5.4-nano",
|
|
37
37
|
},
|
|
@@ -586,9 +586,7 @@ function jsonSchemaToOpenApi3(schema) {
|
|
|
586
586
|
}
|
|
587
587
|
result[key] = convertedProps;
|
|
588
588
|
}
|
|
589
|
-
else if (key === "items" &&
|
|
590
|
-
typeof value === "object" &&
|
|
591
|
-
value !== null) {
|
|
589
|
+
else if (key === "items" && typeof value === "object" && value !== null) {
|
|
592
590
|
result[key] = jsonSchemaToOpenApi3(value);
|
|
593
591
|
}
|
|
594
592
|
else {
|
|
@@ -986,8 +984,140 @@ function isTransientNetworkError(error) {
|
|
|
986
984
|
// Constants
|
|
987
985
|
//
|
|
988
986
|
const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
|
|
987
|
+
const STRUCTURED_OUTPUT_NON_PARSE_STOP_REASONS = new Set([
|
|
988
|
+
"refusal",
|
|
989
|
+
"max_tokens",
|
|
990
|
+
]);
|
|
989
991
|
// Regular expression to parse data URLs: data:mime/type;base64,data
|
|
990
992
|
const DATA_URL_REGEX = /^data:([^;]+);base64,(.+)$/;
|
|
993
|
+
// String formats accepted by Anthropic's structured-output grammar compiler.
|
|
994
|
+
// Other formats are stripped (move to description) so the API does not 400.
|
|
995
|
+
const SUPPORTED_STRING_FORMATS = new Set([
|
|
996
|
+
"date",
|
|
997
|
+
"date-time",
|
|
998
|
+
"duration",
|
|
999
|
+
"email",
|
|
1000
|
+
"hostname",
|
|
1001
|
+
"ipv4",
|
|
1002
|
+
"ipv6",
|
|
1003
|
+
"time",
|
|
1004
|
+
"uri",
|
|
1005
|
+
"uuid",
|
|
1006
|
+
]);
|
|
1007
|
+
// Top-level keywords stripped wholesale before sending: not part of the spec
|
|
1008
|
+
// the API enforces and the validator can reject them.
|
|
1009
|
+
const STRIPPED_TOP_LEVEL_KEYWORDS = new Set(["$schema", "$id"]);
|
|
1010
|
+
// Keywords Anthropic's structured-output grammar does not support. They are
|
|
1011
|
+
// removed from the schema and appended to `description` so the model still
|
|
1012
|
+
// sees the intent.
|
|
1013
|
+
const UNSUPPORTED_CONSTRAINT_KEYWORDS = new Set([
|
|
1014
|
+
"exclusiveMaximum",
|
|
1015
|
+
"exclusiveMinimum",
|
|
1016
|
+
"maxItems",
|
|
1017
|
+
"maxLength",
|
|
1018
|
+
"maxProperties",
|
|
1019
|
+
"maximum",
|
|
1020
|
+
"minLength",
|
|
1021
|
+
"minProperties",
|
|
1022
|
+
"minimum",
|
|
1023
|
+
"multipleOf",
|
|
1024
|
+
"pattern",
|
|
1025
|
+
"patternProperties",
|
|
1026
|
+
"uniqueItems",
|
|
1027
|
+
]);
|
|
1028
|
+
/**
|
|
1029
|
+
* Recursively transform a JSON Schema into the strict shape Anthropic's
|
|
1030
|
+
* structured-output grammar accepts: object types must have
|
|
1031
|
+
* `additionalProperties: false`, unsupported numeric/string/array
|
|
1032
|
+
* constraints are appended to `description`, and unsupported string
|
|
1033
|
+
* formats are stripped. Mirrors @anthropic-ai/sdk's `transformJSONSchema`
|
|
1034
|
+
* but inline so we do not take a runtime dependency on the optional
|
|
1035
|
+
* peer SDK.
|
|
1036
|
+
*/
|
|
1037
|
+
function sanitizeJsonSchemaForAnthropic(schema, isRoot = true) {
|
|
1038
|
+
const result = {};
|
|
1039
|
+
const carriedConstraints = [];
|
|
1040
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1041
|
+
if (isRoot && STRIPPED_TOP_LEVEL_KEYWORDS.has(key)) {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (UNSUPPORTED_CONSTRAINT_KEYWORDS.has(key)) {
|
|
1045
|
+
carriedConstraints.push(`${key}: ${JSON.stringify(value)}`);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (key === "format" && typeof value === "string") {
|
|
1049
|
+
if (SUPPORTED_STRING_FORMATS.has(value)) {
|
|
1050
|
+
result[key] = value;
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
carriedConstraints.push(`format: ${JSON.stringify(value)}`);
|
|
1054
|
+
}
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (key === "minItems" && typeof value === "number") {
|
|
1058
|
+
if (value === 0 || value === 1) {
|
|
1059
|
+
result[key] = value;
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
carriedConstraints.push(`minItems: ${value}`);
|
|
1063
|
+
}
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (key === "properties" &&
|
|
1067
|
+
value &&
|
|
1068
|
+
typeof value === "object" &&
|
|
1069
|
+
!Array.isArray(value)) {
|
|
1070
|
+
const transformedProps = {};
|
|
1071
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
1072
|
+
transformedProps[propName] = isJsonSchema(propSchema)
|
|
1073
|
+
? sanitizeJsonSchemaForAnthropic(propSchema, false)
|
|
1074
|
+
: propSchema;
|
|
1075
|
+
}
|
|
1076
|
+
result[key] = transformedProps;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if ((key === "items" || key === "additionalItems" || key === "contains") &&
|
|
1080
|
+
isJsonSchema(value)) {
|
|
1081
|
+
result[key] = sanitizeJsonSchemaForAnthropic(value, false);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if ((key === "anyOf" || key === "oneOf" || key === "allOf") &&
|
|
1085
|
+
Array.isArray(value)) {
|
|
1086
|
+
const targetKey = key === "oneOf" ? "anyOf" : key;
|
|
1087
|
+
result[targetKey] = value.map((entry) => isJsonSchema(entry)
|
|
1088
|
+
? sanitizeJsonSchemaForAnthropic(entry, false)
|
|
1089
|
+
: entry);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (key === "$defs" && value && typeof value === "object") {
|
|
1093
|
+
const transformedDefs = {};
|
|
1094
|
+
for (const [defName, defSchema] of Object.entries(value)) {
|
|
1095
|
+
transformedDefs[defName] = isJsonSchema(defSchema)
|
|
1096
|
+
? sanitizeJsonSchemaForAnthropic(defSchema, false)
|
|
1097
|
+
: defSchema;
|
|
1098
|
+
}
|
|
1099
|
+
result[key] = transformedDefs;
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
if (key === "additionalProperties") {
|
|
1103
|
+
// Always force `false` on objects below; ignore caller-supplied value.
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
result[key] = value;
|
|
1107
|
+
}
|
|
1108
|
+
if (result.type === "object") {
|
|
1109
|
+
result.additionalProperties = false;
|
|
1110
|
+
}
|
|
1111
|
+
if (carriedConstraints.length > 0) {
|
|
1112
|
+
const existing = typeof result.description === "string" ? result.description : "";
|
|
1113
|
+
const suffix = `{${carriedConstraints.join(", ")}}`;
|
|
1114
|
+
result.description = existing ? `${existing}\n\n${suffix}` : suffix;
|
|
1115
|
+
}
|
|
1116
|
+
return result;
|
|
1117
|
+
}
|
|
1118
|
+
function isJsonSchema(value) {
|
|
1119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1120
|
+
}
|
|
991
1121
|
/**
|
|
992
1122
|
* Parse a data URL into its components
|
|
993
1123
|
*/
|
|
@@ -1069,11 +1199,11 @@ const NOT_RETRYABLE_ERROR_NAMES = [
|
|
|
1069
1199
|
// Patterns (not exact names) so dated variants and future releases are covered
|
|
1070
1200
|
// without code changes — Anthropic is trending toward removing temperature on
|
|
1071
1201
|
// newer Claude models.
|
|
1072
|
-
const MODELS_WITHOUT_TEMPERATURE = [
|
|
1202
|
+
const MODELS_WITHOUT_TEMPERATURE$1 = [
|
|
1073
1203
|
/^claude-opus-4-[789]/,
|
|
1074
1204
|
/^claude-opus-[5-9]/,
|
|
1075
1205
|
];
|
|
1076
|
-
function isTemperatureDeprecationError(error) {
|
|
1206
|
+
function isTemperatureDeprecationError$1(error) {
|
|
1077
1207
|
if (!error || typeof error !== "object")
|
|
1078
1208
|
return false;
|
|
1079
1209
|
const err = error;
|
|
@@ -1083,6 +1213,29 @@ function isTemperatureDeprecationError(error) {
|
|
|
1083
1213
|
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
1084
1214
|
return messages.some((m) => m.toLowerCase().includes("temperature"));
|
|
1085
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Detect 400 errors that indicate the model itself does not support native
|
|
1218
|
+
* structured outputs (`output_config.format`). Citations + structured output
|
|
1219
|
+
* is also a 400 case but is a caller error rather than a model-capability
|
|
1220
|
+
* gap, so we explicitly skip it to avoid masking the real problem under a
|
|
1221
|
+
* tool-emulation retry. The deprecated-`output_format` 400 (API renamed the
|
|
1222
|
+
* field) is also explicitly excluded — that's a code-path bug, not a model
|
|
1223
|
+
* gap; it should propagate so we notice and fix it.
|
|
1224
|
+
*/
|
|
1225
|
+
function isStructuredOutputUnsupportedError$1(error) {
|
|
1226
|
+
if (!error || typeof error !== "object")
|
|
1227
|
+
return false;
|
|
1228
|
+
const err = error;
|
|
1229
|
+
const name = error?.constructor?.name;
|
|
1230
|
+
if (name !== "BadRequestError" && err.status !== 400)
|
|
1231
|
+
return false;
|
|
1232
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
1233
|
+
if (messages.some((m) => /citation/i.test(m)))
|
|
1234
|
+
return false;
|
|
1235
|
+
if (messages.some((m) => /deprecated/i.test(m)))
|
|
1236
|
+
return false;
|
|
1237
|
+
return messages.some((m) => /output_config|output_format|json[_ ]schema|structured/i.test(m));
|
|
1238
|
+
}
|
|
1086
1239
|
//
|
|
1087
1240
|
//
|
|
1088
1241
|
// Main
|
|
@@ -1100,6 +1253,10 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1100
1253
|
// Session-level cache of models observed to reject `temperature` at runtime.
|
|
1101
1254
|
// Populated by executeRequest on 400 errors so repeat calls skip the param.
|
|
1102
1255
|
this.runtimeNoTemperatureModels = new Set();
|
|
1256
|
+
// Session-level cache of models observed to reject `output_format`. When a
|
|
1257
|
+
// model is in this set, buildRequest engages the legacy fake-tool path
|
|
1258
|
+
// instead of native structured output.
|
|
1259
|
+
this.runtimeNoStructuredOutputModels = new Set();
|
|
1103
1260
|
}
|
|
1104
1261
|
rememberModelRejectsTemperature(model) {
|
|
1105
1262
|
this.runtimeNoTemperatureModels.add(model);
|
|
@@ -1107,10 +1264,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1107
1264
|
clearRuntimeNoTemperatureModels() {
|
|
1108
1265
|
this.runtimeNoTemperatureModels.clear();
|
|
1109
1266
|
}
|
|
1267
|
+
rememberModelRejectsStructuredOutput(model) {
|
|
1268
|
+
this.runtimeNoStructuredOutputModels.add(model);
|
|
1269
|
+
}
|
|
1270
|
+
clearRuntimeNoStructuredOutputModels() {
|
|
1271
|
+
this.runtimeNoStructuredOutputModels.clear();
|
|
1272
|
+
}
|
|
1110
1273
|
supportsTemperature(model) {
|
|
1111
1274
|
if (this.runtimeNoTemperatureModels.has(model))
|
|
1112
1275
|
return false;
|
|
1113
|
-
return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
|
|
1276
|
+
return !MODELS_WITHOUT_TEMPERATURE$1.some((pattern) => pattern.test(model));
|
|
1277
|
+
}
|
|
1278
|
+
supportsStructuredOutput(model) {
|
|
1279
|
+
return !this.runtimeNoStructuredOutputModels.has(model);
|
|
1114
1280
|
}
|
|
1115
1281
|
//
|
|
1116
1282
|
// Request Building
|
|
@@ -1179,8 +1345,22 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1179
1345
|
if (request.system) {
|
|
1180
1346
|
anthropicRequest.system = request.system;
|
|
1181
1347
|
}
|
|
1182
|
-
|
|
1183
|
-
anthropicRequest.
|
|
1348
|
+
const useFallbackStructuredOutput = Boolean(request.format) &&
|
|
1349
|
+
!this.supportsStructuredOutput(anthropicRequest.model);
|
|
1350
|
+
const allTools = request.tools
|
|
1351
|
+
? [...request.tools]
|
|
1352
|
+
: [];
|
|
1353
|
+
if (useFallbackStructuredOutput && request.format) {
|
|
1354
|
+
log$1.warn(`[AnthropicAdapter] Using legacy structured_output tool fallback for model ${anthropicRequest.model}; native output_config previously rejected for this model.`);
|
|
1355
|
+
allTools.push({
|
|
1356
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
1357
|
+
description: "Output a structured JSON object, " +
|
|
1358
|
+
"use this before your final response to give structured outputs to the user",
|
|
1359
|
+
parameters: request.format,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
if (allTools.length > 0) {
|
|
1363
|
+
anthropicRequest.tools = allTools.map((tool) => ({
|
|
1184
1364
|
name: tool.name,
|
|
1185
1365
|
description: tool.description,
|
|
1186
1366
|
input_schema: {
|
|
@@ -1189,10 +1369,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1189
1369
|
},
|
|
1190
1370
|
type: "custom",
|
|
1191
1371
|
}));
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1372
|
+
anthropicRequest.tool_choice = useFallbackStructuredOutput
|
|
1373
|
+
? { type: "any" }
|
|
1374
|
+
: { type: "auto" };
|
|
1375
|
+
}
|
|
1376
|
+
// Native structured output: send schema as `output_config.format`. The
|
|
1377
|
+
// legacy tool-emulation path is engaged only as a runtime fallback for
|
|
1378
|
+
// models the API has flagged as not supporting native structured output.
|
|
1379
|
+
if (request.format && !useFallbackStructuredOutput) {
|
|
1380
|
+
anthropicRequest.output_config = {
|
|
1381
|
+
format: {
|
|
1382
|
+
type: "json_schema",
|
|
1383
|
+
schema: request.format,
|
|
1384
|
+
},
|
|
1196
1385
|
};
|
|
1197
1386
|
}
|
|
1198
1387
|
if (request.providerOptions) {
|
|
@@ -1209,8 +1398,13 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1209
1398
|
}
|
|
1210
1399
|
return anthropicRequest;
|
|
1211
1400
|
}
|
|
1212
|
-
formatTools(toolkit,
|
|
1213
|
-
|
|
1401
|
+
formatTools(toolkit,
|
|
1402
|
+
// outputSchema is part of the interface contract but Anthropic now uses
|
|
1403
|
+
// native `output_format` (set in buildRequest), so we no longer inject a
|
|
1404
|
+
// synthetic structured-output tool here.
|
|
1405
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1406
|
+
_outputSchema) {
|
|
1407
|
+
return toolkit.tools.map((tool) => ({
|
|
1214
1408
|
name: tool.name,
|
|
1215
1409
|
description: tool.description,
|
|
1216
1410
|
parameters: {
|
|
@@ -1218,16 +1412,6 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1218
1412
|
type: "object",
|
|
1219
1413
|
},
|
|
1220
1414
|
}));
|
|
1221
|
-
// Add structured output tool if schema is provided
|
|
1222
|
-
if (outputSchema) {
|
|
1223
|
-
tools.push({
|
|
1224
|
-
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
1225
|
-
description: "Output a structured JSON object, " +
|
|
1226
|
-
"use this before your final response to give structured outputs to the user",
|
|
1227
|
-
parameters: outputSchema,
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
return tools;
|
|
1231
1415
|
}
|
|
1232
1416
|
formatOutputSchema(schema) {
|
|
1233
1417
|
let jsonSchema;
|
|
@@ -1246,11 +1430,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1246
1430
|
: naturalZodSchema(schema);
|
|
1247
1431
|
jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
1248
1432
|
}
|
|
1249
|
-
|
|
1250
|
-
if (jsonSchema.$schema) {
|
|
1251
|
-
delete jsonSchema.$schema;
|
|
1252
|
-
}
|
|
1253
|
-
return jsonSchema;
|
|
1433
|
+
return sanitizeJsonSchemaForAnthropic(jsonSchema);
|
|
1254
1434
|
}
|
|
1255
1435
|
//
|
|
1256
1436
|
// API Execution
|
|
@@ -1258,25 +1438,70 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1258
1438
|
async executeRequest(client, request, signal) {
|
|
1259
1439
|
const anthropic = client;
|
|
1260
1440
|
const anthropicRequest = request;
|
|
1441
|
+
const wantsStructuredOutput = Boolean(anthropicRequest.output_config);
|
|
1261
1442
|
try {
|
|
1262
|
-
|
|
1443
|
+
const response = (await anthropic.messages.create(anthropicRequest, signal ? { signal } : undefined));
|
|
1444
|
+
if (wantsStructuredOutput) {
|
|
1445
|
+
response.__jaypieStructuredOutput = true;
|
|
1446
|
+
}
|
|
1447
|
+
return response;
|
|
1263
1448
|
}
|
|
1264
1449
|
catch (error) {
|
|
1265
1450
|
if (signal?.aborted)
|
|
1266
1451
|
return undefined;
|
|
1267
1452
|
// If the model rejected `temperature`, cache it and retry without the param
|
|
1268
1453
|
if (anthropicRequest.temperature !== undefined &&
|
|
1269
|
-
isTemperatureDeprecationError(error)) {
|
|
1454
|
+
isTemperatureDeprecationError$1(error)) {
|
|
1270
1455
|
this.rememberModelRejectsTemperature(anthropicRequest.model);
|
|
1271
1456
|
const retryRequest = { ...anthropicRequest };
|
|
1272
1457
|
delete retryRequest.temperature;
|
|
1273
|
-
|
|
1458
|
+
const response = (await anthropic.messages.create(retryRequest, signal ? { signal } : undefined));
|
|
1459
|
+
if (wantsStructuredOutput) {
|
|
1460
|
+
response.__jaypieStructuredOutput = true;
|
|
1461
|
+
}
|
|
1462
|
+
return response;
|
|
1463
|
+
}
|
|
1464
|
+
// If the model rejected native structured output, cache it and retry
|
|
1465
|
+
// via the legacy fake-tool emulation path.
|
|
1466
|
+
if (wantsStructuredOutput && isStructuredOutputUnsupportedError$1(error)) {
|
|
1467
|
+
const model = anthropicRequest.model;
|
|
1468
|
+
this.rememberModelRejectsStructuredOutput(model);
|
|
1469
|
+
log$1.warn(`[AnthropicAdapter] Model ${model} rejected native output_config; falling back to legacy structured_output tool emulation.`);
|
|
1470
|
+
const fallbackRequest = this.toFallbackStructuredOutputRequest(anthropicRequest);
|
|
1471
|
+
return (await anthropic.messages.create(fallbackRequest, signal ? { signal } : undefined));
|
|
1274
1472
|
}
|
|
1275
1473
|
throw error;
|
|
1276
1474
|
}
|
|
1277
1475
|
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Rebuild a structured-output request without `output_format`, swapping in
|
|
1478
|
+
* the legacy fake-tool emulation. Used as a runtime fallback when a model
|
|
1479
|
+
* rejects native `output_config.format`.
|
|
1480
|
+
*/
|
|
1481
|
+
toFallbackStructuredOutputRequest(request) {
|
|
1482
|
+
const { output_config, ...rest } = request;
|
|
1483
|
+
if (!output_config)
|
|
1484
|
+
return request;
|
|
1485
|
+
const fallbackRequest = { ...rest };
|
|
1486
|
+
const fakeTool = {
|
|
1487
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
1488
|
+
description: "Output a structured JSON object, " +
|
|
1489
|
+
"use this before your final response to give structured outputs to the user",
|
|
1490
|
+
input_schema: {
|
|
1491
|
+
...output_config.format.schema,
|
|
1492
|
+
type: "object",
|
|
1493
|
+
},
|
|
1494
|
+
type: "custom",
|
|
1495
|
+
};
|
|
1496
|
+
fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
|
|
1497
|
+
fallbackRequest.tool_choice = { type: "any" };
|
|
1498
|
+
return fallbackRequest;
|
|
1499
|
+
}
|
|
1278
1500
|
async *executeStreamRequest(client, request, signal) {
|
|
1279
1501
|
const anthropic = client;
|
|
1502
|
+
// Preserve `output_config` when passing through to the SDK by typing
|
|
1503
|
+
// through the local extension instead of the upstream
|
|
1504
|
+
// MessageCreateParams shape.
|
|
1280
1505
|
let streamRequest = {
|
|
1281
1506
|
...request,
|
|
1282
1507
|
stream: true,
|
|
@@ -1287,7 +1512,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1287
1512
|
}
|
|
1288
1513
|
catch (error) {
|
|
1289
1514
|
if (streamRequest.temperature !== undefined &&
|
|
1290
|
-
isTemperatureDeprecationError(error)) {
|
|
1515
|
+
isTemperatureDeprecationError$1(error)) {
|
|
1291
1516
|
this.rememberModelRejectsTemperature(streamRequest.model);
|
|
1292
1517
|
streamRequest = {
|
|
1293
1518
|
...streamRequest,
|
|
@@ -1536,13 +1761,39 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1536
1761
|
}
|
|
1537
1762
|
hasStructuredOutput(response) {
|
|
1538
1763
|
const anthropicResponse = response;
|
|
1539
|
-
//
|
|
1764
|
+
// Native path: executeRequest annotates the response when we sent
|
|
1765
|
+
// `output_format`, so we can detect intent statelessly.
|
|
1766
|
+
if (anthropicResponse.__jaypieStructuredOutput) {
|
|
1767
|
+
return this.extractStructuredOutput(response) !== undefined;
|
|
1768
|
+
}
|
|
1769
|
+
// Fallback path: legacy fake-tool emulation, kept for models that the
|
|
1770
|
+
// runtime has cached as not supporting `output_format`.
|
|
1540
1771
|
const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
|
|
1541
1772
|
return (lastBlock?.type === "tool_use" &&
|
|
1542
1773
|
lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
|
|
1543
1774
|
}
|
|
1544
1775
|
extractStructuredOutput(response) {
|
|
1545
1776
|
const anthropicResponse = response;
|
|
1777
|
+
if (anthropicResponse.__jaypieStructuredOutput) {
|
|
1778
|
+
// Refusal and truncation are explicit non-JSON outcomes per Anthropic
|
|
1779
|
+
// structured-outputs docs — surface the text upstream instead of
|
|
1780
|
+
// forcing a JSON.parse on what is not JSON.
|
|
1781
|
+
if (anthropicResponse.stop_reason &&
|
|
1782
|
+
STRUCTURED_OUTPUT_NON_PARSE_STOP_REASONS.has(anthropicResponse.stop_reason)) {
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
const textBlock = anthropicResponse.content.find((block) => block.type === "text");
|
|
1786
|
+
if (!textBlock)
|
|
1787
|
+
return undefined;
|
|
1788
|
+
try {
|
|
1789
|
+
const parsed = JSON.parse(textBlock.text);
|
|
1790
|
+
return parsed;
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
return undefined;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
// Fallback path: legacy fake-tool emulation
|
|
1546
1797
|
const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
|
|
1547
1798
|
if (lastBlock?.type === "tool_use" &&
|
|
1548
1799
|
lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2) {
|
|
@@ -1571,6 +1822,26 @@ const anthropicAdapter = new AnthropicAdapter();
|
|
|
1571
1822
|
// Constants
|
|
1572
1823
|
//
|
|
1573
1824
|
const STRUCTURED_OUTPUT_TOOL_NAME$1 = "structured_output";
|
|
1825
|
+
// Gemini 3 family supports combining tools (function calling) with native
|
|
1826
|
+
// structured output via `responseJsonSchema`. Earlier Gemini families
|
|
1827
|
+
// (including 2.5 thinking) do not support the combo and fall back to the
|
|
1828
|
+
// legacy `structured_output` fake-tool emulation.
|
|
1829
|
+
const GEMINI_3_PATTERN = /^gemini-3/;
|
|
1830
|
+
/**
|
|
1831
|
+
* Detect 4xx errors that indicate the model itself does not support the
|
|
1832
|
+
* `responseJsonSchema` + tools combo. Triggers the runtime fallback to the
|
|
1833
|
+
* fake-tool emulation path. Other 400s propagate.
|
|
1834
|
+
*/
|
|
1835
|
+
function isStructuredOutputComboUnsupportedError(error) {
|
|
1836
|
+
if (!error || typeof error !== "object")
|
|
1837
|
+
return false;
|
|
1838
|
+
const err = error;
|
|
1839
|
+
const status = err.status ?? err.code;
|
|
1840
|
+
if (status !== 400)
|
|
1841
|
+
return false;
|
|
1842
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
1843
|
+
return messages.some((m) => /response[_ ]?json[_ ]?schema|response[_ ]?schema|response[_ ]?mime|function[_ ]?call|tools/i.test(m));
|
|
1844
|
+
}
|
|
1574
1845
|
// Gemini uses HTTP status codes for error classification
|
|
1575
1846
|
// Documented at: https://ai.google.dev/api/rest/v1beta/Status
|
|
1576
1847
|
const RETRYABLE_STATUS_CODES$1 = [
|
|
@@ -1603,6 +1874,21 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1603
1874
|
super(...arguments);
|
|
1604
1875
|
this.name = PROVIDER.GEMINI.NAME;
|
|
1605
1876
|
this.defaultModel = PROVIDER.GEMINI.MODEL.DEFAULT;
|
|
1877
|
+
// Session-level cache of Gemini 3 models observed to reject the native
|
|
1878
|
+
// `responseJsonSchema` + tools combo. When a model is in this set,
|
|
1879
|
+
// buildRequest engages the legacy fake-tool path instead.
|
|
1880
|
+
this.runtimeNoStructuredOutputComboModels = new Set();
|
|
1881
|
+
}
|
|
1882
|
+
rememberModelRejectsStructuredOutputCombo(model) {
|
|
1883
|
+
this.runtimeNoStructuredOutputComboModels.add(model);
|
|
1884
|
+
}
|
|
1885
|
+
clearRuntimeNoStructuredOutputComboModels() {
|
|
1886
|
+
this.runtimeNoStructuredOutputComboModels.clear();
|
|
1887
|
+
}
|
|
1888
|
+
supportsStructuredOutputCombo(model) {
|
|
1889
|
+
if (this.runtimeNoStructuredOutputComboModels.has(model))
|
|
1890
|
+
return false;
|
|
1891
|
+
return GEMINI_3_PATTERN.test(model);
|
|
1606
1892
|
}
|
|
1607
1893
|
//
|
|
1608
1894
|
// Request Building
|
|
@@ -1631,9 +1917,27 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1631
1917
|
}
|
|
1632
1918
|
}
|
|
1633
1919
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1920
|
+
const hasUserTools = !!(request.tools && request.tools.length > 0);
|
|
1921
|
+
const useNativeCombo = Boolean(request.format) &&
|
|
1922
|
+
hasUserTools &&
|
|
1923
|
+
this.supportsStructuredOutputCombo(geminiRequest.model);
|
|
1924
|
+
// When tools+format are combined and the model does not support the native
|
|
1925
|
+
// combo, inject the legacy `structured_output` fake tool here so the model
|
|
1926
|
+
// is forced to call it before its final answer.
|
|
1927
|
+
const allTools = request.tools
|
|
1928
|
+
? [...request.tools]
|
|
1929
|
+
: [];
|
|
1930
|
+
if (request.format && hasUserTools && !useNativeCombo) {
|
|
1931
|
+
log$1.warn(`[GeminiAdapter] Using legacy structured_output tool fallback for model ${geminiRequest.model}; native responseJsonSchema + tools combo is only available on Gemini 3.`);
|
|
1932
|
+
allTools.push({
|
|
1933
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
1934
|
+
description: "Output a structured JSON object, " +
|
|
1935
|
+
"use this before your final response to give structured outputs to the user",
|
|
1936
|
+
parameters: request.format,
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
if (allTools.length > 0) {
|
|
1940
|
+
const functionDeclarations = allTools.map((tool) => ({
|
|
1637
1941
|
name: tool.name,
|
|
1638
1942
|
description: tool.description,
|
|
1639
1943
|
parameters: tool.parameters,
|
|
@@ -1643,11 +1947,13 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1643
1947
|
tools: [{ functionDeclarations }],
|
|
1644
1948
|
};
|
|
1645
1949
|
}
|
|
1646
|
-
//
|
|
1647
|
-
//
|
|
1648
|
-
//
|
|
1649
|
-
|
|
1650
|
-
|
|
1950
|
+
// Native structured output: send schema as `responseJsonSchema`
|
|
1951
|
+
// (or `responseSchema` for Gemini 2.5+ no-tools path). The legacy
|
|
1952
|
+
// fake-tool emulation only runs when format+tools is combined on a model
|
|
1953
|
+
// that doesn't support the native combo.
|
|
1954
|
+
const wantsNativeStructured = Boolean(request.format) && (!hasUserTools || useNativeCombo);
|
|
1955
|
+
if (wantsNativeStructured) {
|
|
1956
|
+
const useJsonSchema = useNativeCombo || request.providerOptions?.useJsonSchema === true;
|
|
1651
1957
|
if (useJsonSchema) {
|
|
1652
1958
|
geminiRequest.config = {
|
|
1653
1959
|
...geminiRequest.config,
|
|
@@ -1663,11 +1969,11 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1663
1969
|
};
|
|
1664
1970
|
}
|
|
1665
1971
|
}
|
|
1666
|
-
//
|
|
1667
|
-
|
|
1972
|
+
// Legacy fake-tool path needs a system-prompt nudge so the model actually
|
|
1973
|
+
// calls the synthetic tool before its final answer.
|
|
1974
|
+
if (request.format && hasUserTools && !useNativeCombo) {
|
|
1668
1975
|
const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
|
|
1669
1976
|
"to output your answer in the required JSON format.";
|
|
1670
|
-
// Add to system instruction if it exists, otherwise create one
|
|
1671
1977
|
const existingSystem = geminiRequest.config?.systemInstruction || "";
|
|
1672
1978
|
geminiRequest.config = {
|
|
1673
1979
|
...geminiRequest.config,
|
|
@@ -1692,8 +1998,14 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1692
1998
|
}
|
|
1693
1999
|
return geminiRequest;
|
|
1694
2000
|
}
|
|
1695
|
-
formatTools(toolkit,
|
|
1696
|
-
|
|
2001
|
+
formatTools(toolkit,
|
|
2002
|
+
// outputSchema is part of the interface contract but Gemini now handles
|
|
2003
|
+
// structured output via `responseJsonSchema`/`responseSchema` (or the
|
|
2004
|
+
// legacy fake-tool injected in buildRequest as a fallback). We no longer
|
|
2005
|
+
// inject a synthetic structured-output tool here.
|
|
2006
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2007
|
+
_outputSchema) {
|
|
2008
|
+
return toolkit.tools.map((tool) => ({
|
|
1697
2009
|
name: tool.name,
|
|
1698
2010
|
description: tool.description,
|
|
1699
2011
|
parameters: {
|
|
@@ -1701,16 +2013,6 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1701
2013
|
type: "object",
|
|
1702
2014
|
},
|
|
1703
2015
|
}));
|
|
1704
|
-
// Add structured output tool if schema is provided
|
|
1705
|
-
if (outputSchema) {
|
|
1706
|
-
tools.push({
|
|
1707
|
-
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
1708
|
-
description: "Output a structured JSON object, " +
|
|
1709
|
-
"use this before your final response to give structured outputs to the user",
|
|
1710
|
-
parameters: outputSchema,
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
return tools;
|
|
1714
2016
|
}
|
|
1715
2017
|
formatOutputSchema(schema) {
|
|
1716
2018
|
let jsonSchema;
|
|
@@ -1737,6 +2039,8 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1737
2039
|
async executeRequest(client, request, signal) {
|
|
1738
2040
|
const genAI = client;
|
|
1739
2041
|
const geminiRequest = request;
|
|
2042
|
+
const wantsNativeCombo = !!geminiRequest.config?.responseJsonSchema &&
|
|
2043
|
+
!!geminiRequest.config?.tools;
|
|
1740
2044
|
try {
|
|
1741
2045
|
// Cast config to any to bypass strict type checking between our internal types
|
|
1742
2046
|
// and the SDK's types. The SDK will validate at runtime.
|
|
@@ -1750,9 +2054,56 @@ class GeminiAdapter extends BaseProviderAdapter {
|
|
|
1750
2054
|
catch (error) {
|
|
1751
2055
|
if (signal?.aborted)
|
|
1752
2056
|
return undefined;
|
|
2057
|
+
// If the model rejected the native responseJsonSchema + tools combo,
|
|
2058
|
+
// cache it and retry with the legacy fake-tool emulation path.
|
|
2059
|
+
if (wantsNativeCombo && isStructuredOutputComboUnsupportedError(error)) {
|
|
2060
|
+
const model = geminiRequest.model;
|
|
2061
|
+
this.rememberModelRejectsStructuredOutputCombo(model);
|
|
2062
|
+
log$1.warn(`[GeminiAdapter] Model ${model} rejected native responseJsonSchema + tools combo; falling back to legacy structured_output tool emulation.`);
|
|
2063
|
+
const fallbackRequest = this.toFallbackStructuredOutputRequest(geminiRequest);
|
|
2064
|
+
const response = await genAI.models.generateContent({
|
|
2065
|
+
model: fallbackRequest.model,
|
|
2066
|
+
contents: fallbackRequest.contents,
|
|
2067
|
+
config: fallbackRequest.config,
|
|
2068
|
+
});
|
|
2069
|
+
return response;
|
|
2070
|
+
}
|
|
1753
2071
|
throw error;
|
|
1754
2072
|
}
|
|
1755
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Rebuild a Gemini 3 native-combo request without `responseJsonSchema`/
|
|
2076
|
+
* `responseMimeType`, swapping in the legacy fake-tool emulation. Used as
|
|
2077
|
+
* a runtime fallback when a Gemini 3 model rejects the combo.
|
|
2078
|
+
*/
|
|
2079
|
+
toFallbackStructuredOutputRequest(request) {
|
|
2080
|
+
if (!request.config?.responseJsonSchema)
|
|
2081
|
+
return request;
|
|
2082
|
+
const schema = request.config.responseJsonSchema;
|
|
2083
|
+
const newConfig = { ...request.config };
|
|
2084
|
+
delete newConfig.responseJsonSchema;
|
|
2085
|
+
delete newConfig.responseMimeType;
|
|
2086
|
+
const fakeTool = {
|
|
2087
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
2088
|
+
description: "Output a structured JSON object, " +
|
|
2089
|
+
"use this before your final response to give structured outputs to the user",
|
|
2090
|
+
parameters: schema,
|
|
2091
|
+
};
|
|
2092
|
+
const existingDeclarations = newConfig.tools?.[0]?.functionDeclarations ?? [];
|
|
2093
|
+
newConfig.tools = [
|
|
2094
|
+
{ functionDeclarations: [...existingDeclarations, fakeTool] },
|
|
2095
|
+
];
|
|
2096
|
+
const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
|
|
2097
|
+
"to output your answer in the required JSON format.";
|
|
2098
|
+
const existingSystem = newConfig.systemInstruction || "";
|
|
2099
|
+
newConfig.systemInstruction = existingSystem
|
|
2100
|
+
? `${existingSystem}\n\n${structuredOutputInstruction}`
|
|
2101
|
+
: structuredOutputInstruction;
|
|
2102
|
+
return {
|
|
2103
|
+
...request,
|
|
2104
|
+
config: newConfig,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
1756
2107
|
async *executeStreamRequest(client, request, signal) {
|
|
1757
2108
|
const genAI = client;
|
|
1758
2109
|
const geminiRequest = request;
|
|
@@ -2312,6 +2663,27 @@ const NOT_RETRYABLE_ERROR_TYPES = [
|
|
|
2312
2663
|
openai.RateLimitError,
|
|
2313
2664
|
openai.UnprocessableEntityError,
|
|
2314
2665
|
];
|
|
2666
|
+
// Models known not to accept `temperature`.
|
|
2667
|
+
// Patterns (not exact names) so dated variants and future releases are covered
|
|
2668
|
+
// without code changes — OpenAI is removing temperature on newer reasoning
|
|
2669
|
+
// models.
|
|
2670
|
+
const MODELS_WITHOUT_TEMPERATURE = [
|
|
2671
|
+
/^gpt-5\.5/, // gpt-5.5 series deprecated temperature
|
|
2672
|
+
/^o\d/, // o-series reasoning models (o1, o3, o4, ...)
|
|
2673
|
+
];
|
|
2674
|
+
function isTemperatureDeprecationError(error) {
|
|
2675
|
+
if (!error || typeof error !== "object")
|
|
2676
|
+
return false;
|
|
2677
|
+
if (!(error instanceof openai.BadRequestError) &&
|
|
2678
|
+
error.status !== 400) {
|
|
2679
|
+
return false;
|
|
2680
|
+
}
|
|
2681
|
+
const messages = [
|
|
2682
|
+
error.message,
|
|
2683
|
+
error.error?.message,
|
|
2684
|
+
].filter((m) => typeof m === "string");
|
|
2685
|
+
return messages.some((m) => m.toLowerCase().includes("temperature"));
|
|
2686
|
+
}
|
|
2315
2687
|
//
|
|
2316
2688
|
//
|
|
2317
2689
|
// Main
|
|
@@ -2326,6 +2698,20 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
2326
2698
|
super(...arguments);
|
|
2327
2699
|
this.name = PROVIDER.OPENAI.NAME;
|
|
2328
2700
|
this.defaultModel = PROVIDER.OPENAI.MODEL.DEFAULT;
|
|
2701
|
+
// Session-level cache of models observed to reject `temperature` at runtime.
|
|
2702
|
+
// Populated by executeRequest on 400 errors so repeat calls skip the param.
|
|
2703
|
+
this.runtimeNoTemperatureModels = new Set();
|
|
2704
|
+
}
|
|
2705
|
+
rememberModelRejectsTemperature(model) {
|
|
2706
|
+
this.runtimeNoTemperatureModels.add(model);
|
|
2707
|
+
}
|
|
2708
|
+
clearRuntimeNoTemperatureModels() {
|
|
2709
|
+
this.runtimeNoTemperatureModels.clear();
|
|
2710
|
+
}
|
|
2711
|
+
supportsTemperature(model) {
|
|
2712
|
+
if (this.runtimeNoTemperatureModels.has(model))
|
|
2713
|
+
return false;
|
|
2714
|
+
return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
|
|
2329
2715
|
}
|
|
2330
2716
|
//
|
|
2331
2717
|
// Request Building
|
|
@@ -2369,6 +2755,11 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
2369
2755
|
if (request.temperature !== undefined) {
|
|
2370
2756
|
openaiRequest.temperature = request.temperature;
|
|
2371
2757
|
}
|
|
2758
|
+
// Strip temperature for models that don't support it (denylist + runtime cache)
|
|
2759
|
+
if (openaiRequest.temperature !== undefined &&
|
|
2760
|
+
!this.supportsTemperature(openaiRequest.model)) {
|
|
2761
|
+
delete openaiRequest.temperature;
|
|
2762
|
+
}
|
|
2372
2763
|
return openaiRequest;
|
|
2373
2764
|
}
|
|
2374
2765
|
formatTools(toolkit, _outputSchema) {
|
|
@@ -2418,14 +2809,21 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
2418
2809
|
//
|
|
2419
2810
|
async executeRequest(client, request, signal) {
|
|
2420
2811
|
const openai = client;
|
|
2812
|
+
const openaiRequest = request;
|
|
2421
2813
|
try {
|
|
2422
|
-
return await openai.responses.create(
|
|
2423
|
-
// @ts-expect-error OpenAI SDK types don't match our request format exactly
|
|
2424
|
-
request, signal ? { signal } : undefined);
|
|
2814
|
+
return await openai.responses.create(openaiRequest, signal ? { signal } : undefined);
|
|
2425
2815
|
}
|
|
2426
2816
|
catch (error) {
|
|
2427
2817
|
if (signal?.aborted)
|
|
2428
2818
|
return undefined;
|
|
2819
|
+
// If the model rejected `temperature`, cache it and retry without the param
|
|
2820
|
+
if (openaiRequest.temperature !== undefined &&
|
|
2821
|
+
isTemperatureDeprecationError(error)) {
|
|
2822
|
+
this.rememberModelRejectsTemperature(openaiRequest.model);
|
|
2823
|
+
const retryRequest = { ...openaiRequest };
|
|
2824
|
+
delete retryRequest.temperature;
|
|
2825
|
+
return await openai.responses.create(retryRequest, signal ? { signal } : undefined);
|
|
2826
|
+
}
|
|
2429
2827
|
throw error;
|
|
2430
2828
|
}
|
|
2431
2829
|
}
|
|
@@ -2753,6 +3151,23 @@ const openAiAdapter = new OpenAiAdapter();
|
|
|
2753
3151
|
// Constants
|
|
2754
3152
|
//
|
|
2755
3153
|
const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
|
|
3154
|
+
const STRUCTURED_OUTPUT_SCHEMA_NAME = "response";
|
|
3155
|
+
/**
|
|
3156
|
+
* Detect 4xx errors that indicate the model itself does not support
|
|
3157
|
+
* `response_format: json_schema`. Mirrors the Anthropic pattern: we only
|
|
3158
|
+
* trigger the fake-tool fallback when the failure is plausibly a capability
|
|
3159
|
+
* gap, not a generic 400.
|
|
3160
|
+
*/
|
|
3161
|
+
function isStructuredOutputUnsupportedError(error) {
|
|
3162
|
+
if (!error || typeof error !== "object")
|
|
3163
|
+
return false;
|
|
3164
|
+
const err = error;
|
|
3165
|
+
const status = err.status ?? err.statusCode;
|
|
3166
|
+
if (status !== 400 && status !== 422)
|
|
3167
|
+
return false;
|
|
3168
|
+
const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
|
|
3169
|
+
return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
|
|
3170
|
+
}
|
|
2756
3171
|
/**
|
|
2757
3172
|
* Convert standardized content items to OpenRouter format
|
|
2758
3173
|
* Note: OpenRouter does not support native file/image uploads.
|
|
@@ -2791,6 +3206,32 @@ function convertContentToOpenRouter(content) {
|
|
|
2791
3206
|
// OpenRouter SDK error types based on HTTP status codes
|
|
2792
3207
|
const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
|
|
2793
3208
|
const RATE_LIMIT_STATUS_CODE = 429;
|
|
3209
|
+
/**
|
|
3210
|
+
* Walk the JSON schema and force `additionalProperties: false` on every
|
|
3211
|
+
* object node. Required by the OpenAI-style json_schema response_format
|
|
3212
|
+
* (which OpenRouter accepts) when `strict: true`.
|
|
3213
|
+
*/
|
|
3214
|
+
function enforceAdditionalPropertiesFalse(schema) {
|
|
3215
|
+
const stack = [schema];
|
|
3216
|
+
while (stack.length > 0) {
|
|
3217
|
+
const node = stack.pop();
|
|
3218
|
+
if (node.type === "object") {
|
|
3219
|
+
node.additionalProperties = false;
|
|
3220
|
+
}
|
|
3221
|
+
for (const value of Object.values(node)) {
|
|
3222
|
+
if (Array.isArray(value)) {
|
|
3223
|
+
for (const entry of value) {
|
|
3224
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
3225
|
+
stack.push(entry);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
else if (value && typeof value === "object") {
|
|
3230
|
+
stack.push(value);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2794
3235
|
//
|
|
2795
3236
|
//
|
|
2796
3237
|
// Main
|
|
@@ -2806,6 +3247,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2806
3247
|
super(...arguments);
|
|
2807
3248
|
this.name = PROVIDER.OPENROUTER.NAME;
|
|
2808
3249
|
this.defaultModel = PROVIDER.OPENROUTER.MODEL.DEFAULT;
|
|
3250
|
+
// Session-level cache of models observed to reject native
|
|
3251
|
+
// `response_format: json_schema`. When a model is in this set, buildRequest
|
|
3252
|
+
// engages the legacy fake-tool path instead of native structured output.
|
|
3253
|
+
this.runtimeNoStructuredOutputModels = new Set();
|
|
3254
|
+
}
|
|
3255
|
+
rememberModelRejectsStructuredOutput(model) {
|
|
3256
|
+
this.runtimeNoStructuredOutputModels.add(model);
|
|
3257
|
+
}
|
|
3258
|
+
clearRuntimeNoStructuredOutputModels() {
|
|
3259
|
+
this.runtimeNoStructuredOutputModels.clear();
|
|
3260
|
+
}
|
|
3261
|
+
supportsStructuredOutput(model) {
|
|
3262
|
+
return !this.runtimeNoStructuredOutputModels.has(model);
|
|
2809
3263
|
}
|
|
2810
3264
|
//
|
|
2811
3265
|
// Request Building
|
|
@@ -2827,8 +3281,23 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2827
3281
|
if (request.user) {
|
|
2828
3282
|
openRouterRequest.user = request.user;
|
|
2829
3283
|
}
|
|
2830
|
-
|
|
2831
|
-
openRouterRequest.
|
|
3284
|
+
const useFallbackStructuredOutput = Boolean(request.format) &&
|
|
3285
|
+
!this.supportsStructuredOutput(openRouterRequest.model);
|
|
3286
|
+
const allTools = request.tools
|
|
3287
|
+
? [...request.tools]
|
|
3288
|
+
: [];
|
|
3289
|
+
if (useFallbackStructuredOutput && request.format) {
|
|
3290
|
+
log$1.log.warn(`[OpenRouterAdapter] Using legacy structured_output tool fallback for model ${openRouterRequest.model}; native response_format previously rejected for this model.`);
|
|
3291
|
+
allTools.push({
|
|
3292
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
3293
|
+
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
3294
|
+
"After gathering all necessary information (including results from other tools), " +
|
|
3295
|
+
"call this tool with the structured data to complete the request.",
|
|
3296
|
+
parameters: request.format,
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
if (allTools.length > 0) {
|
|
3300
|
+
openRouterRequest.tools = allTools.map((tool) => ({
|
|
2832
3301
|
type: "function",
|
|
2833
3302
|
function: {
|
|
2834
3303
|
name: tool.name,
|
|
@@ -2840,6 +3309,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2840
3309
|
// The structured_output tool prompt already emphasizes it must be called
|
|
2841
3310
|
openRouterRequest.tool_choice = "auto";
|
|
2842
3311
|
}
|
|
3312
|
+
// Native structured output: send schema as `response_format`. The legacy
|
|
3313
|
+
// tool-emulation path is engaged only as a runtime fallback for models the
|
|
3314
|
+
// API has flagged as not supporting native json_schema.
|
|
3315
|
+
if (request.format && !useFallbackStructuredOutput) {
|
|
3316
|
+
openRouterRequest.response_format = {
|
|
3317
|
+
type: "json_schema",
|
|
3318
|
+
json_schema: {
|
|
3319
|
+
name: STRUCTURED_OUTPUT_SCHEMA_NAME,
|
|
3320
|
+
schema: request.format,
|
|
3321
|
+
strict: true,
|
|
3322
|
+
},
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
2843
3325
|
if (request.providerOptions) {
|
|
2844
3326
|
Object.assign(openRouterRequest, request.providerOptions);
|
|
2845
3327
|
}
|
|
@@ -2850,24 +3332,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2850
3332
|
}
|
|
2851
3333
|
return openRouterRequest;
|
|
2852
3334
|
}
|
|
2853
|
-
formatTools(toolkit,
|
|
2854
|
-
|
|
3335
|
+
formatTools(toolkit,
|
|
3336
|
+
// outputSchema is part of the interface contract but OpenRouter now uses
|
|
3337
|
+
// native `response_format` (set in buildRequest), so we no longer inject a
|
|
3338
|
+
// synthetic structured-output tool here. The legacy fake-tool injection
|
|
3339
|
+
// happens in buildRequest only as a runtime fallback.
|
|
3340
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3341
|
+
_outputSchema) {
|
|
3342
|
+
return toolkit.tools.map((tool) => ({
|
|
2855
3343
|
name: tool.name,
|
|
2856
3344
|
description: tool.description,
|
|
2857
3345
|
parameters: tool.parameters,
|
|
2858
3346
|
}));
|
|
2859
|
-
// Add structured output tool if schema is provided
|
|
2860
|
-
// (OpenRouter doesn't have native structured output like OpenAI, so use tool approach)
|
|
2861
|
-
if (outputSchema) {
|
|
2862
|
-
tools.push({
|
|
2863
|
-
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
2864
|
-
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
2865
|
-
"After gathering all necessary information (including results from other tools), " +
|
|
2866
|
-
"call this tool with the structured data to complete the request.",
|
|
2867
|
-
parameters: outputSchema,
|
|
2868
|
-
});
|
|
2869
|
-
}
|
|
2870
|
-
return tools;
|
|
2871
3347
|
}
|
|
2872
3348
|
formatOutputSchema(schema) {
|
|
2873
3349
|
let jsonSchema;
|
|
@@ -2890,6 +3366,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2890
3366
|
if (jsonSchema.$schema) {
|
|
2891
3367
|
delete jsonSchema.$schema;
|
|
2892
3368
|
}
|
|
3369
|
+
// OpenRouter (and most backends behind it) require additionalProperties:
|
|
3370
|
+
// false on every object when using strict json_schema response_format.
|
|
3371
|
+
enforceAdditionalPropertiesFalse(jsonSchema);
|
|
2893
3372
|
return jsonSchema;
|
|
2894
3373
|
}
|
|
2895
3374
|
//
|
|
@@ -2898,34 +3377,99 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
2898
3377
|
async executeRequest(client, request, signal) {
|
|
2899
3378
|
const openRouter = client;
|
|
2900
3379
|
const openRouterRequest = request;
|
|
3380
|
+
const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
|
|
2901
3381
|
try {
|
|
2902
|
-
const response = await openRouter.chat.send({
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
toolChoice: openRouterRequest.tool_choice,
|
|
2907
|
-
user: openRouterRequest.user,
|
|
2908
|
-
}, signal ? { signal } : undefined);
|
|
3382
|
+
const response = (await openRouter.chat.send(this.toSdkChatParams(openRouterRequest), signal ? { signal } : undefined));
|
|
3383
|
+
if (wantsStructuredOutput) {
|
|
3384
|
+
response.__jaypieStructuredOutput = true;
|
|
3385
|
+
}
|
|
2909
3386
|
return response;
|
|
2910
3387
|
}
|
|
2911
3388
|
catch (error) {
|
|
2912
3389
|
if (signal?.aborted)
|
|
2913
3390
|
return undefined;
|
|
3391
|
+
// If the model rejected `response_format`, cache it and retry with the
|
|
3392
|
+
// legacy fake-tool emulation path.
|
|
3393
|
+
if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {
|
|
3394
|
+
const model = openRouterRequest.model;
|
|
3395
|
+
this.rememberModelRejectsStructuredOutput(model);
|
|
3396
|
+
log$1.log.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
|
|
3397
|
+
const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
|
|
3398
|
+
return (await openRouter.chat.send(this.toSdkChatParams(fallbackRequest), signal ? { signal } : undefined));
|
|
3399
|
+
}
|
|
2914
3400
|
throw error;
|
|
2915
3401
|
}
|
|
2916
3402
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3403
|
+
/**
|
|
3404
|
+
* Translate our internal snake_case `OpenRouterRequest` into the SDK's
|
|
3405
|
+
* camelCase shape, forwarding only the fields we care about (the SDK
|
|
3406
|
+
* silently strips unknown fields).
|
|
3407
|
+
*/
|
|
3408
|
+
toSdkChatParams(openRouterRequest) {
|
|
3409
|
+
const params = {
|
|
2922
3410
|
model: openRouterRequest.model,
|
|
2923
3411
|
messages: openRouterRequest.messages,
|
|
2924
3412
|
tools: openRouterRequest.tools,
|
|
2925
3413
|
toolChoice: openRouterRequest.tool_choice,
|
|
2926
3414
|
user: openRouterRequest.user,
|
|
3415
|
+
};
|
|
3416
|
+
if (openRouterRequest.response_format) {
|
|
3417
|
+
const format = openRouterRequest.response_format;
|
|
3418
|
+
if (format.type === "json_schema") {
|
|
3419
|
+
params.responseFormat = {
|
|
3420
|
+
type: "json_schema",
|
|
3421
|
+
jsonSchema: format.json_schema,
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
else {
|
|
3425
|
+
params.responseFormat = format;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
const temperature = openRouterRequest.temperature;
|
|
3429
|
+
if (temperature !== undefined) {
|
|
3430
|
+
params.temperature = temperature;
|
|
3431
|
+
}
|
|
3432
|
+
return params;
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Rebuild a structured-output request without `response_format`, swapping in
|
|
3436
|
+
* the legacy fake-tool emulation. Used as a runtime fallback when a model
|
|
3437
|
+
* rejects native json_schema.
|
|
3438
|
+
*/
|
|
3439
|
+
toFallbackStructuredOutputRequest(request) {
|
|
3440
|
+
if (!request.response_format ||
|
|
3441
|
+
request.response_format.type !== "json_schema") {
|
|
3442
|
+
return request;
|
|
3443
|
+
}
|
|
3444
|
+
const { response_format, ...rest } = request;
|
|
3445
|
+
const fallbackRequest = { ...rest };
|
|
3446
|
+
const schema = response_format.json_schema.schema;
|
|
3447
|
+
const fakeTool = {
|
|
3448
|
+
type: "function",
|
|
3449
|
+
function: {
|
|
3450
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
3451
|
+
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
3452
|
+
"After gathering all necessary information (including results from other tools), " +
|
|
3453
|
+
"call this tool with the structured data to complete the request.",
|
|
3454
|
+
parameters: schema,
|
|
3455
|
+
},
|
|
3456
|
+
};
|
|
3457
|
+
fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
|
|
3458
|
+
fallbackRequest.tool_choice = "auto";
|
|
3459
|
+
return fallbackRequest;
|
|
3460
|
+
}
|
|
3461
|
+
async *executeStreamRequest(client, request, signal) {
|
|
3462
|
+
const openRouter = client;
|
|
3463
|
+
const openRouterRequest = request;
|
|
3464
|
+
// Use chat.send with stream: true for streaming responses.
|
|
3465
|
+
// Cast the result to AsyncIterable: when stream: true, the SDK returns a
|
|
3466
|
+
// stream we can iterate, but the typed result is the union with the
|
|
3467
|
+
// non-stream response.
|
|
3468
|
+
const streamParams = {
|
|
3469
|
+
...this.toSdkChatParams(openRouterRequest),
|
|
2927
3470
|
stream: true,
|
|
2928
|
-
}
|
|
3471
|
+
};
|
|
3472
|
+
const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
|
|
2929
3473
|
// Track current tool call being built
|
|
2930
3474
|
let currentToolCall = null;
|
|
2931
3475
|
// Track usage for final chunk
|
|
@@ -3204,6 +3748,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3204
3748
|
}
|
|
3205
3749
|
hasStructuredOutput(response) {
|
|
3206
3750
|
const openRouterResponse = response;
|
|
3751
|
+
// Native path: executeRequest annotates the response when we sent
|
|
3752
|
+
// `response_format`, so we can detect intent statelessly.
|
|
3753
|
+
if (openRouterResponse.__jaypieStructuredOutput) {
|
|
3754
|
+
return this.extractStructuredOutput(response) !== undefined;
|
|
3755
|
+
}
|
|
3756
|
+
// Fallback path: legacy fake-tool emulation, kept for models that the
|
|
3757
|
+
// runtime has cached as not supporting native `response_format`.
|
|
3207
3758
|
const choice = openRouterResponse.choices[0];
|
|
3208
3759
|
// SDK returns camelCase (toolCalls)
|
|
3209
3760
|
if (!choice?.message?.toolCalls?.length) {
|
|
@@ -3215,6 +3766,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
3215
3766
|
}
|
|
3216
3767
|
extractStructuredOutput(response) {
|
|
3217
3768
|
const openRouterResponse = response;
|
|
3769
|
+
if (openRouterResponse.__jaypieStructuredOutput) {
|
|
3770
|
+
const choice = openRouterResponse.choices[0];
|
|
3771
|
+
const content = choice?.message?.content;
|
|
3772
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
3773
|
+
return undefined;
|
|
3774
|
+
}
|
|
3775
|
+
try {
|
|
3776
|
+
return JSON.parse(content);
|
|
3777
|
+
}
|
|
3778
|
+
catch {
|
|
3779
|
+
return undefined;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
// Fallback path: legacy fake-tool emulation
|
|
3218
3783
|
const choice = openRouterResponse.choices[0];
|
|
3219
3784
|
// SDK returns camelCase (toolCalls)
|
|
3220
3785
|
if (!choice?.message?.toolCalls?.length) {
|
|
@@ -5235,55 +5800,50 @@ async function createTextCompletion$1(client, messages, model, systemMessage) {
|
|
|
5235
5800
|
log$1.log.trace(`Assistant reply: ${text.length} characters`);
|
|
5236
5801
|
return text;
|
|
5237
5802
|
}
|
|
5238
|
-
// Structured output completion
|
|
5803
|
+
// Structured output completion using Anthropic's native `output_config.format`
|
|
5804
|
+
// field. Returns a JsonObject parsed and validated against the caller's
|
|
5805
|
+
// Zod schema.
|
|
5239
5806
|
async function createStructuredCompletion$1(client, messages, model, responseSchema, systemMessage) {
|
|
5240
|
-
log$1.log.trace("Using structured output");
|
|
5241
|
-
// Get the JSON schema for the response
|
|
5807
|
+
log$1.log.trace("Using native structured output");
|
|
5242
5808
|
const schema = responseSchema instanceof v4.z.ZodType
|
|
5243
5809
|
? responseSchema
|
|
5244
5810
|
: naturalZodSchema(responseSchema);
|
|
5245
|
-
|
|
5246
|
-
const
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
try {
|
|
5267
|
-
// Parse the JSON response
|
|
5268
|
-
const jsonStr = jsonMatch[1] || jsonMatch[0];
|
|
5269
|
-
const result = JSON.parse(jsonStr);
|
|
5270
|
-
if (!schema.parse(result)) {
|
|
5271
|
-
throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
|
|
5272
|
-
}
|
|
5273
|
-
log$1.log.trace("Received structured response", { result });
|
|
5274
|
-
return result;
|
|
5275
|
-
}
|
|
5276
|
-
catch {
|
|
5277
|
-
throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
|
|
5278
|
-
}
|
|
5279
|
-
}
|
|
5280
|
-
// If we can't extract JSON
|
|
5811
|
+
const jsonSchema = v4.z.toJSONSchema(schema);
|
|
5812
|
+
const params = {
|
|
5813
|
+
model,
|
|
5814
|
+
messages,
|
|
5815
|
+
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
5816
|
+
output_config: {
|
|
5817
|
+
format: { type: "json_schema", schema: jsonSchema },
|
|
5818
|
+
},
|
|
5819
|
+
};
|
|
5820
|
+
if (systemMessage) {
|
|
5821
|
+
params.system = systemMessage;
|
|
5822
|
+
}
|
|
5823
|
+
const response = (await client.messages.create(params));
|
|
5824
|
+
if (response.stop_reason === "refusal") {
|
|
5825
|
+
throw new Error("Anthropic refused the structured-output request (stop_reason=refusal)");
|
|
5826
|
+
}
|
|
5827
|
+
if (response.stop_reason === "max_tokens") {
|
|
5828
|
+
throw new Error("Anthropic structured-output response was truncated (stop_reason=max_tokens); increase max_tokens");
|
|
5829
|
+
}
|
|
5830
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
5831
|
+
if (!textBlock) {
|
|
5281
5832
|
throw new Error("Failed to parse structured response from Anthropic");
|
|
5282
5833
|
}
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5834
|
+
let parsed;
|
|
5835
|
+
try {
|
|
5836
|
+
parsed = JSON.parse(textBlock.text);
|
|
5837
|
+
}
|
|
5838
|
+
catch {
|
|
5839
|
+
throw new Error("Failed to parse structured response from Anthropic: " + textBlock.text);
|
|
5840
|
+
}
|
|
5841
|
+
const validation = schema.safeParse(parsed);
|
|
5842
|
+
if (!validation.success) {
|
|
5843
|
+
throw new Error(`JSON response from Anthropic does not match schema: ${textBlock.text}`);
|
|
5286
5844
|
}
|
|
5845
|
+
log$1.log.trace("Received structured response", { result: validation.data });
|
|
5846
|
+
return validation.data;
|
|
5287
5847
|
}
|
|
5288
5848
|
|
|
5289
5849
|
// Maps Jaypie roles to Anthropic roles
|