@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/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.4",
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
  */
@@ -1067,11 +1197,11 @@ const NOT_RETRYABLE_ERROR_NAMES = [
1067
1197
  // Patterns (not exact names) so dated variants and future releases are covered
1068
1198
  // without code changes — Anthropic is trending toward removing temperature on
1069
1199
  // newer Claude models.
1070
- const MODELS_WITHOUT_TEMPERATURE = [
1200
+ const MODELS_WITHOUT_TEMPERATURE$1 = [
1071
1201
  /^claude-opus-4-[789]/,
1072
1202
  /^claude-opus-[5-9]/,
1073
1203
  ];
1074
- function isTemperatureDeprecationError(error) {
1204
+ function isTemperatureDeprecationError$1(error) {
1075
1205
  if (!error || typeof error !== "object")
1076
1206
  return false;
1077
1207
  const err = error;
@@ -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,10 +1262,19 @@ 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
- return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
1274
+ return !MODELS_WITHOUT_TEMPERATURE$1.some((pattern) => pattern.test(model));
1275
+ }
1276
+ supportsStructuredOutput(model) {
1277
+ return !this.runtimeNoStructuredOutputModels.has(model);
1112
1278
  }
1113
1279
  //
1114
1280
  // Request Building
@@ -1177,8 +1343,22 @@ class AnthropicAdapter extends BaseProviderAdapter {
1177
1343
  if (request.system) {
1178
1344
  anthropicRequest.system = request.system;
1179
1345
  }
1180
- if (request.tools && request.tools.length > 0) {
1181
- anthropicRequest.tools = request.tools.map((tool) => ({
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
- // Determine tool choice based on whether structured output is requested
1191
- const hasStructuredOutput = request.tools.some((t) => t.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
1192
- anthropicRequest.tool_choice = {
1193
- type: hasStructuredOutput ? "any" : "auto",
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, outputSchema) {
1211
- const tools = toolkit.tools.map((tool) => ({
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
- // Remove $schema property (causes issues with validator)
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,25 +1436,70 @@ 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
- return (await anthropic.messages.create(anthropicRequest, signal ? { signal } : undefined));
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)
1264
1449
  return undefined;
1265
1450
  // If the model rejected `temperature`, cache it and retry without the param
1266
1451
  if (anthropicRequest.temperature !== undefined &&
1267
- isTemperatureDeprecationError(error)) {
1452
+ isTemperatureDeprecationError$1(error)) {
1268
1453
  this.rememberModelRejectsTemperature(anthropicRequest.model);
1269
1454
  const retryRequest = { ...anthropicRequest };
1270
1455
  delete retryRequest.temperature;
1271
- return (await anthropic.messages.create(retryRequest, signal ? { signal } : undefined));
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,
@@ -1285,7 +1510,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
1285
1510
  }
1286
1511
  catch (error) {
1287
1512
  if (streamRequest.temperature !== undefined &&
1288
- isTemperatureDeprecationError(error)) {
1513
+ isTemperatureDeprecationError$1(error)) {
1289
1514
  this.rememberModelRejectsTemperature(streamRequest.model);
1290
1515
  streamRequest = {
1291
1516
  ...streamRequest,
@@ -1534,13 +1759,39 @@ class AnthropicAdapter extends BaseProviderAdapter {
1534
1759
  }
1535
1760
  hasStructuredOutput(response) {
1536
1761
  const anthropicResponse = response;
1537
- // Check if the last content block is a tool_use with structured_output
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
- // Add tools if provided
1633
- if (request.tools && request.tools.length > 0) {
1634
- const functionDeclarations = request.tools.map((tool) => ({
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
- // Add structured output format if provided (but NOT when tools are present)
1645
- // Gemini doesn't support combining function calling with responseMimeType: 'application/json'
1646
- // When tools are present, structured output is handled via the structured_output tool
1647
- if (request.format && !(request.tools && request.tools.length > 0)) {
1648
- const useJsonSchema = request.providerOptions?.useJsonSchema === true;
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
- // When format is specified with tools, add instruction to use structured_output tool
1665
- if (request.format && request.tools && request.tools.length > 0) {
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, outputSchema) {
1694
- const tools = toolkit.tools.map((tool) => ({
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;
@@ -2310,6 +2661,27 @@ const NOT_RETRYABLE_ERROR_TYPES = [
2310
2661
  RateLimitError,
2311
2662
  UnprocessableEntityError,
2312
2663
  ];
2664
+ // Models known not to accept `temperature`.
2665
+ // Patterns (not exact names) so dated variants and future releases are covered
2666
+ // without code changes — OpenAI is removing temperature on newer reasoning
2667
+ // models.
2668
+ const MODELS_WITHOUT_TEMPERATURE = [
2669
+ /^gpt-5\.5/, // gpt-5.5 series deprecated temperature
2670
+ /^o\d/, // o-series reasoning models (o1, o3, o4, ...)
2671
+ ];
2672
+ function isTemperatureDeprecationError(error) {
2673
+ if (!error || typeof error !== "object")
2674
+ return false;
2675
+ if (!(error instanceof BadRequestError) &&
2676
+ error.status !== 400) {
2677
+ return false;
2678
+ }
2679
+ const messages = [
2680
+ error.message,
2681
+ error.error?.message,
2682
+ ].filter((m) => typeof m === "string");
2683
+ return messages.some((m) => m.toLowerCase().includes("temperature"));
2684
+ }
2313
2685
  //
2314
2686
  //
2315
2687
  // Main
@@ -2324,6 +2696,20 @@ class OpenAiAdapter extends BaseProviderAdapter {
2324
2696
  super(...arguments);
2325
2697
  this.name = PROVIDER.OPENAI.NAME;
2326
2698
  this.defaultModel = PROVIDER.OPENAI.MODEL.DEFAULT;
2699
+ // Session-level cache of models observed to reject `temperature` at runtime.
2700
+ // Populated by executeRequest on 400 errors so repeat calls skip the param.
2701
+ this.runtimeNoTemperatureModels = new Set();
2702
+ }
2703
+ rememberModelRejectsTemperature(model) {
2704
+ this.runtimeNoTemperatureModels.add(model);
2705
+ }
2706
+ clearRuntimeNoTemperatureModels() {
2707
+ this.runtimeNoTemperatureModels.clear();
2708
+ }
2709
+ supportsTemperature(model) {
2710
+ if (this.runtimeNoTemperatureModels.has(model))
2711
+ return false;
2712
+ return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
2327
2713
  }
2328
2714
  //
2329
2715
  // Request Building
@@ -2367,6 +2753,11 @@ class OpenAiAdapter extends BaseProviderAdapter {
2367
2753
  if (request.temperature !== undefined) {
2368
2754
  openaiRequest.temperature = request.temperature;
2369
2755
  }
2756
+ // Strip temperature for models that don't support it (denylist + runtime cache)
2757
+ if (openaiRequest.temperature !== undefined &&
2758
+ !this.supportsTemperature(openaiRequest.model)) {
2759
+ delete openaiRequest.temperature;
2760
+ }
2370
2761
  return openaiRequest;
2371
2762
  }
2372
2763
  formatTools(toolkit, _outputSchema) {
@@ -2416,14 +2807,21 @@ class OpenAiAdapter extends BaseProviderAdapter {
2416
2807
  //
2417
2808
  async executeRequest(client, request, signal) {
2418
2809
  const openai = client;
2810
+ const openaiRequest = request;
2419
2811
  try {
2420
- return await openai.responses.create(
2421
- // @ts-expect-error OpenAI SDK types don't match our request format exactly
2422
- request, signal ? { signal } : undefined);
2812
+ return await openai.responses.create(openaiRequest, signal ? { signal } : undefined);
2423
2813
  }
2424
2814
  catch (error) {
2425
2815
  if (signal?.aborted)
2426
2816
  return undefined;
2817
+ // If the model rejected `temperature`, cache it and retry without the param
2818
+ if (openaiRequest.temperature !== undefined &&
2819
+ isTemperatureDeprecationError(error)) {
2820
+ this.rememberModelRejectsTemperature(openaiRequest.model);
2821
+ const retryRequest = { ...openaiRequest };
2822
+ delete retryRequest.temperature;
2823
+ return await openai.responses.create(retryRequest, signal ? { signal } : undefined);
2824
+ }
2427
2825
  throw error;
2428
2826
  }
2429
2827
  }
@@ -2751,6 +3149,23 @@ const openAiAdapter = new OpenAiAdapter();
2751
3149
  // Constants
2752
3150
  //
2753
3151
  const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
3152
+ const STRUCTURED_OUTPUT_SCHEMA_NAME = "response";
3153
+ /**
3154
+ * Detect 4xx errors that indicate the model itself does not support
3155
+ * `response_format: json_schema`. Mirrors the Anthropic pattern: we only
3156
+ * trigger the fake-tool fallback when the failure is plausibly a capability
3157
+ * gap, not a generic 400.
3158
+ */
3159
+ function isStructuredOutputUnsupportedError(error) {
3160
+ if (!error || typeof error !== "object")
3161
+ return false;
3162
+ const err = error;
3163
+ const status = err.status ?? err.statusCode;
3164
+ if (status !== 400 && status !== 422)
3165
+ return false;
3166
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
3167
+ return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
3168
+ }
2754
3169
  /**
2755
3170
  * Convert standardized content items to OpenRouter format
2756
3171
  * Note: OpenRouter does not support native file/image uploads.
@@ -2789,6 +3204,32 @@ function convertContentToOpenRouter(content) {
2789
3204
  // OpenRouter SDK error types based on HTTP status codes
2790
3205
  const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
2791
3206
  const RATE_LIMIT_STATUS_CODE = 429;
3207
+ /**
3208
+ * Walk the JSON schema and force `additionalProperties: false` on every
3209
+ * object node. Required by the OpenAI-style json_schema response_format
3210
+ * (which OpenRouter accepts) when `strict: true`.
3211
+ */
3212
+ function enforceAdditionalPropertiesFalse(schema) {
3213
+ const stack = [schema];
3214
+ while (stack.length > 0) {
3215
+ const node = stack.pop();
3216
+ if (node.type === "object") {
3217
+ node.additionalProperties = false;
3218
+ }
3219
+ for (const value of Object.values(node)) {
3220
+ if (Array.isArray(value)) {
3221
+ for (const entry of value) {
3222
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
3223
+ stack.push(entry);
3224
+ }
3225
+ }
3226
+ }
3227
+ else if (value && typeof value === "object") {
3228
+ stack.push(value);
3229
+ }
3230
+ }
3231
+ }
3232
+ }
2792
3233
  //
2793
3234
  //
2794
3235
  // Main
@@ -2804,6 +3245,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2804
3245
  super(...arguments);
2805
3246
  this.name = PROVIDER.OPENROUTER.NAME;
2806
3247
  this.defaultModel = PROVIDER.OPENROUTER.MODEL.DEFAULT;
3248
+ // Session-level cache of models observed to reject native
3249
+ // `response_format: json_schema`. When a model is in this set, buildRequest
3250
+ // engages the legacy fake-tool path instead of native structured output.
3251
+ this.runtimeNoStructuredOutputModels = new Set();
3252
+ }
3253
+ rememberModelRejectsStructuredOutput(model) {
3254
+ this.runtimeNoStructuredOutputModels.add(model);
3255
+ }
3256
+ clearRuntimeNoStructuredOutputModels() {
3257
+ this.runtimeNoStructuredOutputModels.clear();
3258
+ }
3259
+ supportsStructuredOutput(model) {
3260
+ return !this.runtimeNoStructuredOutputModels.has(model);
2807
3261
  }
2808
3262
  //
2809
3263
  // Request Building
@@ -2825,8 +3279,23 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2825
3279
  if (request.user) {
2826
3280
  openRouterRequest.user = request.user;
2827
3281
  }
2828
- if (request.tools && request.tools.length > 0) {
2829
- openRouterRequest.tools = request.tools.map((tool) => ({
3282
+ const useFallbackStructuredOutput = Boolean(request.format) &&
3283
+ !this.supportsStructuredOutput(openRouterRequest.model);
3284
+ const allTools = request.tools
3285
+ ? [...request.tools]
3286
+ : [];
3287
+ if (useFallbackStructuredOutput && request.format) {
3288
+ log$1.warn(`[OpenRouterAdapter] Using legacy structured_output tool fallback for model ${openRouterRequest.model}; native response_format previously rejected for this model.`);
3289
+ allTools.push({
3290
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
3291
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
3292
+ "After gathering all necessary information (including results from other tools), " +
3293
+ "call this tool with the structured data to complete the request.",
3294
+ parameters: request.format,
3295
+ });
3296
+ }
3297
+ if (allTools.length > 0) {
3298
+ openRouterRequest.tools = allTools.map((tool) => ({
2830
3299
  type: "function",
2831
3300
  function: {
2832
3301
  name: tool.name,
@@ -2838,6 +3307,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2838
3307
  // The structured_output tool prompt already emphasizes it must be called
2839
3308
  openRouterRequest.tool_choice = "auto";
2840
3309
  }
3310
+ // Native structured output: send schema as `response_format`. The legacy
3311
+ // tool-emulation path is engaged only as a runtime fallback for models the
3312
+ // API has flagged as not supporting native json_schema.
3313
+ if (request.format && !useFallbackStructuredOutput) {
3314
+ openRouterRequest.response_format = {
3315
+ type: "json_schema",
3316
+ json_schema: {
3317
+ name: STRUCTURED_OUTPUT_SCHEMA_NAME,
3318
+ schema: request.format,
3319
+ strict: true,
3320
+ },
3321
+ };
3322
+ }
2841
3323
  if (request.providerOptions) {
2842
3324
  Object.assign(openRouterRequest, request.providerOptions);
2843
3325
  }
@@ -2848,24 +3330,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2848
3330
  }
2849
3331
  return openRouterRequest;
2850
3332
  }
2851
- formatTools(toolkit, outputSchema) {
2852
- const tools = toolkit.tools.map((tool) => ({
3333
+ formatTools(toolkit,
3334
+ // outputSchema is part of the interface contract but OpenRouter now uses
3335
+ // native `response_format` (set in buildRequest), so we no longer inject a
3336
+ // synthetic structured-output tool here. The legacy fake-tool injection
3337
+ // happens in buildRequest only as a runtime fallback.
3338
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3339
+ _outputSchema) {
3340
+ return toolkit.tools.map((tool) => ({
2853
3341
  name: tool.name,
2854
3342
  description: tool.description,
2855
3343
  parameters: tool.parameters,
2856
3344
  }));
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
3345
  }
2870
3346
  formatOutputSchema(schema) {
2871
3347
  let jsonSchema;
@@ -2888,6 +3364,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2888
3364
  if (jsonSchema.$schema) {
2889
3365
  delete jsonSchema.$schema;
2890
3366
  }
3367
+ // OpenRouter (and most backends behind it) require additionalProperties:
3368
+ // false on every object when using strict json_schema response_format.
3369
+ enforceAdditionalPropertiesFalse(jsonSchema);
2891
3370
  return jsonSchema;
2892
3371
  }
2893
3372
  //
@@ -2896,34 +3375,99 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2896
3375
  async executeRequest(client, request, signal) {
2897
3376
  const openRouter = client;
2898
3377
  const openRouterRequest = request;
3378
+ const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
2899
3379
  try {
2900
- const response = await openRouter.chat.send({
2901
- model: openRouterRequest.model,
2902
- messages: openRouterRequest.messages,
2903
- tools: openRouterRequest.tools,
2904
- toolChoice: openRouterRequest.tool_choice,
2905
- user: openRouterRequest.user,
2906
- }, signal ? { signal } : undefined);
3380
+ const response = (await openRouter.chat.send(this.toSdkChatParams(openRouterRequest), signal ? { signal } : undefined));
3381
+ if (wantsStructuredOutput) {
3382
+ response.__jaypieStructuredOutput = true;
3383
+ }
2907
3384
  return response;
2908
3385
  }
2909
3386
  catch (error) {
2910
3387
  if (signal?.aborted)
2911
3388
  return undefined;
3389
+ // If the model rejected `response_format`, cache it and retry with the
3390
+ // legacy fake-tool emulation path.
3391
+ if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {
3392
+ const model = openRouterRequest.model;
3393
+ this.rememberModelRejectsStructuredOutput(model);
3394
+ log$1.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
3395
+ const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
3396
+ return (await openRouter.chat.send(this.toSdkChatParams(fallbackRequest), signal ? { signal } : undefined));
3397
+ }
2912
3398
  throw error;
2913
3399
  }
2914
3400
  }
2915
- async *executeStreamRequest(client, request, signal) {
2916
- const openRouter = client;
2917
- const openRouterRequest = request;
2918
- // Use chat.send with stream: true for streaming responses
2919
- const stream = await openRouter.chat.send({
3401
+ /**
3402
+ * Translate our internal snake_case `OpenRouterRequest` into the SDK's
3403
+ * camelCase shape, forwarding only the fields we care about (the SDK
3404
+ * silently strips unknown fields).
3405
+ */
3406
+ toSdkChatParams(openRouterRequest) {
3407
+ const params = {
2920
3408
  model: openRouterRequest.model,
2921
3409
  messages: openRouterRequest.messages,
2922
3410
  tools: openRouterRequest.tools,
2923
3411
  toolChoice: openRouterRequest.tool_choice,
2924
3412
  user: openRouterRequest.user,
3413
+ };
3414
+ if (openRouterRequest.response_format) {
3415
+ const format = openRouterRequest.response_format;
3416
+ if (format.type === "json_schema") {
3417
+ params.responseFormat = {
3418
+ type: "json_schema",
3419
+ jsonSchema: format.json_schema,
3420
+ };
3421
+ }
3422
+ else {
3423
+ params.responseFormat = format;
3424
+ }
3425
+ }
3426
+ const temperature = openRouterRequest.temperature;
3427
+ if (temperature !== undefined) {
3428
+ params.temperature = temperature;
3429
+ }
3430
+ return params;
3431
+ }
3432
+ /**
3433
+ * Rebuild a structured-output request without `response_format`, swapping in
3434
+ * the legacy fake-tool emulation. Used as a runtime fallback when a model
3435
+ * rejects native json_schema.
3436
+ */
3437
+ toFallbackStructuredOutputRequest(request) {
3438
+ if (!request.response_format ||
3439
+ request.response_format.type !== "json_schema") {
3440
+ return request;
3441
+ }
3442
+ const { response_format, ...rest } = request;
3443
+ const fallbackRequest = { ...rest };
3444
+ const schema = response_format.json_schema.schema;
3445
+ const fakeTool = {
3446
+ type: "function",
3447
+ function: {
3448
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
3449
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
3450
+ "After gathering all necessary information (including results from other tools), " +
3451
+ "call this tool with the structured data to complete the request.",
3452
+ parameters: schema,
3453
+ },
3454
+ };
3455
+ fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
3456
+ fallbackRequest.tool_choice = "auto";
3457
+ return fallbackRequest;
3458
+ }
3459
+ async *executeStreamRequest(client, request, signal) {
3460
+ const openRouter = client;
3461
+ const openRouterRequest = request;
3462
+ // Use chat.send with stream: true for streaming responses.
3463
+ // Cast the result to AsyncIterable: when stream: true, the SDK returns a
3464
+ // stream we can iterate, but the typed result is the union with the
3465
+ // non-stream response.
3466
+ const streamParams = {
3467
+ ...this.toSdkChatParams(openRouterRequest),
2925
3468
  stream: true,
2926
- }, signal ? { signal } : undefined);
3469
+ };
3470
+ const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
2927
3471
  // Track current tool call being built
2928
3472
  let currentToolCall = null;
2929
3473
  // Track usage for final chunk
@@ -3202,6 +3746,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3202
3746
  }
3203
3747
  hasStructuredOutput(response) {
3204
3748
  const openRouterResponse = response;
3749
+ // Native path: executeRequest annotates the response when we sent
3750
+ // `response_format`, so we can detect intent statelessly.
3751
+ if (openRouterResponse.__jaypieStructuredOutput) {
3752
+ return this.extractStructuredOutput(response) !== undefined;
3753
+ }
3754
+ // Fallback path: legacy fake-tool emulation, kept for models that the
3755
+ // runtime has cached as not supporting native `response_format`.
3205
3756
  const choice = openRouterResponse.choices[0];
3206
3757
  // SDK returns camelCase (toolCalls)
3207
3758
  if (!choice?.message?.toolCalls?.length) {
@@ -3213,6 +3764,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3213
3764
  }
3214
3765
  extractStructuredOutput(response) {
3215
3766
  const openRouterResponse = response;
3767
+ if (openRouterResponse.__jaypieStructuredOutput) {
3768
+ const choice = openRouterResponse.choices[0];
3769
+ const content = choice?.message?.content;
3770
+ if (typeof content !== "string" || content.length === 0) {
3771
+ return undefined;
3772
+ }
3773
+ try {
3774
+ return JSON.parse(content);
3775
+ }
3776
+ catch {
3777
+ return undefined;
3778
+ }
3779
+ }
3780
+ // Fallback path: legacy fake-tool emulation
3216
3781
  const choice = openRouterResponse.choices[0];
3217
3782
  // SDK returns camelCase (toolCalls)
3218
3783
  if (!choice?.message?.toolCalls?.length) {
@@ -5233,55 +5798,50 @@ async function createTextCompletion$1(client, messages, model, systemMessage) {
5233
5798
  log$1.trace(`Assistant reply: ${text.length} characters`);
5234
5799
  return text;
5235
5800
  }
5236
- // Structured output completion
5801
+ // Structured output completion using Anthropic's native `output_config.format`
5802
+ // field. Returns a JsonObject parsed and validated against the caller's
5803
+ // Zod schema.
5237
5804
  async function createStructuredCompletion$1(client, messages, model, responseSchema, systemMessage) {
5238
- log$1.trace("Using structured output");
5239
- // Get the JSON schema for the response
5805
+ log$1.trace("Using native structured output");
5240
5806
  const schema = responseSchema instanceof z.ZodType
5241
5807
  ? responseSchema
5242
5808
  : naturalZodSchema(responseSchema);
5243
- // Set system message with JSON instructions
5244
- const defaultSystemPrompt = "You will be responding with structured JSON data. " +
5245
- "Format your entire response as a valid JSON object with the following structure: " +
5246
- JSON.stringify(z.toJSONSchema(schema));
5247
- const systemPrompt = systemMessage || defaultSystemPrompt;
5248
- try {
5249
- // Use standard Anthropic API to get response
5250
- const params = {
5251
- model,
5252
- messages,
5253
- max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
5254
- system: systemPrompt,
5255
- };
5256
- const response = await client.messages.create(params);
5257
- // Extract text from response
5258
- const firstContent = response.content[0];
5259
- const responseText = firstContent && "text" in firstContent ? firstContent.text : "";
5260
- // Find JSON in response
5261
- const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/) ||
5262
- responseText.match(/\{[\s\S]*\}/);
5263
- if (jsonMatch) {
5264
- try {
5265
- // Parse the JSON response
5266
- const jsonStr = jsonMatch[1] || jsonMatch[0];
5267
- const result = JSON.parse(jsonStr);
5268
- if (!schema.parse(result)) {
5269
- throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
5270
- }
5271
- log$1.trace("Received structured response", { result });
5272
- return result;
5273
- }
5274
- catch {
5275
- throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
5276
- }
5277
- }
5278
- // If we can't extract JSON
5809
+ const jsonSchema = z.toJSONSchema(schema);
5810
+ const params = {
5811
+ model,
5812
+ messages,
5813
+ max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
5814
+ output_config: {
5815
+ format: { type: "json_schema", schema: jsonSchema },
5816
+ },
5817
+ };
5818
+ if (systemMessage) {
5819
+ params.system = systemMessage;
5820
+ }
5821
+ const response = (await client.messages.create(params));
5822
+ if (response.stop_reason === "refusal") {
5823
+ throw new Error("Anthropic refused the structured-output request (stop_reason=refusal)");
5824
+ }
5825
+ if (response.stop_reason === "max_tokens") {
5826
+ throw new Error("Anthropic structured-output response was truncated (stop_reason=max_tokens); increase max_tokens");
5827
+ }
5828
+ const textBlock = response.content.find((block) => block.type === "text");
5829
+ if (!textBlock) {
5279
5830
  throw new Error("Failed to parse structured response from Anthropic");
5280
5831
  }
5281
- catch (error) {
5282
- log$1.error("Error creating structured completion", { error });
5283
- throw error;
5832
+ let parsed;
5833
+ try {
5834
+ parsed = JSON.parse(textBlock.text);
5835
+ }
5836
+ catch {
5837
+ throw new Error("Failed to parse structured response from Anthropic: " + textBlock.text);
5838
+ }
5839
+ const validation = schema.safeParse(parsed);
5840
+ if (!validation.success) {
5841
+ throw new Error(`JSON response from Anthropic does not match schema: ${textBlock.text}`);
5284
5842
  }
5843
+ log$1.trace("Received structured response", { result: validation.data });
5844
+ return validation.data;
5285
5845
  }
5286
5846
 
5287
5847
  // Maps Jaypie roles to Anthropic roles