@jaypie/llm 1.3.0 → 1.3.2

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.
Files changed (87) hide show
  1. package/README.md +9 -0
  2. package/dist/cjs/constants.d.ts +82 -30
  3. package/dist/cjs/index.cjs +3081 -578
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/index.d.cts +661 -0
  6. package/dist/cjs/index.d.ts +5 -1
  7. package/dist/cjs/observability/llmobs.d.ts +72 -0
  8. package/dist/cjs/operate/OperateLoop.d.ts +7 -0
  9. package/dist/cjs/operate/StreamLoop.d.ts +3 -0
  10. package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +36 -5
  11. package/dist/cjs/operate/adapters/BedrockAdapter.d.ts +78 -0
  12. package/dist/cjs/operate/adapters/{GeminiAdapter.d.ts → GoogleAdapter.d.ts} +19 -9
  13. package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +13 -3
  14. package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +55 -8
  15. package/dist/cjs/operate/adapters/ProviderAdapter.interface.d.ts +5 -3
  16. package/dist/cjs/operate/adapters/XaiAdapter.d.ts +14 -0
  17. package/dist/cjs/operate/adapters/index.d.ts +3 -1
  18. package/dist/cjs/operate/index.d.ts +1 -1
  19. package/dist/cjs/operate/retry/RetryExecutor.d.ts +6 -3
  20. package/dist/cjs/operate/retry/createStaleRejectionGuard.d.ts +19 -0
  21. package/dist/cjs/operate/retry/index.d.ts +1 -0
  22. package/dist/cjs/operate/retry/isTransientNetworkError.d.ts +18 -0
  23. package/dist/cjs/operate/types.d.ts +2 -0
  24. package/dist/cjs/providers/anthropic/utils.d.ts +2 -1
  25. package/dist/cjs/providers/bedrock/BedrockProvider.class.d.ts +21 -0
  26. package/dist/cjs/providers/bedrock/index.d.ts +1 -0
  27. package/dist/cjs/providers/bedrock/utils.d.ts +7 -0
  28. package/dist/cjs/providers/{gemini/GeminiProvider.class.d.ts → google/GoogleProvider.class.d.ts} +1 -1
  29. package/dist/cjs/providers/google/index.d.ts +3 -0
  30. package/dist/{esm/providers/gemini → cjs/providers/google}/types.d.ts +1 -0
  31. package/dist/cjs/providers/{gemini → google}/utils.d.ts +2 -1
  32. package/dist/cjs/providers/openai/utils.d.ts +2 -1
  33. package/dist/cjs/providers/openrouter/utils.d.ts +2 -1
  34. package/dist/{esm/providers/gemini/GeminiProvider.class.d.ts → cjs/providers/xai/XaiProvider.class.d.ts} +1 -1
  35. package/dist/cjs/providers/xai/index.d.ts +1 -0
  36. package/dist/cjs/providers/xai/utils.d.ts +6 -0
  37. package/dist/cjs/types/LlmProvider.interface.d.ts +1 -1
  38. package/dist/cjs/types/LlmStreamChunk.interface.d.ts +2 -0
  39. package/dist/cjs/util/fillFormatArrays.d.ts +17 -0
  40. package/dist/cjs/util/index.d.ts +2 -0
  41. package/dist/cjs/util/jsonSchemaToOpenApi3.d.ts +10 -0
  42. package/dist/cjs/util/logger.d.ts +2 -2
  43. package/dist/cjs/util/maxTurnsFromOptions.d.ts +1 -1
  44. package/dist/esm/constants.d.ts +82 -30
  45. package/dist/esm/index.d.ts +661 -12
  46. package/dist/esm/index.js +3077 -578
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/observability/llmobs.d.ts +72 -0
  49. package/dist/esm/operate/OperateLoop.d.ts +7 -0
  50. package/dist/esm/operate/StreamLoop.d.ts +3 -0
  51. package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +36 -5
  52. package/dist/esm/operate/adapters/BedrockAdapter.d.ts +78 -0
  53. package/dist/esm/operate/adapters/{GeminiAdapter.d.ts → GoogleAdapter.d.ts} +19 -9
  54. package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +13 -3
  55. package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +55 -8
  56. package/dist/esm/operate/adapters/ProviderAdapter.interface.d.ts +5 -3
  57. package/dist/esm/operate/adapters/XaiAdapter.d.ts +14 -0
  58. package/dist/esm/operate/adapters/index.d.ts +3 -1
  59. package/dist/esm/operate/index.d.ts +1 -1
  60. package/dist/esm/operate/retry/RetryExecutor.d.ts +6 -3
  61. package/dist/esm/operate/retry/createStaleRejectionGuard.d.ts +19 -0
  62. package/dist/esm/operate/retry/index.d.ts +1 -0
  63. package/dist/esm/operate/retry/isTransientNetworkError.d.ts +18 -0
  64. package/dist/esm/operate/types.d.ts +2 -0
  65. package/dist/esm/providers/anthropic/utils.d.ts +2 -1
  66. package/dist/esm/providers/bedrock/BedrockProvider.class.d.ts +21 -0
  67. package/dist/esm/providers/bedrock/index.d.ts +1 -0
  68. package/dist/esm/providers/bedrock/utils.d.ts +7 -0
  69. package/dist/esm/providers/google/GoogleProvider.class.d.ts +21 -0
  70. package/dist/esm/providers/google/index.d.ts +3 -0
  71. package/dist/{cjs/providers/gemini → esm/providers/google}/types.d.ts +1 -0
  72. package/dist/esm/providers/{gemini → google}/utils.d.ts +2 -1
  73. package/dist/esm/providers/openai/utils.d.ts +2 -1
  74. package/dist/esm/providers/openrouter/utils.d.ts +2 -1
  75. package/dist/esm/providers/xai/XaiProvider.class.d.ts +21 -0
  76. package/dist/esm/providers/xai/index.d.ts +1 -0
  77. package/dist/esm/providers/xai/utils.d.ts +6 -0
  78. package/dist/esm/types/LlmProvider.interface.d.ts +1 -1
  79. package/dist/esm/types/LlmStreamChunk.interface.d.ts +2 -0
  80. package/dist/esm/util/fillFormatArrays.d.ts +17 -0
  81. package/dist/esm/util/index.d.ts +2 -0
  82. package/dist/esm/util/jsonSchemaToOpenApi3.d.ts +10 -0
  83. package/dist/esm/util/logger.d.ts +2 -2
  84. package/dist/esm/util/maxTurnsFromOptions.d.ts +1 -1
  85. package/package.json +18 -5
  86. package/dist/cjs/providers/gemini/index.d.ts +0 -3
  87. package/dist/esm/providers/gemini/index.d.ts +0 -3
package/dist/esm/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ConfigurationError, BadGatewayError, TooManyRequestsError, NotImplementedError } from '@jaypie/errors';
2
- import log$3, { log as log$2 } from '@jaypie/logger';
2
+ import log$2, { log as log$1 } from '@jaypie/logger';
3
3
  import { z } from 'zod/v4';
4
4
  import { placeholders, JAYPIE, resolveValue, sleep } from '@jaypie/kit';
5
5
  import RandomLib from 'random';
6
6
  import { RateLimitError, APIConnectionError, APIConnectionTimeoutError, InternalServerError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, NotFoundError, PermissionDeniedError, UnprocessableEntityError, OpenAI } from 'openai';
7
7
  import { zodResponseFormat } from 'openai/helpers/zod';
8
+ import { createRequire } from 'module';
9
+ import { pathToFileURL } from 'url';
8
10
  import { PDFDocument } from 'pdf-lib';
9
11
  import { readFile } from 'fs/promises';
10
12
  import { resolve } from 'path';
@@ -12,32 +14,89 @@ import { getS3FileBuffer, getEnvSecret } from '@jaypie/aws';
12
14
  import { fetchWeatherApi } from 'openmeteo';
13
15
 
14
16
  const FIRST_CLASS_PROVIDER = {
17
+ // https://docs.anthropic.com/en/docs/about-claude/models/overview
15
18
  ANTHROPIC: {
16
- DEFAULT: "claude-sonnet-4-5",
17
- LARGE: "claude-opus-4-5",
18
- SMALL: "claude-sonnet-4-5",
19
+ DEFAULT: "claude-sonnet-4-6",
20
+ LARGE: "claude-opus-4-8",
21
+ SMALL: "claude-sonnet-4-6",
19
22
  TINY: "claude-haiku-4-5",
20
23
  },
21
- GEMINI: {
22
- DEFAULT: "gemini-3-pro-preview",
23
- LARGE: "gemini-3-pro-preview",
24
- SMALL: "gemini-3-flash-preview",
25
- TINY: "gemini-3-flash-preview",
24
+ // https://ai.google.dev/gemini-api/docs/models
25
+ GOOGLE: {
26
+ DEFAULT: "gemini-3.1-pro-preview",
27
+ LARGE: "gemini-3.1-pro-preview",
28
+ SMALL: "gemini-3.5-flash",
29
+ TINY: "gemini-3.1-flash-lite",
26
30
  },
31
+ // https://developers.openai.com/api/docs/models
27
32
  OPENAI: {
28
- DEFAULT: "gpt-5.2",
29
- LARGE: "gpt-5.2-pro",
30
- SMALL: "gpt-5-mini",
31
- TINY: "gpt-5-nano",
33
+ DEFAULT: "gpt-5.4",
34
+ LARGE: "gpt-5.5",
35
+ SMALL: "gpt-5.4-mini",
36
+ TINY: "gpt-5.4-nano",
32
37
  },
33
- OPENROUTER: {
34
- DEFAULT: "z-ai/glm-4.7",
35
- LARGE: "z-ai/glm-4.7",
36
- SMALL: "z-ai/glm-4.7",
37
- TINY: "z-ai/glm-4.7",
38
+ // https://docs.x.ai/developers/models
39
+ XAI: {
40
+ DEFAULT: "grok-latest",
41
+ LARGE: "grok-4.3-latest",
42
+ SMALL: "grok-4-1-fast-reasoning",
43
+ TINY: "grok-4-1-fast-non-reasoning",
44
+ },
45
+ };
46
+ const MODEL = {
47
+ // Anthropic
48
+ OPUS: "claude-opus-4-8",
49
+ SONNET: "claude-sonnet-4-6",
50
+ HAIKU: "claude-haiku-4-5",
51
+ FABLE: "claude-fable-5",
52
+ MYTHOS: "claude-mythos-5",
53
+ // Google
54
+ GEMINI_FLASH: "gemini-3.5-flash",
55
+ GEMINI_FLASH_LITE: "gemini-3.1-flash-lite",
56
+ GEMINI_PRO: "gemini-3.1-pro-preview",
57
+ // OpenAI
58
+ GPT: "gpt-5.5",
59
+ GPT_MINI: "gpt-5.4-mini",
60
+ GPT_NANO: "gpt-5.4-nano",
61
+ // xAI
62
+ GROK: "grok-latest",
63
+ };
64
+ const GOOGLE_PROVIDER = {
65
+ // https://ai.google.dev/gemini-api/docs/models
66
+ MODEL: {
67
+ DEFAULT: FIRST_CLASS_PROVIDER.GOOGLE.DEFAULT,
68
+ LARGE: FIRST_CLASS_PROVIDER.GOOGLE.LARGE,
69
+ SMALL: FIRST_CLASS_PROVIDER.GOOGLE.SMALL,
70
+ TINY: FIRST_CLASS_PROVIDER.GOOGLE.TINY,
71
+ },
72
+ MODEL_MATCH_WORDS: ["gemini", "google"],
73
+ NAME: "google",
74
+ ROLE: {
75
+ MODEL: "model",
76
+ USER: "user",
38
77
  },
39
78
  };
40
79
  const PROVIDER = {
80
+ // https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html
81
+ BEDROCK: {
82
+ MODEL: {
83
+ DEFAULT: "amazon.nova-lite-v1:0",
84
+ LARGE: "amazon.nova-pro-v1:0",
85
+ SMALL: "amazon.nova-lite-v1:0",
86
+ TINY: "amazon.nova-micro-v1:0",
87
+ },
88
+ MODEL_MATCH_WORDS: [
89
+ "amazon.nova",
90
+ "amazon.titan",
91
+ "anthropic.claude",
92
+ "cohere.command",
93
+ "meta.llama",
94
+ "mistral.mistral",
95
+ "ai21.",
96
+ ],
97
+ NAME: "bedrock",
98
+ REGION: "AWS_REGION",
99
+ },
41
100
  ANTHROPIC: {
42
101
  // https://docs.anthropic.com/en/docs/about-claude/models/overview
43
102
  MAX_TOKENS: {
@@ -70,21 +129,9 @@ const PROVIDER = {
70
129
  SCHEMA_VERSION: "v2",
71
130
  },
72
131
  },
73
- GEMINI: {
74
- // https://ai.google.dev/gemini-api/docs/models
75
- MODEL: {
76
- DEFAULT: FIRST_CLASS_PROVIDER.GEMINI.DEFAULT,
77
- LARGE: FIRST_CLASS_PROVIDER.GEMINI.LARGE,
78
- SMALL: FIRST_CLASS_PROVIDER.GEMINI.SMALL,
79
- TINY: FIRST_CLASS_PROVIDER.GEMINI.TINY,
80
- },
81
- MODEL_MATCH_WORDS: ["gemini", "google"],
82
- NAME: "gemini",
83
- ROLE: {
84
- MODEL: "model",
85
- USER: "user",
86
- },
87
- },
132
+ /** @deprecated Use PROVIDER.GOOGLE — "Google" is the provider; Gemini is the model family */
133
+ GEMINI: GOOGLE_PROVIDER,
134
+ GOOGLE: GOOGLE_PROVIDER,
88
135
  OPENAI: {
89
136
  // https://platform.openai.com/docs/models
90
137
  MODEL: {
@@ -97,15 +144,11 @@ const PROVIDER = {
97
144
  NAME: "openai",
98
145
  },
99
146
  OPENROUTER: {
100
- // https://openrouter.ai/models
101
- // OpenRouter provides access to hundreds of models from various providers
102
- // The model format is: provider/model-name (e.g., "openai/gpt-4", "anthropic/claude-3-opus")
103
147
  MODEL: {
104
- // Default uses env var OPENROUTER_MODEL if set, otherwise a reasonable default
105
- DEFAULT: FIRST_CLASS_PROVIDER.OPENROUTER.DEFAULT,
106
- LARGE: FIRST_CLASS_PROVIDER.OPENROUTER.LARGE,
107
- SMALL: FIRST_CLASS_PROVIDER.OPENROUTER.SMALL,
108
- TINY: FIRST_CLASS_PROVIDER.OPENROUTER.TINY,
148
+ DEFAULT: `anthropic/${FIRST_CLASS_PROVIDER.ANTHROPIC.DEFAULT}`,
149
+ LARGE: `anthropic/${FIRST_CLASS_PROVIDER.ANTHROPIC.LARGE}`,
150
+ SMALL: `anthropic/${FIRST_CLASS_PROVIDER.ANTHROPIC.SMALL}`,
151
+ TINY: `anthropic/${FIRST_CLASS_PROVIDER.ANTHROPIC.TINY}`,
109
152
  },
110
153
  MODEL_MATCH_WORDS: ["openrouter"],
111
154
  NAME: "openrouter",
@@ -116,6 +159,19 @@ const PROVIDER = {
116
159
  USER: "user",
117
160
  },
118
161
  },
162
+ XAI: {
163
+ // https://docs.x.ai/docs/models
164
+ API_KEY: "XAI_API_KEY",
165
+ BASE_URL: "https://api.x.ai/v1",
166
+ MODEL: {
167
+ DEFAULT: FIRST_CLASS_PROVIDER.XAI.DEFAULT,
168
+ LARGE: FIRST_CLASS_PROVIDER.XAI.LARGE,
169
+ SMALL: FIRST_CLASS_PROVIDER.XAI.SMALL,
170
+ TINY: FIRST_CLASS_PROVIDER.XAI.TINY,
171
+ },
172
+ MODEL_MATCH_WORDS: ["grok", "xai"],
173
+ NAME: "xai",
174
+ },
119
175
  };
120
176
  // Last: Defaults
121
177
  const DEFAULT = {
@@ -131,37 +187,47 @@ const DEFAULT = {
131
187
  const ALL = {
132
188
  BASE: [
133
189
  PROVIDER.ANTHROPIC.MODEL.DEFAULT,
134
- PROVIDER.GEMINI.MODEL.DEFAULT,
190
+ PROVIDER.GOOGLE.MODEL.DEFAULT,
135
191
  PROVIDER.OPENAI.MODEL.DEFAULT,
192
+ PROVIDER.XAI.MODEL.DEFAULT,
136
193
  ],
137
194
  COMBINED: [
138
- PROVIDER.ANTHROPIC.MODEL.DEFAULT,
139
- PROVIDER.ANTHROPIC.MODEL.LARGE,
140
- PROVIDER.ANTHROPIC.MODEL.SMALL,
141
- PROVIDER.ANTHROPIC.MODEL.TINY,
142
- PROVIDER.GEMINI.MODEL.DEFAULT,
143
- PROVIDER.GEMINI.MODEL.LARGE,
144
- PROVIDER.GEMINI.MODEL.SMALL,
145
- PROVIDER.GEMINI.MODEL.TINY,
146
- PROVIDER.OPENAI.MODEL.DEFAULT,
147
- PROVIDER.OPENAI.MODEL.LARGE,
148
- PROVIDER.OPENAI.MODEL.SMALL,
149
- PROVIDER.OPENAI.MODEL.TINY,
195
+ ...new Set([
196
+ PROVIDER.ANTHROPIC.MODEL.DEFAULT,
197
+ PROVIDER.ANTHROPIC.MODEL.LARGE,
198
+ PROVIDER.ANTHROPIC.MODEL.SMALL,
199
+ PROVIDER.ANTHROPIC.MODEL.TINY,
200
+ PROVIDER.GOOGLE.MODEL.DEFAULT,
201
+ PROVIDER.GOOGLE.MODEL.LARGE,
202
+ PROVIDER.GOOGLE.MODEL.SMALL,
203
+ PROVIDER.GOOGLE.MODEL.TINY,
204
+ PROVIDER.OPENAI.MODEL.DEFAULT,
205
+ PROVIDER.OPENAI.MODEL.LARGE,
206
+ PROVIDER.OPENAI.MODEL.SMALL,
207
+ PROVIDER.OPENAI.MODEL.TINY,
208
+ PROVIDER.XAI.MODEL.DEFAULT,
209
+ PROVIDER.XAI.MODEL.LARGE,
210
+ PROVIDER.XAI.MODEL.SMALL,
211
+ PROVIDER.XAI.MODEL.TINY,
212
+ ]),
150
213
  ],
151
214
  LARGE: [
152
215
  PROVIDER.ANTHROPIC.MODEL.LARGE,
153
- PROVIDER.GEMINI.MODEL.LARGE,
216
+ PROVIDER.GOOGLE.MODEL.LARGE,
154
217
  PROVIDER.OPENAI.MODEL.LARGE,
218
+ PROVIDER.XAI.MODEL.LARGE,
155
219
  ],
156
220
  SMALL: [
157
221
  PROVIDER.ANTHROPIC.MODEL.SMALL,
158
- PROVIDER.GEMINI.MODEL.SMALL,
222
+ PROVIDER.GOOGLE.MODEL.SMALL,
159
223
  PROVIDER.OPENAI.MODEL.SMALL,
224
+ PROVIDER.XAI.MODEL.SMALL,
160
225
  ],
161
226
  TINY: [
162
227
  PROVIDER.ANTHROPIC.MODEL.TINY,
163
- PROVIDER.GEMINI.MODEL.TINY,
228
+ PROVIDER.GOOGLE.MODEL.TINY,
164
229
  PROVIDER.OPENAI.MODEL.TINY,
230
+ PROVIDER.XAI.MODEL.TINY,
165
231
  ],
166
232
  };
167
233
 
@@ -169,6 +235,7 @@ var constants = /*#__PURE__*/Object.freeze({
169
235
  __proto__: null,
170
236
  ALL: ALL,
171
237
  DEFAULT: DEFAULT,
238
+ MODEL: MODEL,
172
239
  PROVIDER: PROVIDER
173
240
  });
174
241
 
@@ -187,17 +254,31 @@ function determineModelProvider(input) {
187
254
  provider: PROVIDER.OPENROUTER.NAME,
188
255
  };
189
256
  }
257
+ // Check for explicit bedrock: prefix
258
+ if (input.startsWith("bedrock:")) {
259
+ const model = input.slice("bedrock:".length);
260
+ return {
261
+ model,
262
+ provider: PROVIDER.BEDROCK.NAME,
263
+ };
264
+ }
190
265
  // Check if input is a provider name
266
+ if (input === PROVIDER.BEDROCK.NAME) {
267
+ return {
268
+ model: PROVIDER.BEDROCK.MODEL.DEFAULT,
269
+ provider: PROVIDER.BEDROCK.NAME,
270
+ };
271
+ }
191
272
  if (input === PROVIDER.ANTHROPIC.NAME) {
192
273
  return {
193
274
  model: PROVIDER.ANTHROPIC.MODEL.DEFAULT,
194
275
  provider: PROVIDER.ANTHROPIC.NAME,
195
276
  };
196
277
  }
197
- if (input === PROVIDER.GEMINI.NAME) {
278
+ if (input === PROVIDER.GOOGLE.NAME || input === "gemini") {
198
279
  return {
199
- model: PROVIDER.GEMINI.MODEL.DEFAULT,
200
- provider: PROVIDER.GEMINI.NAME,
280
+ model: PROVIDER.GOOGLE.MODEL.DEFAULT,
281
+ provider: PROVIDER.GOOGLE.NAME,
201
282
  };
202
283
  }
203
284
  if (input === PROVIDER.OPENAI.NAME) {
@@ -212,6 +293,12 @@ function determineModelProvider(input) {
212
293
  provider: PROVIDER.OPENROUTER.NAME,
213
294
  };
214
295
  }
296
+ if (input === PROVIDER.XAI.NAME) {
297
+ return {
298
+ model: PROVIDER.XAI.MODEL.DEFAULT,
299
+ provider: PROVIDER.XAI.NAME,
300
+ };
301
+ }
215
302
  // Check if input matches an Anthropic model exactly
216
303
  for (const [, modelValue] of Object.entries(PROVIDER.ANTHROPIC.MODEL)) {
217
304
  if (input === modelValue) {
@@ -222,11 +309,11 @@ function determineModelProvider(input) {
222
309
  }
223
310
  }
224
311
  // Check if input matches a Gemini model exactly
225
- for (const [, modelValue] of Object.entries(PROVIDER.GEMINI.MODEL)) {
312
+ for (const [, modelValue] of Object.entries(PROVIDER.GOOGLE.MODEL)) {
226
313
  if (input === modelValue) {
227
314
  return {
228
315
  model: input,
229
- provider: PROVIDER.GEMINI.NAME,
316
+ provider: PROVIDER.GOOGLE.NAME,
230
317
  };
231
318
  }
232
319
  }
@@ -248,6 +335,15 @@ function determineModelProvider(input) {
248
335
  };
249
336
  }
250
337
  }
338
+ // Check if input matches an xAI model exactly
339
+ for (const [, modelValue] of Object.entries(PROVIDER.XAI.MODEL)) {
340
+ if (input === modelValue) {
341
+ return {
342
+ model: input,
343
+ provider: PROVIDER.XAI.NAME,
344
+ };
345
+ }
346
+ }
251
347
  // Assume OpenRouter for models containing "/" (e.g., "openai/gpt-4", "anthropic/claude-3-opus")
252
348
  // This check must come before match words so that "openai/gpt-4" is not matched by "openai" keyword
253
349
  if (input.includes("/")) {
@@ -256,8 +352,17 @@ function determineModelProvider(input) {
256
352
  provider: PROVIDER.OPENROUTER.NAME,
257
353
  };
258
354
  }
259
- // Check Anthropic match words
355
+ // Check Bedrock match words (before Anthropic — "anthropic.claude-*" is a Bedrock model ID)
260
356
  const lowerInput = input.toLowerCase();
357
+ for (const matchWord of PROVIDER.BEDROCK.MODEL_MATCH_WORDS) {
358
+ if (lowerInput.includes(matchWord)) {
359
+ return {
360
+ model: input,
361
+ provider: PROVIDER.BEDROCK.NAME,
362
+ };
363
+ }
364
+ }
365
+ // Check Anthropic match words
261
366
  for (const matchWord of PROVIDER.ANTHROPIC.MODEL_MATCH_WORDS) {
262
367
  if (lowerInput.includes(matchWord)) {
263
368
  return {
@@ -267,11 +372,11 @@ function determineModelProvider(input) {
267
372
  }
268
373
  }
269
374
  // Check Gemini match words
270
- for (const matchWord of PROVIDER.GEMINI.MODEL_MATCH_WORDS) {
375
+ for (const matchWord of PROVIDER.GOOGLE.MODEL_MATCH_WORDS) {
271
376
  if (lowerInput.includes(matchWord)) {
272
377
  return {
273
378
  model: input,
274
- provider: PROVIDER.GEMINI.NAME,
379
+ provider: PROVIDER.GOOGLE.NAME,
275
380
  };
276
381
  }
277
382
  }
@@ -294,6 +399,15 @@ function determineModelProvider(input) {
294
399
  }
295
400
  }
296
401
  }
402
+ // Check xAI match words
403
+ for (const matchWord of PROVIDER.XAI.MODEL_MATCH_WORDS) {
404
+ if (lowerInput.includes(matchWord)) {
405
+ return {
406
+ model: input,
407
+ provider: PROVIDER.XAI.NAME,
408
+ };
409
+ }
410
+ }
297
411
  // Check OpenRouter match words
298
412
  for (const matchWord of PROVIDER.OPENROUTER.MODEL_MATCH_WORDS) {
299
413
  if (lowerInput.includes(matchWord)) {
@@ -458,6 +572,154 @@ function extractReasoning(history) {
458
572
  return reasoningTexts;
459
573
  }
460
574
 
575
+ function naturalZodSchema(definition) {
576
+ if (Array.isArray(definition)) {
577
+ if (definition.length === 0) {
578
+ // Handle empty array - accept any[]
579
+ return z.array(z.any());
580
+ }
581
+ else if (definition.length === 1) {
582
+ // Handle array types
583
+ const itemType = definition[0];
584
+ switch (itemType) {
585
+ case String:
586
+ return z.array(z.string());
587
+ case Number:
588
+ return z.array(z.number());
589
+ case Boolean:
590
+ return z.array(z.boolean());
591
+ default:
592
+ if (typeof itemType === "object") {
593
+ // Handle array of objects
594
+ return z.array(naturalZodSchema(itemType));
595
+ }
596
+ // Handle enum arrays
597
+ return z.enum(definition);
598
+ }
599
+ }
600
+ else {
601
+ // Handle enum arrays
602
+ return z.enum(definition);
603
+ }
604
+ }
605
+ else if (definition && typeof definition === "object") {
606
+ if (Object.keys(definition).length === 0) {
607
+ // Handle empty object - accept any key-value pairs
608
+ return z.record(z.string(), z.any());
609
+ }
610
+ else {
611
+ // Handle object with properties
612
+ const schemaShape = {};
613
+ for (const [key, value] of Object.entries(definition)) {
614
+ schemaShape[key] = naturalZodSchema(value);
615
+ }
616
+ return z.object(schemaShape);
617
+ }
618
+ }
619
+ else {
620
+ switch (definition) {
621
+ case String:
622
+ return z.string();
623
+ case Number:
624
+ return z.number();
625
+ case Boolean:
626
+ return z.boolean();
627
+ case Object:
628
+ return z.record(z.string(), z.any());
629
+ case Array:
630
+ return z.array(z.any());
631
+ default:
632
+ throw new Error(`Unsupported type: ${definition}`);
633
+ }
634
+ }
635
+ }
636
+
637
+ //
638
+ //
639
+ // Helpers
640
+ //
641
+ function isPlainObject(value) {
642
+ return typeof value === "object" && value !== null && !Array.isArray(value);
643
+ }
644
+ /**
645
+ * Convert a `format` declaration (Zod schema, NaturalSchema, or a JSON Schema
646
+ * JsonObject) into a plain JSON Schema we can walk. Mirrors the conversion the
647
+ * provider adapters perform in `formatOutputSchema`, but without any
648
+ * provider-specific sanitization.
649
+ */
650
+ function formatToJsonSchema(format) {
651
+ if (format instanceof z.ZodType) {
652
+ return z.toJSONSchema(format);
653
+ }
654
+ if (isPlainObject(format) && format.type === "json_schema") {
655
+ const clone = structuredClone(format);
656
+ clone.type = "object";
657
+ return clone;
658
+ }
659
+ try {
660
+ return z.toJSONSchema(naturalZodSchema(format));
661
+ }
662
+ catch {
663
+ return undefined;
664
+ }
665
+ }
666
+ /**
667
+ * Walk a JSON Schema alongside a parsed value, filling any declared array field
668
+ * that is absent (`undefined`/`null`) with `[]`. Recurses into object
669
+ * properties and array items so nested declared arrays are also backfilled.
670
+ */
671
+ function fillFromSchema(schema, value) {
672
+ if (!isPlainObject(schema)) {
673
+ return value;
674
+ }
675
+ const type = schema.type;
676
+ const isArray = type === "array" || (type === undefined && "items" in schema);
677
+ if (isArray) {
678
+ if (value === undefined || value === null) {
679
+ return [];
680
+ }
681
+ const items = schema.items;
682
+ if (Array.isArray(value) && isPlainObject(items)) {
683
+ return value.map((entry) => fillFromSchema(items, entry));
684
+ }
685
+ return value;
686
+ }
687
+ const isObject = type === "object" || (type === undefined && "properties" in schema);
688
+ if (isObject) {
689
+ if (!isPlainObject(value)) {
690
+ return value;
691
+ }
692
+ const properties = schema.properties;
693
+ if (isPlainObject(properties)) {
694
+ for (const [key, propSchema] of Object.entries(properties)) {
695
+ value[key] = fillFromSchema(propSchema, value[key]);
696
+ }
697
+ }
698
+ return value;
699
+ }
700
+ return value;
701
+ }
702
+ //
703
+ //
704
+ // Main
705
+ //
706
+ /**
707
+ * Ensure every array field declared in `format` is present in `content` as an
708
+ * array. A declared `format` is a schema contract: an empty list should surface
709
+ * as `[]`, not be dropped from the response. Some providers/models omit empty
710
+ * array fields entirely, leaving consumers to read `.length` on `undefined`.
711
+ *
712
+ * Only mutates a (cloned) structured object; strings and non-objects pass
713
+ * through untouched.
714
+ */
715
+ function fillFormatArrays({ content, format, }) {
716
+ const schema = formatToJsonSchema(format);
717
+ if (!schema) {
718
+ return content;
719
+ }
720
+ return fillFromSchema(schema, structuredClone(content));
721
+ }
722
+
461
723
  /**
462
724
  * Converts a string to a standardized LlmInputMessage
463
725
  * @param input - String to format
@@ -505,12 +767,53 @@ function formatOperateInput(input, options) {
505
767
  return [input];
506
768
  }
507
769
 
508
- const getLogger$4 = () => log$2.lib({ lib: JAYPIE.LIB.LLM });
509
- const log$1 = getLogger$4();
770
+ /**
771
+ * Converts a JSON Schema (Draft 2020-12) object to the OpenAPI 3.0 schema subset
772
+ * that Gemini's `responseSchema` accepts. This constrains generation (not just validation)
773
+ * and avoids the `items`-keyword leakage bug in `responseJsonSchema`.
774
+ *
775
+ * Strips: $schema, additionalProperties, $defs, $ref (inlines where possible), const
776
+ * Preserves: type, properties, required, items, enum, description, nullable
777
+ */
778
+ function jsonSchemaToOpenApi3(schema) {
779
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
780
+ return schema;
781
+ }
782
+ const result = {};
783
+ for (const [key, value] of Object.entries(schema)) {
784
+ // Strip JSON Schema keywords not in OpenAPI 3.0 subset
785
+ if (key === "$schema" ||
786
+ key === "$defs" ||
787
+ key === "additionalProperties" ||
788
+ key === "const" ||
789
+ key === "$ref") {
790
+ continue;
791
+ }
792
+ if (key === "properties" &&
793
+ typeof value === "object" &&
794
+ value !== null &&
795
+ !Array.isArray(value)) {
796
+ const convertedProps = {};
797
+ for (const [propKey, propValue] of Object.entries(value)) {
798
+ convertedProps[propKey] = jsonSchemaToOpenApi3(propValue);
799
+ }
800
+ result[key] = convertedProps;
801
+ }
802
+ else if (key === "items" && typeof value === "object" && value !== null) {
803
+ result[key] = jsonSchemaToOpenApi3(value);
804
+ }
805
+ else {
806
+ result[key] = value;
807
+ }
808
+ }
809
+ return result;
810
+ }
811
+
812
+ const getLogger$6 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
510
813
 
511
814
  // Turn policy constants
512
815
  const MAX_TURNS_ABSOLUTE_LIMIT = 72;
513
- const MAX_TURNS_DEFAULT_LIMIT = 12;
816
+ const MAX_TURNS_DEFAULT_LIMIT = 24;
514
817
  /**
515
818
  * Determines the maximum number of turns based on the provided options
516
819
  *
@@ -544,68 +847,6 @@ function maxTurnsFromOptions(options) {
544
847
  return 1;
545
848
  }
546
849
 
547
- function naturalZodSchema(definition) {
548
- if (Array.isArray(definition)) {
549
- if (definition.length === 0) {
550
- // Handle empty array - accept any[]
551
- return z.array(z.any());
552
- }
553
- else if (definition.length === 1) {
554
- // Handle array types
555
- const itemType = definition[0];
556
- switch (itemType) {
557
- case String:
558
- return z.array(z.string());
559
- case Number:
560
- return z.array(z.number());
561
- case Boolean:
562
- return z.array(z.boolean());
563
- default:
564
- if (typeof itemType === "object") {
565
- // Handle array of objects
566
- return z.array(naturalZodSchema(itemType));
567
- }
568
- // Handle enum arrays
569
- return z.enum(definition);
570
- }
571
- }
572
- else {
573
- // Handle enum arrays
574
- return z.enum(definition);
575
- }
576
- }
577
- else if (definition && typeof definition === "object") {
578
- if (Object.keys(definition).length === 0) {
579
- // Handle empty object - accept any key-value pairs
580
- return z.record(z.string(), z.any());
581
- }
582
- else {
583
- // Handle object with properties
584
- const schemaShape = {};
585
- for (const [key, value] of Object.entries(definition)) {
586
- schemaShape[key] = naturalZodSchema(value);
587
- }
588
- return z.object(schemaShape);
589
- }
590
- }
591
- else {
592
- switch (definition) {
593
- case String:
594
- return z.string();
595
- case Number:
596
- return z.number();
597
- case Boolean:
598
- return z.boolean();
599
- case Object:
600
- return z.record(z.string(), z.any());
601
- case Array:
602
- return z.array(z.any());
603
- default:
604
- throw new Error(`Unsupported type: ${definition}`);
605
- }
606
- }
607
- }
608
-
609
850
  //
610
851
  // Constants
611
852
  //
@@ -805,26 +1046,242 @@ var ErrorCategory;
805
1046
  ErrorCategory["Unknown"] = "unknown";
806
1047
  })(ErrorCategory || (ErrorCategory = {}));
807
1048
 
1049
+ /**
1050
+ * Transient network error detection utility.
1051
+ *
1052
+ * Detects low-level Node.js/undici network errors that indicate
1053
+ * a temporary network issue (not a provider API error).
1054
+ * These errors should always be retried.
1055
+ */
808
1056
  //
809
1057
  //
810
1058
  // Constants
811
1059
  //
812
- const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
813
- // Regular expression to parse data URLs: data:mime/type;base64,data
814
- const DATA_URL_REGEX = /^data:([^;]+);base64,(.+)$/;
815
- /**
816
- * Parse a data URL into its components
817
- */
818
- function parseDataUrl(dataUrl) {
819
- const match = dataUrl.match(DATA_URL_REGEX);
820
- if (!match)
821
- return null;
822
- return { mediaType: match[1], data: match[2] };
823
- }
1060
+ /** Error codes from Node.js net/dns subsystems that indicate transient failures */
1061
+ const TRANSIENT_ERROR_CODES = new Set([
1062
+ "ECONNREFUSED",
1063
+ "ECONNRESET",
1064
+ "EAI_AGAIN",
1065
+ "ENETRESET",
1066
+ "ENETUNREACH",
1067
+ "ENOTFOUND",
1068
+ "EPIPE",
1069
+ "ETIMEDOUT",
1070
+ ]);
1071
+ /** Substrings in error messages that indicate transient network issues */
1072
+ const TRANSIENT_MESSAGE_PATTERNS = [
1073
+ "network",
1074
+ "socket hang up",
1075
+ "terminated",
1076
+ ];
1077
+ //
1078
+ //
1079
+ // Helpers
1080
+ //
824
1081
  /**
825
- * Convert standardized content items to Anthropic format
1082
+ * Check a single error (without walking the cause chain)
826
1083
  */
827
- function convertContentToAnthropic(content) {
1084
+ function matchesSingleError(error) {
1085
+ if (!(error instanceof Error))
1086
+ return false;
1087
+ // Check error code (e.g., ECONNRESET)
1088
+ const code = error.code;
1089
+ if (code && TRANSIENT_ERROR_CODES.has(code)) {
1090
+ return true;
1091
+ }
1092
+ // Check error message for transient patterns
1093
+ const message = error.message.toLowerCase();
1094
+ for (const pattern of TRANSIENT_MESSAGE_PATTERNS) {
1095
+ if (message.includes(pattern)) {
1096
+ return true;
1097
+ }
1098
+ }
1099
+ return false;
1100
+ }
1101
+ //
1102
+ //
1103
+ // Main
1104
+ //
1105
+ /**
1106
+ * Detect transient network errors by inspecting the error and its cause chain.
1107
+ *
1108
+ * Undici (Node.js fetch) wraps low-level errors like ECONNRESET inside
1109
+ * `TypeError: terminated`. This function recursively walks `error.cause`
1110
+ * to detect these wrapped errors.
1111
+ *
1112
+ * @param error - The error to inspect
1113
+ * @returns true if the error (or any cause in its chain) is a transient network error
1114
+ */
1115
+ function isTransientNetworkError(error) {
1116
+ let current = error;
1117
+ while (current) {
1118
+ if (matchesSingleError(current)) {
1119
+ return true;
1120
+ }
1121
+ // Walk the cause chain (cause is ES2022, cast for compatibility)
1122
+ const cause = current.cause;
1123
+ if (current instanceof Error && cause) {
1124
+ current = cause;
1125
+ }
1126
+ else {
1127
+ break;
1128
+ }
1129
+ }
1130
+ return false;
1131
+ }
1132
+
1133
+ //
1134
+ //
1135
+ // Constants
1136
+ //
1137
+ const STRUCTURED_OUTPUT_TOOL_NAME$3 = "structured_output";
1138
+ const STRUCTURED_OUTPUT_NON_PARSE_STOP_REASONS = new Set([
1139
+ "refusal",
1140
+ "max_tokens",
1141
+ ]);
1142
+ // Regular expression to parse data URLs: data:mime/type;base64,data
1143
+ const DATA_URL_REGEX$1 = /^data:([^;]+);base64,(.+)$/;
1144
+ // String formats accepted by Anthropic's structured-output grammar compiler.
1145
+ // Other formats are stripped (move to description) so the API does not 400.
1146
+ const SUPPORTED_STRING_FORMATS = new Set([
1147
+ "date",
1148
+ "date-time",
1149
+ "duration",
1150
+ "email",
1151
+ "hostname",
1152
+ "ipv4",
1153
+ "ipv6",
1154
+ "time",
1155
+ "uri",
1156
+ "uuid",
1157
+ ]);
1158
+ // Top-level keywords stripped wholesale before sending: not part of the spec
1159
+ // the API enforces and the validator can reject them.
1160
+ const STRIPPED_TOP_LEVEL_KEYWORDS = new Set(["$schema", "$id"]);
1161
+ // Keywords Anthropic's structured-output grammar does not support. They are
1162
+ // removed from the schema and appended to `description` so the model still
1163
+ // sees the intent.
1164
+ const UNSUPPORTED_CONSTRAINT_KEYWORDS = new Set([
1165
+ "exclusiveMaximum",
1166
+ "exclusiveMinimum",
1167
+ "maxItems",
1168
+ "maxLength",
1169
+ "maxProperties",
1170
+ "maximum",
1171
+ "minLength",
1172
+ "minProperties",
1173
+ "minimum",
1174
+ "multipleOf",
1175
+ "pattern",
1176
+ "patternProperties",
1177
+ "uniqueItems",
1178
+ ]);
1179
+ /**
1180
+ * Recursively transform a JSON Schema into the strict shape Anthropic's
1181
+ * structured-output grammar accepts: object types must have
1182
+ * `additionalProperties: false`, unsupported numeric/string/array
1183
+ * constraints are appended to `description`, and unsupported string
1184
+ * formats are stripped. Mirrors @anthropic-ai/sdk's `transformJSONSchema`
1185
+ * but inline so we do not take a runtime dependency on the optional
1186
+ * peer SDK.
1187
+ */
1188
+ function sanitizeJsonSchemaForAnthropic(schema, isRoot = true) {
1189
+ const result = {};
1190
+ const carriedConstraints = [];
1191
+ for (const [key, value] of Object.entries(schema)) {
1192
+ if (isRoot && STRIPPED_TOP_LEVEL_KEYWORDS.has(key)) {
1193
+ continue;
1194
+ }
1195
+ if (UNSUPPORTED_CONSTRAINT_KEYWORDS.has(key)) {
1196
+ carriedConstraints.push(`${key}: ${JSON.stringify(value)}`);
1197
+ continue;
1198
+ }
1199
+ if (key === "format" && typeof value === "string") {
1200
+ if (SUPPORTED_STRING_FORMATS.has(value)) {
1201
+ result[key] = value;
1202
+ }
1203
+ else {
1204
+ carriedConstraints.push(`format: ${JSON.stringify(value)}`);
1205
+ }
1206
+ continue;
1207
+ }
1208
+ if (key === "minItems" && typeof value === "number") {
1209
+ if (value === 0 || value === 1) {
1210
+ result[key] = value;
1211
+ }
1212
+ else {
1213
+ carriedConstraints.push(`minItems: ${value}`);
1214
+ }
1215
+ continue;
1216
+ }
1217
+ if (key === "properties" &&
1218
+ value &&
1219
+ typeof value === "object" &&
1220
+ !Array.isArray(value)) {
1221
+ const transformedProps = {};
1222
+ for (const [propName, propSchema] of Object.entries(value)) {
1223
+ transformedProps[propName] = isJsonSchema(propSchema)
1224
+ ? sanitizeJsonSchemaForAnthropic(propSchema, false)
1225
+ : propSchema;
1226
+ }
1227
+ result[key] = transformedProps;
1228
+ continue;
1229
+ }
1230
+ if ((key === "items" || key === "additionalItems" || key === "contains") &&
1231
+ isJsonSchema(value)) {
1232
+ result[key] = sanitizeJsonSchemaForAnthropic(value, false);
1233
+ continue;
1234
+ }
1235
+ if ((key === "anyOf" || key === "oneOf" || key === "allOf") &&
1236
+ Array.isArray(value)) {
1237
+ const targetKey = key === "oneOf" ? "anyOf" : key;
1238
+ result[targetKey] = value.map((entry) => isJsonSchema(entry)
1239
+ ? sanitizeJsonSchemaForAnthropic(entry, false)
1240
+ : entry);
1241
+ continue;
1242
+ }
1243
+ if (key === "$defs" && value && typeof value === "object") {
1244
+ const transformedDefs = {};
1245
+ for (const [defName, defSchema] of Object.entries(value)) {
1246
+ transformedDefs[defName] = isJsonSchema(defSchema)
1247
+ ? sanitizeJsonSchemaForAnthropic(defSchema, false)
1248
+ : defSchema;
1249
+ }
1250
+ result[key] = transformedDefs;
1251
+ continue;
1252
+ }
1253
+ if (key === "additionalProperties") {
1254
+ // Always force `false` on objects below; ignore caller-supplied value.
1255
+ continue;
1256
+ }
1257
+ result[key] = value;
1258
+ }
1259
+ if (result.type === "object") {
1260
+ result.additionalProperties = false;
1261
+ }
1262
+ if (carriedConstraints.length > 0) {
1263
+ const existing = typeof result.description === "string" ? result.description : "";
1264
+ const suffix = `{${carriedConstraints.join(", ")}}`;
1265
+ result.description = existing ? `${existing}\n\n${suffix}` : suffix;
1266
+ }
1267
+ return result;
1268
+ }
1269
+ function isJsonSchema(value) {
1270
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1271
+ }
1272
+ /**
1273
+ * Parse a data URL into its components
1274
+ */
1275
+ function parseDataUrl(dataUrl) {
1276
+ const match = dataUrl.match(DATA_URL_REGEX$1);
1277
+ if (!match)
1278
+ return null;
1279
+ return { mediaType: match[1], data: match[2] };
1280
+ }
1281
+ /**
1282
+ * Convert standardized content items to Anthropic format
1283
+ */
1284
+ function convertContentToAnthropic(content) {
828
1285
  if (typeof content === "string") {
829
1286
  return content;
830
1287
  }
@@ -889,6 +1346,47 @@ const NOT_RETRYABLE_ERROR_NAMES = [
889
1346
  "NotFoundError",
890
1347
  "PermissionDeniedError",
891
1348
  ];
1349
+ // Models known not to accept `temperature`.
1350
+ // Patterns (not exact names) so dated variants and future releases are covered
1351
+ // without code changes — Anthropic is trending toward removing temperature on
1352
+ // newer Claude models.
1353
+ const MODELS_WITHOUT_TEMPERATURE$2 = [
1354
+ /^claude-opus-4-[789]/,
1355
+ /^claude-opus-[5-9]/,
1356
+ ];
1357
+ function isTemperatureDeprecationError$3(error) {
1358
+ if (!error || typeof error !== "object")
1359
+ return false;
1360
+ const err = error;
1361
+ const name = error?.constructor?.name;
1362
+ if (name !== "BadRequestError" && err.status !== 400)
1363
+ return false;
1364
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
1365
+ return messages.some((m) => m.toLowerCase().includes("temperature"));
1366
+ }
1367
+ /**
1368
+ * Detect 400 errors that indicate the model itself does not support native
1369
+ * structured outputs (`output_config.format`). Citations + structured output
1370
+ * is also a 400 case but is a caller error rather than a model-capability
1371
+ * gap, so we explicitly skip it to avoid masking the real problem under a
1372
+ * tool-emulation retry. The deprecated-`output_format` 400 (API renamed the
1373
+ * field) is also explicitly excluded — that's a code-path bug, not a model
1374
+ * gap; it should propagate so we notice and fix it.
1375
+ */
1376
+ function isStructuredOutputUnsupportedError$1(error) {
1377
+ if (!error || typeof error !== "object")
1378
+ return false;
1379
+ const err = error;
1380
+ const name = error?.constructor?.name;
1381
+ if (name !== "BadRequestError" && err.status !== 400)
1382
+ return false;
1383
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
1384
+ if (messages.some((m) => /citation/i.test(m)))
1385
+ return false;
1386
+ if (messages.some((m) => /deprecated/i.test(m)))
1387
+ return false;
1388
+ return messages.some((m) => /output_config|output_format|json[_ ]schema|structured/i.test(m));
1389
+ }
892
1390
  //
893
1391
  //
894
1392
  // Main
@@ -903,6 +1401,33 @@ class AnthropicAdapter extends BaseProviderAdapter {
903
1401
  super(...arguments);
904
1402
  this.name = PROVIDER.ANTHROPIC.NAME;
905
1403
  this.defaultModel = PROVIDER.ANTHROPIC.MODEL.DEFAULT;
1404
+ // Session-level cache of models observed to reject `temperature` at runtime.
1405
+ // Populated by executeRequest on 400 errors so repeat calls skip the param.
1406
+ this.runtimeNoTemperatureModels = new Set();
1407
+ // Session-level cache of models observed to reject `output_format`. When a
1408
+ // model is in this set, buildRequest engages the legacy fake-tool path
1409
+ // instead of native structured output.
1410
+ this.runtimeNoStructuredOutputModels = new Set();
1411
+ }
1412
+ rememberModelRejectsTemperature(model) {
1413
+ this.runtimeNoTemperatureModels.add(model);
1414
+ }
1415
+ clearRuntimeNoTemperatureModels() {
1416
+ this.runtimeNoTemperatureModels.clear();
1417
+ }
1418
+ rememberModelRejectsStructuredOutput(model) {
1419
+ this.runtimeNoStructuredOutputModels.add(model);
1420
+ }
1421
+ clearRuntimeNoStructuredOutputModels() {
1422
+ this.runtimeNoStructuredOutputModels.clear();
1423
+ }
1424
+ supportsTemperature(model) {
1425
+ if (this.runtimeNoTemperatureModels.has(model))
1426
+ return false;
1427
+ return !MODELS_WITHOUT_TEMPERATURE$2.some((pattern) => pattern.test(model));
1428
+ }
1429
+ supportsStructuredOutput(model) {
1430
+ return !this.runtimeNoStructuredOutputModels.has(model);
906
1431
  }
907
1432
  //
908
1433
  // Request Building
@@ -971,8 +1496,22 @@ class AnthropicAdapter extends BaseProviderAdapter {
971
1496
  if (request.system) {
972
1497
  anthropicRequest.system = request.system;
973
1498
  }
974
- if (request.tools && request.tools.length > 0) {
975
- anthropicRequest.tools = request.tools.map((tool) => ({
1499
+ const useFallbackStructuredOutput = Boolean(request.format) &&
1500
+ !this.supportsStructuredOutput(anthropicRequest.model);
1501
+ const allTools = request.tools
1502
+ ? [...request.tools]
1503
+ : [];
1504
+ if (useFallbackStructuredOutput && request.format) {
1505
+ log$2.warn(`[AnthropicAdapter] Using legacy structured_output tool fallback for model ${anthropicRequest.model}; native output_config previously rejected for this model.`);
1506
+ allTools.push({
1507
+ name: STRUCTURED_OUTPUT_TOOL_NAME$3,
1508
+ description: "Output a structured JSON object, " +
1509
+ "use this before your final response to give structured outputs to the user",
1510
+ parameters: request.format,
1511
+ });
1512
+ }
1513
+ if (allTools.length > 0) {
1514
+ anthropicRequest.tools = allTools.map((tool) => ({
976
1515
  name: tool.name,
977
1516
  description: tool.description,
978
1517
  input_schema: {
@@ -981,10 +1520,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
981
1520
  },
982
1521
  type: "custom",
983
1522
  }));
984
- // Determine tool choice based on whether structured output is requested
985
- const hasStructuredOutput = request.tools.some((t) => t.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
986
- anthropicRequest.tool_choice = {
987
- type: hasStructuredOutput ? "any" : "auto",
1523
+ anthropicRequest.tool_choice = useFallbackStructuredOutput
1524
+ ? { type: "any" }
1525
+ : { type: "auto" };
1526
+ }
1527
+ // Native structured output: send schema as `output_config.format`. The
1528
+ // legacy tool-emulation path is engaged only as a runtime fallback for
1529
+ // models the API has flagged as not supporting native structured output.
1530
+ if (request.format && !useFallbackStructuredOutput) {
1531
+ anthropicRequest.output_config = {
1532
+ format: {
1533
+ type: "json_schema",
1534
+ schema: request.format,
1535
+ },
988
1536
  };
989
1537
  }
990
1538
  if (request.providerOptions) {
@@ -994,10 +1542,20 @@ class AnthropicAdapter extends BaseProviderAdapter {
994
1542
  if (request.temperature !== undefined) {
995
1543
  anthropicRequest.temperature = request.temperature;
996
1544
  }
1545
+ // Strip temperature for models that don't support it (denylist + runtime cache)
1546
+ if (anthropicRequest.temperature !== undefined &&
1547
+ !this.supportsTemperature(anthropicRequest.model)) {
1548
+ delete anthropicRequest.temperature;
1549
+ }
997
1550
  return anthropicRequest;
998
1551
  }
999
- formatTools(toolkit, outputSchema) {
1000
- const tools = toolkit.tools.map((tool) => ({
1552
+ formatTools(toolkit,
1553
+ // outputSchema is part of the interface contract but Anthropic now uses
1554
+ // native `output_format` (set in buildRequest), so we no longer inject a
1555
+ // synthetic structured-output tool here.
1556
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1557
+ _outputSchema) {
1558
+ return toolkit.tools.map((tool) => ({
1001
1559
  name: tool.name,
1002
1560
  description: tool.description,
1003
1561
  parameters: {
@@ -1005,16 +1563,6 @@ class AnthropicAdapter extends BaseProviderAdapter {
1005
1563
  type: "object",
1006
1564
  },
1007
1565
  }));
1008
- // Add structured output tool if schema is provided
1009
- if (outputSchema) {
1010
- tools.push({
1011
- name: STRUCTURED_OUTPUT_TOOL_NAME$2,
1012
- description: "Output a structured JSON object, " +
1013
- "use this before your final response to give structured outputs to the user",
1014
- parameters: outputSchema,
1015
- });
1016
- }
1017
- return tools;
1018
1566
  }
1019
1567
  formatOutputSchema(schema) {
1020
1568
  let jsonSchema;
@@ -1033,26 +1581,100 @@ class AnthropicAdapter extends BaseProviderAdapter {
1033
1581
  : naturalZodSchema(schema);
1034
1582
  jsonSchema = z.toJSONSchema(zodSchema);
1035
1583
  }
1036
- // Remove $schema property (causes issues with validator)
1037
- if (jsonSchema.$schema) {
1038
- delete jsonSchema.$schema;
1039
- }
1040
- return jsonSchema;
1584
+ return sanitizeJsonSchemaForAnthropic(jsonSchema);
1041
1585
  }
1042
1586
  //
1043
1587
  // API Execution
1044
1588
  //
1045
- async executeRequest(client, request) {
1589
+ async executeRequest(client, request, signal) {
1046
1590
  const anthropic = client;
1047
- return (await anthropic.messages.create(request));
1591
+ const anthropicRequest = request;
1592
+ const wantsStructuredOutput = Boolean(anthropicRequest.output_config);
1593
+ try {
1594
+ const response = (await anthropic.messages.create(anthropicRequest, signal ? { signal } : undefined));
1595
+ if (wantsStructuredOutput) {
1596
+ response.__jaypieStructuredOutput = true;
1597
+ }
1598
+ return response;
1599
+ }
1600
+ catch (error) {
1601
+ if (signal?.aborted)
1602
+ return undefined;
1603
+ // If the model rejected `temperature`, cache it and retry without the param
1604
+ if (anthropicRequest.temperature !== undefined &&
1605
+ isTemperatureDeprecationError$3(error)) {
1606
+ this.rememberModelRejectsTemperature(anthropicRequest.model);
1607
+ const retryRequest = { ...anthropicRequest };
1608
+ delete retryRequest.temperature;
1609
+ const response = (await anthropic.messages.create(retryRequest, signal ? { signal } : undefined));
1610
+ if (wantsStructuredOutput) {
1611
+ response.__jaypieStructuredOutput = true;
1612
+ }
1613
+ return response;
1614
+ }
1615
+ // If the model rejected native structured output, cache it and retry
1616
+ // via the legacy fake-tool emulation path.
1617
+ if (wantsStructuredOutput && isStructuredOutputUnsupportedError$1(error)) {
1618
+ const model = anthropicRequest.model;
1619
+ this.rememberModelRejectsStructuredOutput(model);
1620
+ log$2.warn(`[AnthropicAdapter] Model ${model} rejected native output_config; falling back to legacy structured_output tool emulation.`);
1621
+ const fallbackRequest = this.toFallbackStructuredOutputRequest(anthropicRequest);
1622
+ return (await anthropic.messages.create(fallbackRequest, signal ? { signal } : undefined));
1623
+ }
1624
+ throw error;
1625
+ }
1626
+ }
1627
+ /**
1628
+ * Rebuild a structured-output request without `output_format`, swapping in
1629
+ * the legacy fake-tool emulation. Used as a runtime fallback when a model
1630
+ * rejects native `output_config.format`.
1631
+ */
1632
+ toFallbackStructuredOutputRequest(request) {
1633
+ const { output_config, ...rest } = request;
1634
+ if (!output_config)
1635
+ return request;
1636
+ const fallbackRequest = { ...rest };
1637
+ const fakeTool = {
1638
+ name: STRUCTURED_OUTPUT_TOOL_NAME$3,
1639
+ description: "Output a structured JSON object, " +
1640
+ "use this before your final response to give structured outputs to the user",
1641
+ input_schema: {
1642
+ ...output_config.format.schema,
1643
+ type: "object",
1644
+ },
1645
+ type: "custom",
1646
+ };
1647
+ fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
1648
+ fallbackRequest.tool_choice = { type: "any" };
1649
+ return fallbackRequest;
1048
1650
  }
1049
- async *executeStreamRequest(client, request) {
1651
+ async *executeStreamRequest(client, request, signal) {
1050
1652
  const anthropic = client;
1051
- const streamRequest = {
1653
+ // Preserve `output_config` when passing through to the SDK by typing
1654
+ // through the local extension instead of the upstream
1655
+ // MessageCreateParams shape.
1656
+ let streamRequest = {
1052
1657
  ...request,
1053
1658
  stream: true,
1054
1659
  };
1055
- const stream = await anthropic.messages.create(streamRequest);
1660
+ let stream;
1661
+ try {
1662
+ stream = await anthropic.messages.create(streamRequest, signal ? { signal } : undefined);
1663
+ }
1664
+ catch (error) {
1665
+ if (streamRequest.temperature !== undefined &&
1666
+ isTemperatureDeprecationError$3(error)) {
1667
+ this.rememberModelRejectsTemperature(streamRequest.model);
1668
+ streamRequest = {
1669
+ ...streamRequest,
1670
+ };
1671
+ delete streamRequest.temperature;
1672
+ stream = await anthropic.messages.create(streamRequest, signal ? { signal } : undefined);
1673
+ }
1674
+ else {
1675
+ throw error;
1676
+ }
1677
+ }
1056
1678
  // Track current tool call being built
1057
1679
  let currentToolCall = null;
1058
1680
  // Track usage for final chunk
@@ -1139,43 +1761,659 @@ class AnthropicAdapter extends BaseProviderAdapter {
1139
1761
  //
1140
1762
  // Response Parsing
1141
1763
  //
1142
- parseResponse(response, _options) {
1143
- const anthropicResponse = response;
1144
- const content = this.extractContent(anthropicResponse);
1145
- const hasToolCalls = anthropicResponse.stop_reason === "tool_use";
1764
+ parseResponse(response, _options) {
1765
+ const anthropicResponse = response;
1766
+ const content = this.extractContent(anthropicResponse);
1767
+ const hasToolCalls = anthropicResponse.stop_reason === "tool_use";
1768
+ return {
1769
+ content,
1770
+ hasToolCalls,
1771
+ stopReason: anthropicResponse.stop_reason ?? undefined,
1772
+ usage: this.extractUsage(anthropicResponse, anthropicResponse.model),
1773
+ raw: anthropicResponse,
1774
+ };
1775
+ }
1776
+ extractToolCalls(response) {
1777
+ const anthropicResponse = response;
1778
+ const toolCalls = [];
1779
+ for (const block of anthropicResponse.content) {
1780
+ if (block.type === "tool_use") {
1781
+ toolCalls.push({
1782
+ callId: block.id,
1783
+ name: block.name,
1784
+ arguments: JSON.stringify(block.input),
1785
+ raw: block,
1786
+ });
1787
+ }
1788
+ }
1789
+ return toolCalls;
1790
+ }
1791
+ extractUsage(response, model) {
1792
+ const anthropicResponse = response;
1793
+ // Check for thinking tokens in the usage (extended thinking feature)
1794
+ // Anthropic includes thinking tokens in a separate field when enabled
1795
+ const usage = anthropicResponse.usage;
1796
+ return {
1797
+ input: usage.input_tokens,
1798
+ output: usage.output_tokens,
1799
+ reasoning: usage.thinking_tokens || 0,
1800
+ total: usage.input_tokens + usage.output_tokens,
1801
+ provider: this.name,
1802
+ model,
1803
+ };
1804
+ }
1805
+ //
1806
+ // Tool Result Handling
1807
+ //
1808
+ formatToolResult(toolCall, result) {
1809
+ return {
1810
+ type: "tool_result",
1811
+ tool_use_id: toolCall.callId,
1812
+ content: result.output,
1813
+ };
1814
+ }
1815
+ appendToolResult(request, toolCall, result) {
1816
+ const anthropicRequest = request;
1817
+ const toolCallRaw = toolCall.raw;
1818
+ // Add assistant message with the tool use
1819
+ anthropicRequest.messages.push({
1820
+ role: "assistant",
1821
+ content: [toolCallRaw],
1822
+ });
1823
+ // Add user message with the tool result
1824
+ anthropicRequest.messages.push({
1825
+ role: "user",
1826
+ content: [this.formatToolResult(toolCall, result)],
1827
+ });
1828
+ return anthropicRequest;
1829
+ }
1830
+ //
1831
+ // History Management
1832
+ //
1833
+ responseToHistoryItems(response) {
1834
+ const anthropicResponse = response;
1835
+ const historyItems = [];
1836
+ // Check if this is a tool use response
1837
+ if (anthropicResponse.stop_reason === "tool_use") {
1838
+ // Don't add to history yet - will be added after tool execution
1839
+ return historyItems;
1840
+ }
1841
+ // Include thinking blocks for extended thinking support
1842
+ // Thinking blocks are preserved in history so extractReasoning can find them
1843
+ for (const block of anthropicResponse.content) {
1844
+ if (block.type === "thinking") {
1845
+ // Push raw block - types are loosely checked at runtime
1846
+ // This allows extractReasoning to access the thinking property
1847
+ historyItems.push(block);
1848
+ }
1849
+ }
1850
+ // Extract text content for non-tool responses
1851
+ const textBlock = anthropicResponse.content.find((block) => block.type === "text");
1852
+ if (textBlock) {
1853
+ historyItems.push({
1854
+ content: textBlock.text,
1855
+ role: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
1856
+ type: LlmMessageType.Message,
1857
+ });
1858
+ }
1859
+ return historyItems;
1860
+ }
1861
+ //
1862
+ // Error Classification
1863
+ //
1864
+ classifyError(error) {
1865
+ const errorName = error?.constructor?.name;
1866
+ // Check for rate limit error
1867
+ if (errorName === "RateLimitError") {
1868
+ return {
1869
+ error,
1870
+ category: ErrorCategory.RateLimit,
1871
+ shouldRetry: false,
1872
+ suggestedDelayMs: 60000,
1873
+ };
1874
+ }
1875
+ // Check for retryable errors
1876
+ if (RETRYABLE_ERROR_NAMES.includes(errorName)) {
1877
+ return {
1878
+ error,
1879
+ category: ErrorCategory.Retryable,
1880
+ shouldRetry: true,
1881
+ };
1882
+ }
1883
+ // Check for non-retryable errors
1884
+ if (NOT_RETRYABLE_ERROR_NAMES.includes(errorName)) {
1885
+ return {
1886
+ error,
1887
+ category: ErrorCategory.Unrecoverable,
1888
+ shouldRetry: false,
1889
+ };
1890
+ }
1891
+ // Check for transient network errors (ECONNRESET, etc.)
1892
+ if (isTransientNetworkError(error)) {
1893
+ return {
1894
+ error,
1895
+ category: ErrorCategory.Retryable,
1896
+ shouldRetry: true,
1897
+ };
1898
+ }
1899
+ // Unknown error - treat as potentially retryable
1900
+ return {
1901
+ error,
1902
+ category: ErrorCategory.Unknown,
1903
+ shouldRetry: true,
1904
+ };
1905
+ }
1906
+ //
1907
+ // Provider-Specific Features
1908
+ //
1909
+ isComplete(response) {
1910
+ const anthropicResponse = response;
1911
+ return anthropicResponse.stop_reason !== "tool_use";
1912
+ }
1913
+ hasStructuredOutput(response) {
1914
+ const anthropicResponse = response;
1915
+ // Native path: executeRequest annotates the response when we sent
1916
+ // `output_format`, so we can detect intent statelessly.
1917
+ if (anthropicResponse.__jaypieStructuredOutput) {
1918
+ return this.extractStructuredOutput(response) !== undefined;
1919
+ }
1920
+ // Fallback path: legacy fake-tool emulation, kept for models that the
1921
+ // runtime has cached as not supporting `output_format`.
1922
+ const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
1923
+ return (lastBlock?.type === "tool_use" &&
1924
+ lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$3);
1925
+ }
1926
+ extractStructuredOutput(response) {
1927
+ const anthropicResponse = response;
1928
+ if (anthropicResponse.__jaypieStructuredOutput) {
1929
+ // Refusal and truncation are explicit non-JSON outcomes per Anthropic
1930
+ // structured-outputs docs — surface the text upstream instead of
1931
+ // forcing a JSON.parse on what is not JSON.
1932
+ if (anthropicResponse.stop_reason &&
1933
+ STRUCTURED_OUTPUT_NON_PARSE_STOP_REASONS.has(anthropicResponse.stop_reason)) {
1934
+ return undefined;
1935
+ }
1936
+ const textBlock = anthropicResponse.content.find((block) => block.type === "text");
1937
+ if (!textBlock)
1938
+ return undefined;
1939
+ try {
1940
+ const parsed = JSON.parse(textBlock.text);
1941
+ return parsed;
1942
+ }
1943
+ catch {
1944
+ return undefined;
1945
+ }
1946
+ }
1947
+ // Fallback path: legacy fake-tool emulation
1948
+ const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
1949
+ if (lastBlock?.type === "tool_use" &&
1950
+ lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$3) {
1951
+ return lastBlock.input;
1952
+ }
1953
+ return undefined;
1954
+ }
1955
+ //
1956
+ // Private Helpers
1957
+ //
1958
+ extractContent(response) {
1959
+ // Check for structured output first
1960
+ if (this.hasStructuredOutput(response)) {
1961
+ return this.extractStructuredOutput(response);
1962
+ }
1963
+ // Extract text content
1964
+ const textBlock = response.content.find((block) => block.type === "text");
1965
+ return textBlock?.text;
1966
+ }
1967
+ }
1968
+ // Export singleton instance
1969
+ const anthropicAdapter = new AnthropicAdapter();
1970
+
1971
+ //
1972
+ //
1973
+ // Helpers
1974
+ //
1975
+ // Regular expression to parse data URLs
1976
+ const DATA_URL_REGEX = /^data:([^;]+);base64,(.+)$/;
1977
+ const MIME_TO_DOCUMENT_FORMAT = {
1978
+ "application/pdf": "pdf",
1979
+ "text/csv": "csv",
1980
+ "application/msword": "doc",
1981
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
1982
+ "application/vnd.ms-excel": "xls",
1983
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
1984
+ "text/html": "html",
1985
+ "text/plain": "txt",
1986
+ "text/markdown": "md",
1987
+ };
1988
+ function convertContentToBedrock(content) {
1989
+ if (typeof content === "string") {
1990
+ return [{ text: content }];
1991
+ }
1992
+ return content.map((item) => {
1993
+ if (item.type === LlmMessageType.InputText) {
1994
+ return { text: item.text };
1995
+ }
1996
+ if (item.type === LlmMessageType.InputImage) {
1997
+ const imageUrl = item.image_url || "";
1998
+ const match = imageUrl.match(DATA_URL_REGEX);
1999
+ if (match) {
2000
+ const format = match[1].split("/")[1] || "jpeg";
2001
+ const bytes = Buffer.from(match[2], "base64");
2002
+ return {
2003
+ image: {
2004
+ format,
2005
+ source: { bytes: new Uint8Array(bytes) },
2006
+ },
2007
+ };
2008
+ }
2009
+ return { text: `[Image: ${imageUrl}]` };
2010
+ }
2011
+ if (item.type === LlmMessageType.InputFile) {
2012
+ const fileData = typeof item.file_data === "string" ? item.file_data : "";
2013
+ const match = fileData.match(DATA_URL_REGEX);
2014
+ if (match) {
2015
+ const mimeType = match[1];
2016
+ const documentFormat = MIME_TO_DOCUMENT_FORMAT[mimeType];
2017
+ if (documentFormat) {
2018
+ const bytes = Buffer.from(match[2], "base64");
2019
+ const rawName = item.filename || "document";
2020
+ const name = rawName.replace(/[^a-zA-Z0-9 \-()[\]]/g, "_");
2021
+ return {
2022
+ document: {
2023
+ format: documentFormat,
2024
+ name,
2025
+ source: { bytes: new Uint8Array(bytes) },
2026
+ },
2027
+ };
2028
+ }
2029
+ }
2030
+ return { text: `[File: ${item.filename || "unknown"}]` };
2031
+ }
2032
+ return { text: JSON.stringify(item) };
2033
+ });
2034
+ }
2035
+ //
2036
+ //
2037
+ // Constants / helpers
2038
+ //
2039
+ const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
2040
+ function isOutputConfigUnsupportedError(error) {
2041
+ const msg = error?.message ?? "";
2042
+ return /outputConfig|output_config/i.test(msg);
2043
+ }
2044
+ function isTemperatureDeprecationError$2(error) {
2045
+ const msg = error?.message ?? "";
2046
+ return /temperature.*deprecated|deprecated.*temperature/i.test(msg);
2047
+ }
2048
+ function extractJson(text) {
2049
+ // Try direct parse first
2050
+ try {
2051
+ const parsed = JSON.parse(text);
2052
+ if (typeof parsed === "object" && parsed !== null)
2053
+ return parsed;
2054
+ }
2055
+ catch {
2056
+ // fall through
2057
+ }
2058
+ // Try stripping markdown code fences
2059
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
2060
+ if (fenceMatch) {
2061
+ try {
2062
+ const parsed = JSON.parse(fenceMatch[1].trim());
2063
+ if (typeof parsed === "object" && parsed !== null)
2064
+ return parsed;
2065
+ }
2066
+ catch {
2067
+ // fall through
2068
+ }
2069
+ }
2070
+ return undefined;
2071
+ }
2072
+ //
2073
+ //
2074
+ // Main
2075
+ //
2076
+ class BedrockAdapter extends BaseProviderAdapter {
2077
+ constructor() {
2078
+ super(...arguments);
2079
+ this.name = PROVIDER.BEDROCK.NAME;
2080
+ this.defaultModel = PROVIDER.BEDROCK.MODEL.DEFAULT;
2081
+ this._modelsFallbackToStructuredOutputTool = new Set();
2082
+ this._modelsWithoutTemperature = new Set();
2083
+ }
2084
+ rememberModelRejectsOutputConfig(model) {
2085
+ this._modelsFallbackToStructuredOutputTool.add(model);
2086
+ }
2087
+ useFakeToolForStructuredOutput(model) {
2088
+ return this._modelsFallbackToStructuredOutputTool.has(model);
2089
+ }
2090
+ rememberModelRejectsTemperature(model) {
2091
+ this._modelsWithoutTemperature.add(model);
2092
+ }
2093
+ supportsTemperature(model) {
2094
+ return !this._modelsWithoutTemperature.has(model);
2095
+ }
2096
+ //
2097
+ // Request Building
2098
+ //
2099
+ buildRequest(request) {
2100
+ const messages = [];
2101
+ for (const msg of request.messages) {
2102
+ const typedMsg = msg;
2103
+ if (typedMsg.role === "system")
2104
+ continue;
2105
+ if (typedMsg.type === LlmMessageType.FunctionCall) {
2106
+ let parsedInput;
2107
+ try {
2108
+ parsedInput = JSON.parse(typedMsg.arguments || "{}");
2109
+ }
2110
+ catch {
2111
+ parsedInput = {};
2112
+ }
2113
+ messages.push({
2114
+ role: "assistant",
2115
+ content: [
2116
+ {
2117
+ toolUse: {
2118
+ toolUseId: typedMsg.call_id || "",
2119
+ name: typedMsg.name || "",
2120
+ input: parsedInput,
2121
+ },
2122
+ },
2123
+ ],
2124
+ });
2125
+ continue;
2126
+ }
2127
+ if (typedMsg.type === LlmMessageType.FunctionCallOutput) {
2128
+ messages.push({
2129
+ role: "user",
2130
+ content: [
2131
+ {
2132
+ toolResult: {
2133
+ toolUseId: typedMsg.call_id || "",
2134
+ content: [{ text: typedMsg.output || "" }],
2135
+ },
2136
+ },
2137
+ ],
2138
+ });
2139
+ continue;
2140
+ }
2141
+ if (typedMsg.role && typedMsg.content !== undefined) {
2142
+ messages.push({
2143
+ role: typedMsg.role,
2144
+ content: convertContentToBedrock(typedMsg.content),
2145
+ });
2146
+ }
2147
+ }
2148
+ const model = request.model || this.defaultModel;
2149
+ const bedrockRequest = {
2150
+ modelId: model,
2151
+ messages,
2152
+ inferenceConfig: {
2153
+ maxTokens: 4096,
2154
+ },
2155
+ };
2156
+ if (request.system) {
2157
+ bedrockRequest.system = [{ text: request.system }];
2158
+ }
2159
+ if (request.temperature !== undefined && this.supportsTemperature(model)) {
2160
+ bedrockRequest.inferenceConfig = {
2161
+ ...bedrockRequest.inferenceConfig,
2162
+ temperature: request.temperature,
2163
+ };
2164
+ }
2165
+ if (request.tools && request.tools.length > 0) {
2166
+ bedrockRequest.toolConfig = {
2167
+ tools: request.tools.map((tool) => ({
2168
+ toolSpec: {
2169
+ name: tool.name,
2170
+ description: tool.description,
2171
+ inputSchema: {
2172
+ json: tool.parameters,
2173
+ },
2174
+ },
2175
+ })),
2176
+ };
2177
+ }
2178
+ if (request.instructions && messages.length > 0) {
2179
+ const lastMsg = messages[messages.length - 1];
2180
+ if (lastMsg.content.length > 0) {
2181
+ const firstBlock = lastMsg.content[0];
2182
+ if ("text" in firstBlock) {
2183
+ firstBlock.text = firstBlock.text + "\n\n" + request.instructions;
2184
+ }
2185
+ }
2186
+ }
2187
+ if (request.format) {
2188
+ if (this.useFakeToolForStructuredOutput(model)) {
2189
+ const fakeTool = {
2190
+ toolSpec: {
2191
+ name: STRUCTURED_OUTPUT_TOOL_NAME$2,
2192
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
2193
+ "After gathering all necessary information (including results from other tools), " +
2194
+ "call this tool with the structured data to complete the request.",
2195
+ inputSchema: { json: request.format },
2196
+ },
2197
+ };
2198
+ bedrockRequest.toolConfig = {
2199
+ tools: [
2200
+ ...(bedrockRequest.toolConfig?.tools ?? []),
2201
+ fakeTool,
2202
+ ],
2203
+ };
2204
+ }
2205
+ else {
2206
+ bedrockRequest.outputConfig = {
2207
+ textFormat: {
2208
+ type: "json_schema",
2209
+ structure: {
2210
+ jsonSchema: {
2211
+ schema: JSON.stringify(request.format),
2212
+ name: "structured_output",
2213
+ },
2214
+ },
2215
+ },
2216
+ };
2217
+ }
2218
+ }
2219
+ if (request.providerOptions) {
2220
+ Object.assign(bedrockRequest, request.providerOptions);
2221
+ }
2222
+ return bedrockRequest;
2223
+ }
2224
+ formatTools(toolkit) {
2225
+ return toolkit.tools.map((tool) => ({
2226
+ name: tool.name,
2227
+ description: tool.description,
2228
+ parameters: {
2229
+ ...tool.parameters,
2230
+ type: "object",
2231
+ },
2232
+ }));
2233
+ }
2234
+ formatOutputSchema(schema) {
2235
+ const zodSchema = schema instanceof z.ZodType
2236
+ ? schema
2237
+ : naturalZodSchema(schema);
2238
+ return z.toJSONSchema(zodSchema);
2239
+ }
2240
+ //
2241
+ // API Execution
2242
+ //
2243
+ async executeRequest(client, request, signal) {
2244
+ const bedrockClient = client;
2245
+ const bedrockRequest = request;
2246
+ const { ConverseCommand } = await import('@aws-sdk/client-bedrock-runtime');
2247
+ const wantsStructuredOutput = Boolean(bedrockRequest.outputConfig);
2248
+ try {
2249
+ const response = (await bedrockClient.send(new ConverseCommand(bedrockRequest), signal ? { abortSignal: signal } : undefined));
2250
+ if (wantsStructuredOutput)
2251
+ response.__jaypieStructuredOutput = true;
2252
+ return response;
2253
+ }
2254
+ catch (error) {
2255
+ if (bedrockRequest.inferenceConfig?.temperature !== undefined &&
2256
+ isTemperatureDeprecationError$2(error)) {
2257
+ this.rememberModelRejectsTemperature(bedrockRequest.modelId || this.defaultModel);
2258
+ const retryRequest = {
2259
+ ...bedrockRequest,
2260
+ inferenceConfig: { ...bedrockRequest.inferenceConfig },
2261
+ };
2262
+ delete retryRequest.inferenceConfig.temperature;
2263
+ const response = (await bedrockClient.send(new ConverseCommand(retryRequest), signal ? { abortSignal: signal } : undefined));
2264
+ if (wantsStructuredOutput)
2265
+ response.__jaypieStructuredOutput = true;
2266
+ return response;
2267
+ }
2268
+ if (wantsStructuredOutput && isOutputConfigUnsupportedError(error)) {
2269
+ const model = bedrockRequest.modelId || this.defaultModel;
2270
+ this.rememberModelRejectsOutputConfig(model);
2271
+ const fallbackRequest = this.toFallbackStructuredOutputRequest(bedrockRequest);
2272
+ return (await bedrockClient.send(new ConverseCommand(fallbackRequest), signal ? { abortSignal: signal } : undefined));
2273
+ }
2274
+ throw error;
2275
+ }
2276
+ }
2277
+ toFallbackStructuredOutputRequest(request) {
2278
+ const { outputConfig, ...rest } = request;
2279
+ if (!outputConfig?.textFormat?.structure)
2280
+ return request;
2281
+ let schema;
2282
+ try {
2283
+ schema = JSON.parse(outputConfig.textFormat.structure.jsonSchema?.schema ?? "{}");
2284
+ }
2285
+ catch {
2286
+ schema = {};
2287
+ }
2288
+ const fakeTool = {
2289
+ toolSpec: {
2290
+ name: STRUCTURED_OUTPUT_TOOL_NAME$2,
2291
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
2292
+ "After gathering all necessary information (including results from other tools), " +
2293
+ "call this tool with the structured data to complete the request.",
2294
+ inputSchema: { json: schema },
2295
+ },
2296
+ };
2297
+ return {
2298
+ ...rest,
2299
+ toolConfig: {
2300
+ tools: [...(rest.toolConfig?.tools ?? []), fakeTool],
2301
+ },
2302
+ };
2303
+ }
2304
+ async *executeStreamRequest(client, request, signal) {
2305
+ const bedrockClient = client;
2306
+ const bedrockRequest = request;
2307
+ const { ConverseStreamCommand } = await import('@aws-sdk/client-bedrock-runtime');
2308
+ const response = await bedrockClient.send(new ConverseStreamCommand(bedrockRequest), signal ? { abortSignal: signal } : undefined);
2309
+ if (!response.stream)
2310
+ return;
2311
+ let currentToolCall = null;
2312
+ let inputTokens = 0;
2313
+ let outputTokens = 0;
2314
+ const model = bedrockRequest.modelId || this.defaultModel;
2315
+ for await (const event of response.stream) {
2316
+ if (event.contentBlockStart?.start?.toolUse) {
2317
+ const toolUse = event.contentBlockStart.start.toolUse;
2318
+ currentToolCall = {
2319
+ toolUseId: toolUse.toolUseId || "",
2320
+ name: toolUse.name || "",
2321
+ arguments: "",
2322
+ };
2323
+ }
2324
+ else if (event.contentBlockDelta?.delta) {
2325
+ const delta = event.contentBlockDelta.delta;
2326
+ if (delta.text !== undefined) {
2327
+ yield { type: LlmStreamChunkType.Text, content: delta.text };
2328
+ }
2329
+ else if (delta.toolUse?.input && currentToolCall) {
2330
+ currentToolCall.arguments += delta.toolUse.input;
2331
+ }
2332
+ }
2333
+ else if (event.contentBlockStop && currentToolCall) {
2334
+ yield {
2335
+ type: LlmStreamChunkType.ToolCall,
2336
+ toolCall: {
2337
+ id: currentToolCall.toolUseId,
2338
+ name: currentToolCall.name,
2339
+ arguments: currentToolCall.arguments,
2340
+ },
2341
+ };
2342
+ currentToolCall = null;
2343
+ }
2344
+ else if (event.metadata?.usage) {
2345
+ inputTokens = event.metadata.usage.inputTokens ?? 0;
2346
+ outputTokens = event.metadata.usage.outputTokens ?? 0;
2347
+ }
2348
+ else if (event.messageStop) {
2349
+ yield {
2350
+ type: LlmStreamChunkType.Done,
2351
+ usage: [
2352
+ {
2353
+ input: inputTokens,
2354
+ output: outputTokens,
2355
+ reasoning: 0,
2356
+ total: inputTokens + outputTokens,
2357
+ provider: this.name,
2358
+ model,
2359
+ },
2360
+ ],
2361
+ };
2362
+ }
2363
+ }
2364
+ }
2365
+ //
2366
+ // Response Parsing
2367
+ //
2368
+ parseResponse(response, options) {
2369
+ const bedrockResponse = response;
2370
+ const message = bedrockResponse.output?.message;
2371
+ const rawContent = this.extractContentFromMessage(message);
2372
+ let content = rawContent;
2373
+ if (options?.format && typeof rawContent === "string") {
2374
+ content = extractJson(rawContent) ?? rawContent;
2375
+ }
2376
+ // Don't surface structured_output fake tool as a real tool call
2377
+ const allToolUses = (bedrockResponse.output?.message?.content ?? []).filter((b) => "toolUse" in b);
2378
+ const hasOnlyStructuredOutputTool = allToolUses.length > 0 &&
2379
+ allToolUses.every((b) => b.toolUse.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
2380
+ const hasToolCalls = bedrockResponse.stopReason === "tool_use" && !hasOnlyStructuredOutputTool;
1146
2381
  return {
1147
2382
  content,
1148
2383
  hasToolCalls,
1149
- stopReason: anthropicResponse.stop_reason ?? undefined,
1150
- usage: this.extractUsage(anthropicResponse, anthropicResponse.model),
1151
- raw: anthropicResponse,
2384
+ stopReason: bedrockResponse.stopReason ?? undefined,
2385
+ usage: this.extractUsage(bedrockResponse, bedrockResponse.modelId ||
2386
+ this.defaultModel),
2387
+ raw: bedrockResponse,
1152
2388
  };
1153
2389
  }
1154
2390
  extractToolCalls(response) {
1155
- const anthropicResponse = response;
2391
+ const bedrockResponse = response;
2392
+ const content = bedrockResponse.output?.message?.content ?? [];
1156
2393
  const toolCalls = [];
1157
- for (const block of anthropicResponse.content) {
1158
- if (block.type === "tool_use") {
2394
+ for (const block of content) {
2395
+ const typedBlock = block;
2396
+ if ("toolUse" in typedBlock && typedBlock.toolUse) {
2397
+ const toolUse = typedBlock.toolUse;
1159
2398
  toolCalls.push({
1160
- callId: block.id,
1161
- name: block.name,
1162
- arguments: JSON.stringify(block.input),
1163
- raw: block,
2399
+ callId: toolUse.toolUseId,
2400
+ name: toolUse.name,
2401
+ arguments: JSON.stringify(toolUse.input),
2402
+ raw: typedBlock,
1164
2403
  });
1165
2404
  }
1166
2405
  }
1167
2406
  return toolCalls;
1168
2407
  }
1169
2408
  extractUsage(response, model) {
1170
- const anthropicResponse = response;
1171
- // Check for thinking tokens in the usage (extended thinking feature)
1172
- // Anthropic includes thinking tokens in a separate field when enabled
1173
- const usage = anthropicResponse.usage;
2409
+ const bedrockResponse = response;
2410
+ const usage = bedrockResponse.usage;
1174
2411
  return {
1175
- input: usage.input_tokens,
1176
- output: usage.output_tokens,
1177
- reasoning: usage.thinking_tokens || 0,
1178
- total: usage.input_tokens + usage.output_tokens,
2412
+ input: usage?.inputTokens ?? 0,
2413
+ output: usage?.outputTokens ?? 0,
2414
+ reasoning: 0,
2415
+ total: usage?.totalTokens ??
2416
+ (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
1179
2417
  provider: this.name,
1180
2418
  model,
1181
2419
  };
@@ -1185,52 +2423,40 @@ class AnthropicAdapter extends BaseProviderAdapter {
1185
2423
  //
1186
2424
  formatToolResult(toolCall, result) {
1187
2425
  return {
1188
- type: "tool_result",
1189
- tool_use_id: toolCall.callId,
1190
- content: result.output,
2426
+ toolResult: {
2427
+ toolUseId: toolCall.callId,
2428
+ content: [{ text: result.output }],
2429
+ },
1191
2430
  };
1192
2431
  }
1193
2432
  appendToolResult(request, toolCall, result) {
1194
- const anthropicRequest = request;
2433
+ const bedrockRequest = request;
1195
2434
  const toolCallRaw = toolCall.raw;
1196
- // Add assistant message with the tool use
1197
- anthropicRequest.messages.push({
2435
+ bedrockRequest.messages.push({
1198
2436
  role: "assistant",
1199
2437
  content: [toolCallRaw],
1200
2438
  });
1201
- // Add user message with the tool result
1202
- anthropicRequest.messages.push({
2439
+ bedrockRequest.messages.push({
1203
2440
  role: "user",
1204
2441
  content: [this.formatToolResult(toolCall, result)],
1205
2442
  });
1206
- return anthropicRequest;
2443
+ return bedrockRequest;
1207
2444
  }
1208
2445
  //
1209
2446
  // History Management
1210
2447
  //
1211
2448
  responseToHistoryItems(response) {
1212
- const anthropicResponse = response;
2449
+ const bedrockResponse = response;
1213
2450
  const historyItems = [];
1214
- // Check if this is a tool use response
1215
- if (anthropicResponse.stop_reason === "tool_use") {
1216
- // Don't add to history yet - will be added after tool execution
2451
+ if (bedrockResponse.stopReason === "tool_use") {
1217
2452
  return historyItems;
1218
2453
  }
1219
- // Include thinking blocks for extended thinking support
1220
- // Thinking blocks are preserved in history so extractReasoning can find them
1221
- for (const block of anthropicResponse.content) {
1222
- if (block.type === "thinking") {
1223
- // Push raw block - types are loosely checked at runtime
1224
- // This allows extractReasoning to access the thinking property
1225
- historyItems.push(block);
1226
- }
1227
- }
1228
- // Extract text content for non-tool responses
1229
- const textBlock = anthropicResponse.content.find((block) => block.type === "text");
2454
+ const content = bedrockResponse.output?.message?.content ?? [];
2455
+ const textBlock = content.find((block) => "text" in block);
1230
2456
  if (textBlock) {
1231
2457
  historyItems.push({
1232
2458
  content: textBlock.text,
1233
- role: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
2459
+ role: "assistant",
1234
2460
  type: LlmMessageType.Message,
1235
2461
  });
1236
2462
  }
@@ -1241,8 +2467,10 @@ class AnthropicAdapter extends BaseProviderAdapter {
1241
2467
  //
1242
2468
  classifyError(error) {
1243
2469
  const errorName = error?.constructor?.name;
1244
- // Check for rate limit error
1245
- if (errorName === "RateLimitError") {
2470
+ const errorMessage = error?.message ?? "";
2471
+ if (errorName === "ThrottlingException" ||
2472
+ errorMessage.includes("ThrottlingException") ||
2473
+ errorMessage.includes("Too Many Requests")) {
1246
2474
  return {
1247
2475
  error,
1248
2476
  category: ErrorCategory.RateLimit,
@@ -1250,23 +2478,34 @@ class AnthropicAdapter extends BaseProviderAdapter {
1250
2478
  suggestedDelayMs: 60000,
1251
2479
  };
1252
2480
  }
1253
- // Check for retryable errors
1254
- if (RETRYABLE_ERROR_NAMES.includes(errorName)) {
2481
+ if (errorName === "ServiceUnavailableException" ||
2482
+ errorName === "InternalServerException" ||
2483
+ errorMessage.includes("ServiceUnavailableException") ||
2484
+ errorMessage.includes("InternalServerException")) {
1255
2485
  return {
1256
2486
  error,
1257
2487
  category: ErrorCategory.Retryable,
1258
2488
  shouldRetry: true,
1259
2489
  };
1260
2490
  }
1261
- // Check for non-retryable errors
1262
- if (NOT_RETRYABLE_ERROR_NAMES.includes(errorName)) {
2491
+ if (errorName === "AccessDeniedException" ||
2492
+ errorName === "ValidationException" ||
2493
+ errorName === "ResourceNotFoundException" ||
2494
+ errorMessage.includes("AccessDeniedException") ||
2495
+ errorMessage.includes("ValidationException")) {
1263
2496
  return {
1264
2497
  error,
1265
2498
  category: ErrorCategory.Unrecoverable,
1266
2499
  shouldRetry: false,
1267
2500
  };
1268
2501
  }
1269
- // Unknown error - treat as potentially retryable
2502
+ if (isTransientNetworkError(error)) {
2503
+ return {
2504
+ error,
2505
+ category: ErrorCategory.Retryable,
2506
+ shouldRetry: true,
2507
+ };
2508
+ }
1270
2509
  return {
1271
2510
  error,
1272
2511
  category: ErrorCategory.Unknown,
@@ -1274,49 +2513,87 @@ class AnthropicAdapter extends BaseProviderAdapter {
1274
2513
  };
1275
2514
  }
1276
2515
  //
1277
- // Provider-Specific Features
2516
+ // Structured Output
1278
2517
  //
1279
- isComplete(response) {
1280
- const anthropicResponse = response;
1281
- return anthropicResponse.stop_reason !== "tool_use";
1282
- }
1283
2518
  hasStructuredOutput(response) {
1284
- const anthropicResponse = response;
1285
- // Check if the last content block is a tool_use with structured_output
1286
- const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
1287
- return (lastBlock?.type === "tool_use" &&
1288
- lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
2519
+ const bedrockResponse = response;
2520
+ if (bedrockResponse.__jaypieStructuredOutput) {
2521
+ return this.extractStructuredOutput(response) !== undefined;
2522
+ }
2523
+ // Fake-tool path: last content block is a structured_output toolUse
2524
+ const content = (bedrockResponse.output?.message?.content ??
2525
+ []);
2526
+ const last = content[content.length - 1];
2527
+ return (!!last &&
2528
+ "toolUse" in last &&
2529
+ last.toolUse.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
1289
2530
  }
1290
2531
  extractStructuredOutput(response) {
1291
- const anthropicResponse = response;
1292
- const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
1293
- if (lastBlock?.type === "tool_use" &&
1294
- lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2) {
1295
- return lastBlock.input;
2532
+ const bedrockResponse = response;
2533
+ if (bedrockResponse.__jaypieStructuredOutput) {
2534
+ const content = (bedrockResponse.output?.message?.content ??
2535
+ []);
2536
+ const textBlock = content.find((b) => "text" in b);
2537
+ if (!textBlock)
2538
+ return undefined;
2539
+ return extractJson(textBlock.text);
2540
+ }
2541
+ // Fake-tool path
2542
+ const content = (bedrockResponse.output?.message?.content ??
2543
+ []);
2544
+ const last = content[content.length - 1];
2545
+ if (last &&
2546
+ "toolUse" in last &&
2547
+ last.toolUse.name === STRUCTURED_OUTPUT_TOOL_NAME$2) {
2548
+ return last.toolUse.input;
1296
2549
  }
1297
2550
  return undefined;
1298
2551
  }
1299
2552
  //
2553
+ // Completion Detection
2554
+ //
2555
+ isComplete(response) {
2556
+ const bedrockResponse = response;
2557
+ return bedrockResponse.stopReason !== "tool_use";
2558
+ }
2559
+ //
1300
2560
  // Private Helpers
1301
2561
  //
1302
- extractContent(response) {
1303
- // Check for structured output first
1304
- if (this.hasStructuredOutput(response)) {
1305
- return this.extractStructuredOutput(response);
1306
- }
1307
- // Extract text content
1308
- const textBlock = response.content.find((block) => block.type === "text");
2562
+ extractContentFromMessage(message) {
2563
+ if (!message?.content)
2564
+ return undefined;
2565
+ const content = message.content;
2566
+ const textBlock = content.find((block) => "text" in block);
1309
2567
  return textBlock?.text;
1310
2568
  }
1311
2569
  }
1312
- // Export singleton instance
1313
- const anthropicAdapter = new AnthropicAdapter();
2570
+ const bedrockAdapter = new BedrockAdapter();
1314
2571
 
1315
2572
  //
1316
2573
  //
1317
2574
  // Constants
1318
2575
  //
1319
2576
  const STRUCTURED_OUTPUT_TOOL_NAME$1 = "structured_output";
2577
+ // Gemini 3 family supports combining tools (function calling) with native
2578
+ // structured output via `responseJsonSchema`. Earlier Gemini families
2579
+ // (including 2.5 thinking) do not support the combo and fall back to the
2580
+ // legacy `structured_output` fake-tool emulation.
2581
+ const GEMINI_3_PATTERN = /^gemini-3/;
2582
+ /**
2583
+ * Detect 4xx errors that indicate the model itself does not support the
2584
+ * `responseJsonSchema` + tools combo. Triggers the runtime fallback to the
2585
+ * fake-tool emulation path. Other 400s propagate.
2586
+ */
2587
+ function isStructuredOutputComboUnsupportedError(error) {
2588
+ if (!error || typeof error !== "object")
2589
+ return false;
2590
+ const err = error;
2591
+ const status = err.status ?? err.code;
2592
+ if (status !== 400)
2593
+ return false;
2594
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
2595
+ return messages.some((m) => /response[_ ]?json[_ ]?schema|response[_ ]?schema|response[_ ]?mime|function[_ ]?call|tools/i.test(m));
2596
+ }
1320
2597
  // Gemini uses HTTP status codes for error classification
1321
2598
  // Documented at: https://ai.google.dev/api/rest/v1beta/Status
1322
2599
  const RETRYABLE_STATUS_CODES$1 = [
@@ -1340,15 +2617,30 @@ const NOT_RETRYABLE_STATUS_CODES = [
1340
2617
  // Main
1341
2618
  //
1342
2619
  /**
1343
- * GeminiAdapter implements the ProviderAdapter interface for Google's Gemini API.
2620
+ * GoogleAdapter implements the ProviderAdapter interface for Google's Gemini API.
1344
2621
  * It handles request building, response parsing, and error classification
1345
2622
  * specific to Gemini's generateContent API.
1346
2623
  */
1347
- class GeminiAdapter extends BaseProviderAdapter {
2624
+ class GoogleAdapter extends BaseProviderAdapter {
1348
2625
  constructor() {
1349
2626
  super(...arguments);
1350
- this.name = PROVIDER.GEMINI.NAME;
1351
- this.defaultModel = PROVIDER.GEMINI.MODEL.DEFAULT;
2627
+ this.name = PROVIDER.GOOGLE.NAME;
2628
+ this.defaultModel = PROVIDER.GOOGLE.MODEL.DEFAULT;
2629
+ // Session-level cache of Gemini 3 models observed to reject the native
2630
+ // `responseJsonSchema` + tools combo. When a model is in this set,
2631
+ // buildRequest engages the legacy fake-tool path instead.
2632
+ this.runtimeNoStructuredOutputComboModels = new Set();
2633
+ }
2634
+ rememberModelRejectsStructuredOutputCombo(model) {
2635
+ this.runtimeNoStructuredOutputComboModels.add(model);
2636
+ }
2637
+ clearRuntimeNoStructuredOutputComboModels() {
2638
+ this.runtimeNoStructuredOutputComboModels.clear();
2639
+ }
2640
+ supportsStructuredOutputCombo(model) {
2641
+ if (this.runtimeNoStructuredOutputComboModels.has(model))
2642
+ return false;
2643
+ return GEMINI_3_PATTERN.test(model);
1352
2644
  }
1353
2645
  //
1354
2646
  // Request Building
@@ -1377,9 +2669,27 @@ class GeminiAdapter extends BaseProviderAdapter {
1377
2669
  }
1378
2670
  }
1379
2671
  }
1380
- // Add tools if provided
1381
- if (request.tools && request.tools.length > 0) {
1382
- const functionDeclarations = request.tools.map((tool) => ({
2672
+ const hasUserTools = !!(request.tools && request.tools.length > 0);
2673
+ const useNativeCombo = Boolean(request.format) &&
2674
+ hasUserTools &&
2675
+ this.supportsStructuredOutputCombo(geminiRequest.model);
2676
+ // When tools+format are combined and the model does not support the native
2677
+ // combo, inject the legacy `structured_output` fake tool here so the model
2678
+ // is forced to call it before its final answer.
2679
+ const allTools = request.tools
2680
+ ? [...request.tools]
2681
+ : [];
2682
+ if (request.format && hasUserTools && !useNativeCombo) {
2683
+ log$2.warn(`[GoogleAdapter] Using legacy structured_output tool fallback for model ${geminiRequest.model}; native responseJsonSchema + tools combo is only available on Gemini 3.`);
2684
+ allTools.push({
2685
+ name: STRUCTURED_OUTPUT_TOOL_NAME$1,
2686
+ description: "Output a structured JSON object, " +
2687
+ "use this before your final response to give structured outputs to the user",
2688
+ parameters: request.format,
2689
+ });
2690
+ }
2691
+ if (allTools.length > 0) {
2692
+ const functionDeclarations = allTools.map((tool) => ({
1383
2693
  name: tool.name,
1384
2694
  description: tool.description,
1385
2695
  parameters: tool.parameters,
@@ -1389,21 +2699,33 @@ class GeminiAdapter extends BaseProviderAdapter {
1389
2699
  tools: [{ functionDeclarations }],
1390
2700
  };
1391
2701
  }
1392
- // Add structured output format if provided (but NOT when tools are present)
1393
- // Gemini doesn't support combining function calling with responseMimeType: 'application/json'
1394
- // When tools are present, structured output is handled via the structured_output tool
1395
- if (request.format && !(request.tools && request.tools.length > 0)) {
1396
- geminiRequest.config = {
1397
- ...geminiRequest.config,
1398
- responseMimeType: "application/json",
1399
- responseJsonSchema: request.format,
1400
- };
2702
+ // Native structured output: send schema as `responseJsonSchema`
2703
+ // (or `responseSchema` for Gemini 2.5+ no-tools path). The legacy
2704
+ // fake-tool emulation only runs when format+tools is combined on a model
2705
+ // that doesn't support the native combo.
2706
+ const wantsNativeStructured = Boolean(request.format) && (!hasUserTools || useNativeCombo);
2707
+ if (wantsNativeStructured) {
2708
+ const useJsonSchema = useNativeCombo || request.providerOptions?.useJsonSchema === true;
2709
+ if (useJsonSchema) {
2710
+ geminiRequest.config = {
2711
+ ...geminiRequest.config,
2712
+ responseMimeType: "application/json",
2713
+ responseJsonSchema: request.format,
2714
+ };
2715
+ }
2716
+ else {
2717
+ geminiRequest.config = {
2718
+ ...geminiRequest.config,
2719
+ responseMimeType: "application/json",
2720
+ responseSchema: jsonSchemaToOpenApi3(request.format),
2721
+ };
2722
+ }
1401
2723
  }
1402
- // When format is specified with tools, add instruction to use structured_output tool
1403
- if (request.format && request.tools && request.tools.length > 0) {
2724
+ // Legacy fake-tool path needs a system-prompt nudge so the model actually
2725
+ // calls the synthetic tool before its final answer.
2726
+ if (request.format && hasUserTools && !useNativeCombo) {
1404
2727
  const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
1405
2728
  "to output your answer in the required JSON format.";
1406
- // Add to system instruction if it exists, otherwise create one
1407
2729
  const existingSystem = geminiRequest.config?.systemInstruction || "";
1408
2730
  geminiRequest.config = {
1409
2731
  ...geminiRequest.config,
@@ -1428,8 +2750,14 @@ class GeminiAdapter extends BaseProviderAdapter {
1428
2750
  }
1429
2751
  return geminiRequest;
1430
2752
  }
1431
- formatTools(toolkit, outputSchema) {
1432
- const tools = toolkit.tools.map((tool) => ({
2753
+ formatTools(toolkit,
2754
+ // outputSchema is part of the interface contract but Gemini now handles
2755
+ // structured output via `responseJsonSchema`/`responseSchema` (or the
2756
+ // legacy fake-tool injected in buildRequest as a fallback). We no longer
2757
+ // inject a synthetic structured-output tool here.
2758
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2759
+ _outputSchema) {
2760
+ return toolkit.tools.map((tool) => ({
1433
2761
  name: tool.name,
1434
2762
  description: tool.description,
1435
2763
  parameters: {
@@ -1437,16 +2765,6 @@ class GeminiAdapter extends BaseProviderAdapter {
1437
2765
  type: "object",
1438
2766
  },
1439
2767
  }));
1440
- // Add structured output tool if schema is provided
1441
- if (outputSchema) {
1442
- tools.push({
1443
- name: STRUCTURED_OUTPUT_TOOL_NAME$1,
1444
- description: "Output a structured JSON object, " +
1445
- "use this before your final response to give structured outputs to the user",
1446
- parameters: outputSchema,
1447
- });
1448
- }
1449
- return tools;
1450
2768
  }
1451
2769
  formatOutputSchema(schema) {
1452
2770
  let jsonSchema;
@@ -1465,28 +2783,80 @@ class GeminiAdapter extends BaseProviderAdapter {
1465
2783
  : naturalZodSchema(schema);
1466
2784
  jsonSchema = z.toJSONSchema(zodSchema);
1467
2785
  }
1468
- // Remove $schema property (Gemini doesn't need it)
1469
- if (jsonSchema.$schema) {
1470
- delete jsonSchema.$schema;
1471
- }
1472
- return jsonSchema;
2786
+ return jsonSchemaToOpenApi3(jsonSchema);
1473
2787
  }
1474
2788
  //
1475
2789
  // API Execution
1476
2790
  //
1477
- async executeRequest(client, request) {
2791
+ async executeRequest(client, request, signal) {
1478
2792
  const genAI = client;
1479
2793
  const geminiRequest = request;
1480
- // Cast config to any to bypass strict type checking between our internal types
1481
- // and the SDK's types. The SDK will validate at runtime.
1482
- const response = await genAI.models.generateContent({
1483
- model: geminiRequest.model,
1484
- contents: geminiRequest.contents,
1485
- config: geminiRequest.config,
1486
- });
1487
- return response;
2794
+ const wantsNativeCombo = !!geminiRequest.config?.responseJsonSchema &&
2795
+ !!geminiRequest.config?.tools;
2796
+ try {
2797
+ // Cast config to any to bypass strict type checking between our internal types
2798
+ // and the SDK's types. The SDK will validate at runtime.
2799
+ const response = await genAI.models.generateContent({
2800
+ model: geminiRequest.model,
2801
+ contents: geminiRequest.contents,
2802
+ config: geminiRequest.config,
2803
+ });
2804
+ return response;
2805
+ }
2806
+ catch (error) {
2807
+ if (signal?.aborted)
2808
+ return undefined;
2809
+ // If the model rejected the native responseJsonSchema + tools combo,
2810
+ // cache it and retry with the legacy fake-tool emulation path.
2811
+ if (wantsNativeCombo && isStructuredOutputComboUnsupportedError(error)) {
2812
+ const model = geminiRequest.model;
2813
+ this.rememberModelRejectsStructuredOutputCombo(model);
2814
+ log$2.warn(`[GoogleAdapter] Model ${model} rejected native responseJsonSchema + tools combo; falling back to legacy structured_output tool emulation.`);
2815
+ const fallbackRequest = this.toFallbackStructuredOutputRequest(geminiRequest);
2816
+ const response = await genAI.models.generateContent({
2817
+ model: fallbackRequest.model,
2818
+ contents: fallbackRequest.contents,
2819
+ config: fallbackRequest.config,
2820
+ });
2821
+ return response;
2822
+ }
2823
+ throw error;
2824
+ }
2825
+ }
2826
+ /**
2827
+ * Rebuild a Gemini 3 native-combo request without `responseJsonSchema`/
2828
+ * `responseMimeType`, swapping in the legacy fake-tool emulation. Used as
2829
+ * a runtime fallback when a Gemini 3 model rejects the combo.
2830
+ */
2831
+ toFallbackStructuredOutputRequest(request) {
2832
+ if (!request.config?.responseJsonSchema)
2833
+ return request;
2834
+ const schema = request.config.responseJsonSchema;
2835
+ const newConfig = { ...request.config };
2836
+ delete newConfig.responseJsonSchema;
2837
+ delete newConfig.responseMimeType;
2838
+ const fakeTool = {
2839
+ name: STRUCTURED_OUTPUT_TOOL_NAME$1,
2840
+ description: "Output a structured JSON object, " +
2841
+ "use this before your final response to give structured outputs to the user",
2842
+ parameters: schema,
2843
+ };
2844
+ const existingDeclarations = newConfig.tools?.[0]?.functionDeclarations ?? [];
2845
+ newConfig.tools = [
2846
+ { functionDeclarations: [...existingDeclarations, fakeTool] },
2847
+ ];
2848
+ const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
2849
+ "to output your answer in the required JSON format.";
2850
+ const existingSystem = newConfig.systemInstruction || "";
2851
+ newConfig.systemInstruction = existingSystem
2852
+ ? `${existingSystem}\n\n${structuredOutputInstruction}`
2853
+ : structuredOutputInstruction;
2854
+ return {
2855
+ ...request,
2856
+ config: newConfig,
2857
+ };
1488
2858
  }
1489
- async *executeStreamRequest(client, request) {
2859
+ async *executeStreamRequest(client, request, signal) {
1490
2860
  const genAI = client;
1491
2861
  const geminiRequest = request;
1492
2862
  // Use generateContentStream for streaming
@@ -1495,8 +2865,6 @@ class GeminiAdapter extends BaseProviderAdapter {
1495
2865
  contents: geminiRequest.contents,
1496
2866
  config: geminiRequest.config,
1497
2867
  });
1498
- // Track current function call being built
1499
- let currentFunctionCall = null;
1500
2868
  // Track usage for final chunk
1501
2869
  let inputTokens = 0;
1502
2870
  let outputTokens = 0;
@@ -1517,11 +2885,16 @@ class GeminiAdapter extends BaseProviderAdapter {
1517
2885
  // Handle function calls
1518
2886
  if (part.functionCall) {
1519
2887
  const functionCall = part.functionCall;
1520
- currentFunctionCall = {
2888
+ const currentFunctionCall = {
1521
2889
  id: functionCall.id || this.generateCallId(),
1522
2890
  name: functionCall.name || "",
1523
2891
  arguments: functionCall.args || {},
1524
2892
  };
2893
+ // Preserve thoughtSignature for Gemini 3 models
2894
+ // Required to maintain tool call context between turns
2895
+ const metadata = part.thoughtSignature
2896
+ ? { thoughtSignature: part.thoughtSignature }
2897
+ : undefined;
1525
2898
  // Emit the function call immediately
1526
2899
  yield {
1527
2900
  type: LlmStreamChunkType.ToolCall,
@@ -1529,9 +2902,9 @@ class GeminiAdapter extends BaseProviderAdapter {
1529
2902
  id: currentFunctionCall.id,
1530
2903
  name: currentFunctionCall.name,
1531
2904
  arguments: JSON.stringify(currentFunctionCall.arguments),
2905
+ metadata,
1532
2906
  },
1533
2907
  };
1534
- currentFunctionCall = null;
1535
2908
  }
1536
2909
  }
1537
2910
  }
@@ -1618,14 +2991,24 @@ class GeminiAdapter extends BaseProviderAdapter {
1618
2991
  // Tool Result Handling
1619
2992
  //
1620
2993
  formatToolResult(toolCall, result) {
1621
- // Gemini expects the response to be the actual result object, not wrapped in "result"
1622
- // The output from StandardToolResult is JSON-stringified, so we need to parse it
2994
+ // Gemini's `function_response.response` is a protobuf Struct, which only
2995
+ // accepts plain objects. Some models (e.g. gemini-3.1-pro) accept scalar
2996
+ // values silently; gemini-3.1-flash-lite rejects them. Parse the output
2997
+ // and wrap any non-object value (string, number, boolean, array, null)
2998
+ // in `{ result: value }` so every tool result is a valid Struct.
1623
2999
  let responseData;
1624
3000
  try {
1625
- responseData = JSON.parse(result.output);
3001
+ const parsed = JSON.parse(result.output);
3002
+ if (parsed !== null &&
3003
+ typeof parsed === "object" &&
3004
+ !Array.isArray(parsed)) {
3005
+ responseData = parsed;
3006
+ }
3007
+ else {
3008
+ responseData = { result: parsed };
3009
+ }
1626
3010
  }
1627
3011
  catch {
1628
- // If parsing fails, wrap the output as a string result
1629
3012
  responseData = { result: result.output };
1630
3013
  }
1631
3014
  return {
@@ -1748,6 +3131,14 @@ class GeminiAdapter extends BaseProviderAdapter {
1748
3131
  shouldRetry: true,
1749
3132
  };
1750
3133
  }
3134
+ // Check for transient network errors (ECONNRESET, etc.)
3135
+ if (isTransientNetworkError(error)) {
3136
+ return {
3137
+ error,
3138
+ category: ErrorCategory.Retryable,
3139
+ shouldRetry: true,
3140
+ };
3141
+ }
1751
3142
  // Unknown error - treat as potentially retryable
1752
3143
  return {
1753
3144
  error,
@@ -1981,7 +3372,17 @@ class GeminiAdapter extends BaseProviderAdapter {
1981
3372
  return JSON.parse(textContent);
1982
3373
  }
1983
3374
  catch {
1984
- // If parsing fails, return the original string
3375
+ // Strip markdown code fences and retry (Gemini sometimes wraps JSON in fences)
3376
+ const jsonMatch = textContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/) ||
3377
+ textContent.match(/\{[\s\S]*\}/);
3378
+ if (jsonMatch) {
3379
+ try {
3380
+ return JSON.parse(jsonMatch[1] || jsonMatch[0]);
3381
+ }
3382
+ catch {
3383
+ // Fall through to return original string
3384
+ }
3385
+ }
1985
3386
  return textContent;
1986
3387
  }
1987
3388
  }
@@ -1992,7 +3393,7 @@ class GeminiAdapter extends BaseProviderAdapter {
1992
3393
  }
1993
3394
  }
1994
3395
  // Export singleton instance
1995
- const geminiAdapter = new GeminiAdapter();
3396
+ const googleAdapter = new GoogleAdapter();
1996
3397
 
1997
3398
  // Patterns for OpenAI reasoning models that support extended thinking
1998
3399
  const REASONING_MODEL_PATTERNS = [
@@ -2024,6 +3425,27 @@ const NOT_RETRYABLE_ERROR_TYPES = [
2024
3425
  RateLimitError,
2025
3426
  UnprocessableEntityError,
2026
3427
  ];
3428
+ // Models known not to accept `temperature`.
3429
+ // Patterns (not exact names) so dated variants and future releases are covered
3430
+ // without code changes — OpenAI is removing temperature on newer reasoning
3431
+ // models.
3432
+ const MODELS_WITHOUT_TEMPERATURE$1 = [
3433
+ /^gpt-5\.5/, // gpt-5.5 series deprecated temperature
3434
+ /^o\d/, // o-series reasoning models (o1, o3, o4, ...)
3435
+ ];
3436
+ function isTemperatureDeprecationError$1(error) {
3437
+ if (!error || typeof error !== "object")
3438
+ return false;
3439
+ if (!(error instanceof BadRequestError) &&
3440
+ error.status !== 400) {
3441
+ return false;
3442
+ }
3443
+ const messages = [
3444
+ error.message,
3445
+ error.error?.message,
3446
+ ].filter((m) => typeof m === "string");
3447
+ return messages.some((m) => m.toLowerCase().includes("temperature"));
3448
+ }
2027
3449
  //
2028
3450
  //
2029
3451
  // Main
@@ -2038,6 +3460,20 @@ class OpenAiAdapter extends BaseProviderAdapter {
2038
3460
  super(...arguments);
2039
3461
  this.name = PROVIDER.OPENAI.NAME;
2040
3462
  this.defaultModel = PROVIDER.OPENAI.MODEL.DEFAULT;
3463
+ // Session-level cache of models observed to reject `temperature` at runtime.
3464
+ // Populated by executeRequest on 400 errors so repeat calls skip the param.
3465
+ this.runtimeNoTemperatureModels = new Set();
3466
+ }
3467
+ rememberModelRejectsTemperature(model) {
3468
+ this.runtimeNoTemperatureModels.add(model);
3469
+ }
3470
+ clearRuntimeNoTemperatureModels() {
3471
+ this.runtimeNoTemperatureModels.clear();
3472
+ }
3473
+ supportsTemperature(model) {
3474
+ if (this.runtimeNoTemperatureModels.has(model))
3475
+ return false;
3476
+ return !MODELS_WITHOUT_TEMPERATURE$1.some((pattern) => pattern.test(model));
2041
3477
  }
2042
3478
  //
2043
3479
  // Request Building
@@ -2046,7 +3482,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
2046
3482
  const model = request.model || this.defaultModel;
2047
3483
  const openaiRequest = {
2048
3484
  model,
2049
- input: request.messages,
3485
+ input: this.sanitizeInputItems(request.messages),
2050
3486
  };
2051
3487
  if (request.user) {
2052
3488
  openaiRequest.user = request.user;
@@ -2081,6 +3517,11 @@ class OpenAiAdapter extends BaseProviderAdapter {
2081
3517
  if (request.temperature !== undefined) {
2082
3518
  openaiRequest.temperature = request.temperature;
2083
3519
  }
3520
+ // Strip temperature for models that don't support it (denylist + runtime cache)
3521
+ if (openaiRequest.temperature !== undefined &&
3522
+ !this.supportsTemperature(openaiRequest.model)) {
3523
+ delete openaiRequest.temperature;
3524
+ }
2084
3525
  return openaiRequest;
2085
3526
  }
2086
3527
  formatTools(toolkit, _outputSchema) {
@@ -2128,19 +3569,34 @@ class OpenAiAdapter extends BaseProviderAdapter {
2128
3569
  //
2129
3570
  // API Execution
2130
3571
  //
2131
- async executeRequest(client, request) {
3572
+ async executeRequest(client, request, signal) {
2132
3573
  const openai = client;
2133
- // @ts-expect-error OpenAI SDK types don't match our request format exactly
2134
- return await openai.responses.create(request);
3574
+ const openaiRequest = request;
3575
+ try {
3576
+ return await openai.responses.create(openaiRequest, signal ? { signal } : undefined);
3577
+ }
3578
+ catch (error) {
3579
+ if (signal?.aborted)
3580
+ return undefined;
3581
+ // If the model rejected `temperature`, cache it and retry without the param
3582
+ if (openaiRequest.temperature !== undefined &&
3583
+ isTemperatureDeprecationError$1(error)) {
3584
+ this.rememberModelRejectsTemperature(openaiRequest.model);
3585
+ const retryRequest = { ...openaiRequest };
3586
+ delete retryRequest.temperature;
3587
+ return await openai.responses.create(retryRequest, signal ? { signal } : undefined);
3588
+ }
3589
+ throw error;
3590
+ }
2135
3591
  }
2136
- async *executeStreamRequest(client, request) {
3592
+ async *executeStreamRequest(client, request, signal) {
2137
3593
  const openai = client;
2138
3594
  const baseRequest = request;
2139
3595
  const streamRequest = {
2140
3596
  ...baseRequest,
2141
3597
  stream: true,
2142
3598
  };
2143
- const stream = await openai.responses.create(streamRequest);
3599
+ const stream = await openai.responses.create(streamRequest, signal ? { signal } : undefined);
2144
3600
  // Track current function call being built
2145
3601
  let currentFunctionCall = null;
2146
3602
  // Track usage for final chunk
@@ -2190,6 +3646,9 @@ class OpenAiAdapter extends BaseProviderAdapter {
2190
3646
  id: currentFunctionCall.callId,
2191
3647
  name: currentFunctionCall.name,
2192
3648
  arguments: currentFunctionCall.arguments,
3649
+ // Preserve the item ID (fc_...) separately from call_id (call_...)
3650
+ // OpenAI Responses API requires both with correct prefixes
3651
+ metadata: { itemId: currentFunctionCall.id },
2193
3652
  },
2194
3653
  };
2195
3654
  currentFunctionCall = null;
@@ -2350,6 +3809,14 @@ class OpenAiAdapter extends BaseProviderAdapter {
2350
3809
  };
2351
3810
  }
2352
3811
  }
3812
+ // Check for transient network errors (ECONNRESET, etc.)
3813
+ if (isTransientNetworkError(error)) {
3814
+ return {
3815
+ error,
3816
+ category: ErrorCategory.Retryable,
3817
+ shouldRetry: true,
3818
+ };
3819
+ }
2353
3820
  // Unknown error - treat as potentially retryable
2354
3821
  return {
2355
3822
  error,
@@ -2376,6 +3843,31 @@ class OpenAiAdapter extends BaseProviderAdapter {
2376
3843
  //
2377
3844
  // Private Helpers
2378
3845
  //
3846
+ /**
3847
+ * Sanitize history items for the OpenAI Responses API.
3848
+ * Strips fields not accepted by specific item types (e.g., `name` on
3849
+ * function_call_output items) to prevent 400 "Unknown parameter" errors.
3850
+ */
3851
+ sanitizeInputItems(messages) {
3852
+ return messages.map((item) => {
3853
+ const typedItem = item;
3854
+ // function_call_output only accepts: type, call_id, output, id, status
3855
+ if (typedItem.type === LlmMessageType.FunctionCallOutput) {
3856
+ const sanitized = {
3857
+ type: typedItem.type,
3858
+ call_id: typedItem.call_id,
3859
+ output: typedItem.output,
3860
+ };
3861
+ if (typedItem.id)
3862
+ sanitized.id = typedItem.id;
3863
+ if (typedItem.status)
3864
+ sanitized.status = typedItem.status;
3865
+ return sanitized;
3866
+ }
3867
+ // All other items pass through as-is
3868
+ return item;
3869
+ });
3870
+ }
2379
3871
  hasToolCalls(response) {
2380
3872
  if (!response.output || !Array.isArray(response.output)) {
2381
3873
  return false;
@@ -2421,10 +3913,48 @@ const openAiAdapter = new OpenAiAdapter();
2421
3913
  // Constants
2422
3914
  //
2423
3915
  const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
3916
+ const STRUCTURED_OUTPUT_SCHEMA_NAME = "response";
3917
+ /**
3918
+ * Detect 4xx errors that indicate the model itself does not support
3919
+ * `response_format: json_schema`. Mirrors the Anthropic pattern: we only
3920
+ * trigger the fake-tool fallback when the failure is plausibly a capability
3921
+ * gap, not a generic 400.
3922
+ */
3923
+ function isStructuredOutputUnsupportedError(error) {
3924
+ if (!error || typeof error !== "object")
3925
+ return false;
3926
+ const err = error;
3927
+ const status = err.status ?? err.statusCode;
3928
+ if (status !== 400 && status !== 422)
3929
+ return false;
3930
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
3931
+ return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
3932
+ }
3933
+ // OpenRouter routes that don't accept `temperature`. Patterns match the
3934
+ // vendor-prefixed route id (e.g. `openai/gpt-5.5`) so dated variants are
3935
+ // covered without code changes.
3936
+ const MODELS_WITHOUT_TEMPERATURE = [
3937
+ /^openai\/gpt-5\.5/,
3938
+ /^openai\/o\d/,
3939
+ /^anthropic\/claude-opus-4-[789]/,
3940
+ /^anthropic\/claude-opus-[5-9]/,
3941
+ ];
3942
+ function isTemperatureDeprecationError(error) {
3943
+ if (!error || typeof error !== "object")
3944
+ return false;
3945
+ const err = error;
3946
+ const status = err.status ?? err.statusCode;
3947
+ if (status !== 400)
3948
+ return false;
3949
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
3950
+ return messages.some((m) => m.toLowerCase().includes("temperature"));
3951
+ }
2424
3952
  /**
2425
- * Convert standardized content items to OpenRouter format
2426
- * Note: OpenRouter does not support native file/image uploads.
2427
- * Images and files are discarded with a warning.
3953
+ * Convert standardized content items to OpenRouter format. Images become
3954
+ * `image_url` parts and files become `file` parts; both pass through to
3955
+ * OpenRouter which routes to the selected backend. Backends that don't
3956
+ * support the modality 4xx — that's a model-capability mismatch, surfaced
3957
+ * by the call rather than silently dropped here.
2428
3958
  */
2429
3959
  function convertContentToOpenRouter(content) {
2430
3960
  if (typeof content === "string") {
@@ -2432,25 +3962,38 @@ function convertContentToOpenRouter(content) {
2432
3962
  }
2433
3963
  const parts = [];
2434
3964
  for (const item of content) {
2435
- // Text content - pass through
2436
3965
  if (item.type === LlmMessageType.InputText) {
2437
3966
  parts.push({ type: "text", text: item.text });
2438
3967
  continue;
2439
3968
  }
2440
- // Image content - warn and discard
2441
3969
  if (item.type === LlmMessageType.InputImage) {
2442
- log$2.warn("OpenRouter does not support image uploads; image discarded");
3970
+ const url = item.image_url ?? "";
3971
+ if (!url) {
3972
+ log$1.warn("OpenRouter image content missing image_url; image discarded");
3973
+ continue;
3974
+ }
3975
+ parts.push({ type: "image_url", imageUrl: { url } });
2443
3976
  continue;
2444
3977
  }
2445
- // File/Document content - warn and discard
2446
3978
  if (item.type === LlmMessageType.InputFile) {
2447
- log$2.warn({ filename: item.filename }, "OpenRouter does not support file uploads; file discarded");
3979
+ const fileData = typeof item.file_data === "string" ? item.file_data : "";
3980
+ if (!fileData) {
3981
+ log$1.warn({ filename: item.filename }, "OpenRouter file content missing file_data; file discarded");
3982
+ continue;
3983
+ }
3984
+ parts.push({
3985
+ type: "file",
3986
+ file: {
3987
+ filename: item.filename,
3988
+ fileData,
3989
+ },
3990
+ });
2448
3991
  continue;
2449
3992
  }
2450
3993
  // Unknown type - warn and skip
2451
- log$2.warn({ item }, "Unknown content type for OpenRouter; discarded");
3994
+ log$1.warn({ item }, "Unknown content type for OpenRouter; discarded");
2452
3995
  }
2453
- // If no text parts remain, return empty string to avoid empty array
3996
+ // If no parts remain, return empty string to avoid empty array
2454
3997
  if (parts.length === 0) {
2455
3998
  return "";
2456
3999
  }
@@ -2459,6 +4002,32 @@ function convertContentToOpenRouter(content) {
2459
4002
  // OpenRouter SDK error types based on HTTP status codes
2460
4003
  const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
2461
4004
  const RATE_LIMIT_STATUS_CODE = 429;
4005
+ /**
4006
+ * Walk the JSON schema and force `additionalProperties: false` on every
4007
+ * object node. Required by the OpenAI-style json_schema response_format
4008
+ * (which OpenRouter accepts) when `strict: true`.
4009
+ */
4010
+ function enforceAdditionalPropertiesFalse(schema) {
4011
+ const stack = [schema];
4012
+ while (stack.length > 0) {
4013
+ const node = stack.pop();
4014
+ if (node.type === "object") {
4015
+ node.additionalProperties = false;
4016
+ }
4017
+ for (const value of Object.values(node)) {
4018
+ if (Array.isArray(value)) {
4019
+ for (const entry of value) {
4020
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
4021
+ stack.push(entry);
4022
+ }
4023
+ }
4024
+ }
4025
+ else if (value && typeof value === "object") {
4026
+ stack.push(value);
4027
+ }
4028
+ }
4029
+ }
4030
+ }
2462
4031
  //
2463
4032
  //
2464
4033
  // Main
@@ -2474,6 +4043,33 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2474
4043
  super(...arguments);
2475
4044
  this.name = PROVIDER.OPENROUTER.NAME;
2476
4045
  this.defaultModel = PROVIDER.OPENROUTER.MODEL.DEFAULT;
4046
+ // Session-level cache of models observed to reject native
4047
+ // `response_format: json_schema`. When a model is in this set, buildRequest
4048
+ // engages the legacy fake-tool path instead of native structured output.
4049
+ this.runtimeNoStructuredOutputModels = new Set();
4050
+ // Session-level cache of routes observed to reject `temperature`. Populated
4051
+ // by executeRequest on 400 errors so repeat calls skip the param.
4052
+ this.runtimeNoTemperatureModels = new Set();
4053
+ }
4054
+ rememberModelRejectsStructuredOutput(model) {
4055
+ this.runtimeNoStructuredOutputModels.add(model);
4056
+ }
4057
+ clearRuntimeNoStructuredOutputModels() {
4058
+ this.runtimeNoStructuredOutputModels.clear();
4059
+ }
4060
+ supportsStructuredOutput(model) {
4061
+ return !this.runtimeNoStructuredOutputModels.has(model);
4062
+ }
4063
+ rememberModelRejectsTemperature(model) {
4064
+ this.runtimeNoTemperatureModels.add(model);
4065
+ }
4066
+ clearRuntimeNoTemperatureModels() {
4067
+ this.runtimeNoTemperatureModels.clear();
4068
+ }
4069
+ supportsTemperature(model) {
4070
+ if (this.runtimeNoTemperatureModels.has(model))
4071
+ return false;
4072
+ return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
2477
4073
  }
2478
4074
  //
2479
4075
  // Request Building
@@ -2495,8 +4091,23 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2495
4091
  if (request.user) {
2496
4092
  openRouterRequest.user = request.user;
2497
4093
  }
2498
- if (request.tools && request.tools.length > 0) {
2499
- openRouterRequest.tools = request.tools.map((tool) => ({
4094
+ const useFallbackStructuredOutput = Boolean(request.format) &&
4095
+ !this.supportsStructuredOutput(openRouterRequest.model);
4096
+ const allTools = request.tools
4097
+ ? [...request.tools]
4098
+ : [];
4099
+ if (useFallbackStructuredOutput && request.format) {
4100
+ log$1.warn(`[OpenRouterAdapter] Using legacy structured_output tool fallback for model ${openRouterRequest.model}; native response_format previously rejected for this model.`);
4101
+ allTools.push({
4102
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
4103
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
4104
+ "After gathering all necessary information (including results from other tools), " +
4105
+ "call this tool with the structured data to complete the request.",
4106
+ parameters: request.format,
4107
+ });
4108
+ }
4109
+ if (allTools.length > 0) {
4110
+ openRouterRequest.tools = allTools.map((tool) => ({
2500
4111
  type: "function",
2501
4112
  function: {
2502
4113
  name: tool.name,
@@ -2508,6 +4119,19 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2508
4119
  // The structured_output tool prompt already emphasizes it must be called
2509
4120
  openRouterRequest.tool_choice = "auto";
2510
4121
  }
4122
+ // Native structured output: send schema as `response_format`. The legacy
4123
+ // tool-emulation path is engaged only as a runtime fallback for models the
4124
+ // API has flagged as not supporting native json_schema.
4125
+ if (request.format && !useFallbackStructuredOutput) {
4126
+ openRouterRequest.response_format = {
4127
+ type: "json_schema",
4128
+ json_schema: {
4129
+ name: STRUCTURED_OUTPUT_SCHEMA_NAME,
4130
+ schema: request.format,
4131
+ strict: true,
4132
+ },
4133
+ };
4134
+ }
2511
4135
  if (request.providerOptions) {
2512
4136
  Object.assign(openRouterRequest, request.providerOptions);
2513
4137
  }
@@ -2516,26 +4140,26 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2516
4140
  openRouterRequest.temperature =
2517
4141
  request.temperature;
2518
4142
  }
4143
+ // Strip temperature for routes that don't support it (denylist + runtime cache)
4144
+ const requestRecord = openRouterRequest;
4145
+ if (requestRecord.temperature !== undefined &&
4146
+ !this.supportsTemperature(openRouterRequest.model)) {
4147
+ delete requestRecord.temperature;
4148
+ }
2519
4149
  return openRouterRequest;
2520
4150
  }
2521
- formatTools(toolkit, outputSchema) {
2522
- const tools = toolkit.tools.map((tool) => ({
4151
+ formatTools(toolkit,
4152
+ // outputSchema is part of the interface contract but OpenRouter now uses
4153
+ // native `response_format` (set in buildRequest), so we no longer inject a
4154
+ // synthetic structured-output tool here. The legacy fake-tool injection
4155
+ // happens in buildRequest only as a runtime fallback.
4156
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4157
+ _outputSchema) {
4158
+ return toolkit.tools.map((tool) => ({
2523
4159
  name: tool.name,
2524
4160
  description: tool.description,
2525
4161
  parameters: tool.parameters,
2526
4162
  }));
2527
- // Add structured output tool if schema is provided
2528
- // (OpenRouter doesn't have native structured output like OpenAI, so use tool approach)
2529
- if (outputSchema) {
2530
- tools.push({
2531
- name: STRUCTURED_OUTPUT_TOOL_NAME,
2532
- description: "REQUIRED: You MUST call this tool to provide your final response. " +
2533
- "After gathering all necessary information (including results from other tools), " +
2534
- "call this tool with the structured data to complete the request.",
2535
- parameters: outputSchema,
2536
- });
2537
- }
2538
- return tools;
2539
4163
  }
2540
4164
  formatOutputSchema(schema) {
2541
4165
  let jsonSchema;
@@ -2548,45 +4172,141 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2548
4172
  jsonSchema.type = "object"; // Normalize type
2549
4173
  }
2550
4174
  else {
2551
- // Convert NaturalSchema to JSON schema through Zod
4175
+ // Convert NaturalSchema to JSON schema through Zod. Re-spread into a
4176
+ // plain object: Zod v4's z.toJSONSchema returns an object that carries
4177
+ // a non-configurable `~standard` Standard-Schema interop marker as a
4178
+ // non-enumerable own property. Anthropic's output_config.format
4179
+ // validation is strict and rejects unknown properties when OpenRouter
4180
+ // forwards the schema, so we drop the marker here. JSON.stringify
4181
+ // already skips it but the OpenRouter SDK enumerates own properties
4182
+ // during serialization.
2552
4183
  const zodSchema = schema instanceof z.ZodType
2553
4184
  ? schema
2554
4185
  : naturalZodSchema(schema);
2555
- jsonSchema = z.toJSONSchema(zodSchema);
4186
+ jsonSchema = { ...z.toJSONSchema(zodSchema) };
2556
4187
  }
2557
4188
  // Remove $schema property (can cause issues with some providers)
2558
4189
  if (jsonSchema.$schema) {
2559
4190
  delete jsonSchema.$schema;
2560
4191
  }
4192
+ // OpenRouter (and most backends behind it) require additionalProperties:
4193
+ // false on every object when using strict json_schema response_format.
4194
+ enforceAdditionalPropertiesFalse(jsonSchema);
2561
4195
  return jsonSchema;
2562
4196
  }
2563
4197
  //
2564
4198
  // API Execution
2565
4199
  //
2566
- async executeRequest(client, request) {
4200
+ async executeRequest(client, request, signal) {
2567
4201
  const openRouter = client;
2568
4202
  const openRouterRequest = request;
2569
- const response = await openRouter.chat.send({
4203
+ const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
4204
+ try {
4205
+ const response = (await openRouter.chat.send(this.toSdkChatParams(openRouterRequest), signal ? { signal } : undefined));
4206
+ if (wantsStructuredOutput) {
4207
+ response.__jaypieStructuredOutput = true;
4208
+ }
4209
+ return response;
4210
+ }
4211
+ catch (error) {
4212
+ if (signal?.aborted)
4213
+ return undefined;
4214
+ // If the route rejected `temperature` (e.g., openai/gpt-5.5 forwarding),
4215
+ // cache it and retry without the param.
4216
+ const requestRecord = openRouterRequest;
4217
+ if (requestRecord.temperature !== undefined &&
4218
+ isTemperatureDeprecationError(error)) {
4219
+ this.rememberModelRejectsTemperature(openRouterRequest.model);
4220
+ const retryRequest = { ...openRouterRequest };
4221
+ delete retryRequest.temperature;
4222
+ const response = (await openRouter.chat.send(this.toSdkChatParams(retryRequest), signal ? { signal } : undefined));
4223
+ if (wantsStructuredOutput) {
4224
+ response.__jaypieStructuredOutput = true;
4225
+ }
4226
+ return response;
4227
+ }
4228
+ // If the model rejected `response_format`, cache it and retry with the
4229
+ // legacy fake-tool emulation path.
4230
+ if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {
4231
+ const model = openRouterRequest.model;
4232
+ this.rememberModelRejectsStructuredOutput(model);
4233
+ log$1.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
4234
+ const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
4235
+ return (await openRouter.chat.send(this.toSdkChatParams(fallbackRequest), signal ? { signal } : undefined));
4236
+ }
4237
+ throw error;
4238
+ }
4239
+ }
4240
+ /**
4241
+ * Translate our internal snake_case `OpenRouterRequest` into the SDK's
4242
+ * camelCase shape, forwarding only the fields we care about (the SDK
4243
+ * silently strips unknown fields).
4244
+ */
4245
+ toSdkChatParams(openRouterRequest) {
4246
+ const params = {
2570
4247
  model: openRouterRequest.model,
2571
4248
  messages: openRouterRequest.messages,
2572
4249
  tools: openRouterRequest.tools,
2573
4250
  toolChoice: openRouterRequest.tool_choice,
2574
4251
  user: openRouterRequest.user,
2575
- });
2576
- return response;
4252
+ };
4253
+ if (openRouterRequest.response_format) {
4254
+ const format = openRouterRequest.response_format;
4255
+ if (format.type === "json_schema") {
4256
+ params.responseFormat = {
4257
+ type: "json_schema",
4258
+ jsonSchema: format.json_schema,
4259
+ };
4260
+ }
4261
+ else {
4262
+ params.responseFormat = format;
4263
+ }
4264
+ }
4265
+ const temperature = openRouterRequest.temperature;
4266
+ if (temperature !== undefined) {
4267
+ params.temperature = temperature;
4268
+ }
4269
+ return params;
4270
+ }
4271
+ /**
4272
+ * Rebuild a structured-output request without `response_format`, swapping in
4273
+ * the legacy fake-tool emulation. Used as a runtime fallback when a model
4274
+ * rejects native json_schema.
4275
+ */
4276
+ toFallbackStructuredOutputRequest(request) {
4277
+ if (!request.response_format ||
4278
+ request.response_format.type !== "json_schema") {
4279
+ return request;
4280
+ }
4281
+ const { response_format, ...rest } = request;
4282
+ const fallbackRequest = { ...rest };
4283
+ const schema = response_format.json_schema.schema;
4284
+ const fakeTool = {
4285
+ type: "function",
4286
+ function: {
4287
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
4288
+ description: "REQUIRED: You MUST call this tool to provide your final response. " +
4289
+ "After gathering all necessary information (including results from other tools), " +
4290
+ "call this tool with the structured data to complete the request.",
4291
+ parameters: schema,
4292
+ },
4293
+ };
4294
+ fallbackRequest.tools = [...(fallbackRequest.tools ?? []), fakeTool];
4295
+ fallbackRequest.tool_choice = "auto";
4296
+ return fallbackRequest;
2577
4297
  }
2578
- async *executeStreamRequest(client, request) {
4298
+ async *executeStreamRequest(client, request, signal) {
2579
4299
  const openRouter = client;
2580
4300
  const openRouterRequest = request;
2581
- // Use chat.send with stream: true for streaming responses
2582
- const stream = await openRouter.chat.send({
2583
- model: openRouterRequest.model,
2584
- messages: openRouterRequest.messages,
2585
- tools: openRouterRequest.tools,
2586
- toolChoice: openRouterRequest.tool_choice,
2587
- user: openRouterRequest.user,
4301
+ // Use chat.send with stream: true for streaming responses.
4302
+ // Cast the result to AsyncIterable: when stream: true, the SDK returns a
4303
+ // stream we can iterate, but the typed result is the union with the
4304
+ // non-stream response.
4305
+ const streamParams = {
4306
+ ...this.toSdkChatParams(openRouterRequest),
2588
4307
  stream: true,
2589
- });
4308
+ };
4309
+ const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
2590
4310
  // Track current tool call being built
2591
4311
  let currentToolCall = null;
2592
4312
  // Track usage for final chunk
@@ -2836,6 +4556,14 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2836
4556
  suggestedDelayMs: 60000,
2837
4557
  };
2838
4558
  }
4559
+ // Check for transient network errors (ECONNRESET, etc.)
4560
+ if (isTransientNetworkError(error)) {
4561
+ return {
4562
+ error,
4563
+ category: ErrorCategory.Retryable,
4564
+ shouldRetry: true,
4565
+ };
4566
+ }
2839
4567
  // Unknown error - treat as potentially retryable
2840
4568
  return {
2841
4569
  error,
@@ -2857,6 +4585,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2857
4585
  }
2858
4586
  hasStructuredOutput(response) {
2859
4587
  const openRouterResponse = response;
4588
+ // Native path: executeRequest annotates the response when we sent
4589
+ // `response_format`, so we can detect intent statelessly.
4590
+ if (openRouterResponse.__jaypieStructuredOutput) {
4591
+ return this.extractStructuredOutput(response) !== undefined;
4592
+ }
4593
+ // Fallback path: legacy fake-tool emulation, kept for models that the
4594
+ // runtime has cached as not supporting native `response_format`.
2860
4595
  const choice = openRouterResponse.choices[0];
2861
4596
  // SDK returns camelCase (toolCalls)
2862
4597
  if (!choice?.message?.toolCalls?.length) {
@@ -2868,6 +4603,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2868
4603
  }
2869
4604
  extractStructuredOutput(response) {
2870
4605
  const openRouterResponse = response;
4606
+ if (openRouterResponse.__jaypieStructuredOutput) {
4607
+ const choice = openRouterResponse.choices[0];
4608
+ const content = choice?.message?.content;
4609
+ if (typeof content !== "string" || content.length === 0) {
4610
+ return undefined;
4611
+ }
4612
+ try {
4613
+ return JSON.parse(content);
4614
+ }
4615
+ catch {
4616
+ return undefined;
4617
+ }
4618
+ }
4619
+ // Fallback path: legacy fake-tool emulation
2871
4620
  const choice = openRouterResponse.choices[0];
2872
4621
  // SDK returns camelCase (toolCalls)
2873
4622
  if (!choice?.message?.toolCalls?.length) {
@@ -2993,8 +4742,54 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2993
4742
  // Export singleton instance
2994
4743
  const openRouterAdapter = new OpenRouterAdapter();
2995
4744
 
4745
+ /**
4746
+ * Error-message substrings that indicate a transient xAI media-ingest flake.
4747
+ * The xAI ingest service enters a bad state after ~12 consecutive file-bearing
4748
+ * calls and rejects subsequent requests with HTTP 400. The condition is
4749
+ * self-clearing, so these errors should be retried with backoff rather than
4750
+ * surfaced as unrecoverable.
4751
+ *
4752
+ * See: github.com/finlaysonstudio/jaypie issue #301
4753
+ */
4754
+ const TRANSIENT_INGEST_MESSAGE_PATTERNS = [
4755
+ "failed to ingest inline file bytes",
4756
+ ];
4757
+ function isTransientIngestError(error) {
4758
+ if (!(error instanceof BadRequestError))
4759
+ return false;
4760
+ const message = (error.message ?? "").toLowerCase();
4761
+ return TRANSIENT_INGEST_MESSAGE_PATTERNS.some((pattern) => message.includes(pattern));
4762
+ }
4763
+ /**
4764
+ * XaiAdapter extends OpenAiAdapter since xAI (Grok) uses an OpenAI-compatible API.
4765
+ * Name, default model, and transient-ingest-error detection are overridden;
4766
+ * all request building, response parsing, tool handling, and streaming are
4767
+ * inherited.
4768
+ */
4769
+ class XaiAdapter extends OpenAiAdapter {
4770
+ constructor() {
4771
+ super(...arguments);
4772
+ // @ts-expect-error Narrowing override: xAI name differs from parent's literal "openai"
4773
+ this.name = PROVIDER.XAI.NAME;
4774
+ // @ts-expect-error Narrowing override: xAI default model differs from parent's literal
4775
+ this.defaultModel = PROVIDER.XAI.MODEL.DEFAULT;
4776
+ }
4777
+ classifyError(error) {
4778
+ if (isTransientIngestError(error)) {
4779
+ return {
4780
+ error,
4781
+ category: ErrorCategory.Retryable,
4782
+ shouldRetry: true,
4783
+ };
4784
+ }
4785
+ return super.classifyError(error);
4786
+ }
4787
+ }
4788
+ // Export singleton instance
4789
+ const xaiAdapter = new XaiAdapter();
4790
+
2996
4791
  const DEFAULT_TOOL_TYPE = "function";
2997
- const log = log$2.lib({ lib: JAYPIE.LIB.LLM });
4792
+ const log = log$1.lib({ lib: JAYPIE.LIB.LLM });
2998
4793
  function logToolMessage(message, context) {
2999
4794
  log.trace.var({ [context.name]: message });
3000
4795
  }
@@ -3128,6 +4923,198 @@ class Toolkit {
3128
4923
  }
3129
4924
  }
3130
4925
 
4926
+ //
4927
+ //
4928
+ // Constants
4929
+ //
4930
+ const ENV = {
4931
+ DD_LLMOBS_ENABLED: "DD_LLMOBS_ENABLED",
4932
+ };
4933
+ const MODULE = {
4934
+ // Computed at runtime so bundlers (esbuild) do not attempt to include
4935
+ // dd-trace, which is provided by the Datadog Lambda layer at runtime.
4936
+ DD_TRACE: ["dd", "trace"].join("-"),
4937
+ };
4938
+ //
4939
+ //
4940
+ // Helpers
4941
+ //
4942
+ // CJS/ESM compatible require - handles bundling to CJS where import.meta.url
4943
+ // becomes undefined (mirrors packages/testkit/src/mock/original.ts).
4944
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4945
+ // @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
4946
+ const requireModule = typeof __filename !== "undefined"
4947
+ ? createRequire(pathToFileURL(__filename).href)
4948
+ : createRequire(import.meta.url);
4949
+ let resolved = false;
4950
+ let cachedSdk$4 = null;
4951
+ /**
4952
+ * Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
4953
+ * when dd-trace is absent or the SDK surface is unexpected. Cached after the
4954
+ * first attempt.
4955
+ */
4956
+ function resolveLlmObs() {
4957
+ if (resolved) {
4958
+ return cachedSdk$4;
4959
+ }
4960
+ resolved = true;
4961
+ try {
4962
+ const ddTrace = requireModule(MODULE.DD_TRACE);
4963
+ const tracer = ddTrace?.default ?? ddTrace;
4964
+ const llmobs = tracer?.llmobs;
4965
+ if (llmobs &&
4966
+ typeof llmobs.trace === "function" &&
4967
+ typeof llmobs.annotate === "function") {
4968
+ cachedSdk$4 = llmobs;
4969
+ }
4970
+ }
4971
+ catch {
4972
+ cachedSdk$4 = null;
4973
+ }
4974
+ return cachedSdk$4;
4975
+ }
4976
+ //
4977
+ //
4978
+ // Main
4979
+ //
4980
+ /**
4981
+ * True when LLM Observability emission is opted in via `DD_LLMOBS_ENABLED`.
4982
+ * Accepts any truthy value except "false"/"0".
4983
+ */
4984
+ function isLlmObsEnabled() {
4985
+ const value = process.env[ENV.DD_LLMOBS_ENABLED];
4986
+ if (!value) {
4987
+ return false;
4988
+ }
4989
+ const normalized = value.trim().toLowerCase();
4990
+ return normalized !== "" && normalized !== "false" && normalized !== "0";
4991
+ }
4992
+ /** Resolve the SDK only when enabled and available. */
4993
+ function activeSdk() {
4994
+ if (!isLlmObsEnabled()) {
4995
+ return null;
4996
+ }
4997
+ return resolveLlmObs();
4998
+ }
4999
+ /**
5000
+ * Run `fn` inside an LLM Observability span. When emission is disabled or
5001
+ * dd-trace is absent, `fn` is invoked directly with no overhead. The active
5002
+ * span is established for the duration of `fn`, so nested `withLlmObsSpan`
5003
+ * calls (and `annotateLlmObs`) attach as children/annotations automatically.
5004
+ */
5005
+ async function withLlmObsSpan(options, fn) {
5006
+ const sdk = activeSdk();
5007
+ if (!sdk) {
5008
+ return fn();
5009
+ }
5010
+ let started = false;
5011
+ try {
5012
+ return (await sdk.trace(options, () => {
5013
+ started = true;
5014
+ return fn();
5015
+ }));
5016
+ }
5017
+ catch (error) {
5018
+ // If `fn` ran, propagate its error (the SDK re-throws after finishing the
5019
+ // span). Only swallow failures from the instrumentation itself, re-running
5020
+ // `fn` so observability never breaks the underlying call.
5021
+ if (started) {
5022
+ throw error;
5023
+ }
5024
+ getLogger$6().warn("[llmobs] span emission failed");
5025
+ getLogger$6().var({ error });
5026
+ return fn();
5027
+ }
5028
+ }
5029
+ /**
5030
+ * Annotate the currently active LLM Observability span. No-op when disabled.
5031
+ */
5032
+ function annotateLlmObs(annotation) {
5033
+ const sdk = activeSdk();
5034
+ if (!sdk) {
5035
+ return;
5036
+ }
5037
+ try {
5038
+ sdk.annotate(undefined, annotation);
5039
+ }
5040
+ catch (error) {
5041
+ getLogger$6().warn("[llmobs] annotate failed");
5042
+ getLogger$6().var({ error });
5043
+ }
5044
+ }
5045
+ /**
5046
+ * Open a manual span that is finished later. Used by streaming, where work is
5047
+ * yielded incrementally and cannot be enclosed in a single callback. The span
5048
+ * is kept open via a deferred promise resolved by `finish()`. Returns null when
5049
+ * emission is disabled or dd-trace is absent.
5050
+ *
5051
+ * Note: because the span is held open across `yield` boundaries (outside the
5052
+ * SDK's AsyncLocalStorage scope), manual spans are emitted flat rather than
5053
+ * nested. Annotations are applied to the explicit span reference.
5054
+ */
5055
+ function openLlmObsSpan(options) {
5056
+ const sdk = activeSdk();
5057
+ if (!sdk) {
5058
+ return null;
5059
+ }
5060
+ try {
5061
+ let capturedSpan;
5062
+ let release;
5063
+ const held = new Promise((resolve) => {
5064
+ release = resolve;
5065
+ });
5066
+ // trace() runs the callback synchronously, capturing the span; the returned
5067
+ // pending promise keeps the span open until finish() resolves it.
5068
+ void sdk.trace(options, (span) => {
5069
+ capturedSpan = span;
5070
+ return held;
5071
+ });
5072
+ let finished = false;
5073
+ return {
5074
+ annotate: (annotation) => {
5075
+ try {
5076
+ sdk.annotate(capturedSpan, annotation);
5077
+ }
5078
+ catch (error) {
5079
+ getLogger$6().warn("[llmobs] annotate failed");
5080
+ getLogger$6().var({ error });
5081
+ }
5082
+ },
5083
+ finish: () => {
5084
+ if (finished) {
5085
+ return;
5086
+ }
5087
+ finished = true;
5088
+ release?.();
5089
+ },
5090
+ };
5091
+ }
5092
+ catch (error) {
5093
+ getLogger$6().warn("[llmobs] span emission failed");
5094
+ getLogger$6().var({ error });
5095
+ return null;
5096
+ }
5097
+ }
5098
+ //
5099
+ //
5100
+ // Mapping Helpers
5101
+ //
5102
+ /**
5103
+ * Sum an `LlmUsage` array into LLM Observability metric keys. Returns undefined
5104
+ * when there is no usage data so callers can omit the `metrics` field.
5105
+ */
5106
+ function usageToLlmObsMetrics(usage) {
5107
+ if (!usage || usage.length === 0) {
5108
+ return undefined;
5109
+ }
5110
+ const metrics = usage.reduce((sum, item) => ({
5111
+ input_tokens: sum.input_tokens + (item.input ?? 0),
5112
+ output_tokens: sum.output_tokens + (item.output ?? 0),
5113
+ total_tokens: sum.total_tokens + (item.total ?? 0),
5114
+ }), { input_tokens: 0, output_tokens: 0, total_tokens: 0 });
5115
+ return metrics;
5116
+ }
5117
+
3131
5118
  //
3132
5119
  //
3133
5120
  // Main
@@ -3774,6 +5761,78 @@ class RetryPolicy {
3774
5761
  // Export a default policy instance
3775
5762
  const defaultRetryPolicy = new RetryPolicy();
3776
5763
 
5764
+ //
5765
+ //
5766
+ // Helpers
5767
+ //
5768
+ /**
5769
+ * Compare an unhandled rejection reason against errors the retry loop has
5770
+ * already caught. Reference equality first, then fall back to message + name
5771
+ * — providers sometimes surface twin rejections as fresh Error instances
5772
+ * rebuilt from the same upstream failure.
5773
+ */
5774
+ function matchesCaughtError(reason, caught) {
5775
+ if (caught.has(reason))
5776
+ return true;
5777
+ if (!(reason instanceof Error))
5778
+ return false;
5779
+ for (const handled of caught) {
5780
+ if (handled instanceof Error &&
5781
+ handled.name === reason.name &&
5782
+ handled.message === reason.message) {
5783
+ return true;
5784
+ }
5785
+ }
5786
+ return false;
5787
+ }
5788
+ //
5789
+ //
5790
+ // Main
5791
+ //
5792
+ /**
5793
+ * Create a guard that suppresses unhandled rejections firing as siblings of
5794
+ * an error the retry loop has already caught. Provider SDKs occasionally
5795
+ * surface a single upstream failure as twin rejections — the retry layer
5796
+ * accepts responsibility for the first; this guard prevents the second from
5797
+ * crashing the host while the retry is in flight.
5798
+ *
5799
+ * The guard also continues to suppress transient socket teardown errors
5800
+ * (e.g. undici `TypeError: terminated`) emitted between attempts.
5801
+ */
5802
+ function createStaleRejectionGuard() {
5803
+ const log = getLogger$6();
5804
+ const caughtErrors = new Set();
5805
+ let listener;
5806
+ return {
5807
+ install() {
5808
+ if (listener)
5809
+ return;
5810
+ listener = (reason, promise) => {
5811
+ if (isTransientNetworkError(reason)) {
5812
+ promise?.catch?.(() => { });
5813
+ log.trace("Suppressed stale socket error during retry");
5814
+ return;
5815
+ }
5816
+ if (matchesCaughtError(reason, caughtErrors)) {
5817
+ promise?.catch?.(() => { });
5818
+ log.trace("Suppressed sibling rejection of already-handled error");
5819
+ }
5820
+ };
5821
+ process.on("unhandledRejection", listener);
5822
+ },
5823
+ remove() {
5824
+ if (listener) {
5825
+ process.removeListener("unhandledRejection", listener);
5826
+ listener = undefined;
5827
+ }
5828
+ caughtErrors.clear();
5829
+ },
5830
+ recordCaught(error) {
5831
+ caughtErrors.add(error);
5832
+ },
5833
+ };
5834
+ }
5835
+
3777
5836
  //
3778
5837
  //
3779
5838
  // Main
@@ -3789,67 +5848,85 @@ class RetryExecutor {
3789
5848
  this.errorClassifier = config.errorClassifier;
3790
5849
  }
3791
5850
  /**
3792
- * Execute an operation with retry logic
5851
+ * Execute an operation with retry logic.
5852
+ * Each attempt receives an AbortSignal. On failure, the signal is aborted
5853
+ * before sleeping — this kills lingering socket callbacks from the previous
5854
+ * request and prevents stale async errors from escaping the retry loop.
3793
5855
  *
3794
- * @param operation - The async operation to execute
5856
+ * @param operation - The async operation to execute (receives AbortSignal)
3795
5857
  * @param options - Execution options including context and hooks
3796
5858
  * @returns The result of the operation
3797
5859
  * @throws BadGatewayError if all retries are exhausted or error is not retryable
3798
5860
  */
3799
5861
  async execute(operation, options) {
5862
+ const log = getLogger$6();
3800
5863
  let attempt = 0;
3801
- while (true) {
3802
- try {
3803
- const result = await operation();
3804
- if (attempt > 0) {
3805
- log$1.debug(`API call succeeded after ${attempt} retries`);
3806
- }
3807
- return result;
3808
- }
3809
- catch (error) {
3810
- // Check if we've exhausted retries
3811
- if (!this.policy.shouldRetry(attempt)) {
3812
- log$1.error(`API call failed after ${this.policy.maxRetries} retries`);
3813
- log$1.var({ error });
3814
- await this.hookRunner.runOnUnrecoverableError(options.hooks, {
3815
- input: options.context.input,
3816
- options: options.context.options,
3817
- providerRequest: options.context.providerRequest,
3818
- error,
3819
- });
3820
- const errorMessage = error instanceof Error ? error.message : String(error);
3821
- throw new BadGatewayError(errorMessage);
5864
+ // Guard against stale rejections firing on a subsequent microtask after
5865
+ // the retry layer has already caught the originating error: undici socket
5866
+ // teardown (TypeError: terminated) and twin upstream-SDK rejections
5867
+ // (e.g. issue #336 — OpenRouter SyntaxError siblings).
5868
+ const guard = createStaleRejectionGuard();
5869
+ try {
5870
+ while (true) {
5871
+ const controller = new AbortController();
5872
+ try {
5873
+ const result = await operation(controller.signal);
5874
+ if (attempt > 0) {
5875
+ log.debug(`API call succeeded after ${attempt} retries`);
5876
+ }
5877
+ return result;
3822
5878
  }
3823
- // Check if error is not retryable
3824
- if (!this.errorClassifier.isRetryable(error)) {
3825
- log$1.error("API call failed with non-retryable error");
3826
- log$1.var({ error });
3827
- await this.hookRunner.runOnUnrecoverableError(options.hooks, {
5879
+ catch (error) {
5880
+ controller.abort("retry");
5881
+ guard.recordCaught(error);
5882
+ guard.install();
5883
+ // Check if we've exhausted retries
5884
+ if (!this.policy.shouldRetry(attempt)) {
5885
+ log.error(`API call failed after ${this.policy.maxRetries} retries`);
5886
+ log.var({ error });
5887
+ await this.hookRunner.runOnUnrecoverableError(options.hooks, {
5888
+ input: options.context.input,
5889
+ options: options.context.options,
5890
+ providerRequest: options.context.providerRequest,
5891
+ error,
5892
+ });
5893
+ const errorMessage = error instanceof Error ? error.message : String(error);
5894
+ throw new BadGatewayError(errorMessage);
5895
+ }
5896
+ // Check if error is not retryable
5897
+ if (!this.errorClassifier.isRetryable(error)) {
5898
+ log.error("API call failed with non-retryable error");
5899
+ log.var({ error });
5900
+ await this.hookRunner.runOnUnrecoverableError(options.hooks, {
5901
+ input: options.context.input,
5902
+ options: options.context.options,
5903
+ providerRequest: options.context.providerRequest,
5904
+ error,
5905
+ });
5906
+ const errorMessage = error instanceof Error ? error.message : String(error);
5907
+ throw new BadGatewayError(errorMessage);
5908
+ }
5909
+ // Warn if this is an unknown error type
5910
+ if (!this.errorClassifier.isKnownError(error)) {
5911
+ log.warn("API returned unknown error type, will retry");
5912
+ log.var({ error });
5913
+ }
5914
+ const delay = this.policy.getDelayForAttempt(attempt);
5915
+ log.warn(`API call failed. Retrying in ${delay}ms...`);
5916
+ await this.hookRunner.runOnRetryableError(options.hooks, {
3828
5917
  input: options.context.input,
3829
5918
  options: options.context.options,
3830
5919
  providerRequest: options.context.providerRequest,
3831
5920
  error,
3832
5921
  });
3833
- const errorMessage = error instanceof Error ? error.message : String(error);
3834
- throw new BadGatewayError(errorMessage);
3835
- }
3836
- // Warn if this is an unknown error type
3837
- if (!this.errorClassifier.isKnownError(error)) {
3838
- log$1.warn("API returned unknown error type, will retry");
3839
- log$1.var({ error });
5922
+ await sleep(delay);
5923
+ attempt++;
3840
5924
  }
3841
- const delay = this.policy.getDelayForAttempt(attempt);
3842
- log$1.warn(`API call failed. Retrying in ${delay}ms...`);
3843
- await this.hookRunner.runOnRetryableError(options.hooks, {
3844
- input: options.context.input,
3845
- options: options.context.options,
3846
- providerRequest: options.context.providerRequest,
3847
- error,
3848
- });
3849
- await sleep(delay);
3850
- attempt++;
3851
5925
  }
3852
5926
  }
5927
+ finally {
5928
+ guard.remove();
5929
+ }
3853
5930
  }
3854
5931
  }
3855
5932
 
@@ -3860,6 +5937,7 @@ class RetryExecutor {
3860
5937
  const ERROR$1 = {
3861
5938
  BAD_FUNCTION_CALL: "Bad Function Call",
3862
5939
  };
5940
+ const MAX_CONSECUTIVE_TOOL_ERRORS = 6;
3863
5941
  //
3864
5942
  //
3865
5943
  // Helpers
@@ -3900,37 +5978,54 @@ class OperateLoop {
3900
5978
  * Execute the operate loop for multi-turn conversations with tool calling.
3901
5979
  */
3902
5980
  async execute(input, options = {}) {
5981
+ const log = getLogger$6();
3903
5982
  // Log what was passed to operate
3904
- log$1.trace("[operate] Starting operate loop");
3905
- log$1.var({ "operate.input": input });
3906
- log$1.var({ "operate.options": options });
5983
+ log.trace("[operate] Starting operate loop");
5984
+ log.var({ "operate.input": input });
5985
+ log.var({ "operate.options": options });
3907
5986
  // Initialize state
3908
5987
  const state = await this.initializeState(input, options);
3909
5988
  const context = this.createContext(options);
3910
- // Build initial request
3911
- let request = this.buildInitialRequest(state, options);
3912
- // Multi-turn loop
3913
- while (state.currentTurn < state.maxTurns) {
3914
- state.currentTurn++;
3915
- // Execute one turn with retry logic
3916
- const shouldContinue = await this.executeOneTurn(request, state, context, options);
3917
- if (!shouldContinue) {
3918
- break;
5989
+ const modelName = options.model ?? this.adapter.defaultModel;
5990
+ // Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
5991
+ // Child llm/tool spans nest under it via the SDK's active-span context.
5992
+ return withLlmObsSpan({
5993
+ kind: state.toolkit ? "agent" : "llm",
5994
+ modelName,
5995
+ modelProvider: this.adapter.name,
5996
+ name: "jaypie.llm.operate",
5997
+ }, async () => {
5998
+ // Build initial request
5999
+ let request = this.buildInitialRequest(state, options);
6000
+ // Multi-turn loop
6001
+ while (state.currentTurn < state.maxTurns) {
6002
+ state.currentTurn++;
6003
+ // Execute one turn with retry logic
6004
+ const shouldContinue = await this.executeOneTurn(request, state, context, options);
6005
+ if (!shouldContinue) {
6006
+ break;
6007
+ }
6008
+ // Rebuild request with updated history for next turn
6009
+ request = {
6010
+ format: state.formattedFormat,
6011
+ instructions: options.instructions,
6012
+ messages: state.currentInput,
6013
+ model: modelName,
6014
+ providerOptions: options.providerOptions,
6015
+ system: options.system,
6016
+ temperature: options.temperature,
6017
+ tools: state.formattedTools,
6018
+ user: options.user,
6019
+ };
3919
6020
  }
3920
- // Rebuild request with updated history for next turn
3921
- request = {
3922
- format: state.formattedFormat,
3923
- instructions: options.instructions,
3924
- messages: state.currentInput,
3925
- model: options.model ?? this.adapter.defaultModel,
3926
- providerOptions: options.providerOptions,
3927
- system: options.system,
3928
- temperature: options.temperature,
3929
- tools: state.formattedTools,
3930
- user: options.user,
3931
- };
3932
- }
3933
- return state.responseBuilder.build();
6021
+ const response = state.responseBuilder.build();
6022
+ annotateLlmObs({
6023
+ inputData: input,
6024
+ metrics: usageToLlmObsMetrics(response.usage),
6025
+ outputData: response.content,
6026
+ });
6027
+ return response;
6028
+ });
3934
6029
  }
3935
6030
  //
3936
6031
  // Private Methods
@@ -3982,6 +6077,7 @@ class OperateLoop {
3982
6077
  }
3983
6078
  }
3984
6079
  return {
6080
+ consecutiveToolErrors: 0,
3985
6081
  currentInput: processedInput.history,
3986
6082
  currentTurn: 0,
3987
6083
  formattedFormat,
@@ -4011,6 +6107,7 @@ class OperateLoop {
4011
6107
  };
4012
6108
  }
4013
6109
  async executeOneTurn(request, state, context, options) {
6110
+ const log = getLogger$6();
4014
6111
  // Create error classifier from adapter
4015
6112
  const errorClassifier = createErrorClassifier(this.adapter);
4016
6113
  // Create retry executor for this turn
@@ -4022,28 +6119,42 @@ class OperateLoop {
4022
6119
  // Build provider-specific request
4023
6120
  const providerRequest = this.adapter.buildRequest(request);
4024
6121
  // Log what was passed to the model
4025
- log$1.trace("[operate] Calling model");
4026
- log$1.var({ "operate.request": providerRequest });
6122
+ log.trace("[operate] Calling model");
6123
+ log.var({ "operate.request": providerRequest });
4027
6124
  // Execute beforeEachModelRequest hook
4028
6125
  await this.hookRunnerInstance.runBeforeModelRequest(context.hooks, {
4029
6126
  input: state.currentInput,
4030
6127
  options,
4031
6128
  providerRequest,
4032
6129
  });
4033
- // Execute with retry (RetryExecutor handles error hooks and throws appropriate errors)
4034
- const response = await retryExecutor.execute(() => this.adapter.executeRequest(this.client, providerRequest), {
4035
- context: {
4036
- input: state.currentInput,
4037
- options,
4038
- providerRequest,
4039
- },
4040
- hooks: context.hooks,
6130
+ // Execute with retry inside a child llm span (no-op when llmobs disabled).
6131
+ // RetryExecutor handles error hooks and throws appropriate errors.
6132
+ const { parsed, response } = await withLlmObsSpan({
6133
+ kind: "llm",
6134
+ modelName: options.model ?? this.adapter.defaultModel,
6135
+ modelProvider: this.adapter.name,
6136
+ name: "jaypie.llm.model",
6137
+ }, async () => {
6138
+ const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
6139
+ context: {
6140
+ input: state.currentInput,
6141
+ options,
6142
+ providerRequest,
6143
+ },
6144
+ hooks: context.hooks,
6145
+ });
6146
+ // Log what was returned from the model
6147
+ log.trace("[operate] Model response received");
6148
+ log.var({ "operate.response": response });
6149
+ // Parse response
6150
+ const parsed = this.adapter.parseResponse(response, options);
6151
+ annotateLlmObs({
6152
+ inputData: state.currentInput,
6153
+ metrics: usageToLlmObsMetrics(parsed.usage ? [parsed.usage] : undefined),
6154
+ outputData: parsed.content ?? "",
6155
+ });
6156
+ return { parsed, response };
4041
6157
  });
4042
- // Log what was returned from the model
4043
- log$1.trace("[operate] Model response received");
4044
- log$1.var({ "operate.response": response });
4045
- // Parse response
4046
- const parsed = this.adapter.parseResponse(response, options);
4047
6158
  // Track usage
4048
6159
  if (parsed.usage) {
4049
6160
  state.responseBuilder.addUsage(parsed.usage);
@@ -4064,7 +6175,7 @@ class OperateLoop {
4064
6175
  if (this.adapter.hasStructuredOutput(response)) {
4065
6176
  const structuredOutput = this.adapter.extractStructuredOutput(response);
4066
6177
  if (structuredOutput) {
4067
- state.responseBuilder.setContent(structuredOutput);
6178
+ state.responseBuilder.setContent(this.applyFormatArrayDefaults(structuredOutput, options));
4068
6179
  state.responseBuilder.complete();
4069
6180
  return false; // Stop loop
4070
6181
  }
@@ -4088,11 +6199,19 @@ class OperateLoop {
4088
6199
  args: toolCall.arguments,
4089
6200
  toolName: toolCall.name,
4090
6201
  });
4091
- // Call the tool
4092
- log$1.trace(`[operate] Calling tool - ${toolCall.name}`);
4093
- const result = await state.toolkit.call({
4094
- arguments: toolCall.arguments,
4095
- name: toolCall.name,
6202
+ // Call the tool inside a child tool span (no-op when disabled)
6203
+ log.trace(`[operate] Calling tool - ${toolCall.name}`);
6204
+ const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
6205
+ const result = await state.toolkit.call({
6206
+ arguments: toolCall.arguments,
6207
+ name: toolCall.name,
6208
+ });
6209
+ annotateLlmObs({
6210
+ inputData: toolCall.arguments,
6211
+ metadata: { tool: toolCall.name },
6212
+ outputData: result,
6213
+ });
6214
+ return result;
4096
6215
  });
4097
6216
  // Execute afterEachTool hook
4098
6217
  await this.hookRunnerInstance.runAfterTool(context.hooks, {
@@ -4106,6 +6225,8 @@ class OperateLoop {
4106
6225
  output: JSON.stringify(result),
4107
6226
  success: true,
4108
6227
  };
6228
+ // Reset consecutive error counter on success
6229
+ state.consecutiveToolErrors = 0;
4109
6230
  // Update provider request with tool result
4110
6231
  currentProviderRequest = this.adapter.appendToolResult(currentProviderRequest, toolCall, formattedResult);
4111
6232
  // Sync state from updated request
@@ -4132,15 +6253,41 @@ class OperateLoop {
4132
6253
  status: jaypieError.status,
4133
6254
  title: ERROR$1.BAD_FUNCTION_CALL,
4134
6255
  });
4135
- log$1.error(`Error executing function call ${toolCall.name}`);
4136
- log$1.var({ error });
6256
+ // Add error tool_result to history so the tool_use block is not orphaned.
6257
+ // Without this, the next turn's request would have a tool_use without a
6258
+ // matching tool_result, causing Anthropic API to reject with 400.
6259
+ const errorResult = {
6260
+ callId: toolCall.callId,
6261
+ output: JSON.stringify({
6262
+ error: error.message || "Tool execution failed",
6263
+ }),
6264
+ success: false,
6265
+ error: error.message,
6266
+ };
6267
+ const toolResultFormatted = this.adapter.formatToolResult(toolCall, errorResult);
6268
+ state.responseBuilder.appendToHistory(toolResultFormatted);
6269
+ log.warn(`Error executing function call ${toolCall.name}`);
6270
+ log.var({ error });
6271
+ // Track consecutive errors and stop if threshold reached
6272
+ state.consecutiveToolErrors++;
6273
+ if (state.consecutiveToolErrors >= MAX_CONSECUTIVE_TOOL_ERRORS) {
6274
+ const detail = `Stopped after ${MAX_CONSECUTIVE_TOOL_ERRORS} consecutive tool errors`;
6275
+ log.warn(detail);
6276
+ state.responseBuilder.setError({
6277
+ detail,
6278
+ status: 502,
6279
+ title: ERROR$1.BAD_FUNCTION_CALL,
6280
+ });
6281
+ state.responseBuilder.incomplete();
6282
+ return false; // Stop loop
6283
+ }
4137
6284
  }
4138
6285
  }
4139
6286
  // Check if we've reached max turns
4140
6287
  if (state.currentTurn >= state.maxTurns) {
4141
6288
  const error = new TooManyRequestsError();
4142
6289
  const detail = `Model requested function call but exceeded ${state.maxTurns} turns`;
4143
- log$1.warn(detail);
6290
+ log.warn(detail);
4144
6291
  state.responseBuilder.setError({
4145
6292
  detail,
4146
6293
  status: error.status,
@@ -4153,7 +6300,7 @@ class OperateLoop {
4153
6300
  }
4154
6301
  }
4155
6302
  // No tool calls or no toolkit - we're done
4156
- state.responseBuilder.setContent(parsed.content);
6303
+ state.responseBuilder.setContent(this.applyFormatArrayDefaults(parsed.content, options));
4157
6304
  state.responseBuilder.complete();
4158
6305
  // Add final history items
4159
6306
  const historyItems = this.adapter.responseToHistoryItems(parsed.raw);
@@ -4162,6 +6309,20 @@ class OperateLoop {
4162
6309
  }
4163
6310
  return false; // Stop loop
4164
6311
  }
6312
+ /**
6313
+ * Backfill declared array fields when a `format` is supplied. A declared
6314
+ * `format` is a schema contract: an empty array field should surface as `[]`
6315
+ * rather than be dropped by a provider/model that omits empty lists.
6316
+ */
6317
+ applyFormatArrayDefaults(content, options) {
6318
+ if (!options.format) {
6319
+ return content;
6320
+ }
6321
+ if (typeof content !== "object" || content === null) {
6322
+ return content;
6323
+ }
6324
+ return fillFormatArrays({ content, format: options.format });
6325
+ }
4165
6326
  /**
4166
6327
  * Sync the current input state from the updated provider request.
4167
6328
  * This is necessary because appendToolResult modifies the provider-specific request,
@@ -4287,12 +6448,14 @@ class StreamLoop {
4287
6448
  this.client = config.client;
4288
6449
  this.hookRunnerInstance = config.hookRunner ?? hookRunner;
4289
6450
  this.inputProcessorInstance = config.inputProcessor ?? inputProcessor;
6451
+ this.retryPolicy = config.retryPolicy ?? defaultRetryPolicy;
4290
6452
  }
4291
6453
  /**
4292
6454
  * Execute the streaming loop for multi-turn conversations with tool calling.
4293
6455
  * Yields stream chunks as they become available.
4294
6456
  */
4295
6457
  async *execute(input, options = {}) {
6458
+ const log = getLogger$6();
4296
6459
  // Verify adapter supports streaming
4297
6460
  if (!this.adapter.executeStreamRequest) {
4298
6461
  throw new BadGatewayError(`Provider ${this.adapter.name} does not support streaming`);
@@ -4312,12 +6475,12 @@ class StreamLoop {
4312
6475
  }
4313
6476
  // If we have tool calls, process them
4314
6477
  if (toolCalls && toolCalls.length > 0 && state.toolkit) {
4315
- yield* this.processToolCalls(toolCalls, state, context, options);
6478
+ yield* this.processToolCalls(toolCalls, state, context);
4316
6479
  // Check if we've reached max turns
4317
6480
  if (state.currentTurn >= state.maxTurns) {
4318
6481
  const error = new TooManyRequestsError();
4319
6482
  const detail = `Model requested function call but exceeded ${state.maxTurns} turns`;
4320
- log$1.warn(detail);
6483
+ log.warn(detail);
4321
6484
  yield {
4322
6485
  type: LlmStreamChunkType.Error,
4323
6486
  error: {
@@ -4380,6 +6543,7 @@ class StreamLoop {
4380
6543
  ? this.adapter.formatTools(toolkit, formattedFormat)
4381
6544
  : undefined;
4382
6545
  return {
6546
+ consecutiveToolErrors: 0,
4383
6547
  currentInput: processedInput.history,
4384
6548
  currentTurn: 0,
4385
6549
  formattedFormat,
@@ -4409,6 +6573,7 @@ class StreamLoop {
4409
6573
  };
4410
6574
  }
4411
6575
  async *executeOneStreamingTurn(request, state, context, options) {
6576
+ const log = getLogger$6();
4412
6577
  // Build provider-specific request
4413
6578
  const providerRequest = this.adapter.buildRequest(request);
4414
6579
  // Execute beforeEachModelRequest hook
@@ -4417,34 +6582,117 @@ class StreamLoop {
4417
6582
  options,
4418
6583
  providerRequest,
4419
6584
  });
6585
+ // Open a manual llm span held open across the streamed turn. Flat (not
6586
+ // nested) because it spans yield boundaries; no-op when llmobs disabled.
6587
+ const llmSpan = openLlmObsSpan({
6588
+ kind: "llm",
6589
+ modelName: options.model ?? this.adapter.defaultModel,
6590
+ modelProvider: this.adapter.name,
6591
+ name: "jaypie.llm.model",
6592
+ });
6593
+ const inputSnapshot = [...state.currentInput];
6594
+ let streamedText = "";
4420
6595
  // Collect tool calls from the stream
4421
6596
  const collectedToolCalls = [];
4422
- // Execute streaming request
4423
- const streamGenerator = this.adapter.executeStreamRequest(this.client, providerRequest);
4424
- for await (const chunk of streamGenerator) {
4425
- // Pass through text chunks
4426
- if (chunk.type === LlmStreamChunkType.Text) {
4427
- yield chunk;
4428
- }
4429
- // Collect tool calls
4430
- if (chunk.type === LlmStreamChunkType.ToolCall) {
4431
- collectedToolCalls.push({
4432
- callId: chunk.toolCall.id,
4433
- name: chunk.toolCall.name,
4434
- arguments: chunk.toolCall.arguments,
4435
- raw: chunk.toolCall,
4436
- });
4437
- yield chunk;
4438
- }
4439
- // Track usage from done chunk (but don't yield it yet - we'll emit our own)
4440
- if (chunk.type === LlmStreamChunkType.Done && chunk.usage) {
4441
- state.usageItems.push(...chunk.usage);
4442
- }
4443
- // Pass through error chunks
4444
- if (chunk.type === LlmStreamChunkType.Error) {
4445
- yield chunk;
6597
+ // Retry loop for connection-level failures
6598
+ let attempt = 0;
6599
+ let chunksYielded = false;
6600
+ // Guard against stale rejections firing after the stream loop has already
6601
+ // caught the originating error: undici socket teardown and twin
6602
+ // upstream-SDK rejections (issue #336).
6603
+ const guard = createStaleRejectionGuard();
6604
+ try {
6605
+ while (true) {
6606
+ const controller = new AbortController();
6607
+ try {
6608
+ // Execute streaming request
6609
+ const streamGenerator = this.adapter.executeStreamRequest(this.client, providerRequest, controller.signal);
6610
+ for await (const chunk of streamGenerator) {
6611
+ // Pass through text chunks
6612
+ if (chunk.type === LlmStreamChunkType.Text) {
6613
+ chunksYielded = true;
6614
+ streamedText += chunk.content ?? "";
6615
+ yield chunk;
6616
+ }
6617
+ // Collect tool calls
6618
+ if (chunk.type === LlmStreamChunkType.ToolCall) {
6619
+ chunksYielded = true;
6620
+ collectedToolCalls.push({
6621
+ callId: chunk.toolCall.id,
6622
+ name: chunk.toolCall.name,
6623
+ arguments: chunk.toolCall.arguments,
6624
+ raw: chunk.toolCall,
6625
+ });
6626
+ yield chunk;
6627
+ }
6628
+ // Track usage from done chunk (but don't yield it yet - we'll emit our own)
6629
+ if (chunk.type === LlmStreamChunkType.Done && chunk.usage) {
6630
+ state.usageItems.push(...chunk.usage);
6631
+ }
6632
+ // Pass through error chunks
6633
+ if (chunk.type === LlmStreamChunkType.Error) {
6634
+ chunksYielded = true;
6635
+ yield chunk;
6636
+ }
6637
+ }
6638
+ // Stream completed successfully
6639
+ if (attempt > 0) {
6640
+ log.debug(`Stream request succeeded after ${attempt} retries`);
6641
+ }
6642
+ break;
6643
+ }
6644
+ catch (error) {
6645
+ controller.abort("retry");
6646
+ guard.recordCaught(error);
6647
+ guard.install();
6648
+ // If chunks were already yielded, we can't transparently retry
6649
+ if (chunksYielded) {
6650
+ const errorMessage = error instanceof Error ? error.message : String(error);
6651
+ log.error("Stream failed after partial data was delivered");
6652
+ log.var({ error });
6653
+ yield {
6654
+ type: LlmStreamChunkType.Error,
6655
+ error: {
6656
+ detail: errorMessage,
6657
+ status: 502,
6658
+ title: "Stream Error",
6659
+ },
6660
+ };
6661
+ llmSpan?.annotate({
6662
+ inputData: inputSnapshot,
6663
+ metrics: usageToLlmObsMetrics(state.usageItems),
6664
+ outputData: streamedText,
6665
+ });
6666
+ llmSpan?.finish();
6667
+ return { shouldContinue: false };
6668
+ }
6669
+ // Check if we've exhausted retries or error is not retryable
6670
+ if (!this.retryPolicy.shouldRetry(attempt) ||
6671
+ !this.adapter.isRetryableError(error)) {
6672
+ log.error(`Stream request failed after ${this.retryPolicy.maxRetries} retries`);
6673
+ log.var({ error });
6674
+ const errorMessage = error instanceof Error ? error.message : String(error);
6675
+ llmSpan?.finish();
6676
+ throw new BadGatewayError(errorMessage);
6677
+ }
6678
+ const delay = this.retryPolicy.getDelayForAttempt(attempt);
6679
+ log.warn(`Stream request failed. Retrying in ${delay}ms...`);
6680
+ log.var({ error });
6681
+ await sleep(delay);
6682
+ attempt++;
6683
+ }
4446
6684
  }
4447
6685
  }
6686
+ finally {
6687
+ guard.remove();
6688
+ }
6689
+ // Annotate and finish the streamed llm span (no-op when disabled)
6690
+ llmSpan?.annotate({
6691
+ inputData: inputSnapshot,
6692
+ metrics: usageToLlmObsMetrics(state.usageItems),
6693
+ outputData: streamedText,
6694
+ });
6695
+ llmSpan?.finish();
4448
6696
  // Execute afterEachModelResponse hook
4449
6697
  await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
4450
6698
  content: "",
@@ -4458,19 +6706,29 @@ class StreamLoop {
4458
6706
  if (collectedToolCalls.length > 0 && state.toolkit && state.maxTurns > 1) {
4459
6707
  // Add tool calls to history
4460
6708
  for (const toolCall of collectedToolCalls) {
4461
- state.currentInput.push({
6709
+ // Extract provider-specific metadata from the stream chunk
6710
+ const metadata = toolCall.raw?.metadata;
6711
+ const historyItem = {
4462
6712
  type: LlmMessageType.FunctionCall,
4463
6713
  name: toolCall.name,
4464
6714
  arguments: toolCall.arguments,
4465
6715
  call_id: toolCall.callId,
4466
- id: toolCall.callId,
4467
- });
6716
+ // Use provider item ID if available (e.g., OpenAI fc_... prefix),
6717
+ // otherwise fall back to callId
6718
+ id: metadata?.itemId || toolCall.callId,
6719
+ };
6720
+ // Preserve provider-specific fields (e.g., Gemini thoughtSignature)
6721
+ if (metadata?.thoughtSignature) {
6722
+ historyItem.thoughtSignature = metadata.thoughtSignature;
6723
+ }
6724
+ state.currentInput.push(historyItem);
4468
6725
  }
4469
6726
  return { shouldContinue: true, toolCalls: collectedToolCalls };
4470
6727
  }
4471
6728
  return { shouldContinue: false };
4472
6729
  }
4473
- async *processToolCalls(toolCalls, state, context, _options) {
6730
+ async *processToolCalls(toolCalls, state, context) {
6731
+ const log = getLogger$6();
4474
6732
  for (const toolCall of toolCalls) {
4475
6733
  try {
4476
6734
  // Execute beforeEachTool hook
@@ -4478,11 +6736,19 @@ class StreamLoop {
4478
6736
  args: toolCall.arguments,
4479
6737
  toolName: toolCall.name,
4480
6738
  });
4481
- // Call the tool
4482
- log$1.trace(`[stream] Calling tool - ${toolCall.name}`);
4483
- const result = await state.toolkit.call({
4484
- arguments: toolCall.arguments,
4485
- name: toolCall.name,
6739
+ // Call the tool inside a child tool span (no-op when disabled)
6740
+ log.trace(`[stream] Calling tool - ${toolCall.name}`);
6741
+ const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
6742
+ const result = await state.toolkit.call({
6743
+ arguments: toolCall.arguments,
6744
+ name: toolCall.name,
6745
+ });
6746
+ annotateLlmObs({
6747
+ inputData: toolCall.arguments,
6748
+ metadata: { tool: toolCall.name },
6749
+ outputData: result,
6750
+ });
6751
+ return result;
4486
6752
  });
4487
6753
  // Execute afterEachTool hook
4488
6754
  await this.hookRunnerInstance.runAfterTool(context.hooks, {
@@ -4490,6 +6756,8 @@ class StreamLoop {
4490
6756
  result,
4491
6757
  toolName: toolCall.name,
4492
6758
  });
6759
+ // Reset consecutive error counter on success
6760
+ state.consecutiveToolErrors = 0;
4493
6761
  // Yield tool result chunk
4494
6762
  yield {
4495
6763
  type: LlmStreamChunkType.ToolResult,
@@ -4528,8 +6796,35 @@ class StreamLoop {
4528
6796
  title: ERROR.BAD_FUNCTION_CALL,
4529
6797
  },
4530
6798
  };
4531
- log$1.error(`Error executing function call ${toolCall.name}`);
4532
- log$1.var({ error });
6799
+ // Add error tool_result to history so the tool_use block is not orphaned.
6800
+ // Without this, the next turn's request would have a tool_use without a
6801
+ // matching tool_result, causing Anthropic API to reject with 400.
6802
+ const errorOutput = JSON.stringify({
6803
+ error: error.message || "Tool execution failed",
6804
+ });
6805
+ state.currentInput.push({
6806
+ type: LlmMessageType.FunctionCallOutput,
6807
+ output: errorOutput,
6808
+ call_id: toolCall.callId,
6809
+ name: toolCall.name,
6810
+ });
6811
+ log.warn(`Error executing function call ${toolCall.name}`);
6812
+ log.var({ error });
6813
+ // Track consecutive errors and stop if threshold reached
6814
+ state.consecutiveToolErrors++;
6815
+ if (state.consecutiveToolErrors >= MAX_CONSECUTIVE_TOOL_ERRORS) {
6816
+ const stopDetail = `Stopped after ${MAX_CONSECUTIVE_TOOL_ERRORS} consecutive tool errors`;
6817
+ log.warn(stopDetail);
6818
+ yield {
6819
+ type: LlmStreamChunkType.Error,
6820
+ error: {
6821
+ detail: stopDetail,
6822
+ status: 502,
6823
+ title: ERROR.BAD_FUNCTION_CALL,
6824
+ },
6825
+ };
6826
+ return; // Stop processing tools
6827
+ }
4533
6828
  }
4534
6829
  }
4535
6830
  }
@@ -4587,28 +6882,28 @@ function createStreamLoop(config) {
4587
6882
  }
4588
6883
 
4589
6884
  // SDK loader with caching
4590
- let cachedSdk$2 = null;
4591
- async function loadSdk$2() {
4592
- if (cachedSdk$2)
4593
- return cachedSdk$2;
6885
+ let cachedSdk$3 = null;
6886
+ async function loadSdk$3() {
6887
+ if (cachedSdk$3)
6888
+ return cachedSdk$3;
4594
6889
  try {
4595
- cachedSdk$2 = await import('@anthropic-ai/sdk');
4596
- return cachedSdk$2;
6890
+ cachedSdk$3 = await import('@anthropic-ai/sdk');
6891
+ return cachedSdk$3;
4597
6892
  }
4598
6893
  catch {
4599
6894
  throw new ConfigurationError("@anthropic-ai/sdk is required but not installed. Run: npm install @anthropic-ai/sdk");
4600
6895
  }
4601
6896
  }
4602
6897
  // Logger
4603
- const getLogger$3 = () => log$2.lib({ lib: JAYPIE.LIB.LLM });
6898
+ const getLogger$5 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
4604
6899
  // Client initialization
4605
- async function initializeClient$3({ apiKey, } = {}) {
4606
- const logger = getLogger$3();
6900
+ async function initializeClient$5({ apiKey, } = {}) {
6901
+ const logger = getLogger$5();
4607
6902
  const resolvedApiKey = apiKey || (await getEnvSecret("ANTHROPIC_API_KEY"));
4608
6903
  if (!resolvedApiKey) {
4609
6904
  throw new ConfigurationError("The application could not resolve the required API key: ANTHROPIC_API_KEY");
4610
6905
  }
4611
- const sdk = await loadSdk$2();
6906
+ const sdk = await loadSdk$3();
4612
6907
  const client = new sdk.default({ apiKey: resolvedApiKey });
4613
6908
  logger.trace("Initialized Anthropic client");
4614
6909
  return client;
@@ -4629,7 +6924,7 @@ function formatUserMessage$3(message, { data, placeholders: placeholders$1 } = {
4629
6924
  };
4630
6925
  }
4631
6926
  function prepareMessages$3(message, { data, placeholders } = {}) {
4632
- const logger = getLogger$3();
6927
+ const logger = getLogger$5();
4633
6928
  const messages = [];
4634
6929
  // Add user message (necessary for all requests)
4635
6930
  const userMessage = formatUserMessage$3(message, { data, placeholders });
@@ -4639,7 +6934,7 @@ function prepareMessages$3(message, { data, placeholders } = {}) {
4639
6934
  }
4640
6935
  // Basic text completion
4641
6936
  async function createTextCompletion$1(client, messages, model, systemMessage) {
4642
- log$2.trace("Using text output (unstructured)");
6937
+ log$1.trace("Using text output (unstructured)");
4643
6938
  const params = {
4644
6939
  model,
4645
6940
  messages,
@@ -4648,63 +6943,58 @@ async function createTextCompletion$1(client, messages, model, systemMessage) {
4648
6943
  // Add system instruction if provided
4649
6944
  if (systemMessage) {
4650
6945
  params.system = systemMessage;
4651
- log$2.trace(`System message: ${systemMessage.length} characters`);
6946
+ log$1.trace(`System message: ${systemMessage.length} characters`);
4652
6947
  }
4653
6948
  const response = await client.messages.create(params);
4654
6949
  const firstContent = response.content[0];
4655
6950
  const text = firstContent && "text" in firstContent ? firstContent.text : "";
4656
- log$2.trace(`Assistant reply: ${text.length} characters`);
6951
+ log$1.trace(`Assistant reply: ${text.length} characters`);
4657
6952
  return text;
4658
6953
  }
4659
- // Structured output completion
6954
+ // Structured output completion using Anthropic's native `output_config.format`
6955
+ // field. Returns a JsonObject parsed and validated against the caller's
6956
+ // Zod schema.
4660
6957
  async function createStructuredCompletion$1(client, messages, model, responseSchema, systemMessage) {
4661
- log$2.trace("Using structured output");
4662
- // Get the JSON schema for the response
6958
+ log$1.trace("Using native structured output");
4663
6959
  const schema = responseSchema instanceof z.ZodType
4664
6960
  ? responseSchema
4665
6961
  : naturalZodSchema(responseSchema);
4666
- // Set system message with JSON instructions
4667
- const defaultSystemPrompt = "You will be responding with structured JSON data. " +
4668
- "Format your entire response as a valid JSON object with the following structure: " +
4669
- JSON.stringify(z.toJSONSchema(schema));
4670
- const systemPrompt = systemMessage || defaultSystemPrompt;
4671
- try {
4672
- // Use standard Anthropic API to get response
4673
- const params = {
4674
- model,
4675
- messages,
4676
- max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
4677
- system: systemPrompt,
4678
- };
4679
- const response = await client.messages.create(params);
4680
- // Extract text from response
4681
- const firstContent = response.content[0];
4682
- const responseText = firstContent && "text" in firstContent ? firstContent.text : "";
4683
- // Find JSON in response
4684
- const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/) ||
4685
- responseText.match(/\{[\s\S]*\}/);
4686
- if (jsonMatch) {
4687
- try {
4688
- // Parse the JSON response
4689
- const jsonStr = jsonMatch[1] || jsonMatch[0];
4690
- const result = JSON.parse(jsonStr);
4691
- if (!schema.parse(result)) {
4692
- throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
4693
- }
4694
- log$2.trace("Received structured response", { result });
4695
- return result;
4696
- }
4697
- catch {
4698
- throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
4699
- }
4700
- }
4701
- // If we can't extract JSON
6962
+ const jsonSchema = z.toJSONSchema(schema);
6963
+ const params = {
6964
+ model,
6965
+ messages,
6966
+ max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
6967
+ output_config: {
6968
+ format: { type: "json_schema", schema: jsonSchema },
6969
+ },
6970
+ };
6971
+ if (systemMessage) {
6972
+ params.system = systemMessage;
6973
+ }
6974
+ const response = (await client.messages.create(params));
6975
+ if (response.stop_reason === "refusal") {
6976
+ throw new Error("Anthropic refused the structured-output request (stop_reason=refusal)");
6977
+ }
6978
+ if (response.stop_reason === "max_tokens") {
6979
+ throw new Error("Anthropic structured-output response was truncated (stop_reason=max_tokens); increase max_tokens");
6980
+ }
6981
+ const textBlock = response.content.find((block) => block.type === "text");
6982
+ if (!textBlock) {
4702
6983
  throw new Error("Failed to parse structured response from Anthropic");
4703
6984
  }
4704
- catch (error) {
4705
- log$2.error("Error creating structured completion", { error });
4706
- throw error;
6985
+ let parsed;
6986
+ try {
6987
+ parsed = JSON.parse(textBlock.text);
6988
+ }
6989
+ catch {
6990
+ throw new Error("Failed to parse structured response from Anthropic: " + textBlock.text);
6991
+ }
6992
+ const validation = schema.safeParse(parsed);
6993
+ if (!validation.success) {
6994
+ throw new Error(`JSON response from Anthropic does not match schema: ${textBlock.text}`);
4707
6995
  }
6996
+ log$1.trace("Received structured response", { result: validation.data });
6997
+ return validation.data;
4708
6998
  }
4709
6999
 
4710
7000
  // Maps Jaypie roles to Anthropic roles
@@ -4718,7 +7008,7 @@ async function createStructuredCompletion$1(client, messages, model, responseSch
4718
7008
  // Main class implementation
4719
7009
  class AnthropicProvider {
4720
7010
  constructor(model = PROVIDER.ANTHROPIC.MODEL.DEFAULT, { apiKey } = {}) {
4721
- this.log = getLogger$3();
7011
+ this.log = getLogger$5();
4722
7012
  this.conversationHistory = [];
4723
7013
  this.model = model;
4724
7014
  this.apiKey = apiKey;
@@ -4727,7 +7017,7 @@ class AnthropicProvider {
4727
7017
  if (this._client) {
4728
7018
  return this._client;
4729
7019
  }
4730
- this._client = await initializeClient$3({ apiKey: this.apiKey });
7020
+ this._client = await initializeClient$5({ apiKey: this.apiKey });
4731
7021
  return this._client;
4732
7022
  }
4733
7023
  async getOperateLoop() {
@@ -4802,6 +7092,87 @@ class AnthropicProvider {
4802
7092
  }
4803
7093
  }
4804
7094
 
7095
+ let cachedSdk$2 = null;
7096
+ async function loadSdk$2() {
7097
+ if (cachedSdk$2)
7098
+ return cachedSdk$2;
7099
+ try {
7100
+ cachedSdk$2 = await import('@aws-sdk/client-bedrock-runtime');
7101
+ return cachedSdk$2;
7102
+ }
7103
+ catch {
7104
+ throw new ConfigurationError("@aws-sdk/client-bedrock-runtime is required but not installed. Run: npm install @aws-sdk/client-bedrock-runtime");
7105
+ }
7106
+ }
7107
+ const getLogger$4 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
7108
+ async function initializeClient$4({ region, } = {}) {
7109
+ const logger = getLogger$4();
7110
+ const resolvedRegion = region || process.env.AWS_REGION || "us-east-1";
7111
+ const sdk = await loadSdk$2();
7112
+ const client = new sdk.BedrockRuntimeClient({ region: resolvedRegion });
7113
+ logger.trace("Initialized Bedrock client");
7114
+ return client;
7115
+ }
7116
+
7117
+ class BedrockProvider {
7118
+ constructor(model = PROVIDER.BEDROCK.MODEL.DEFAULT, { region } = {}) {
7119
+ this.log = getLogger$4();
7120
+ this.conversationHistory = [];
7121
+ this.model = model;
7122
+ this.region = region;
7123
+ }
7124
+ async getClient() {
7125
+ if (this._client)
7126
+ return this._client;
7127
+ this._client = await initializeClient$4({ region: this.region });
7128
+ return this._client;
7129
+ }
7130
+ async getOperateLoop() {
7131
+ if (this._operateLoop)
7132
+ return this._operateLoop;
7133
+ const client = await this.getClient();
7134
+ this._operateLoop = createOperateLoop({ adapter: bedrockAdapter, client });
7135
+ return this._operateLoop;
7136
+ }
7137
+ async getStreamLoop() {
7138
+ if (this._streamLoop)
7139
+ return this._streamLoop;
7140
+ const client = await this.getClient();
7141
+ this._streamLoop = createStreamLoop({ adapter: bedrockAdapter, client });
7142
+ return this._streamLoop;
7143
+ }
7144
+ async send(message, options) {
7145
+ const operateLoop = await this.getOperateLoop();
7146
+ const mergedOptions = { ...options, model: options?.model ?? this.model };
7147
+ const response = await operateLoop.execute(message, mergedOptions);
7148
+ return response.content ?? "";
7149
+ }
7150
+ async operate(input, options = {}) {
7151
+ const operateLoop = await this.getOperateLoop();
7152
+ const mergedOptions = { ...options, model: options.model ?? this.model };
7153
+ if (this.conversationHistory.length > 0) {
7154
+ mergedOptions.history = options.history
7155
+ ? [...this.conversationHistory, ...options.history]
7156
+ : [...this.conversationHistory];
7157
+ }
7158
+ const response = await operateLoop.execute(input, mergedOptions);
7159
+ if (response.history && response.history.length > 0) {
7160
+ this.conversationHistory = response.history;
7161
+ }
7162
+ return response;
7163
+ }
7164
+ async *stream(input, options = {}) {
7165
+ const streamLoop = await this.getStreamLoop();
7166
+ const mergedOptions = { ...options, model: options.model ?? this.model };
7167
+ if (this.conversationHistory.length > 0) {
7168
+ mergedOptions.history = options.history
7169
+ ? [...this.conversationHistory, ...options.history]
7170
+ : [...this.conversationHistory];
7171
+ }
7172
+ yield* streamLoop.execute(input, mergedOptions);
7173
+ }
7174
+ }
7175
+
4805
7176
  // SDK loader with caching
4806
7177
  let cachedSdk$1 = null;
4807
7178
  async function loadSdk$1() {
@@ -4816,10 +7187,10 @@ async function loadSdk$1() {
4816
7187
  }
4817
7188
  }
4818
7189
  // Logger
4819
- const getLogger$2 = () => log$2.lib({ lib: JAYPIE.LIB.LLM });
7190
+ const getLogger$3 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
4820
7191
  // Client initialization
4821
- async function initializeClient$2({ apiKey, } = {}) {
4822
- const logger = getLogger$2();
7192
+ async function initializeClient$3({ apiKey, } = {}) {
7193
+ const logger = getLogger$3();
4823
7194
  const resolvedApiKey = apiKey || (await getEnvSecret("GEMINI_API_KEY"));
4824
7195
  if (!resolvedApiKey) {
4825
7196
  throw new ConfigurationError("The application could not resolve the requested keys");
@@ -4839,21 +7210,20 @@ function formatUserMessage$2(message, { data, placeholders: placeholders$1 } = {
4839
7210
  };
4840
7211
  }
4841
7212
  function prepareMessages$2(message, { data, placeholders } = {}) {
4842
- const logger = getLogger$2();
7213
+ const logger = getLogger$3();
4843
7214
  const messages = [];
4844
- let systemInstruction;
4845
7215
  // Note: Gemini handles system prompts differently via systemInstruction config
4846
7216
  // This function is kept for compatibility but system prompts should be passed
4847
7217
  // via the systemInstruction parameter in generateContent
4848
7218
  const userMessage = formatUserMessage$2(message, { data, placeholders });
4849
7219
  messages.push(userMessage);
4850
7220
  logger.trace(`User message: ${userMessage.content?.length} characters`);
4851
- return { messages, systemInstruction };
7221
+ return { messages, systemInstruction: undefined };
4852
7222
  }
4853
7223
 
4854
- class GeminiProvider {
4855
- constructor(model = PROVIDER.GEMINI.MODEL.DEFAULT, { apiKey } = {}) {
4856
- this.log = getLogger$2();
7224
+ class GoogleProvider {
7225
+ constructor(model = PROVIDER.GOOGLE.MODEL.DEFAULT, { apiKey } = {}) {
7226
+ this.log = getLogger$3();
4857
7227
  this.conversationHistory = [];
4858
7228
  this.model = model;
4859
7229
  this.apiKey = apiKey;
@@ -4862,7 +7232,7 @@ class GeminiProvider {
4862
7232
  if (this._client) {
4863
7233
  return this._client;
4864
7234
  }
4865
- this._client = await initializeClient$2({ apiKey: this.apiKey });
7235
+ this._client = await initializeClient$3({ apiKey: this.apiKey });
4866
7236
  return this._client;
4867
7237
  }
4868
7238
  async getOperateLoop() {
@@ -4871,7 +7241,7 @@ class GeminiProvider {
4871
7241
  }
4872
7242
  const client = await this.getClient();
4873
7243
  this._operateLoop = createOperateLoop({
4874
- adapter: geminiAdapter,
7244
+ adapter: googleAdapter,
4875
7245
  client,
4876
7246
  });
4877
7247
  return this._operateLoop;
@@ -4882,7 +7252,7 @@ class GeminiProvider {
4882
7252
  }
4883
7253
  const client = await this.getClient();
4884
7254
  this._streamLoop = createStreamLoop({
4885
- adapter: geminiAdapter,
7255
+ adapter: googleAdapter,
4886
7256
  client,
4887
7257
  });
4888
7258
  return this._streamLoop;
@@ -4918,6 +7288,17 @@ class GeminiProvider {
4918
7288
  return JSON.parse(text);
4919
7289
  }
4920
7290
  catch {
7291
+ // Strip markdown code fences and retry (Gemini sometimes wraps JSON in fences)
7292
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/) ||
7293
+ text.match(/\{[\s\S]*\}/);
7294
+ if (jsonMatch) {
7295
+ try {
7296
+ return JSON.parse(jsonMatch[1] || jsonMatch[0]);
7297
+ }
7298
+ catch {
7299
+ // Fall through to return original text
7300
+ }
7301
+ }
4921
7302
  return text || "";
4922
7303
  }
4923
7304
  }
@@ -4956,10 +7337,10 @@ class GeminiProvider {
4956
7337
  }
4957
7338
 
4958
7339
  // Logger
4959
- const getLogger$1 = () => log$2.lib({ lib: JAYPIE.LIB.LLM });
7340
+ const getLogger$2 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
4960
7341
  // Client initialization
4961
- async function initializeClient$1({ apiKey, } = {}) {
4962
- const logger = getLogger$1();
7342
+ async function initializeClient$2({ apiKey, } = {}) {
7343
+ const logger = getLogger$2();
4963
7344
  const resolvedApiKey = apiKey || (await getEnvSecret("OPENAI_API_KEY"));
4964
7345
  if (!resolvedApiKey) {
4965
7346
  throw new ConfigurationError("The application could not resolve the requested keys");
@@ -4987,7 +7368,7 @@ function formatUserMessage$1(message, { data, placeholders: placeholders$1 } = {
4987
7368
  };
4988
7369
  }
4989
7370
  function prepareMessages$1(message, { system, data, placeholders } = {}) {
4990
- const logger = getLogger$1();
7371
+ const logger = getLogger$2();
4991
7372
  const messages = [];
4992
7373
  if (system) {
4993
7374
  const systemMessage = formatSystemMessage$1(system, { data, placeholders });
@@ -5001,7 +7382,7 @@ function prepareMessages$1(message, { system, data, placeholders } = {}) {
5001
7382
  }
5002
7383
  // Completion requests
5003
7384
  async function createStructuredCompletion(client, { messages, responseSchema, model, }) {
5004
- const logger = getLogger$1();
7385
+ const logger = getLogger$2();
5005
7386
  logger.trace("Using structured output");
5006
7387
  const zodSchema = responseSchema instanceof z.ZodType
5007
7388
  ? responseSchema
@@ -5032,7 +7413,7 @@ async function createStructuredCompletion(client, { messages, responseSchema, mo
5032
7413
  return completion.choices[0].message.parsed;
5033
7414
  }
5034
7415
  async function createTextCompletion(client, { messages, model, }) {
5035
- const logger = getLogger$1();
7416
+ const logger = getLogger$2();
5036
7417
  logger.trace("Using text output (unstructured)");
5037
7418
  const completion = await client.chat.completions.create({
5038
7419
  messages,
@@ -5044,7 +7425,7 @@ async function createTextCompletion(client, { messages, model, }) {
5044
7425
 
5045
7426
  class OpenAiProvider {
5046
7427
  constructor(model = PROVIDER.OPENAI.MODEL.DEFAULT, { apiKey } = {}) {
5047
- this.log = getLogger$1();
7428
+ this.log = getLogger$2();
5048
7429
  this.conversationHistory = [];
5049
7430
  this.model = model;
5050
7431
  this.apiKey = apiKey;
@@ -5053,7 +7434,7 @@ class OpenAiProvider {
5053
7434
  if (this._client) {
5054
7435
  return this._client;
5055
7436
  }
5056
- this._client = await initializeClient$1({ apiKey: this.apiKey });
7437
+ this._client = await initializeClient$2({ apiKey: this.apiKey });
5057
7438
  return this._client;
5058
7439
  }
5059
7440
  async getOperateLoop() {
@@ -5140,10 +7521,10 @@ async function loadSdk() {
5140
7521
  }
5141
7522
  }
5142
7523
  // Logger
5143
- const getLogger = () => log$2.lib({ lib: JAYPIE.LIB.LLM });
7524
+ const getLogger$1 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
5144
7525
  // Client initialization
5145
- async function initializeClient({ apiKey, } = {}) {
5146
- const logger = getLogger();
7526
+ async function initializeClient$1({ apiKey, } = {}) {
7527
+ const logger = getLogger$1();
5147
7528
  const resolvedApiKey = apiKey || (await getEnvSecret("OPENROUTER_API_KEY"));
5148
7529
  if (!resolvedApiKey) {
5149
7530
  throw new ConfigurationError("The application could not resolve the requested keys");
@@ -5176,7 +7557,7 @@ function formatUserMessage(message, { data, placeholders: placeholders$1 } = {})
5176
7557
  };
5177
7558
  }
5178
7559
  function prepareMessages(message, { system, data, placeholders } = {}) {
5179
- const logger = getLogger();
7560
+ const logger = getLogger$1();
5180
7561
  const messages = [];
5181
7562
  if (system) {
5182
7563
  const systemMessage = formatSystemMessage(system, { data, placeholders });
@@ -5191,7 +7572,7 @@ function prepareMessages(message, { system, data, placeholders } = {}) {
5191
7572
 
5192
7573
  class OpenRouterProvider {
5193
7574
  constructor(model = getDefaultModel(), { apiKey } = {}) {
5194
- this.log = getLogger();
7575
+ this.log = getLogger$1();
5195
7576
  this.conversationHistory = [];
5196
7577
  this.model = model;
5197
7578
  this.apiKey = apiKey;
@@ -5200,7 +7581,7 @@ class OpenRouterProvider {
5200
7581
  if (this._client) {
5201
7582
  return this._client;
5202
7583
  }
5203
- this._client = await initializeClient({ apiKey: this.apiKey });
7584
+ this._client = await initializeClient$1({ apiKey: this.apiKey });
5204
7585
  return this._client;
5205
7586
  }
5206
7587
  async getOperateLoop() {
@@ -5288,11 +7669,116 @@ class OpenRouterProvider {
5288
7669
  }
5289
7670
  }
5290
7671
 
7672
+ // Logger
7673
+ const getLogger = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
7674
+ // Client initialization
7675
+ async function initializeClient({ apiKey, } = {}) {
7676
+ const logger = getLogger();
7677
+ const resolvedApiKey = apiKey || (await getEnvSecret(PROVIDER.XAI.API_KEY));
7678
+ if (!resolvedApiKey) {
7679
+ throw new ConfigurationError("The application could not resolve the requested keys");
7680
+ }
7681
+ const client = new OpenAI({
7682
+ apiKey: resolvedApiKey,
7683
+ baseURL: PROVIDER.XAI.BASE_URL,
7684
+ });
7685
+ logger.trace("Initialized xAI client");
7686
+ return client;
7687
+ }
7688
+
7689
+ class XaiProvider {
7690
+ constructor(model = PROVIDER.XAI.MODEL.DEFAULT, { apiKey } = {}) {
7691
+ this.log = getLogger$2();
7692
+ this.conversationHistory = [];
7693
+ this.model = model;
7694
+ this.apiKey = apiKey;
7695
+ }
7696
+ async getClient() {
7697
+ if (this._client) {
7698
+ return this._client;
7699
+ }
7700
+ this._client = await initializeClient({ apiKey: this.apiKey });
7701
+ return this._client;
7702
+ }
7703
+ async getOperateLoop() {
7704
+ if (this._operateLoop) {
7705
+ return this._operateLoop;
7706
+ }
7707
+ const client = await this.getClient();
7708
+ this._operateLoop = createOperateLoop({
7709
+ adapter: xaiAdapter,
7710
+ client,
7711
+ });
7712
+ return this._operateLoop;
7713
+ }
7714
+ async getStreamLoop() {
7715
+ if (this._streamLoop) {
7716
+ return this._streamLoop;
7717
+ }
7718
+ const client = await this.getClient();
7719
+ this._streamLoop = createStreamLoop({
7720
+ adapter: xaiAdapter,
7721
+ client,
7722
+ });
7723
+ return this._streamLoop;
7724
+ }
7725
+ async send(message, options) {
7726
+ const client = await this.getClient();
7727
+ const messages = prepareMessages$1(message, options || {});
7728
+ const modelToUse = options?.model || this.model;
7729
+ if (options?.response) {
7730
+ return createStructuredCompletion(client, {
7731
+ messages,
7732
+ responseSchema: options.response,
7733
+ model: modelToUse,
7734
+ });
7735
+ }
7736
+ return createTextCompletion(client, {
7737
+ messages,
7738
+ model: modelToUse,
7739
+ });
7740
+ }
7741
+ async operate(input, options = {}) {
7742
+ const operateLoop = await this.getOperateLoop();
7743
+ const mergedOptions = { ...options, model: options.model ?? this.model };
7744
+ // Create a merged history including both the tracked history and any explicitly provided history
7745
+ if (this.conversationHistory.length > 0) {
7746
+ // If options.history exists, merge with instance history, otherwise use instance history
7747
+ mergedOptions.history = options.history
7748
+ ? [...this.conversationHistory, ...options.history]
7749
+ : [...this.conversationHistory];
7750
+ }
7751
+ // Execute operate loop
7752
+ const response = await operateLoop.execute(input, mergedOptions);
7753
+ // Update conversation history with the new history from the response
7754
+ if (response.history && response.history.length > 0) {
7755
+ this.conversationHistory = response.history;
7756
+ }
7757
+ return response;
7758
+ }
7759
+ async *stream(input, options = {}) {
7760
+ const streamLoop = await this.getStreamLoop();
7761
+ const mergedOptions = { ...options, model: options.model ?? this.model };
7762
+ // Create a merged history including both the tracked history and any explicitly provided history
7763
+ if (this.conversationHistory.length > 0) {
7764
+ mergedOptions.history = options.history
7765
+ ? [...this.conversationHistory, ...options.history]
7766
+ : [...this.conversationHistory];
7767
+ }
7768
+ // Execute stream loop
7769
+ yield* streamLoop.execute(input, mergedOptions);
7770
+ }
7771
+ }
7772
+
5291
7773
  class Llm {
5292
7774
  constructor(providerName = DEFAULT.PROVIDER.NAME, options = {}) {
5293
7775
  const { fallback, model } = options;
5294
7776
  let finalProvider = providerName;
5295
7777
  let finalModel = model;
7778
+ // Legacy: accept "gemini" but warn
7779
+ if (providerName === "gemini") {
7780
+ log$2.warn(`Provider "gemini" is deprecated, use "${PROVIDER.GOOGLE.NAME}" instead`);
7781
+ }
5296
7782
  if (model) {
5297
7783
  const modelDetermined = determineModelProvider(model);
5298
7784
  finalModel = modelDetermined.model;
@@ -5307,6 +7793,10 @@ class Llm {
5307
7793
  throw new ConfigurationError(`Unable to determine provider from: ${providerName}`);
5308
7794
  }
5309
7795
  finalProvider = providerDetermined.provider;
7796
+ // When providerName is actually a model name, extract the model (#213)
7797
+ if (!finalModel && providerName !== providerDetermined.provider) {
7798
+ finalModel = providerDetermined.model;
7799
+ }
5310
7800
  }
5311
7801
  // Handle conflicts: if both providerName and model specify different providers
5312
7802
  if (model && providerName !== DEFAULT.PROVIDER.NAME) {
@@ -5329,8 +7819,10 @@ class Llm {
5329
7819
  switch (providerName) {
5330
7820
  case PROVIDER.ANTHROPIC.NAME:
5331
7821
  return new AnthropicProvider(model || PROVIDER.ANTHROPIC.MODEL.DEFAULT, { apiKey });
5332
- case PROVIDER.GEMINI.NAME:
5333
- return new GeminiProvider(model || PROVIDER.GEMINI.MODEL.DEFAULT, {
7822
+ case PROVIDER.BEDROCK.NAME:
7823
+ return new BedrockProvider(model || PROVIDER.BEDROCK.MODEL.DEFAULT);
7824
+ case PROVIDER.GOOGLE.NAME:
7825
+ return new GoogleProvider(model || PROVIDER.GOOGLE.MODEL.DEFAULT, {
5334
7826
  apiKey,
5335
7827
  });
5336
7828
  case PROVIDER.OPENAI.NAME:
@@ -5341,6 +7833,10 @@ class Llm {
5341
7833
  return new OpenRouterProvider(model || PROVIDER.OPENROUTER.MODEL.DEFAULT, {
5342
7834
  apiKey,
5343
7835
  });
7836
+ case PROVIDER.XAI.NAME:
7837
+ return new XaiProvider(model || PROVIDER.XAI.MODEL.DEFAULT, {
7838
+ apiKey,
7839
+ });
5344
7840
  default:
5345
7841
  throw new ConfigurationError(`Unsupported provider: ${providerName}`);
5346
7842
  }
@@ -5395,7 +7891,7 @@ class Llm {
5395
7891
  }
5396
7892
  catch (error) {
5397
7893
  lastError = error;
5398
- log$3.warn(`Provider ${this._provider} failed`, {
7894
+ log$2.warn(`Provider ${this._provider} failed`, {
5399
7895
  error: lastError.message,
5400
7896
  fallbacksRemaining: fallbackChain.length,
5401
7897
  });
@@ -5415,7 +7911,7 @@ class Llm {
5415
7911
  }
5416
7912
  catch (error) {
5417
7913
  lastError = error;
5418
- log$3.warn(`Fallback provider ${fallbackConfig.provider} failed`, {
7914
+ log$2.warn(`Fallback provider ${fallbackConfig.provider} failed`, {
5419
7915
  error: lastError.message,
5420
7916
  fallbacksRemaining: fallbackChain.length - attempts + 1,
5421
7917
  });
@@ -5584,16 +8080,17 @@ const roll = {
5584
8080
  },
5585
8081
  type: "function",
5586
8082
  call: ({ number = 1, sides = 6 } = {}) => {
8083
+ const log = getLogger$6();
5587
8084
  const rng = random$1();
5588
8085
  const rolls = [];
5589
8086
  let total = 0;
5590
8087
  const parsedNumber = tryParseNumber(number, {
5591
8088
  defaultValue: 1,
5592
- warnFunction: log$1.warn,
8089
+ warnFunction: log.warn,
5593
8090
  });
5594
8091
  const parsedSides = tryParseNumber(sides, {
5595
8092
  defaultValue: 6,
5596
- warnFunction: log$1.warn,
8093
+ warnFunction: log.warn,
5597
8094
  });
5598
8095
  for (let i = 0; i < parsedNumber; i++) {
5599
8096
  const rollValue = rng({ min: 1, max: parsedSides, integer: true });
@@ -5746,8 +8243,10 @@ const weather = {
5746
8243
  }
5747
8244
  catch (error) {
5748
8245
  if (error instanceof Error) {
8246
+ // eslint-disable-next-line preserve-caught-error -- package targets ES2020; Error `cause` option requires ES2022
5749
8247
  throw new Error(`Weather API error: ${error.message}`);
5750
8248
  }
8249
+ // eslint-disable-next-line preserve-caught-error -- package targets ES2020; Error `cause` option requires ES2022
5751
8250
  throw new Error("Unknown error occurred while fetching weather data");
5752
8251
  }
5753
8252
  },
@@ -5779,5 +8278,5 @@ const toolkit = new JaypieToolkit(tools);
5779
8278
  [LlmMessageRole.Developer]: "user",
5780
8279
  });
5781
8280
 
5782
- export { GeminiProvider, JaypieToolkit, constants as LLM, Llm, LlmMessageRole, LlmMessageType, LlmStreamChunkType, OpenRouterProvider, Toolkit, extractReasoning, isLlmOperateInput, isLlmOperateInputContent, isLlmOperateInputFile, isLlmOperateInputImage, toolkit, tools };
8281
+ export { BedrockProvider, GoogleProvider as GeminiProvider, GoogleProvider, JaypieToolkit, constants as LLM, Llm, LlmMessageRole, LlmMessageType, LlmStreamChunkType, OpenRouterProvider, Toolkit, XaiProvider, extractReasoning, isLlmOperateInput, isLlmOperateInputContent, isLlmOperateInputFile, isLlmOperateInputImage, toolkit, tools };
5783
8282
  //# sourceMappingURL=index.js.map