@jaypie/llm 1.2.28 → 1.2.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.4",
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
  */
@@ -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,11 +1264,20 @@ 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
1276
  return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
1114
1277
  }
1278
+ supportsStructuredOutput(model) {
1279
+ return !this.runtimeNoStructuredOutputModels.has(model);
1280
+ }
1115
1281
  //
1116
1282
  // Request Building
1117
1283
  //
@@ -1179,8 +1345,22 @@ class AnthropicAdapter extends BaseProviderAdapter {
1179
1345
  if (request.system) {
1180
1346
  anthropicRequest.system = request.system;
1181
1347
  }
1182
- if (request.tools && request.tools.length > 0) {
1183
- anthropicRequest.tools = request.tools.map((tool) => ({
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
- // Determine tool choice based on whether structured output is requested
1193
- const hasStructuredOutput = request.tools.some((t) => t.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
1194
- anthropicRequest.tool_choice = {
1195
- type: hasStructuredOutput ? "any" : "auto",
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, outputSchema) {
1213
- const tools = toolkit.tools.map((tool) => ({
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
- // Remove $schema property (causes issues with validator)
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,8 +1438,13 @@ 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
- return (await anthropic.messages.create(anthropicRequest, signal ? { signal } : undefined));
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)
@@ -1270,13 +1455,53 @@ class AnthropicAdapter extends BaseProviderAdapter {
1270
1455
  this.rememberModelRejectsTemperature(anthropicRequest.model);
1271
1456
  const retryRequest = { ...anthropicRequest };
1272
1457
  delete retryRequest.temperature;
1273
- return (await anthropic.messages.create(retryRequest, signal ? { signal } : undefined));
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,
@@ -1536,13 +1761,39 @@ class AnthropicAdapter extends BaseProviderAdapter {
1536
1761
  }
1537
1762
  hasStructuredOutput(response) {
1538
1763
  const anthropicResponse = response;
1539
- // Check if the last content block is a tool_use with structured_output
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
- // Add tools if provided
1635
- if (request.tools && request.tools.length > 0) {
1636
- const functionDeclarations = request.tools.map((tool) => ({
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
- // Add structured output format if provided (but NOT when tools are present)
1647
- // Gemini doesn't support combining function calling with responseMimeType: 'application/json'
1648
- // When tools are present, structured output is handled via the structured_output tool
1649
- if (request.format && !(request.tools && request.tools.length > 0)) {
1650
- const useJsonSchema = request.providerOptions?.useJsonSchema === true;
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
- // When format is specified with tools, add instruction to use structured_output tool
1667
- if (request.format && request.tools && request.tools.length > 0) {
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, outputSchema) {
1696
- const tools = toolkit.tools.map((tool) => ({
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;
@@ -2753,6 +3104,23 @@ const openAiAdapter = new OpenAiAdapter();
2753
3104
  // Constants
2754
3105
  //
2755
3106
  const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
3107
+ const STRUCTURED_OUTPUT_SCHEMA_NAME = "response";
3108
+ /**
3109
+ * Detect 4xx errors that indicate the model itself does not support
3110
+ * `response_format: json_schema`. Mirrors the Anthropic pattern: we only
3111
+ * trigger the fake-tool fallback when the failure is plausibly a capability
3112
+ * gap, not a generic 400.
3113
+ */
3114
+ function isStructuredOutputUnsupportedError(error) {
3115
+ if (!error || typeof error !== "object")
3116
+ return false;
3117
+ const err = error;
3118
+ const status = err.status ?? err.statusCode;
3119
+ if (status !== 400 && status !== 422)
3120
+ return false;
3121
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
3122
+ return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
3123
+ }
2756
3124
  /**
2757
3125
  * Convert standardized content items to OpenRouter format
2758
3126
  * Note: OpenRouter does not support native file/image uploads.
@@ -2791,6 +3159,32 @@ function convertContentToOpenRouter(content) {
2791
3159
  // OpenRouter SDK error types based on HTTP status codes
2792
3160
  const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
2793
3161
  const RATE_LIMIT_STATUS_CODE = 429;
3162
+ /**
3163
+ * Walk the JSON schema and force `additionalProperties: false` on every
3164
+ * object node. Required by the OpenAI-style json_schema response_format
3165
+ * (which OpenRouter accepts) when `strict: true`.
3166
+ */
3167
+ function enforceAdditionalPropertiesFalse(schema) {
3168
+ const stack = [schema];
3169
+ while (stack.length > 0) {
3170
+ const node = stack.pop();
3171
+ if (node.type === "object") {
3172
+ node.additionalProperties = false;
3173
+ }
3174
+ for (const value of Object.values(node)) {
3175
+ if (Array.isArray(value)) {
3176
+ for (const entry of value) {
3177
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
3178
+ stack.push(entry);
3179
+ }
3180
+ }
3181
+ }
3182
+ else if (value && typeof value === "object") {
3183
+ stack.push(value);
3184
+ }
3185
+ }
3186
+ }
3187
+ }
2794
3188
  //
2795
3189
  //
2796
3190
  // Main
@@ -2806,6 +3200,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2806
3200
  super(...arguments);
2807
3201
  this.name = PROVIDER.OPENROUTER.NAME;
2808
3202
  this.defaultModel = PROVIDER.OPENROUTER.MODEL.DEFAULT;
3203
+ // Session-level cache of models observed to reject native
3204
+ // `response_format: json_schema`. When a model is in this set, buildRequest
3205
+ // engages the legacy fake-tool path instead of native structured output.
3206
+ this.runtimeNoStructuredOutputModels = new Set();
3207
+ }
3208
+ rememberModelRejectsStructuredOutput(model) {
3209
+ this.runtimeNoStructuredOutputModels.add(model);
3210
+ }
3211
+ clearRuntimeNoStructuredOutputModels() {
3212
+ this.runtimeNoStructuredOutputModels.clear();
3213
+ }
3214
+ supportsStructuredOutput(model) {
3215
+ return !this.runtimeNoStructuredOutputModels.has(model);
2809
3216
  }
2810
3217
  //
2811
3218
  // Request Building
@@ -2827,8 +3234,23 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2827
3234
  if (request.user) {
2828
3235
  openRouterRequest.user = request.user;
2829
3236
  }
2830
- if (request.tools && request.tools.length > 0) {
2831
- openRouterRequest.tools = request.tools.map((tool) => ({
3237
+ const useFallbackStructuredOutput = Boolean(request.format) &&
3238
+ !this.supportsStructuredOutput(openRouterRequest.model);
3239
+ const allTools = request.tools
3240
+ ? [...request.tools]
3241
+ : [];
3242
+ if (useFallbackStructuredOutput && request.format) {
3243
+ log$1.log.warn(`[OpenRouterAdapter] Using legacy structured_output tool fallback for model ${openRouterRequest.model}; native response_format previously rejected for this model.`);
3244
+ allTools.push({
3245
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
3246
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
3247
+ "After gathering all necessary information (including results from other tools), " +
3248
+ "call this tool with the structured data to complete the request.",
3249
+ parameters: request.format,
3250
+ });
3251
+ }
3252
+ if (allTools.length > 0) {
3253
+ openRouterRequest.tools = allTools.map((tool) => ({
2832
3254
  type: "function",
2833
3255
  function: {
2834
3256
  name: tool.name,
@@ -2840,6 +3262,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2840
3262
  // The structured_output tool prompt already emphasizes it must be called
2841
3263
  openRouterRequest.tool_choice = "auto";
2842
3264
  }
3265
+ // Native structured output: send schema as `response_format`. The legacy
3266
+ // tool-emulation path is engaged only as a runtime fallback for models the
3267
+ // API has flagged as not supporting native json_schema.
3268
+ if (request.format && !useFallbackStructuredOutput) {
3269
+ openRouterRequest.response_format = {
3270
+ type: "json_schema",
3271
+ json_schema: {
3272
+ name: STRUCTURED_OUTPUT_SCHEMA_NAME,
3273
+ schema: request.format,
3274
+ strict: true,
3275
+ },
3276
+ };
3277
+ }
2843
3278
  if (request.providerOptions) {
2844
3279
  Object.assign(openRouterRequest, request.providerOptions);
2845
3280
  }
@@ -2850,24 +3285,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2850
3285
  }
2851
3286
  return openRouterRequest;
2852
3287
  }
2853
- formatTools(toolkit, outputSchema) {
2854
- const tools = toolkit.tools.map((tool) => ({
3288
+ formatTools(toolkit,
3289
+ // outputSchema is part of the interface contract but OpenRouter now uses
3290
+ // native `response_format` (set in buildRequest), so we no longer inject a
3291
+ // synthetic structured-output tool here. The legacy fake-tool injection
3292
+ // happens in buildRequest only as a runtime fallback.
3293
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3294
+ _outputSchema) {
3295
+ return toolkit.tools.map((tool) => ({
2855
3296
  name: tool.name,
2856
3297
  description: tool.description,
2857
3298
  parameters: tool.parameters,
2858
3299
  }));
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
3300
  }
2872
3301
  formatOutputSchema(schema) {
2873
3302
  let jsonSchema;
@@ -2890,6 +3319,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2890
3319
  if (jsonSchema.$schema) {
2891
3320
  delete jsonSchema.$schema;
2892
3321
  }
3322
+ // OpenRouter (and most backends behind it) require additionalProperties:
3323
+ // false on every object when using strict json_schema response_format.
3324
+ enforceAdditionalPropertiesFalse(jsonSchema);
2893
3325
  return jsonSchema;
2894
3326
  }
2895
3327
  //
@@ -2898,34 +3330,99 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2898
3330
  async executeRequest(client, request, signal) {
2899
3331
  const openRouter = client;
2900
3332
  const openRouterRequest = request;
3333
+ const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
2901
3334
  try {
2902
- const response = await openRouter.chat.send({
2903
- model: openRouterRequest.model,
2904
- messages: openRouterRequest.messages,
2905
- tools: openRouterRequest.tools,
2906
- toolChoice: openRouterRequest.tool_choice,
2907
- user: openRouterRequest.user,
2908
- }, signal ? { signal } : undefined);
3335
+ const response = (await openRouter.chat.send(this.toSdkChatParams(openRouterRequest), signal ? { signal } : undefined));
3336
+ if (wantsStructuredOutput) {
3337
+ response.__jaypieStructuredOutput = true;
3338
+ }
2909
3339
  return response;
2910
3340
  }
2911
3341
  catch (error) {
2912
3342
  if (signal?.aborted)
2913
3343
  return undefined;
3344
+ // If the model rejected `response_format`, cache it and retry with the
3345
+ // legacy fake-tool emulation path.
3346
+ if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {
3347
+ const model = openRouterRequest.model;
3348
+ this.rememberModelRejectsStructuredOutput(model);
3349
+ log$1.log.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
3350
+ const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
3351
+ return (await openRouter.chat.send(this.toSdkChatParams(fallbackRequest), signal ? { signal } : undefined));
3352
+ }
2914
3353
  throw error;
2915
3354
  }
2916
3355
  }
2917
- async *executeStreamRequest(client, request, signal) {
2918
- const openRouter = client;
2919
- const openRouterRequest = request;
2920
- // Use chat.send with stream: true for streaming responses
2921
- const stream = await openRouter.chat.send({
3356
+ /**
3357
+ * Translate our internal snake_case `OpenRouterRequest` into the SDK's
3358
+ * camelCase shape, forwarding only the fields we care about (the SDK
3359
+ * silently strips unknown fields).
3360
+ */
3361
+ toSdkChatParams(openRouterRequest) {
3362
+ const params = {
2922
3363
  model: openRouterRequest.model,
2923
3364
  messages: openRouterRequest.messages,
2924
3365
  tools: openRouterRequest.tools,
2925
3366
  toolChoice: openRouterRequest.tool_choice,
2926
3367
  user: openRouterRequest.user,
3368
+ };
3369
+ if (openRouterRequest.response_format) {
3370
+ const format = openRouterRequest.response_format;
3371
+ if (format.type === "json_schema") {
3372
+ params.responseFormat = {
3373
+ type: "json_schema",
3374
+ jsonSchema: format.json_schema,
3375
+ };
3376
+ }
3377
+ else {
3378
+ params.responseFormat = format;
3379
+ }
3380
+ }
3381
+ const temperature = openRouterRequest.temperature;
3382
+ if (temperature !== undefined) {
3383
+ params.temperature = temperature;
3384
+ }
3385
+ return params;
3386
+ }
3387
+ /**
3388
+ * Rebuild a structured-output request without `response_format`, swapping in
3389
+ * the legacy fake-tool emulation. Used as a runtime fallback when a model
3390
+ * rejects native json_schema.
3391
+ */
3392
+ toFallbackStructuredOutputRequest(request) {
3393
+ if (!request.response_format ||
3394
+ request.response_format.type !== "json_schema") {
3395
+ return request;
3396
+ }
3397
+ const { response_format, ...rest } = request;
3398
+ const fallbackRequest = { ...rest };
3399
+ const schema = response_format.json_schema.schema;
3400
+ const fakeTool = {
3401
+ type: "function",
3402
+ function: {
3403
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
3404
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
3405
+ "After gathering all necessary information (including results from other tools), " +
3406
+ "call this tool with the structured data to complete the request.",
3407
+ parameters: schema,
3408
+ },
3409
+ };
3410
+ fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
3411
+ fallbackRequest.tool_choice = "auto";
3412
+ return fallbackRequest;
3413
+ }
3414
+ async *executeStreamRequest(client, request, signal) {
3415
+ const openRouter = client;
3416
+ const openRouterRequest = request;
3417
+ // Use chat.send with stream: true for streaming responses.
3418
+ // Cast the result to AsyncIterable: when stream: true, the SDK returns a
3419
+ // stream we can iterate, but the typed result is the union with the
3420
+ // non-stream response.
3421
+ const streamParams = {
3422
+ ...this.toSdkChatParams(openRouterRequest),
2927
3423
  stream: true,
2928
- }, signal ? { signal } : undefined);
3424
+ };
3425
+ const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
2929
3426
  // Track current tool call being built
2930
3427
  let currentToolCall = null;
2931
3428
  // Track usage for final chunk
@@ -3204,6 +3701,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3204
3701
  }
3205
3702
  hasStructuredOutput(response) {
3206
3703
  const openRouterResponse = response;
3704
+ // Native path: executeRequest annotates the response when we sent
3705
+ // `response_format`, so we can detect intent statelessly.
3706
+ if (openRouterResponse.__jaypieStructuredOutput) {
3707
+ return this.extractStructuredOutput(response) !== undefined;
3708
+ }
3709
+ // Fallback path: legacy fake-tool emulation, kept for models that the
3710
+ // runtime has cached as not supporting native `response_format`.
3207
3711
  const choice = openRouterResponse.choices[0];
3208
3712
  // SDK returns camelCase (toolCalls)
3209
3713
  if (!choice?.message?.toolCalls?.length) {
@@ -3215,6 +3719,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3215
3719
  }
3216
3720
  extractStructuredOutput(response) {
3217
3721
  const openRouterResponse = response;
3722
+ if (openRouterResponse.__jaypieStructuredOutput) {
3723
+ const choice = openRouterResponse.choices[0];
3724
+ const content = choice?.message?.content;
3725
+ if (typeof content !== "string" || content.length === 0) {
3726
+ return undefined;
3727
+ }
3728
+ try {
3729
+ return JSON.parse(content);
3730
+ }
3731
+ catch {
3732
+ return undefined;
3733
+ }
3734
+ }
3735
+ // Fallback path: legacy fake-tool emulation
3218
3736
  const choice = openRouterResponse.choices[0];
3219
3737
  // SDK returns camelCase (toolCalls)
3220
3738
  if (!choice?.message?.toolCalls?.length) {
@@ -3340,10 +3858,29 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3340
3858
  // Export singleton instance
3341
3859
  const openRouterAdapter = new OpenRouterAdapter();
3342
3860
 
3861
+ /**
3862
+ * Error-message substrings that indicate a transient xAI media-ingest flake.
3863
+ * The xAI ingest service enters a bad state after ~12 consecutive file-bearing
3864
+ * calls and rejects subsequent requests with HTTP 400. The condition is
3865
+ * self-clearing, so these errors should be retried with backoff rather than
3866
+ * surfaced as unrecoverable.
3867
+ *
3868
+ * See: github.com/finlaysonstudio/jaypie issue #301
3869
+ */
3870
+ const TRANSIENT_INGEST_MESSAGE_PATTERNS = [
3871
+ "failed to ingest inline file bytes",
3872
+ ];
3873
+ function isTransientIngestError(error) {
3874
+ if (!(error instanceof openai.BadRequestError))
3875
+ return false;
3876
+ const message = (error.message ?? "").toLowerCase();
3877
+ return TRANSIENT_INGEST_MESSAGE_PATTERNS.some((pattern) => message.includes(pattern));
3878
+ }
3343
3879
  /**
3344
3880
  * XaiAdapter extends OpenAiAdapter since xAI (Grok) uses an OpenAI-compatible API.
3345
- * Only the name and default model are overridden; all request building, response parsing,
3346
- * error classification, tool handling, and streaming are inherited.
3881
+ * Name, default model, and transient-ingest-error detection are overridden;
3882
+ * all request building, response parsing, tool handling, and streaming are
3883
+ * inherited.
3347
3884
  */
3348
3885
  class XaiAdapter extends OpenAiAdapter {
3349
3886
  constructor() {
@@ -3353,6 +3890,16 @@ class XaiAdapter extends OpenAiAdapter {
3353
3890
  // @ts-expect-error Narrowing override: xAI default model differs from parent's literal
3354
3891
  this.defaultModel = PROVIDER.XAI.MODEL.DEFAULT;
3355
3892
  }
3893
+ classifyError(error) {
3894
+ if (isTransientIngestError(error)) {
3895
+ return {
3896
+ error,
3897
+ category: ErrorCategory.Retryable,
3898
+ shouldRetry: true,
3899
+ };
3900
+ }
3901
+ return super.classifyError(error);
3902
+ }
3356
3903
  }
3357
3904
  // Export singleton instance
3358
3905
  const xaiAdapter = new XaiAdapter();
@@ -5206,55 +5753,50 @@ async function createTextCompletion$1(client, messages, model, systemMessage) {
5206
5753
  log$1.log.trace(`Assistant reply: ${text.length} characters`);
5207
5754
  return text;
5208
5755
  }
5209
- // Structured output completion
5756
+ // Structured output completion using Anthropic's native `output_config.format`
5757
+ // field. Returns a JsonObject parsed and validated against the caller's
5758
+ // Zod schema.
5210
5759
  async function createStructuredCompletion$1(client, messages, model, responseSchema, systemMessage) {
5211
- log$1.log.trace("Using structured output");
5212
- // Get the JSON schema for the response
5760
+ log$1.log.trace("Using native structured output");
5213
5761
  const schema = responseSchema instanceof v4.z.ZodType
5214
5762
  ? responseSchema
5215
5763
  : naturalZodSchema(responseSchema);
5216
- // Set system message with JSON instructions
5217
- const defaultSystemPrompt = "You will be responding with structured JSON data. " +
5218
- "Format your entire response as a valid JSON object with the following structure: " +
5219
- JSON.stringify(v4.z.toJSONSchema(schema));
5220
- const systemPrompt = systemMessage || defaultSystemPrompt;
5221
- try {
5222
- // Use standard Anthropic API to get response
5223
- const params = {
5224
- model,
5225
- messages,
5226
- max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
5227
- system: systemPrompt,
5228
- };
5229
- const response = await client.messages.create(params);
5230
- // Extract text from response
5231
- const firstContent = response.content[0];
5232
- const responseText = firstContent && "text" in firstContent ? firstContent.text : "";
5233
- // Find JSON in response
5234
- const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/) ||
5235
- responseText.match(/\{[\s\S]*\}/);
5236
- if (jsonMatch) {
5237
- try {
5238
- // Parse the JSON response
5239
- const jsonStr = jsonMatch[1] || jsonMatch[0];
5240
- const result = JSON.parse(jsonStr);
5241
- if (!schema.parse(result)) {
5242
- throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
5243
- }
5244
- log$1.log.trace("Received structured response", { result });
5245
- return result;
5246
- }
5247
- catch {
5248
- throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
5249
- }
5250
- }
5251
- // If we can't extract JSON
5764
+ const jsonSchema = v4.z.toJSONSchema(schema);
5765
+ const params = {
5766
+ model,
5767
+ messages,
5768
+ max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
5769
+ output_config: {
5770
+ format: { type: "json_schema", schema: jsonSchema },
5771
+ },
5772
+ };
5773
+ if (systemMessage) {
5774
+ params.system = systemMessage;
5775
+ }
5776
+ const response = (await client.messages.create(params));
5777
+ if (response.stop_reason === "refusal") {
5778
+ throw new Error("Anthropic refused the structured-output request (stop_reason=refusal)");
5779
+ }
5780
+ if (response.stop_reason === "max_tokens") {
5781
+ throw new Error("Anthropic structured-output response was truncated (stop_reason=max_tokens); increase max_tokens");
5782
+ }
5783
+ const textBlock = response.content.find((block) => block.type === "text");
5784
+ if (!textBlock) {
5252
5785
  throw new Error("Failed to parse structured response from Anthropic");
5253
5786
  }
5254
- catch (error) {
5255
- log$1.log.error("Error creating structured completion", { error });
5256
- throw error;
5787
+ let parsed;
5788
+ try {
5789
+ parsed = JSON.parse(textBlock.text);
5790
+ }
5791
+ catch {
5792
+ throw new Error("Failed to parse structured response from Anthropic: " + textBlock.text);
5793
+ }
5794
+ const validation = schema.safeParse(parsed);
5795
+ if (!validation.success) {
5796
+ throw new Error(`JSON response from Anthropic does not match schema: ${textBlock.text}`);
5257
5797
  }
5798
+ log$1.log.trace("Received structured response", { result: validation.data });
5799
+ return validation.data;
5258
5800
  }
5259
5801
 
5260
5802
  // Maps Jaypie roles to Anthropic roles