@jaypie/llm 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/cjs/constants.d.ts +82 -30
- package/dist/cjs/index.cjs +3081 -578
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +5 -1
- package/dist/cjs/observability/llmobs.d.ts +72 -0
- package/dist/cjs/operate/OperateLoop.d.ts +7 -0
- package/dist/cjs/operate/StreamLoop.d.ts +3 -0
- package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +36 -5
- package/dist/cjs/operate/adapters/BedrockAdapter.d.ts +78 -0
- package/dist/cjs/operate/adapters/{GeminiAdapter.d.ts → GoogleAdapter.d.ts} +19 -9
- package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +13 -3
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +55 -8
- package/dist/cjs/operate/adapters/ProviderAdapter.interface.d.ts +5 -3
- package/dist/cjs/operate/adapters/XaiAdapter.d.ts +14 -0
- package/dist/cjs/operate/adapters/index.d.ts +3 -1
- package/dist/cjs/operate/index.d.ts +1 -1
- package/dist/cjs/operate/retry/RetryExecutor.d.ts +6 -3
- package/dist/cjs/operate/retry/createStaleRejectionGuard.d.ts +19 -0
- package/dist/cjs/operate/retry/index.d.ts +1 -0
- package/dist/cjs/operate/retry/isTransientNetworkError.d.ts +18 -0
- package/dist/cjs/operate/types.d.ts +2 -0
- package/dist/cjs/providers/anthropic/utils.d.ts +1 -1
- package/dist/cjs/providers/bedrock/BedrockProvider.class.d.ts +21 -0
- package/dist/cjs/providers/bedrock/index.d.ts +1 -0
- package/dist/cjs/providers/bedrock/utils.d.ts +6 -0
- package/dist/cjs/providers/{gemini/GeminiProvider.class.d.ts → google/GoogleProvider.class.d.ts} +1 -1
- package/dist/cjs/providers/google/index.d.ts +3 -0
- package/dist/{esm/providers/gemini → cjs/providers/google}/types.d.ts +1 -0
- package/dist/cjs/providers/{gemini → google}/utils.d.ts +1 -1
- package/dist/cjs/providers/openai/utils.d.ts +1 -1
- package/dist/cjs/providers/openrouter/utils.d.ts +1 -1
- package/dist/{esm/providers/gemini/GeminiProvider.class.d.ts → cjs/providers/xai/XaiProvider.class.d.ts} +1 -1
- package/dist/cjs/providers/xai/index.d.ts +1 -0
- package/dist/cjs/providers/xai/utils.d.ts +5 -0
- package/dist/cjs/types/LlmProvider.interface.d.ts +1 -1
- package/dist/cjs/types/LlmStreamChunk.interface.d.ts +2 -0
- package/dist/cjs/util/fillFormatArrays.d.ts +17 -0
- package/dist/cjs/util/index.d.ts +2 -0
- package/dist/cjs/util/jsonSchemaToOpenApi3.d.ts +10 -0
- package/dist/cjs/util/logger.d.ts +1 -2
- package/dist/cjs/util/maxTurnsFromOptions.d.ts +1 -1
- package/dist/esm/constants.d.ts +82 -30
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.js +3077 -578
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/llmobs.d.ts +72 -0
- package/dist/esm/operate/OperateLoop.d.ts +7 -0
- package/dist/esm/operate/StreamLoop.d.ts +3 -0
- package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +36 -5
- package/dist/esm/operate/adapters/BedrockAdapter.d.ts +78 -0
- package/dist/esm/operate/adapters/{GeminiAdapter.d.ts → GoogleAdapter.d.ts} +19 -9
- package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +13 -3
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +55 -8
- package/dist/esm/operate/adapters/ProviderAdapter.interface.d.ts +5 -3
- package/dist/esm/operate/adapters/XaiAdapter.d.ts +14 -0
- package/dist/esm/operate/adapters/index.d.ts +3 -1
- package/dist/esm/operate/index.d.ts +1 -1
- package/dist/esm/operate/retry/RetryExecutor.d.ts +6 -3
- package/dist/esm/operate/retry/createStaleRejectionGuard.d.ts +19 -0
- package/dist/esm/operate/retry/index.d.ts +1 -0
- package/dist/esm/operate/retry/isTransientNetworkError.d.ts +18 -0
- package/dist/esm/operate/types.d.ts +2 -0
- package/dist/esm/providers/anthropic/utils.d.ts +1 -1
- package/dist/esm/providers/bedrock/BedrockProvider.class.d.ts +21 -0
- package/dist/esm/providers/bedrock/index.d.ts +1 -0
- package/dist/esm/providers/bedrock/utils.d.ts +6 -0
- package/dist/esm/providers/google/GoogleProvider.class.d.ts +21 -0
- package/dist/esm/providers/google/index.d.ts +3 -0
- package/dist/{cjs/providers/gemini → esm/providers/google}/types.d.ts +1 -0
- package/dist/esm/providers/{gemini → google}/utils.d.ts +1 -1
- package/dist/esm/providers/openai/utils.d.ts +1 -1
- package/dist/esm/providers/openrouter/utils.d.ts +1 -1
- package/dist/esm/providers/xai/XaiProvider.class.d.ts +21 -0
- package/dist/esm/providers/xai/index.d.ts +1 -0
- package/dist/esm/providers/xai/utils.d.ts +5 -0
- package/dist/esm/types/LlmProvider.interface.d.ts +1 -1
- package/dist/esm/types/LlmStreamChunk.interface.d.ts +2 -0
- package/dist/esm/util/fillFormatArrays.d.ts +17 -0
- package/dist/esm/util/index.d.ts +2 -0
- package/dist/esm/util/jsonSchemaToOpenApi3.d.ts +10 -0
- package/dist/esm/util/logger.d.ts +1 -2
- package/dist/esm/util/maxTurnsFromOptions.d.ts +1 -1
- package/package.json +8 -1
- package/dist/cjs/providers/gemini/index.d.ts +0 -3
- 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$
|
|
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-
|
|
17
|
-
LARGE: "claude-opus-4-
|
|
18
|
-
SMALL: "claude-sonnet-4-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
29
|
-
LARGE: "gpt-5.
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
190
|
+
PROVIDER.GOOGLE.MODEL.DEFAULT,
|
|
135
191
|
PROVIDER.OPENAI.MODEL.DEFAULT,
|
|
192
|
+
PROVIDER.XAI.MODEL.DEFAULT,
|
|
136
193
|
],
|
|
137
194
|
COMBINED: [
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
278
|
+
if (input === PROVIDER.GOOGLE.NAME || input === "gemini") {
|
|
198
279
|
return {
|
|
199
|
-
model: PROVIDER.
|
|
200
|
-
provider: PROVIDER.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
509
|
-
|
|
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 =
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
*
|
|
1082
|
+
* Check a single error (without walking the cause chain)
|
|
826
1083
|
*/
|
|
827
|
-
function
|
|
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
|
-
|
|
975
|
-
anthropicRequest.
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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,
|
|
1000
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1150
|
-
usage: this.extractUsage(
|
|
1151
|
-
|
|
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
|
|
2391
|
+
const bedrockResponse = response;
|
|
2392
|
+
const content = bedrockResponse.output?.message?.content ?? [];
|
|
1156
2393
|
const toolCalls = [];
|
|
1157
|
-
for (const block of
|
|
1158
|
-
|
|
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:
|
|
1161
|
-
name:
|
|
1162
|
-
arguments: JSON.stringify(
|
|
1163
|
-
raw:
|
|
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
|
|
1171
|
-
|
|
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
|
|
1176
|
-
output: usage
|
|
1177
|
-
reasoning:
|
|
1178
|
-
total: usage
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
2426
|
+
toolResult: {
|
|
2427
|
+
toolUseId: toolCall.callId,
|
|
2428
|
+
content: [{ text: result.output }],
|
|
2429
|
+
},
|
|
1191
2430
|
};
|
|
1192
2431
|
}
|
|
1193
2432
|
appendToolResult(request, toolCall, result) {
|
|
1194
|
-
const
|
|
2433
|
+
const bedrockRequest = request;
|
|
1195
2434
|
const toolCallRaw = toolCall.raw;
|
|
1196
|
-
|
|
1197
|
-
anthropicRequest.messages.push({
|
|
2435
|
+
bedrockRequest.messages.push({
|
|
1198
2436
|
role: "assistant",
|
|
1199
2437
|
content: [toolCallRaw],
|
|
1200
2438
|
});
|
|
1201
|
-
|
|
1202
|
-
anthropicRequest.messages.push({
|
|
2439
|
+
bedrockRequest.messages.push({
|
|
1203
2440
|
role: "user",
|
|
1204
2441
|
content: [this.formatToolResult(toolCall, result)],
|
|
1205
2442
|
});
|
|
1206
|
-
return
|
|
2443
|
+
return bedrockRequest;
|
|
1207
2444
|
}
|
|
1208
2445
|
//
|
|
1209
2446
|
// History Management
|
|
1210
2447
|
//
|
|
1211
2448
|
responseToHistoryItems(response) {
|
|
1212
|
-
const
|
|
2449
|
+
const bedrockResponse = response;
|
|
1213
2450
|
const historyItems = [];
|
|
1214
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
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:
|
|
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
|
-
|
|
1245
|
-
if (errorName === "
|
|
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
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
2624
|
+
class GoogleAdapter extends BaseProviderAdapter {
|
|
1348
2625
|
constructor() {
|
|
1349
2626
|
super(...arguments);
|
|
1350
|
-
this.name = PROVIDER.
|
|
1351
|
-
this.defaultModel = PROVIDER.
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
//
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
//
|
|
1403
|
-
|
|
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,
|
|
1432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
1622
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
2134
|
-
|
|
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
|
-
*
|
|
2427
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
3994
|
+
log$1.warn({ item }, "Unknown content type for OpenRouter; discarded");
|
|
2452
3995
|
}
|
|
2453
|
-
// If no
|
|
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
|
-
|
|
2499
|
-
openRouterRequest.
|
|
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,
|
|
2522
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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$
|
|
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
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
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
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
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
|
-
|
|
3834
|
-
|
|
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
|
|
3905
|
-
log
|
|
3906
|
-
log
|
|
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
|
-
|
|
3911
|
-
|
|
3912
|
-
//
|
|
3913
|
-
|
|
3914
|
-
state.
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
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
|
|
4026
|
-
log
|
|
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
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
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
|
|
4093
|
-
const result = await
|
|
4094
|
-
|
|
4095
|
-
|
|
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
|
-
|
|
4136
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
4483
|
-
const result = await
|
|
4484
|
-
|
|
4485
|
-
|
|
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
|
-
|
|
4532
|
-
|
|
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$
|
|
4591
|
-
async function loadSdk$
|
|
4592
|
-
if (cachedSdk$
|
|
4593
|
-
return cachedSdk$
|
|
6885
|
+
let cachedSdk$3 = null;
|
|
6886
|
+
async function loadSdk$3() {
|
|
6887
|
+
if (cachedSdk$3)
|
|
6888
|
+
return cachedSdk$3;
|
|
4594
6889
|
try {
|
|
4595
|
-
cachedSdk$
|
|
4596
|
-
return cachedSdk$
|
|
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$
|
|
6898
|
+
const getLogger$5 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
4604
6899
|
// Client initialization
|
|
4605
|
-
async function initializeClient$
|
|
4606
|
-
const logger = getLogger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
4667
|
-
const
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
7190
|
+
const getLogger$3 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
4820
7191
|
// Client initialization
|
|
4821
|
-
async function initializeClient$
|
|
4822
|
-
const logger = getLogger$
|
|
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$
|
|
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
|
|
4855
|
-
constructor(model = PROVIDER.
|
|
4856
|
-
this.log = getLogger$
|
|
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$
|
|
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:
|
|
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:
|
|
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$
|
|
7340
|
+
const getLogger$2 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
4960
7341
|
// Client initialization
|
|
4961
|
-
async function initializeClient$
|
|
4962
|
-
const logger = getLogger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
5333
|
-
return new
|
|
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$
|
|
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$
|
|
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
|
|
8089
|
+
warnFunction: log.warn,
|
|
5593
8090
|
});
|
|
5594
8091
|
const parsedSides = tryParseNumber(sides, {
|
|
5595
8092
|
defaultValue: 6,
|
|
5596
|
-
warnFunction: log
|
|
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
|