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