@jaypie/llm 1.1.29 → 1.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/constants.d.ts +38 -1
- package/dist/cjs/index.cjs +2975 -986
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/operate/OperateLoop.d.ts +61 -0
- package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +32 -0
- package/dist/cjs/operate/adapters/GeminiAdapter.d.ts +37 -0
- package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +30 -0
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +96 -0
- package/dist/cjs/operate/adapters/ProviderAdapter.interface.d.ts +177 -0
- package/dist/cjs/operate/adapters/index.d.ts +6 -0
- package/dist/cjs/operate/hooks/HookRunner.d.ts +78 -0
- package/dist/cjs/operate/hooks/index.d.ts +2 -0
- package/dist/cjs/operate/index.d.ts +14 -0
- package/dist/cjs/operate/input/InputProcessor.d.ts +40 -0
- package/dist/cjs/operate/input/index.d.ts +2 -0
- package/dist/cjs/operate/response/ResponseBuilder.d.ts +89 -0
- package/dist/cjs/operate/response/index.d.ts +2 -0
- package/dist/cjs/operate/retry/RetryExecutor.d.ts +39 -0
- package/dist/cjs/operate/retry/RetryPolicy.d.ts +35 -0
- package/dist/cjs/operate/retry/index.d.ts +4 -0
- package/dist/cjs/operate/types.d.ts +136 -0
- package/dist/cjs/providers/anthropic/AnthropicProvider.class.d.ts +2 -0
- package/dist/cjs/providers/gemini/GeminiProvider.class.d.ts +17 -0
- package/dist/cjs/providers/gemini/index.d.ts +3 -0
- package/dist/cjs/providers/gemini/types.d.ts +185 -0
- package/dist/cjs/providers/gemini/utils.d.ts +51 -0
- package/dist/cjs/providers/openai/OpenAiProvider.class.d.ts +2 -0
- package/dist/cjs/providers/openrouter/OpenRouterProvider.class.d.ts +17 -0
- package/dist/cjs/providers/openrouter/index.d.ts +2 -0
- package/dist/cjs/providers/openrouter/utils.d.ts +49 -0
- package/dist/esm/constants.d.ts +38 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2976 -989
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operate/OperateLoop.d.ts +61 -0
- package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +32 -0
- package/dist/esm/operate/adapters/GeminiAdapter.d.ts +37 -0
- package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +30 -0
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +96 -0
- package/dist/esm/operate/adapters/ProviderAdapter.interface.d.ts +177 -0
- package/dist/esm/operate/adapters/index.d.ts +6 -0
- package/dist/esm/operate/hooks/HookRunner.d.ts +78 -0
- package/dist/esm/operate/hooks/index.d.ts +2 -0
- package/dist/esm/operate/index.d.ts +14 -0
- package/dist/esm/operate/input/InputProcessor.d.ts +40 -0
- package/dist/esm/operate/input/index.d.ts +2 -0
- package/dist/esm/operate/response/ResponseBuilder.d.ts +89 -0
- package/dist/esm/operate/response/index.d.ts +2 -0
- package/dist/esm/operate/retry/RetryExecutor.d.ts +39 -0
- package/dist/esm/operate/retry/RetryPolicy.d.ts +35 -0
- package/dist/esm/operate/retry/index.d.ts +4 -0
- package/dist/esm/operate/types.d.ts +136 -0
- package/dist/esm/providers/anthropic/AnthropicProvider.class.d.ts +2 -0
- package/dist/esm/providers/gemini/GeminiProvider.class.d.ts +17 -0
- package/dist/esm/providers/gemini/index.d.ts +3 -0
- package/dist/esm/providers/gemini/types.d.ts +185 -0
- package/dist/esm/providers/gemini/utils.d.ts +51 -0
- package/dist/esm/providers/openai/OpenAiProvider.class.d.ts +2 -0
- package/dist/esm/providers/openrouter/OpenRouterProvider.class.d.ts +17 -0
- package/dist/esm/providers/openrouter/index.d.ts +2 -0
- package/dist/esm/providers/openrouter/utils.d.ts +49 -0
- package/package.json +7 -5
- package/dist/cjs/providers/anthropic/operate.d.ts +0 -16
- package/dist/cjs/providers/openai/operate.d.ts +0 -26
- package/dist/esm/providers/anthropic/operate.d.ts +0 -16
- package/dist/esm/providers/openai/operate.d.ts +0 -26
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,17 +1,64 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var errors = require('@jaypie/errors');
|
|
4
|
+
var Anthropic = require('@anthropic-ai/sdk');
|
|
5
|
+
var v4 = require('zod/v4');
|
|
4
6
|
var core = require('@jaypie/core');
|
|
7
|
+
var RandomLib = require('random');
|
|
5
8
|
var openai = require('openai');
|
|
6
9
|
var zod = require('openai/helpers/zod');
|
|
7
|
-
var v4 = require('zod/v4');
|
|
8
|
-
var RandomLib = require('random');
|
|
9
10
|
var aws = require('@jaypie/aws');
|
|
10
|
-
var
|
|
11
|
-
var
|
|
11
|
+
var genai = require('@google/genai');
|
|
12
|
+
var sdk = require('@openrouter/sdk');
|
|
12
13
|
var openmeteo = require('openmeteo');
|
|
13
14
|
|
|
14
15
|
const PROVIDER = {
|
|
16
|
+
OPENROUTER: {
|
|
17
|
+
// https://openrouter.ai/models
|
|
18
|
+
// OpenRouter provides access to hundreds of models from various providers
|
|
19
|
+
// The model format is: provider/model-name (e.g., "openai/gpt-4", "anthropic/claude-3-opus")
|
|
20
|
+
MODEL: {
|
|
21
|
+
// Default uses env var OPENROUTER_MODEL if set, otherwise a reasonable default
|
|
22
|
+
DEFAULT: "openai/gpt-4o",
|
|
23
|
+
SMALL: "openai/gpt-4o-mini",
|
|
24
|
+
LARGE: "anthropic/claude-3-opus",
|
|
25
|
+
TINY: "openai/gpt-4o-mini",
|
|
26
|
+
},
|
|
27
|
+
MODEL_MATCH_WORDS: ["openrouter"],
|
|
28
|
+
NAME: "openrouter",
|
|
29
|
+
ROLE: {
|
|
30
|
+
ASSISTANT: "assistant",
|
|
31
|
+
SYSTEM: "system",
|
|
32
|
+
TOOL: "tool",
|
|
33
|
+
USER: "user",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
GEMINI: {
|
|
37
|
+
// https://ai.google.dev/gemini-api/docs/models
|
|
38
|
+
MODEL: {
|
|
39
|
+
// Jaypie Aliases
|
|
40
|
+
DEFAULT: "gemini-2.5-flash",
|
|
41
|
+
SMALL: "gemini-2.5-flash",
|
|
42
|
+
LARGE: "gemini-2.5-pro",
|
|
43
|
+
TINY: "gemini-2.0-flash-lite",
|
|
44
|
+
// Gemini 2.5 Models
|
|
45
|
+
GEMINI_2_5_PRO: "gemini-2.5-pro",
|
|
46
|
+
GEMINI_2_5_FLASH: "gemini-2.5-flash",
|
|
47
|
+
// Gemini 2.0 Models
|
|
48
|
+
GEMINI_2_0_FLASH: "gemini-2.0-flash",
|
|
49
|
+
GEMINI_2_0_FLASH_LITE: "gemini-2.0-flash-lite",
|
|
50
|
+
// Gemini 1.5 Models (backward compatibility)
|
|
51
|
+
GEMINI_1_5_PRO: "gemini-1.5-pro",
|
|
52
|
+
GEMINI_1_5_FLASH: "gemini-1.5-flash",
|
|
53
|
+
GEMINI_1_5_FLASH_8B: "gemini-1.5-flash-8b",
|
|
54
|
+
},
|
|
55
|
+
MODEL_MATCH_WORDS: ["gemini", "google"],
|
|
56
|
+
NAME: "gemini",
|
|
57
|
+
ROLE: {
|
|
58
|
+
MODEL: "model",
|
|
59
|
+
USER: "user",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
15
62
|
ANTHROPIC: {
|
|
16
63
|
// https://docs.anthropic.com/en/docs/about-claude/models/overview
|
|
17
64
|
MODEL: {
|
|
@@ -120,12 +167,24 @@ function determineModelProvider(input) {
|
|
|
120
167
|
provider: PROVIDER.ANTHROPIC.NAME,
|
|
121
168
|
};
|
|
122
169
|
}
|
|
170
|
+
if (input === PROVIDER.GEMINI.NAME) {
|
|
171
|
+
return {
|
|
172
|
+
model: PROVIDER.GEMINI.MODEL.DEFAULT,
|
|
173
|
+
provider: PROVIDER.GEMINI.NAME,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
123
176
|
if (input === PROVIDER.OPENAI.NAME) {
|
|
124
177
|
return {
|
|
125
178
|
model: PROVIDER.OPENAI.MODEL.DEFAULT,
|
|
126
179
|
provider: PROVIDER.OPENAI.NAME,
|
|
127
180
|
};
|
|
128
181
|
}
|
|
182
|
+
if (input === PROVIDER.OPENROUTER.NAME) {
|
|
183
|
+
return {
|
|
184
|
+
model: PROVIDER.OPENROUTER.MODEL.DEFAULT,
|
|
185
|
+
provider: PROVIDER.OPENROUTER.NAME,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
129
188
|
// Check if input matches an Anthropic model exactly
|
|
130
189
|
for (const [, modelValue] of Object.entries(PROVIDER.ANTHROPIC.MODEL)) {
|
|
131
190
|
if (input === modelValue) {
|
|
@@ -135,6 +194,15 @@ function determineModelProvider(input) {
|
|
|
135
194
|
};
|
|
136
195
|
}
|
|
137
196
|
}
|
|
197
|
+
// Check if input matches a Gemini model exactly
|
|
198
|
+
for (const [, modelValue] of Object.entries(PROVIDER.GEMINI.MODEL)) {
|
|
199
|
+
if (input === modelValue) {
|
|
200
|
+
return {
|
|
201
|
+
model: input,
|
|
202
|
+
provider: PROVIDER.GEMINI.NAME,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
138
206
|
// Check if input matches an OpenAI model exactly
|
|
139
207
|
for (const [, modelValue] of Object.entries(PROVIDER.OPENAI.MODEL)) {
|
|
140
208
|
if (input === modelValue) {
|
|
@@ -144,6 +212,15 @@ function determineModelProvider(input) {
|
|
|
144
212
|
};
|
|
145
213
|
}
|
|
146
214
|
}
|
|
215
|
+
// Check if input matches an OpenRouter model exactly
|
|
216
|
+
for (const [, modelValue] of Object.entries(PROVIDER.OPENROUTER.MODEL)) {
|
|
217
|
+
if (input === modelValue) {
|
|
218
|
+
return {
|
|
219
|
+
model: input,
|
|
220
|
+
provider: PROVIDER.OPENROUTER.NAME,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
147
224
|
// Check Anthropic match words
|
|
148
225
|
const lowerInput = input.toLowerCase();
|
|
149
226
|
for (const matchWord of PROVIDER.ANTHROPIC.MODEL_MATCH_WORDS) {
|
|
@@ -154,6 +231,15 @@ function determineModelProvider(input) {
|
|
|
154
231
|
};
|
|
155
232
|
}
|
|
156
233
|
}
|
|
234
|
+
// Check Gemini match words
|
|
235
|
+
for (const matchWord of PROVIDER.GEMINI.MODEL_MATCH_WORDS) {
|
|
236
|
+
if (lowerInput.includes(matchWord)) {
|
|
237
|
+
return {
|
|
238
|
+
model: input,
|
|
239
|
+
provider: PROVIDER.GEMINI.NAME,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
157
243
|
// Check OpenAI match words
|
|
158
244
|
for (const matchWord of PROVIDER.OPENAI.MODEL_MATCH_WORDS) {
|
|
159
245
|
if (typeof matchWord === "string") {
|
|
@@ -173,135 +259,57 @@ function determineModelProvider(input) {
|
|
|
173
259
|
}
|
|
174
260
|
}
|
|
175
261
|
}
|
|
262
|
+
// Check OpenRouter match words
|
|
263
|
+
for (const matchWord of PROVIDER.OPENROUTER.MODEL_MATCH_WORDS) {
|
|
264
|
+
if (lowerInput.includes(matchWord)) {
|
|
265
|
+
return {
|
|
266
|
+
model: input,
|
|
267
|
+
provider: PROVIDER.OPENROUTER.NAME,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
176
271
|
// Default fallback if model not recognized
|
|
177
272
|
return {
|
|
178
273
|
model: input,
|
|
179
274
|
};
|
|
180
275
|
}
|
|
181
276
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
277
|
+
//
|
|
278
|
+
//
|
|
279
|
+
// Abstract Base Adapter
|
|
280
|
+
//
|
|
281
|
+
/**
|
|
282
|
+
* BaseProviderAdapter provides default implementations for common adapter methods.
|
|
283
|
+
* Providers can extend this class to reduce boilerplate.
|
|
284
|
+
*/
|
|
285
|
+
class BaseProviderAdapter {
|
|
286
|
+
/**
|
|
287
|
+
* Default implementation checks if error is retryable via classifyError
|
|
288
|
+
*/
|
|
289
|
+
isRetryableError(error) {
|
|
290
|
+
const classified = this.classifyError(error);
|
|
291
|
+
return classified.shouldRetry;
|
|
193
292
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (toolCopy.parameters?.properties) {
|
|
201
|
-
if (!toolCopy.parameters.properties.__Explanation) {
|
|
202
|
-
toolCopy.parameters.properties.__Explanation = {
|
|
203
|
-
type: "string",
|
|
204
|
-
description: `Clearly state why the tool is being called and what larger question it helps answer. For example, "I am checking the location API because the user asked for nearby pizza and I need coordinates to proceed"`,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Set default type if not provided
|
|
210
|
-
if (!toolCopy.type) {
|
|
211
|
-
toolCopy.type = DEFAULT_TOOL_TYPE;
|
|
212
|
-
}
|
|
213
|
-
return toolCopy;
|
|
214
|
-
});
|
|
293
|
+
/**
|
|
294
|
+
* Default implementation checks error category via classifyError
|
|
295
|
+
*/
|
|
296
|
+
isRateLimitError(error) {
|
|
297
|
+
const classified = this.classifyError(error);
|
|
298
|
+
return classified.category === "rate_limit";
|
|
215
299
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
parsedArgs = JSON.parse(args);
|
|
224
|
-
if (this.explain) {
|
|
225
|
-
delete parsedArgs.__Explanation;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
parsedArgs = args;
|
|
230
|
-
}
|
|
231
|
-
if (this.log !== false) {
|
|
232
|
-
try {
|
|
233
|
-
const context = {
|
|
234
|
-
name,
|
|
235
|
-
args: parsedArgs,
|
|
236
|
-
};
|
|
237
|
-
let message;
|
|
238
|
-
if (this.explain) {
|
|
239
|
-
context.explanation = parsedArgs.__Explanation;
|
|
240
|
-
}
|
|
241
|
-
if (tool.message) {
|
|
242
|
-
if (typeof tool.message === "string") {
|
|
243
|
-
log$1.trace("[Toolkit] Tool provided string message");
|
|
244
|
-
message = tool.message;
|
|
245
|
-
}
|
|
246
|
-
else if (typeof tool.message === "function") {
|
|
247
|
-
log$1.trace("[Toolkit] Tool provided function message");
|
|
248
|
-
log$1.trace("[Toolkit] Resolving message result");
|
|
249
|
-
message = await core.resolveValue(tool.message(parsedArgs, { name }));
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
log$1.warn("[Toolkit] Tool provided unknown message type");
|
|
253
|
-
message = String(tool.message);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
log$1.trace("[Toolkit] Log tool call with default message");
|
|
258
|
-
message = `${tool.name}:${JSON.stringify(parsedArgs)}`;
|
|
259
|
-
}
|
|
260
|
-
if (typeof this.log === "function") {
|
|
261
|
-
log$1.trace("[Toolkit] Log tool call with custom logger");
|
|
262
|
-
await core.resolveValue(this.log(message, context));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
log$1.trace("[Toolkit] Log tool call with default logger");
|
|
266
|
-
logToolMessage(message, context);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
log$1.error("[Toolkit] Caught error during logToolCall");
|
|
271
|
-
log$1.var({ error });
|
|
272
|
-
log$1.debug("[Toolkit] Continuing...");
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return await core.resolveValue(tool.call(parsedArgs));
|
|
300
|
+
/**
|
|
301
|
+
* Default implementation returns false - override for providers with native structured output
|
|
302
|
+
*/
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
304
|
+
hasStructuredOutput(_response) {
|
|
305
|
+
return false;
|
|
276
306
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
if (options.warn !== false) {
|
|
285
|
-
if (typeof this.log === "function") {
|
|
286
|
-
this.log(`[Toolkit] Tool '${tool.name}' already exists, replacing with new tool`, { name: tool.name, args: {} });
|
|
287
|
-
}
|
|
288
|
-
else if (this.log) {
|
|
289
|
-
log$1.warn(`[Toolkit] Tool '${tool.name}' already exists, replacing with new tool`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
this._tools[existingIndex] = tool;
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
this._tools.push(tool);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (Object.prototype.hasOwnProperty.call(options, "log")) {
|
|
299
|
-
this.log = options.log;
|
|
300
|
-
}
|
|
301
|
-
if (Object.prototype.hasOwnProperty.call(options, "explain")) {
|
|
302
|
-
this.explain = options.explain;
|
|
303
|
-
}
|
|
304
|
-
return this;
|
|
307
|
+
/**
|
|
308
|
+
* Default implementation returns undefined - override for providers with native structured output
|
|
309
|
+
*/
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
311
|
+
extractStructuredOutput(_response) {
|
|
312
|
+
return undefined;
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
|
|
@@ -379,8 +387,8 @@ function formatOperateInput(input, options) {
|
|
|
379
387
|
return [input];
|
|
380
388
|
}
|
|
381
389
|
|
|
382
|
-
const getLogger$
|
|
383
|
-
const log = getLogger$
|
|
390
|
+
const getLogger$4 = () => core.log.lib({ lib: core.JAYPIE.LIB.LLM });
|
|
391
|
+
const log$1 = getLogger$4();
|
|
384
392
|
|
|
385
393
|
// Turn policy constants
|
|
386
394
|
const MAX_TURNS_ABSOLUTE_LIMIT = 72;
|
|
@@ -660,532 +668,2786 @@ function tryParseNumber(input, options) {
|
|
|
660
668
|
}
|
|
661
669
|
}
|
|
662
670
|
|
|
671
|
+
//
|
|
672
|
+
//
|
|
673
|
+
// Error Classification Types
|
|
674
|
+
//
|
|
675
|
+
/**
|
|
676
|
+
* Categories of errors for retry logic
|
|
677
|
+
*/
|
|
678
|
+
var ErrorCategory;
|
|
679
|
+
(function (ErrorCategory) {
|
|
680
|
+
/** Error is transient and can be retried */
|
|
681
|
+
ErrorCategory["Retryable"] = "retryable";
|
|
682
|
+
/** Error is due to rate limiting */
|
|
683
|
+
ErrorCategory["RateLimit"] = "rate_limit";
|
|
684
|
+
/** Error cannot be recovered from */
|
|
685
|
+
ErrorCategory["Unrecoverable"] = "unrecoverable";
|
|
686
|
+
/** Error type is unknown */
|
|
687
|
+
ErrorCategory["Unknown"] = "unknown";
|
|
688
|
+
})(ErrorCategory || (ErrorCategory = {}));
|
|
689
|
+
|
|
663
690
|
//
|
|
664
691
|
//
|
|
665
692
|
// Constants
|
|
666
693
|
//
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const RETRY_BACKOFF_FACTOR = 2; // Exponential backoff multiplier
|
|
673
|
-
const RETRYABLE_ERRORS = [
|
|
674
|
-
openai.APIConnectionError,
|
|
675
|
-
openai.APIConnectionTimeoutError,
|
|
676
|
-
openai.InternalServerError,
|
|
694
|
+
const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
|
|
695
|
+
const RETRYABLE_ERROR_TYPES$1 = [
|
|
696
|
+
Anthropic.APIConnectionError,
|
|
697
|
+
Anthropic.APIConnectionTimeoutError,
|
|
698
|
+
Anthropic.InternalServerError,
|
|
677
699
|
];
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
openai.NotFoundError,
|
|
684
|
-
openai.PermissionDeniedError,
|
|
685
|
-
openai.RateLimitError,
|
|
686
|
-
openai.UnprocessableEntityError,
|
|
700
|
+
const NOT_RETRYABLE_ERROR_TYPES$1 = [
|
|
701
|
+
Anthropic.AuthenticationError,
|
|
702
|
+
Anthropic.BadRequestError,
|
|
703
|
+
Anthropic.NotFoundError,
|
|
704
|
+
Anthropic.PermissionDeniedError,
|
|
687
705
|
];
|
|
688
|
-
const ERROR = {
|
|
689
|
-
BAD_FUNCTION_CALL: "Bad Function Call",
|
|
690
|
-
};
|
|
691
706
|
//
|
|
692
707
|
//
|
|
693
|
-
//
|
|
708
|
+
// Main
|
|
694
709
|
//
|
|
695
710
|
/**
|
|
696
|
-
*
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
return `${exports.LlmMessageType.FunctionCall}:${output.name}${output.arguments}#${output.call_id}`;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Extracts content from OpenAI response output array
|
|
711
|
+
* AnthropicAdapter implements the ProviderAdapter interface for Anthropic's API.
|
|
712
|
+
* It handles request building, response parsing, and error classification
|
|
713
|
+
* specific to Anthropic's Messages API.
|
|
703
714
|
*/
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
return rawContent;
|
|
715
|
+
class AnthropicAdapter extends BaseProviderAdapter {
|
|
716
|
+
constructor() {
|
|
717
|
+
super(...arguments);
|
|
718
|
+
this.name = PROVIDER.ANTHROPIC.NAME;
|
|
719
|
+
this.defaultModel = PROVIDER.ANTHROPIC.MODEL.DEFAULT;
|
|
720
|
+
}
|
|
721
|
+
//
|
|
722
|
+
// Request Building
|
|
723
|
+
//
|
|
724
|
+
buildRequest(request) {
|
|
725
|
+
// Convert messages to Anthropic format (remove 'type' property)
|
|
726
|
+
const messages = request.messages.map((msg) => {
|
|
727
|
+
const anthropicMsg = structuredClone(msg);
|
|
728
|
+
delete anthropicMsg.type;
|
|
729
|
+
return anthropicMsg;
|
|
730
|
+
});
|
|
731
|
+
// Append instructions to last message if provided
|
|
732
|
+
if (request.instructions && messages.length > 0) {
|
|
733
|
+
const lastMsg = messages[messages.length - 1];
|
|
734
|
+
if (typeof lastMsg.content === "string") {
|
|
735
|
+
lastMsg.content = lastMsg.content + "\n\n" + request.instructions;
|
|
727
736
|
}
|
|
728
737
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Creates the request options for the OpenAI API call
|
|
741
|
-
*
|
|
742
|
-
* @param input - The formatted input messages
|
|
743
|
-
* @param options - The LLM operation options
|
|
744
|
-
* @returns The request options for the OpenAI API
|
|
745
|
-
*/
|
|
746
|
-
function createRequestOptions(input, options = {}) {
|
|
747
|
-
const requestOptions = {
|
|
748
|
-
model: options?.model || PROVIDER.OPENAI.MODEL.DEFAULT,
|
|
749
|
-
input,
|
|
750
|
-
};
|
|
751
|
-
// Add user if provided
|
|
752
|
-
if (options?.user) {
|
|
753
|
-
requestOptions.user = options.user;
|
|
754
|
-
}
|
|
755
|
-
// Add any provider-specific options
|
|
756
|
-
if (options?.providerOptions) {
|
|
757
|
-
Object.assign(requestOptions, options.providerOptions);
|
|
758
|
-
}
|
|
759
|
-
// Handle instructions or system message
|
|
760
|
-
if (options?.instructions) {
|
|
761
|
-
// Apply placeholders to instructions if data is provided and placeholders.instructions is undefined or true
|
|
762
|
-
requestOptions.instructions =
|
|
763
|
-
options.data &&
|
|
764
|
-
(options.placeholders?.instructions === undefined ||
|
|
765
|
-
options.placeholders?.instructions)
|
|
766
|
-
? core.placeholders(options.instructions, options.data)
|
|
767
|
-
: options.instructions;
|
|
768
|
-
}
|
|
769
|
-
// Handle developer message as system message
|
|
770
|
-
// @ts-expect-error Testing invalid option
|
|
771
|
-
if (options?.developer) {
|
|
772
|
-
log.warn("Developer message provided but not supported. Using as system message.");
|
|
773
|
-
// @ts-expect-error Testing invalid option
|
|
774
|
-
requestOptions.system = options.developer;
|
|
775
|
-
}
|
|
776
|
-
// Handle structured output format
|
|
777
|
-
if (options?.format) {
|
|
778
|
-
// Check if format is a JsonObject with type "json_schema"
|
|
779
|
-
if (typeof options.format === "object" &&
|
|
780
|
-
options.format !== null &&
|
|
781
|
-
!Array.isArray(options.format) &&
|
|
782
|
-
options.format.type === "json_schema") {
|
|
783
|
-
// Direct pass-through for JsonObject with type "json_schema"
|
|
784
|
-
requestOptions.text = {
|
|
785
|
-
format: options.format,
|
|
786
|
-
};
|
|
738
|
+
const anthropicRequest = {
|
|
739
|
+
model: (request.model ||
|
|
740
|
+
this.defaultModel),
|
|
741
|
+
messages,
|
|
742
|
+
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
743
|
+
stream: false,
|
|
744
|
+
};
|
|
745
|
+
if (request.system) {
|
|
746
|
+
anthropicRequest.system = request.system;
|
|
787
747
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
:
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
// Temporary hack because of OpenAI requires additional_properties to be false on all objects
|
|
796
|
-
const checks = [jsonSchema];
|
|
797
|
-
while (checks.length > 0) {
|
|
798
|
-
const current = checks[0];
|
|
799
|
-
if (current.type == "object") {
|
|
800
|
-
current.additionalProperties = false;
|
|
801
|
-
}
|
|
802
|
-
Object.keys(current).forEach((key) => {
|
|
803
|
-
if (typeof current[key] == "object" && current[key] !== null) {
|
|
804
|
-
checks.push(current[key]);
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
checks.shift();
|
|
808
|
-
}
|
|
809
|
-
// Set up structured output format in the format expected by the test
|
|
810
|
-
requestOptions.text = {
|
|
811
|
-
format: {
|
|
812
|
-
name: responseFormat.json_schema.name,
|
|
813
|
-
schema: jsonSchema, // Replace this with responseFormat.json_schema.schema once OpenAI supports Zod v4
|
|
814
|
-
strict: responseFormat.json_schema.strict,
|
|
815
|
-
type: responseFormat.type,
|
|
748
|
+
if (request.tools && request.tools.length > 0) {
|
|
749
|
+
anthropicRequest.tools = request.tools.map((tool) => ({
|
|
750
|
+
name: tool.name,
|
|
751
|
+
description: tool.description,
|
|
752
|
+
input_schema: {
|
|
753
|
+
...tool.parameters,
|
|
754
|
+
type: "object",
|
|
816
755
|
},
|
|
756
|
+
type: "custom",
|
|
757
|
+
}));
|
|
758
|
+
// Determine tool choice based on whether structured output is requested
|
|
759
|
+
const hasStructuredOutput = request.tools.some((t) => t.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
|
|
760
|
+
anthropicRequest.tool_choice = {
|
|
761
|
+
type: hasStructuredOutput ? "any" : "auto",
|
|
817
762
|
};
|
|
818
763
|
}
|
|
764
|
+
if (request.providerOptions) {
|
|
765
|
+
Object.assign(anthropicRequest, request.providerOptions);
|
|
766
|
+
}
|
|
767
|
+
return anthropicRequest;
|
|
819
768
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
769
|
+
formatTools(toolkit, outputSchema) {
|
|
770
|
+
const tools = toolkit.tools.map((tool) => ({
|
|
771
|
+
name: tool.name,
|
|
772
|
+
description: tool.description,
|
|
773
|
+
parameters: {
|
|
774
|
+
...tool.parameters,
|
|
775
|
+
type: "object",
|
|
776
|
+
},
|
|
777
|
+
}));
|
|
778
|
+
// Add structured output tool if schema is provided
|
|
779
|
+
if (outputSchema) {
|
|
780
|
+
tools.push({
|
|
781
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$2,
|
|
782
|
+
description: "Output a structured JSON object, " +
|
|
783
|
+
"use this before your final response to give structured outputs to the user",
|
|
784
|
+
parameters: outputSchema,
|
|
785
|
+
});
|
|
826
786
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
787
|
+
return tools;
|
|
788
|
+
}
|
|
789
|
+
formatOutputSchema(schema) {
|
|
790
|
+
let jsonSchema;
|
|
791
|
+
// Check if schema is already a JsonObject with type "json_schema"
|
|
792
|
+
if (typeof schema === "object" &&
|
|
793
|
+
schema !== null &&
|
|
794
|
+
!Array.isArray(schema) &&
|
|
795
|
+
schema.type === "json_schema") {
|
|
796
|
+
jsonSchema = structuredClone(schema);
|
|
797
|
+
jsonSchema.type = "object"; // Validator does not recognize "json_schema"
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
// Convert NaturalSchema to JSON schema through Zod
|
|
801
|
+
const zodSchema = schema instanceof v4.z.ZodType
|
|
802
|
+
? schema
|
|
803
|
+
: naturalZodSchema(schema);
|
|
804
|
+
jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
831
805
|
}
|
|
832
|
-
|
|
833
|
-
|
|
806
|
+
// Remove $schema property (causes issues with validator)
|
|
807
|
+
if (jsonSchema.$schema) {
|
|
808
|
+
delete jsonSchema.$schema;
|
|
834
809
|
}
|
|
810
|
+
return jsonSchema;
|
|
835
811
|
}
|
|
836
|
-
return requestOptions;
|
|
837
|
-
}
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
// Main
|
|
841
|
-
//
|
|
842
|
-
async function operate$1(input, options = {}, context = {
|
|
843
|
-
client: new openai.OpenAI(),
|
|
844
|
-
}) {
|
|
845
812
|
//
|
|
813
|
+
// API Execution
|
|
814
|
+
//
|
|
815
|
+
async executeRequest(client, request) {
|
|
816
|
+
const anthropic = client;
|
|
817
|
+
return (await anthropic.messages.create(request));
|
|
818
|
+
}
|
|
846
819
|
//
|
|
847
|
-
//
|
|
820
|
+
// Response Parsing
|
|
848
821
|
//
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
822
|
+
parseResponse(response, _options) {
|
|
823
|
+
const anthropicResponse = response;
|
|
824
|
+
const content = this.extractContent(anthropicResponse);
|
|
825
|
+
const hasToolCalls = anthropicResponse.stop_reason === "tool_use";
|
|
826
|
+
return {
|
|
827
|
+
content,
|
|
828
|
+
hasToolCalls,
|
|
829
|
+
stopReason: anthropicResponse.stop_reason ?? undefined,
|
|
830
|
+
usage: this.extractUsage(anthropicResponse, anthropicResponse.model),
|
|
831
|
+
raw: anthropicResponse,
|
|
832
|
+
};
|
|
852
833
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (options?.data &&
|
|
868
|
-
(options.placeholders?.input === undefined || options.placeholders?.input)) {
|
|
869
|
-
currentInput = formatOperateInput(input, {
|
|
870
|
-
data: options?.data,
|
|
871
|
-
});
|
|
834
|
+
extractToolCalls(response) {
|
|
835
|
+
const anthropicResponse = response;
|
|
836
|
+
const toolCalls = [];
|
|
837
|
+
for (const block of anthropicResponse.content) {
|
|
838
|
+
if (block.type === "tool_use") {
|
|
839
|
+
toolCalls.push({
|
|
840
|
+
callId: block.id,
|
|
841
|
+
name: block.name,
|
|
842
|
+
arguments: JSON.stringify(block.input),
|
|
843
|
+
raw: block,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return toolCalls;
|
|
872
848
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
849
|
+
extractUsage(response, model) {
|
|
850
|
+
const anthropicResponse = response;
|
|
851
|
+
return {
|
|
852
|
+
input: anthropicResponse.usage.input_tokens,
|
|
853
|
+
output: anthropicResponse.usage.output_tokens,
|
|
854
|
+
reasoning: 0, // Anthropic doesn't expose reasoning tokens
|
|
855
|
+
total: anthropicResponse.usage.input_tokens +
|
|
856
|
+
anthropicResponse.usage.output_tokens,
|
|
857
|
+
provider: this.name,
|
|
858
|
+
model,
|
|
859
|
+
};
|
|
876
860
|
}
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
content:
|
|
885
|
-
role: exports.LlmMessageRole.System,
|
|
886
|
-
type: exports.LlmMessageType.Message,
|
|
861
|
+
//
|
|
862
|
+
// Tool Result Handling
|
|
863
|
+
//
|
|
864
|
+
formatToolResult(toolCall, result) {
|
|
865
|
+
return {
|
|
866
|
+
type: "tool_result",
|
|
867
|
+
tool_use_id: toolCall.callId,
|
|
868
|
+
content: result.output,
|
|
887
869
|
};
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
870
|
+
}
|
|
871
|
+
appendToolResult(request, toolCall, result) {
|
|
872
|
+
const anthropicRequest = request;
|
|
873
|
+
const toolCallRaw = toolCall.raw;
|
|
874
|
+
// Add assistant message with the tool use
|
|
875
|
+
anthropicRequest.messages.push({
|
|
876
|
+
role: "assistant",
|
|
877
|
+
content: [toolCallRaw],
|
|
878
|
+
});
|
|
879
|
+
// Add user message with the tool result
|
|
880
|
+
anthropicRequest.messages.push({
|
|
881
|
+
role: "user",
|
|
882
|
+
content: [this.formatToolResult(toolCall, result)],
|
|
883
|
+
});
|
|
884
|
+
return anthropicRequest;
|
|
885
|
+
}
|
|
886
|
+
//
|
|
887
|
+
// History Management
|
|
888
|
+
//
|
|
889
|
+
responseToHistoryItems(response) {
|
|
890
|
+
const anthropicResponse = response;
|
|
891
|
+
const historyItems = [];
|
|
892
|
+
// Check if this is a tool use response
|
|
893
|
+
if (anthropicResponse.stop_reason === "tool_use") {
|
|
894
|
+
// Don't add to history yet - will be added after tool execution
|
|
895
|
+
return historyItems;
|
|
896
|
+
}
|
|
897
|
+
// Extract text content for non-tool responses
|
|
898
|
+
const textBlock = anthropicResponse.content.find((block) => block.type === "text");
|
|
899
|
+
if (textBlock) {
|
|
900
|
+
historyItems.push({
|
|
901
|
+
content: textBlock.text,
|
|
902
|
+
role: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
903
|
+
type: exports.LlmMessageType.Message,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return historyItems;
|
|
907
|
+
}
|
|
908
|
+
//
|
|
909
|
+
// Error Classification
|
|
910
|
+
//
|
|
911
|
+
classifyError(error) {
|
|
912
|
+
// Check for rate limit error
|
|
913
|
+
if (error instanceof Anthropic.RateLimitError) {
|
|
914
|
+
return {
|
|
915
|
+
error,
|
|
916
|
+
category: ErrorCategory.RateLimit,
|
|
917
|
+
shouldRetry: false,
|
|
918
|
+
suggestedDelayMs: 60000,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
// Check for retryable errors
|
|
922
|
+
for (const ErrorType of RETRYABLE_ERROR_TYPES$1) {
|
|
923
|
+
if (error instanceof ErrorType) {
|
|
924
|
+
return {
|
|
925
|
+
error,
|
|
926
|
+
category: ErrorCategory.Retryable,
|
|
927
|
+
shouldRetry: true,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// Check for non-retryable errors
|
|
932
|
+
for (const ErrorType of NOT_RETRYABLE_ERROR_TYPES$1) {
|
|
933
|
+
if (error instanceof ErrorType) {
|
|
934
|
+
return {
|
|
935
|
+
error,
|
|
936
|
+
category: ErrorCategory.Unrecoverable,
|
|
937
|
+
shouldRetry: false,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// Unknown error - treat as potentially retryable
|
|
942
|
+
return {
|
|
943
|
+
error,
|
|
944
|
+
category: ErrorCategory.Unknown,
|
|
945
|
+
shouldRetry: true,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
//
|
|
949
|
+
// Provider-Specific Features
|
|
950
|
+
//
|
|
951
|
+
isComplete(response) {
|
|
952
|
+
const anthropicResponse = response;
|
|
953
|
+
return anthropicResponse.stop_reason !== "tool_use";
|
|
954
|
+
}
|
|
955
|
+
hasStructuredOutput(response) {
|
|
956
|
+
const anthropicResponse = response;
|
|
957
|
+
// Check if the last content block is a tool_use with structured_output
|
|
958
|
+
const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
|
|
959
|
+
return (lastBlock?.type === "tool_use" &&
|
|
960
|
+
lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2);
|
|
961
|
+
}
|
|
962
|
+
extractStructuredOutput(response) {
|
|
963
|
+
const anthropicResponse = response;
|
|
964
|
+
const lastBlock = anthropicResponse.content[anthropicResponse.content.length - 1];
|
|
965
|
+
if (lastBlock?.type === "tool_use" &&
|
|
966
|
+
lastBlock.name === STRUCTURED_OUTPUT_TOOL_NAME$2) {
|
|
967
|
+
return lastBlock.input;
|
|
968
|
+
}
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
//
|
|
972
|
+
// Private Helpers
|
|
973
|
+
//
|
|
974
|
+
extractContent(response) {
|
|
975
|
+
// Check for structured output first
|
|
976
|
+
if (this.hasStructuredOutput(response)) {
|
|
977
|
+
return this.extractStructuredOutput(response);
|
|
978
|
+
}
|
|
979
|
+
// Extract text content
|
|
980
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
981
|
+
return textBlock?.text;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Export singleton instance
|
|
985
|
+
const anthropicAdapter = new AnthropicAdapter();
|
|
986
|
+
|
|
987
|
+
//
|
|
988
|
+
//
|
|
989
|
+
// Constants
|
|
990
|
+
//
|
|
991
|
+
const STRUCTURED_OUTPUT_TOOL_NAME$1 = "structured_output";
|
|
992
|
+
// Gemini uses HTTP status codes for error classification
|
|
993
|
+
// Documented at: https://ai.google.dev/api/rest/v1beta/Status
|
|
994
|
+
const RETRYABLE_STATUS_CODES$1 = [
|
|
995
|
+
408, // Request Timeout
|
|
996
|
+
429, // Too Many Requests (Rate Limit)
|
|
997
|
+
500, // Internal Server Error
|
|
998
|
+
502, // Bad Gateway
|
|
999
|
+
503, // Service Unavailable
|
|
1000
|
+
504, // Gateway Timeout
|
|
1001
|
+
];
|
|
1002
|
+
const NOT_RETRYABLE_STATUS_CODES = [
|
|
1003
|
+
400, // Bad Request
|
|
1004
|
+
401, // Unauthorized
|
|
1005
|
+
403, // Forbidden
|
|
1006
|
+
404, // Not Found
|
|
1007
|
+
409, // Conflict
|
|
1008
|
+
422, // Unprocessable Entity
|
|
1009
|
+
];
|
|
1010
|
+
//
|
|
1011
|
+
//
|
|
1012
|
+
// Main
|
|
1013
|
+
//
|
|
1014
|
+
/**
|
|
1015
|
+
* GeminiAdapter implements the ProviderAdapter interface for Google's Gemini API.
|
|
1016
|
+
* It handles request building, response parsing, and error classification
|
|
1017
|
+
* specific to Gemini's generateContent API.
|
|
1018
|
+
*/
|
|
1019
|
+
class GeminiAdapter extends BaseProviderAdapter {
|
|
1020
|
+
constructor() {
|
|
1021
|
+
super(...arguments);
|
|
1022
|
+
this.name = PROVIDER.GEMINI.NAME;
|
|
1023
|
+
this.defaultModel = PROVIDER.GEMINI.MODEL.DEFAULT;
|
|
1024
|
+
}
|
|
1025
|
+
//
|
|
1026
|
+
// Request Building
|
|
1027
|
+
//
|
|
1028
|
+
buildRequest(request) {
|
|
1029
|
+
// Convert messages to Gemini format (Content[])
|
|
1030
|
+
const contents = this.convertMessagesToContents(request.messages);
|
|
1031
|
+
const geminiRequest = {
|
|
1032
|
+
model: request.model || this.defaultModel,
|
|
1033
|
+
contents,
|
|
1034
|
+
};
|
|
1035
|
+
// Add system instruction if provided
|
|
1036
|
+
if (request.system) {
|
|
1037
|
+
geminiRequest.config = {
|
|
1038
|
+
...geminiRequest.config,
|
|
1039
|
+
systemInstruction: request.system,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
// Append instructions to the last user message if provided
|
|
1043
|
+
if (request.instructions && contents.length > 0) {
|
|
1044
|
+
const lastContent = contents[contents.length - 1];
|
|
1045
|
+
if (lastContent.role === "user" && lastContent.parts) {
|
|
1046
|
+
const lastPart = lastContent.parts[lastContent.parts.length - 1];
|
|
1047
|
+
if (lastPart.text) {
|
|
1048
|
+
lastPart.text = lastPart.text + "\n\n" + request.instructions;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// Add tools if provided
|
|
1053
|
+
if (request.tools && request.tools.length > 0) {
|
|
1054
|
+
const functionDeclarations = request.tools.map((tool) => ({
|
|
1055
|
+
name: tool.name,
|
|
1056
|
+
description: tool.description,
|
|
1057
|
+
parameters: tool.parameters,
|
|
1058
|
+
}));
|
|
1059
|
+
geminiRequest.config = {
|
|
1060
|
+
...geminiRequest.config,
|
|
1061
|
+
tools: [{ functionDeclarations }],
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
// Add structured output format if provided (but NOT when tools are present)
|
|
1065
|
+
// Gemini doesn't support combining function calling with responseMimeType: 'application/json'
|
|
1066
|
+
// When tools are present, structured output is handled via the structured_output tool
|
|
1067
|
+
if (request.format && !(request.tools && request.tools.length > 0)) {
|
|
1068
|
+
geminiRequest.config = {
|
|
1069
|
+
...geminiRequest.config,
|
|
1070
|
+
responseMimeType: "application/json",
|
|
1071
|
+
responseJsonSchema: request.format,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
// When format is specified with tools, add instruction to use structured_output tool
|
|
1075
|
+
if (request.format && request.tools && request.tools.length > 0) {
|
|
1076
|
+
const structuredOutputInstruction = "IMPORTANT: Before providing your final response, you MUST use the structured_output tool " +
|
|
1077
|
+
"to output your answer in the required JSON format.";
|
|
1078
|
+
// Add to system instruction if it exists, otherwise create one
|
|
1079
|
+
const existingSystem = geminiRequest.config?.systemInstruction || "";
|
|
1080
|
+
geminiRequest.config = {
|
|
1081
|
+
...geminiRequest.config,
|
|
1082
|
+
systemInstruction: existingSystem
|
|
1083
|
+
? `${existingSystem}\n\n${structuredOutputInstruction}`
|
|
1084
|
+
: structuredOutputInstruction,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
// Add provider-specific options
|
|
1088
|
+
if (request.providerOptions) {
|
|
1089
|
+
geminiRequest.config = {
|
|
1090
|
+
...geminiRequest.config,
|
|
1091
|
+
...request.providerOptions,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return geminiRequest;
|
|
1095
|
+
}
|
|
1096
|
+
formatTools(toolkit, outputSchema) {
|
|
1097
|
+
const tools = toolkit.tools.map((tool) => ({
|
|
1098
|
+
name: tool.name,
|
|
1099
|
+
description: tool.description,
|
|
1100
|
+
parameters: {
|
|
1101
|
+
...tool.parameters,
|
|
1102
|
+
type: "object",
|
|
1103
|
+
},
|
|
1104
|
+
}));
|
|
1105
|
+
// Add structured output tool if schema is provided
|
|
1106
|
+
if (outputSchema) {
|
|
1107
|
+
tools.push({
|
|
1108
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
1109
|
+
description: "Output a structured JSON object, " +
|
|
1110
|
+
"use this before your final response to give structured outputs to the user",
|
|
1111
|
+
parameters: outputSchema,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
return tools;
|
|
1115
|
+
}
|
|
1116
|
+
formatOutputSchema(schema) {
|
|
1117
|
+
let jsonSchema;
|
|
1118
|
+
// Check if schema is already a JsonObject with type "json_schema"
|
|
1119
|
+
if (typeof schema === "object" &&
|
|
1120
|
+
schema !== null &&
|
|
1121
|
+
!Array.isArray(schema) &&
|
|
1122
|
+
schema.type === "json_schema") {
|
|
1123
|
+
jsonSchema = structuredClone(schema);
|
|
1124
|
+
jsonSchema.type = "object";
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
// Convert NaturalSchema to JSON schema through Zod
|
|
1128
|
+
const zodSchema = schema instanceof v4.z.ZodType
|
|
1129
|
+
? schema
|
|
1130
|
+
: naturalZodSchema(schema);
|
|
1131
|
+
jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
1132
|
+
}
|
|
1133
|
+
// Remove $schema property (Gemini doesn't need it)
|
|
1134
|
+
if (jsonSchema.$schema) {
|
|
1135
|
+
delete jsonSchema.$schema;
|
|
1136
|
+
}
|
|
1137
|
+
return jsonSchema;
|
|
1138
|
+
}
|
|
1139
|
+
//
|
|
1140
|
+
// API Execution
|
|
1141
|
+
//
|
|
1142
|
+
async executeRequest(client, request) {
|
|
1143
|
+
const genAI = client;
|
|
1144
|
+
const geminiRequest = request;
|
|
1145
|
+
// Cast config to any to bypass strict type checking between our internal types
|
|
1146
|
+
// and the SDK's types. The SDK will validate at runtime.
|
|
1147
|
+
const response = await genAI.models.generateContent({
|
|
1148
|
+
model: geminiRequest.model,
|
|
1149
|
+
contents: geminiRequest.contents,
|
|
1150
|
+
config: geminiRequest.config,
|
|
1151
|
+
});
|
|
1152
|
+
return response;
|
|
1153
|
+
}
|
|
1154
|
+
//
|
|
1155
|
+
// Response Parsing
|
|
1156
|
+
//
|
|
1157
|
+
parseResponse(response, options) {
|
|
1158
|
+
const geminiResponse = response;
|
|
1159
|
+
const content = this.extractContent(geminiResponse, options);
|
|
1160
|
+
const hasToolCalls = this.hasToolCalls(geminiResponse);
|
|
1161
|
+
return {
|
|
1162
|
+
content,
|
|
1163
|
+
hasToolCalls,
|
|
1164
|
+
stopReason: this.getFinishReason(geminiResponse),
|
|
1165
|
+
usage: this.extractUsage(geminiResponse, options?.model || this.defaultModel),
|
|
1166
|
+
raw: geminiResponse,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
extractToolCalls(response) {
|
|
1170
|
+
const geminiResponse = response;
|
|
1171
|
+
const toolCalls = [];
|
|
1172
|
+
const candidate = geminiResponse.candidates?.[0];
|
|
1173
|
+
if (!candidate?.content?.parts) {
|
|
1174
|
+
return toolCalls;
|
|
1175
|
+
}
|
|
1176
|
+
for (const part of candidate.content.parts) {
|
|
1177
|
+
if (part.functionCall) {
|
|
1178
|
+
const functionCall = part.functionCall;
|
|
1179
|
+
toolCalls.push({
|
|
1180
|
+
callId: functionCall.id || this.generateCallId(),
|
|
1181
|
+
name: functionCall.name || "",
|
|
1182
|
+
arguments: JSON.stringify(functionCall.args || {}),
|
|
1183
|
+
raw: part,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return toolCalls;
|
|
1188
|
+
}
|
|
1189
|
+
extractUsage(response, model) {
|
|
1190
|
+
const geminiResponse = response;
|
|
1191
|
+
if (!geminiResponse.usageMetadata) {
|
|
1192
|
+
return {
|
|
1193
|
+
input: 0,
|
|
1194
|
+
output: 0,
|
|
1195
|
+
reasoning: 0,
|
|
1196
|
+
total: 0,
|
|
1197
|
+
provider: this.name,
|
|
1198
|
+
model,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
const usage = geminiResponse.usageMetadata;
|
|
1202
|
+
return {
|
|
1203
|
+
input: usage.promptTokenCount || 0,
|
|
1204
|
+
output: usage.candidatesTokenCount || 0,
|
|
1205
|
+
reasoning: usage.thoughtsTokenCount || 0,
|
|
1206
|
+
total: usage.totalTokenCount || 0,
|
|
1207
|
+
provider: this.name,
|
|
1208
|
+
model,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
//
|
|
1212
|
+
// Tool Result Handling
|
|
1213
|
+
//
|
|
1214
|
+
formatToolResult(toolCall, result) {
|
|
1215
|
+
// Gemini expects the response to be the actual result object, not wrapped in "result"
|
|
1216
|
+
// The output from StandardToolResult is JSON-stringified, so we need to parse it
|
|
1217
|
+
let responseData;
|
|
1218
|
+
try {
|
|
1219
|
+
responseData = JSON.parse(result.output);
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
// If parsing fails, wrap the output as a string result
|
|
1223
|
+
responseData = { result: result.output };
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
functionResponse: {
|
|
1227
|
+
name: toolCall.name,
|
|
1228
|
+
response: responseData,
|
|
1229
|
+
},
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
appendToolResult(request, toolCall, result) {
|
|
1233
|
+
const geminiRequest = request;
|
|
1234
|
+
const toolCallRaw = toolCall.raw;
|
|
1235
|
+
const toolResponse = this.formatToolResult(toolCall, result);
|
|
1236
|
+
// Get the last two contents to check for existing tool call/response pattern
|
|
1237
|
+
const lastContent = geminiRequest.contents[geminiRequest.contents.length - 1];
|
|
1238
|
+
const secondLastContent = geminiRequest.contents[geminiRequest.contents.length - 2];
|
|
1239
|
+
// Check if we're adding to an existing tool call batch
|
|
1240
|
+
// Pattern: [..., model (with functionCall), user (with functionResponse)]
|
|
1241
|
+
const hasExistingToolBatch = lastContent?.role === "user" &&
|
|
1242
|
+
lastContent?.parts?.some((p) => p.functionResponse) &&
|
|
1243
|
+
secondLastContent?.role === "model" &&
|
|
1244
|
+
secondLastContent?.parts?.some((p) => p.functionCall);
|
|
1245
|
+
if (hasExistingToolBatch) {
|
|
1246
|
+
// Append to existing batch
|
|
1247
|
+
secondLastContent.parts.push(toolCallRaw);
|
|
1248
|
+
lastContent.parts.push(toolResponse);
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
// Create new batch
|
|
1252
|
+
geminiRequest.contents.push({
|
|
1253
|
+
role: "model",
|
|
1254
|
+
parts: [toolCallRaw],
|
|
1255
|
+
});
|
|
1256
|
+
geminiRequest.contents.push({
|
|
1257
|
+
role: "user",
|
|
1258
|
+
parts: [toolResponse],
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return geminiRequest;
|
|
1262
|
+
}
|
|
1263
|
+
//
|
|
1264
|
+
// History Management
|
|
1265
|
+
//
|
|
1266
|
+
responseToHistoryItems(response) {
|
|
1267
|
+
const geminiResponse = response;
|
|
1268
|
+
const historyItems = [];
|
|
1269
|
+
const candidate = geminiResponse.candidates?.[0];
|
|
1270
|
+
if (!candidate?.content?.parts) {
|
|
1271
|
+
return historyItems;
|
|
1272
|
+
}
|
|
1273
|
+
// Check if this is a function call response
|
|
1274
|
+
const hasFunctionCalls = candidate.content.parts.some((part) => part.functionCall);
|
|
1275
|
+
if (hasFunctionCalls) {
|
|
1276
|
+
// Don't add to history yet - will be added after tool execution
|
|
1277
|
+
return historyItems;
|
|
1278
|
+
}
|
|
1279
|
+
// Extract text content for non-tool responses
|
|
1280
|
+
const textParts = candidate.content.parts.filter((part) => part.text && !part.thought);
|
|
1281
|
+
if (textParts.length > 0) {
|
|
1282
|
+
const textContent = textParts.map((part) => part.text).join("");
|
|
1283
|
+
historyItems.push({
|
|
1284
|
+
content: textContent,
|
|
1285
|
+
role: exports.LlmMessageRole.Assistant,
|
|
1286
|
+
type: exports.LlmMessageType.Message,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
return historyItems;
|
|
1290
|
+
}
|
|
1291
|
+
//
|
|
1292
|
+
// Error Classification
|
|
1293
|
+
//
|
|
1294
|
+
classifyError(error) {
|
|
1295
|
+
const geminiError = error;
|
|
1296
|
+
// Extract status code from error
|
|
1297
|
+
const statusCode = geminiError.status || geminiError.code;
|
|
1298
|
+
// Check for rate limit error (429)
|
|
1299
|
+
if (statusCode === 429) {
|
|
1300
|
+
return {
|
|
1301
|
+
error,
|
|
1302
|
+
category: ErrorCategory.RateLimit,
|
|
1303
|
+
shouldRetry: false,
|
|
1304
|
+
suggestedDelayMs: 60000,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
// Check for retryable errors
|
|
1308
|
+
if (typeof statusCode === "number" &&
|
|
1309
|
+
RETRYABLE_STATUS_CODES$1.includes(statusCode)) {
|
|
1310
|
+
return {
|
|
1311
|
+
error,
|
|
1312
|
+
category: ErrorCategory.Retryable,
|
|
1313
|
+
shouldRetry: true,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
// Check for non-retryable errors
|
|
1317
|
+
if (typeof statusCode === "number" &&
|
|
1318
|
+
NOT_RETRYABLE_STATUS_CODES.includes(statusCode)) {
|
|
1319
|
+
return {
|
|
1320
|
+
error,
|
|
1321
|
+
category: ErrorCategory.Unrecoverable,
|
|
1322
|
+
shouldRetry: false,
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
// Check error message for common patterns
|
|
1326
|
+
const errorMessage = geminiError.message || String(error) || "";
|
|
1327
|
+
if (errorMessage.includes("rate limit") ||
|
|
1328
|
+
errorMessage.includes("quota exceeded")) {
|
|
1329
|
+
return {
|
|
1330
|
+
error,
|
|
1331
|
+
category: ErrorCategory.RateLimit,
|
|
1332
|
+
shouldRetry: false,
|
|
1333
|
+
suggestedDelayMs: 60000,
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (errorMessage.includes("timeout") ||
|
|
1337
|
+
errorMessage.includes("connection") ||
|
|
1338
|
+
errorMessage.includes("ECONNREFUSED")) {
|
|
1339
|
+
return {
|
|
1340
|
+
error,
|
|
1341
|
+
category: ErrorCategory.Retryable,
|
|
1342
|
+
shouldRetry: true,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
// Unknown error - treat as potentially retryable
|
|
1346
|
+
return {
|
|
1347
|
+
error,
|
|
1348
|
+
category: ErrorCategory.Unknown,
|
|
1349
|
+
shouldRetry: true,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
//
|
|
1353
|
+
// Provider-Specific Features
|
|
1354
|
+
//
|
|
1355
|
+
isComplete(response) {
|
|
1356
|
+
const geminiResponse = response;
|
|
1357
|
+
const candidate = geminiResponse.candidates?.[0];
|
|
1358
|
+
if (!candidate?.content?.parts) {
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
// Check if there are any function calls
|
|
1362
|
+
const hasFunctionCalls = candidate.content.parts.some((part) => part.functionCall);
|
|
1363
|
+
return !hasFunctionCalls;
|
|
1364
|
+
}
|
|
1365
|
+
hasStructuredOutput(response) {
|
|
1366
|
+
const geminiResponse = response;
|
|
1367
|
+
const candidate = geminiResponse.candidates?.[0];
|
|
1368
|
+
if (!candidate?.content?.parts) {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
// Check if the last part is a function call with structured_output
|
|
1372
|
+
const lastPart = candidate.content.parts[candidate.content.parts.length - 1];
|
|
1373
|
+
return lastPart?.functionCall?.name === STRUCTURED_OUTPUT_TOOL_NAME$1;
|
|
1374
|
+
}
|
|
1375
|
+
extractStructuredOutput(response) {
|
|
1376
|
+
const geminiResponse = response;
|
|
1377
|
+
const candidate = geminiResponse.candidates?.[0];
|
|
1378
|
+
if (!candidate?.content?.parts) {
|
|
1379
|
+
return undefined;
|
|
1380
|
+
}
|
|
1381
|
+
const lastPart = candidate.content.parts[candidate.content.parts.length - 1];
|
|
1382
|
+
if (lastPart?.functionCall?.name === STRUCTURED_OUTPUT_TOOL_NAME$1) {
|
|
1383
|
+
return lastPart.functionCall.args;
|
|
1384
|
+
}
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}
|
|
1387
|
+
//
|
|
1388
|
+
// Private Helpers
|
|
1389
|
+
//
|
|
1390
|
+
convertMessagesToContents(messages) {
|
|
1391
|
+
const contents = [];
|
|
1392
|
+
for (const message of messages) {
|
|
1393
|
+
// Handle input/output messages
|
|
1394
|
+
if ("role" in message && "content" in message) {
|
|
1395
|
+
const role = this.mapRole(message.role);
|
|
1396
|
+
const content = typeof message.content === "string"
|
|
1397
|
+
? message.content
|
|
1398
|
+
: JSON.stringify(message.content);
|
|
1399
|
+
contents.push({
|
|
1400
|
+
role,
|
|
1401
|
+
parts: [{ text: content }],
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
// Handle function call items
|
|
1405
|
+
else if ("type" in message &&
|
|
1406
|
+
message.type === exports.LlmMessageType.FunctionCall) {
|
|
1407
|
+
contents.push({
|
|
1408
|
+
role: "model",
|
|
1409
|
+
parts: [
|
|
1410
|
+
{
|
|
1411
|
+
functionCall: {
|
|
1412
|
+
name: message.name,
|
|
1413
|
+
args: JSON.parse(message.arguments || "{}"),
|
|
1414
|
+
id: message.call_id,
|
|
1415
|
+
},
|
|
1416
|
+
},
|
|
1417
|
+
],
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
// Handle function call output items
|
|
1421
|
+
else if ("type" in message &&
|
|
1422
|
+
message.type === exports.LlmMessageType.FunctionCallOutput) {
|
|
1423
|
+
// Parse the output to get the actual response object
|
|
1424
|
+
let responseData;
|
|
1425
|
+
try {
|
|
1426
|
+
responseData = JSON.parse(message.output || "{}");
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
responseData = { result: message.output };
|
|
1430
|
+
}
|
|
1431
|
+
contents.push({
|
|
1432
|
+
role: "user",
|
|
1433
|
+
parts: [
|
|
1434
|
+
{
|
|
1435
|
+
functionResponse: {
|
|
1436
|
+
name: message.name || "function",
|
|
1437
|
+
response: responseData,
|
|
1438
|
+
},
|
|
1439
|
+
},
|
|
1440
|
+
],
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return contents;
|
|
1445
|
+
}
|
|
1446
|
+
mapRole(role) {
|
|
1447
|
+
switch (role) {
|
|
1448
|
+
case exports.LlmMessageRole.User:
|
|
1449
|
+
case exports.LlmMessageRole.System:
|
|
1450
|
+
case exports.LlmMessageRole.Developer:
|
|
1451
|
+
return "user";
|
|
1452
|
+
case exports.LlmMessageRole.Assistant:
|
|
1453
|
+
return "model";
|
|
1454
|
+
default:
|
|
1455
|
+
return "user";
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
hasToolCalls(response) {
|
|
1459
|
+
const candidate = response.candidates?.[0];
|
|
1460
|
+
if (!candidate?.content?.parts) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
return candidate.content.parts.some((part) => part.functionCall);
|
|
1464
|
+
}
|
|
1465
|
+
getFinishReason(response) {
|
|
1466
|
+
const candidate = response.candidates?.[0];
|
|
1467
|
+
return candidate?.finishReason;
|
|
1468
|
+
}
|
|
1469
|
+
extractContent(response, options) {
|
|
1470
|
+
// Check for structured output first
|
|
1471
|
+
if (this.hasStructuredOutput(response)) {
|
|
1472
|
+
return this.extractStructuredOutput(response);
|
|
1473
|
+
}
|
|
1474
|
+
const candidate = response.candidates?.[0];
|
|
1475
|
+
if (!candidate?.content?.parts) {
|
|
1476
|
+
return undefined;
|
|
1477
|
+
}
|
|
1478
|
+
// Extract text content (excluding thought parts)
|
|
1479
|
+
const textParts = candidate.content.parts.filter((part) => part.text && !part.thought);
|
|
1480
|
+
if (textParts.length === 0) {
|
|
1481
|
+
return undefined;
|
|
1482
|
+
}
|
|
1483
|
+
const textContent = textParts.map((part) => part.text).join("");
|
|
1484
|
+
// If format is provided, try to parse the content as JSON
|
|
1485
|
+
if (options?.format && typeof textContent === "string") {
|
|
1486
|
+
try {
|
|
1487
|
+
return JSON.parse(textContent);
|
|
1488
|
+
}
|
|
1489
|
+
catch {
|
|
1490
|
+
// If parsing fails, return the original string
|
|
1491
|
+
return textContent;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return textContent;
|
|
1495
|
+
}
|
|
1496
|
+
generateCallId() {
|
|
1497
|
+
return `call_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
// Export singleton instance
|
|
1501
|
+
const geminiAdapter = new GeminiAdapter();
|
|
1502
|
+
|
|
1503
|
+
//
|
|
1504
|
+
//
|
|
1505
|
+
// Constants
|
|
1506
|
+
//
|
|
1507
|
+
const RETRYABLE_ERROR_TYPES = [
|
|
1508
|
+
openai.APIConnectionError,
|
|
1509
|
+
openai.APIConnectionTimeoutError,
|
|
1510
|
+
openai.InternalServerError,
|
|
1511
|
+
];
|
|
1512
|
+
const NOT_RETRYABLE_ERROR_TYPES = [
|
|
1513
|
+
openai.APIUserAbortError,
|
|
1514
|
+
openai.AuthenticationError,
|
|
1515
|
+
openai.BadRequestError,
|
|
1516
|
+
openai.ConflictError,
|
|
1517
|
+
openai.NotFoundError,
|
|
1518
|
+
openai.PermissionDeniedError,
|
|
1519
|
+
openai.RateLimitError,
|
|
1520
|
+
openai.UnprocessableEntityError,
|
|
1521
|
+
];
|
|
1522
|
+
//
|
|
1523
|
+
//
|
|
1524
|
+
// Main
|
|
1525
|
+
//
|
|
1526
|
+
/**
|
|
1527
|
+
* OpenAiAdapter implements the ProviderAdapter interface for OpenAI's API.
|
|
1528
|
+
* It handles request building, response parsing, and error classification
|
|
1529
|
+
* specific to OpenAI's Responses API.
|
|
1530
|
+
*/
|
|
1531
|
+
class OpenAiAdapter extends BaseProviderAdapter {
|
|
1532
|
+
constructor() {
|
|
1533
|
+
super(...arguments);
|
|
1534
|
+
this.name = PROVIDER.OPENAI.NAME;
|
|
1535
|
+
this.defaultModel = PROVIDER.OPENAI.MODEL.DEFAULT;
|
|
1536
|
+
}
|
|
1537
|
+
//
|
|
1538
|
+
// Request Building
|
|
1539
|
+
//
|
|
1540
|
+
buildRequest(request) {
|
|
1541
|
+
const openaiRequest = {
|
|
1542
|
+
model: request.model || this.defaultModel,
|
|
1543
|
+
input: request.messages,
|
|
1544
|
+
};
|
|
1545
|
+
if (request.user) {
|
|
1546
|
+
openaiRequest.user = request.user;
|
|
1547
|
+
}
|
|
1548
|
+
if (request.instructions) {
|
|
1549
|
+
openaiRequest.instructions = request.instructions;
|
|
1550
|
+
}
|
|
1551
|
+
if (request.tools && request.tools.length > 0) {
|
|
1552
|
+
openaiRequest.tools = request.tools.map((tool) => ({
|
|
1553
|
+
type: "function",
|
|
1554
|
+
name: tool.name,
|
|
1555
|
+
description: tool.description,
|
|
1556
|
+
parameters: tool.parameters,
|
|
1557
|
+
}));
|
|
1558
|
+
}
|
|
1559
|
+
if (request.format) {
|
|
1560
|
+
openaiRequest.text = {
|
|
1561
|
+
format: request.format,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
if (request.providerOptions) {
|
|
1565
|
+
Object.assign(openaiRequest, request.providerOptions);
|
|
1566
|
+
}
|
|
1567
|
+
return openaiRequest;
|
|
1568
|
+
}
|
|
1569
|
+
formatTools(toolkit, _outputSchema) {
|
|
1570
|
+
return toolkit.tools.map((tool) => ({
|
|
1571
|
+
name: tool.name,
|
|
1572
|
+
description: tool.description,
|
|
1573
|
+
parameters: tool.parameters,
|
|
1574
|
+
}));
|
|
1575
|
+
}
|
|
1576
|
+
formatOutputSchema(schema) {
|
|
1577
|
+
// Check if schema is already a JsonObject with type "json_schema"
|
|
1578
|
+
if (typeof schema === "object" &&
|
|
1579
|
+
schema !== null &&
|
|
1580
|
+
!Array.isArray(schema) &&
|
|
1581
|
+
schema.type === "json_schema") {
|
|
1582
|
+
return schema;
|
|
1583
|
+
}
|
|
1584
|
+
// Convert NaturalSchema to Zod schema if needed
|
|
1585
|
+
const zodSchema = schema instanceof v4.z.ZodType
|
|
1586
|
+
? schema
|
|
1587
|
+
: naturalZodSchema(schema);
|
|
1588
|
+
const responseFormat = zod.zodResponseFormat(zodSchema, "response");
|
|
1589
|
+
const jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
1590
|
+
// OpenAI requires additionalProperties to be false on all objects
|
|
1591
|
+
const checks = [jsonSchema];
|
|
1592
|
+
while (checks.length > 0) {
|
|
1593
|
+
const current = checks[0];
|
|
1594
|
+
if (current.type === "object") {
|
|
1595
|
+
current.additionalProperties = false;
|
|
1596
|
+
}
|
|
1597
|
+
Object.keys(current).forEach((key) => {
|
|
1598
|
+
if (typeof current[key] === "object" && current[key] !== null) {
|
|
1599
|
+
checks.push(current[key]);
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
checks.shift();
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
name: responseFormat.json_schema.name,
|
|
1606
|
+
schema: jsonSchema,
|
|
1607
|
+
strict: responseFormat.json_schema.strict,
|
|
1608
|
+
type: responseFormat.type,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
//
|
|
1612
|
+
// API Execution
|
|
1613
|
+
//
|
|
1614
|
+
async executeRequest(client, request) {
|
|
1615
|
+
const openai = client;
|
|
1616
|
+
// @ts-expect-error OpenAI SDK types don't match our request format exactly
|
|
1617
|
+
return await openai.responses.create(request);
|
|
1618
|
+
}
|
|
1619
|
+
//
|
|
1620
|
+
// Response Parsing
|
|
1621
|
+
//
|
|
1622
|
+
parseResponse(response, options) {
|
|
1623
|
+
const openaiResponse = response;
|
|
1624
|
+
const content = this.extractContent(openaiResponse, options);
|
|
1625
|
+
const hasToolCalls = this.hasToolCalls(openaiResponse);
|
|
1626
|
+
return {
|
|
1627
|
+
content,
|
|
1628
|
+
hasToolCalls,
|
|
1629
|
+
stopReason: openaiResponse.status,
|
|
1630
|
+
usage: this.extractUsage(openaiResponse, options?.model || this.defaultModel),
|
|
1631
|
+
raw: openaiResponse,
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
extractToolCalls(response) {
|
|
1635
|
+
const openaiResponse = response;
|
|
1636
|
+
const toolCalls = [];
|
|
1637
|
+
if (!openaiResponse.output || !Array.isArray(openaiResponse.output)) {
|
|
1638
|
+
return toolCalls;
|
|
1639
|
+
}
|
|
1640
|
+
for (const output of openaiResponse.output) {
|
|
1641
|
+
if (output.type === exports.LlmMessageType.FunctionCall) {
|
|
1642
|
+
toolCalls.push({
|
|
1643
|
+
callId: output.call_id,
|
|
1644
|
+
name: output.name,
|
|
1645
|
+
arguments: output.arguments,
|
|
1646
|
+
raw: output,
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return toolCalls;
|
|
1651
|
+
}
|
|
1652
|
+
extractUsage(response, model) {
|
|
1653
|
+
const openaiResponse = response;
|
|
1654
|
+
if (!openaiResponse.usage) {
|
|
1655
|
+
return {
|
|
1656
|
+
input: 0,
|
|
1657
|
+
output: 0,
|
|
1658
|
+
reasoning: 0,
|
|
1659
|
+
total: 0,
|
|
1660
|
+
provider: this.name,
|
|
1661
|
+
model,
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
return {
|
|
1665
|
+
input: openaiResponse.usage.input_tokens || 0,
|
|
1666
|
+
output: openaiResponse.usage.output_tokens || 0,
|
|
1667
|
+
reasoning: openaiResponse.usage.output_tokens_details?.reasoning_tokens || 0,
|
|
1668
|
+
total: openaiResponse.usage.total_tokens || 0,
|
|
1669
|
+
provider: this.name,
|
|
1670
|
+
model,
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
//
|
|
1674
|
+
// Tool Result Handling
|
|
1675
|
+
//
|
|
1676
|
+
formatToolResult(toolCall, result) {
|
|
1677
|
+
return {
|
|
1678
|
+
call_id: toolCall.callId,
|
|
1679
|
+
output: result.output,
|
|
1680
|
+
type: exports.LlmMessageType.FunctionCallOutput,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
appendToolResult(request, toolCall, result) {
|
|
1684
|
+
const openaiRequest = request;
|
|
1685
|
+
const input = openaiRequest.input;
|
|
1686
|
+
// Note: The function_call item has already been added via responseToHistoryItems
|
|
1687
|
+
// which is called before processing tool calls. This includes any required
|
|
1688
|
+
// reasoning items that precede the function_call.
|
|
1689
|
+
// Add only the function call result
|
|
1690
|
+
input.push({
|
|
1691
|
+
call_id: toolCall.callId,
|
|
1692
|
+
output: result.output,
|
|
1693
|
+
type: exports.LlmMessageType.FunctionCallOutput,
|
|
1694
|
+
});
|
|
1695
|
+
return openaiRequest;
|
|
1696
|
+
}
|
|
1697
|
+
//
|
|
1698
|
+
// History Management
|
|
1699
|
+
//
|
|
1700
|
+
responseToHistoryItems(response) {
|
|
1701
|
+
const openaiResponse = response;
|
|
1702
|
+
const historyItems = [];
|
|
1703
|
+
if (!openaiResponse.output || !Array.isArray(openaiResponse.output)) {
|
|
1704
|
+
return historyItems;
|
|
1705
|
+
}
|
|
1706
|
+
// Include all output items, including reasoning items
|
|
1707
|
+
// OpenAI requires reasoning items to be present when a function_call references them
|
|
1708
|
+
for (const output of openaiResponse.output) {
|
|
1709
|
+
historyItems.push(output);
|
|
1710
|
+
}
|
|
1711
|
+
return historyItems;
|
|
1712
|
+
}
|
|
1713
|
+
//
|
|
1714
|
+
// Error Classification
|
|
1715
|
+
//
|
|
1716
|
+
classifyError(error) {
|
|
1717
|
+
// Check for rate limit error
|
|
1718
|
+
if (error instanceof openai.RateLimitError) {
|
|
1719
|
+
return {
|
|
1720
|
+
error,
|
|
1721
|
+
category: ErrorCategory.RateLimit,
|
|
1722
|
+
shouldRetry: false, // Rate limit requires waiting, not immediate retry
|
|
1723
|
+
suggestedDelayMs: 60000, // 1 minute default
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
// Check for retryable errors
|
|
1727
|
+
for (const ErrorType of RETRYABLE_ERROR_TYPES) {
|
|
1728
|
+
if (error instanceof ErrorType) {
|
|
1729
|
+
return {
|
|
1730
|
+
error,
|
|
1731
|
+
category: ErrorCategory.Retryable,
|
|
1732
|
+
shouldRetry: true,
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
// Check for non-retryable errors
|
|
1737
|
+
for (const ErrorType of NOT_RETRYABLE_ERROR_TYPES) {
|
|
1738
|
+
if (error instanceof ErrorType) {
|
|
1739
|
+
return {
|
|
1740
|
+
error,
|
|
1741
|
+
category: ErrorCategory.Unrecoverable,
|
|
1742
|
+
shouldRetry: false,
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
// Unknown error - treat as potentially retryable
|
|
1747
|
+
return {
|
|
1748
|
+
error,
|
|
1749
|
+
category: ErrorCategory.Unknown,
|
|
1750
|
+
shouldRetry: true,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
//
|
|
1754
|
+
// Provider-Specific Features
|
|
1755
|
+
//
|
|
1756
|
+
isComplete(response) {
|
|
1757
|
+
const openaiResponse = response;
|
|
1758
|
+
// Check if there are any function calls in the output
|
|
1759
|
+
if (!openaiResponse.output || !Array.isArray(openaiResponse.output)) {
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
for (const output of openaiResponse.output) {
|
|
1763
|
+
if (output.type === exports.LlmMessageType.FunctionCall) {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
//
|
|
1770
|
+
// Private Helpers
|
|
1771
|
+
//
|
|
1772
|
+
hasToolCalls(response) {
|
|
1773
|
+
if (!response.output || !Array.isArray(response.output)) {
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
return response.output.some((output) => output.type === exports.LlmMessageType.FunctionCall);
|
|
1777
|
+
}
|
|
1778
|
+
extractContent(response, options) {
|
|
1779
|
+
if (!response.output || !Array.isArray(response.output)) {
|
|
1780
|
+
return undefined;
|
|
1781
|
+
}
|
|
1782
|
+
for (const output of response.output) {
|
|
1783
|
+
if (output.type === exports.LlmMessageType.Message) {
|
|
1784
|
+
if (output.content?.[0] &&
|
|
1785
|
+
output.content[0].type === exports.LlmMessageType.OutputText) {
|
|
1786
|
+
const rawContent = output.content[0].text;
|
|
1787
|
+
// If format is provided, try to parse the content as JSON
|
|
1788
|
+
if (options?.format && typeof rawContent === "string") {
|
|
1789
|
+
try {
|
|
1790
|
+
return JSON.parse(rawContent);
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
// If parsing fails, return the original string
|
|
1794
|
+
return rawContent;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return rawContent;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
// Skip reasoning and function call items when extracting content
|
|
1801
|
+
if (output.type === "reasoning" ||
|
|
1802
|
+
output.type === exports.LlmMessageType.FunctionCall) {
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return undefined;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
// Export singleton instance
|
|
1810
|
+
const openAiAdapter = new OpenAiAdapter();
|
|
1811
|
+
|
|
1812
|
+
//
|
|
1813
|
+
//
|
|
1814
|
+
// Constants
|
|
1815
|
+
//
|
|
1816
|
+
const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
|
|
1817
|
+
// OpenRouter SDK error types based on HTTP status codes
|
|
1818
|
+
const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
|
|
1819
|
+
const RATE_LIMIT_STATUS_CODE = 429;
|
|
1820
|
+
//
|
|
1821
|
+
//
|
|
1822
|
+
// Main
|
|
1823
|
+
//
|
|
1824
|
+
/**
|
|
1825
|
+
* OpenRouterAdapter implements the ProviderAdapter interface for OpenRouter's API.
|
|
1826
|
+
* OpenRouter provides a unified API to access hundreds of AI models through OpenAI-compatible endpoints.
|
|
1827
|
+
* It handles request building, response parsing, and error classification
|
|
1828
|
+
* specific to OpenRouter's Chat Completions API.
|
|
1829
|
+
*/
|
|
1830
|
+
class OpenRouterAdapter extends BaseProviderAdapter {
|
|
1831
|
+
constructor() {
|
|
1832
|
+
super(...arguments);
|
|
1833
|
+
this.name = PROVIDER.OPENROUTER.NAME;
|
|
1834
|
+
this.defaultModel = PROVIDER.OPENROUTER.MODEL.DEFAULT;
|
|
1835
|
+
}
|
|
1836
|
+
//
|
|
1837
|
+
// Request Building
|
|
1838
|
+
//
|
|
1839
|
+
buildRequest(request) {
|
|
1840
|
+
// Convert messages to OpenRouter format (OpenAI-compatible)
|
|
1841
|
+
const messages = this.convertMessagesToOpenRouter(request.messages, request.system);
|
|
1842
|
+
// Append instructions to last message if provided
|
|
1843
|
+
if (request.instructions && messages.length > 0) {
|
|
1844
|
+
const lastMsg = messages[messages.length - 1];
|
|
1845
|
+
if (lastMsg.content && typeof lastMsg.content === "string") {
|
|
1846
|
+
lastMsg.content = lastMsg.content + "\n\n" + request.instructions;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
const openRouterRequest = {
|
|
1850
|
+
model: request.model || this.defaultModel,
|
|
1851
|
+
messages,
|
|
1852
|
+
};
|
|
1853
|
+
if (request.user) {
|
|
1854
|
+
openRouterRequest.user = request.user;
|
|
1855
|
+
}
|
|
1856
|
+
if (request.tools && request.tools.length > 0) {
|
|
1857
|
+
openRouterRequest.tools = request.tools.map((tool) => ({
|
|
1858
|
+
type: "function",
|
|
1859
|
+
function: {
|
|
1860
|
+
name: tool.name,
|
|
1861
|
+
description: tool.description,
|
|
1862
|
+
parameters: tool.parameters,
|
|
1863
|
+
},
|
|
1864
|
+
}));
|
|
1865
|
+
// Determine tool choice based on whether structured output is requested
|
|
1866
|
+
const hasStructuredOutput = request.tools.some((t) => t.name === STRUCTURED_OUTPUT_TOOL_NAME);
|
|
1867
|
+
openRouterRequest.tool_choice = hasStructuredOutput ? "required" : "auto";
|
|
1868
|
+
}
|
|
1869
|
+
if (request.providerOptions) {
|
|
1870
|
+
Object.assign(openRouterRequest, request.providerOptions);
|
|
1871
|
+
}
|
|
1872
|
+
return openRouterRequest;
|
|
1873
|
+
}
|
|
1874
|
+
formatTools(toolkit, outputSchema) {
|
|
1875
|
+
const tools = toolkit.tools.map((tool) => ({
|
|
1876
|
+
name: tool.name,
|
|
1877
|
+
description: tool.description,
|
|
1878
|
+
parameters: tool.parameters,
|
|
1879
|
+
}));
|
|
1880
|
+
// Add structured output tool if schema is provided
|
|
1881
|
+
// (OpenRouter doesn't have native structured output like OpenAI, so use tool approach)
|
|
1882
|
+
if (outputSchema) {
|
|
1883
|
+
tools.push({
|
|
1884
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
1885
|
+
description: "REQUIRED: You MUST call this tool to provide your final response. " +
|
|
1886
|
+
"After gathering all necessary information (including results from other tools), " +
|
|
1887
|
+
"call this tool with the structured data to complete the request.",
|
|
1888
|
+
parameters: outputSchema,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
return tools;
|
|
1892
|
+
}
|
|
1893
|
+
formatOutputSchema(schema) {
|
|
1894
|
+
let jsonSchema;
|
|
1895
|
+
// Check if schema is already a JsonObject with type "json_schema"
|
|
1896
|
+
if (typeof schema === "object" &&
|
|
1897
|
+
schema !== null &&
|
|
1898
|
+
!Array.isArray(schema) &&
|
|
1899
|
+
schema.type === "json_schema") {
|
|
1900
|
+
jsonSchema = structuredClone(schema);
|
|
1901
|
+
jsonSchema.type = "object"; // Normalize type
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
// Convert NaturalSchema to JSON schema through Zod
|
|
1905
|
+
const zodSchema = schema instanceof v4.z.ZodType
|
|
1906
|
+
? schema
|
|
1907
|
+
: naturalZodSchema(schema);
|
|
1908
|
+
jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
1909
|
+
}
|
|
1910
|
+
// Remove $schema property (can cause issues with some providers)
|
|
1911
|
+
if (jsonSchema.$schema) {
|
|
1912
|
+
delete jsonSchema.$schema;
|
|
1913
|
+
}
|
|
1914
|
+
return jsonSchema;
|
|
1915
|
+
}
|
|
1916
|
+
//
|
|
1917
|
+
// API Execution
|
|
1918
|
+
//
|
|
1919
|
+
async executeRequest(client, request) {
|
|
1920
|
+
const openRouter = client;
|
|
1921
|
+
const openRouterRequest = request;
|
|
1922
|
+
const response = await openRouter.chat.send({
|
|
1923
|
+
model: openRouterRequest.model,
|
|
1924
|
+
messages: openRouterRequest.messages,
|
|
1925
|
+
tools: openRouterRequest.tools,
|
|
1926
|
+
toolChoice: openRouterRequest.tool_choice,
|
|
1927
|
+
user: openRouterRequest.user,
|
|
1928
|
+
});
|
|
1929
|
+
return response;
|
|
1930
|
+
}
|
|
1931
|
+
//
|
|
1932
|
+
// Response Parsing
|
|
1933
|
+
//
|
|
1934
|
+
parseResponse(response, _options) {
|
|
1935
|
+
const openRouterResponse = response;
|
|
1936
|
+
const choice = openRouterResponse.choices[0];
|
|
1937
|
+
const content = this.extractContent(openRouterResponse);
|
|
1938
|
+
const hasToolCalls = this.hasToolCalls(openRouterResponse);
|
|
1939
|
+
// SDK returns camelCase (finishReason), but support snake_case as fallback
|
|
1940
|
+
const stopReason = choice?.finishReason ?? choice?.finish_reason ?? undefined;
|
|
1941
|
+
return {
|
|
1942
|
+
content,
|
|
1943
|
+
hasToolCalls,
|
|
1944
|
+
stopReason,
|
|
1945
|
+
usage: this.extractUsage(openRouterResponse, openRouterResponse.model),
|
|
1946
|
+
raw: openRouterResponse,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
extractToolCalls(response) {
|
|
1950
|
+
const openRouterResponse = response;
|
|
1951
|
+
const toolCalls = [];
|
|
1952
|
+
const choice = openRouterResponse.choices[0];
|
|
1953
|
+
// SDK returns camelCase (toolCalls)
|
|
1954
|
+
if (!choice?.message?.toolCalls) {
|
|
1955
|
+
return toolCalls;
|
|
1956
|
+
}
|
|
1957
|
+
for (const toolCall of choice.message.toolCalls) {
|
|
1958
|
+
toolCalls.push({
|
|
1959
|
+
callId: toolCall.id,
|
|
1960
|
+
name: toolCall.function.name,
|
|
1961
|
+
arguments: toolCall.function.arguments,
|
|
1962
|
+
raw: toolCall,
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return toolCalls;
|
|
1966
|
+
}
|
|
1967
|
+
extractUsage(response, model) {
|
|
1968
|
+
const openRouterResponse = response;
|
|
1969
|
+
if (!openRouterResponse.usage) {
|
|
1970
|
+
return {
|
|
1971
|
+
input: 0,
|
|
1972
|
+
output: 0,
|
|
1973
|
+
reasoning: 0,
|
|
1974
|
+
total: 0,
|
|
1975
|
+
provider: this.name,
|
|
1976
|
+
model,
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
// SDK returns camelCase, but support snake_case as fallback
|
|
1980
|
+
const usage = openRouterResponse.usage;
|
|
1981
|
+
return {
|
|
1982
|
+
input: usage.promptTokens || usage.prompt_tokens || 0,
|
|
1983
|
+
output: usage.completionTokens || usage.completion_tokens || 0,
|
|
1984
|
+
reasoning: 0, // OpenRouter doesn't expose reasoning tokens in standard format
|
|
1985
|
+
total: usage.totalTokens || usage.total_tokens || 0,
|
|
1986
|
+
provider: this.name,
|
|
1987
|
+
model,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
//
|
|
1991
|
+
// Tool Result Handling
|
|
1992
|
+
//
|
|
1993
|
+
formatToolResult(toolCall, result) {
|
|
1994
|
+
return {
|
|
1995
|
+
role: "tool",
|
|
1996
|
+
toolCallId: toolCall.callId,
|
|
1997
|
+
content: result.output,
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
appendToolResult(request, toolCall, result) {
|
|
2001
|
+
const openRouterRequest = request;
|
|
2002
|
+
const toolCallRaw = toolCall.raw;
|
|
2003
|
+
// Add assistant message with the tool call (SDK uses camelCase)
|
|
2004
|
+
openRouterRequest.messages.push({
|
|
2005
|
+
role: "assistant",
|
|
2006
|
+
content: null,
|
|
2007
|
+
toolCalls: [toolCallRaw],
|
|
2008
|
+
});
|
|
2009
|
+
// Add tool result message
|
|
2010
|
+
openRouterRequest.messages.push(this.formatToolResult(toolCall, result));
|
|
2011
|
+
return openRouterRequest;
|
|
2012
|
+
}
|
|
2013
|
+
//
|
|
2014
|
+
// History Management
|
|
2015
|
+
//
|
|
2016
|
+
responseToHistoryItems(response) {
|
|
2017
|
+
const openRouterResponse = response;
|
|
2018
|
+
const historyItems = [];
|
|
2019
|
+
const choice = openRouterResponse.choices[0];
|
|
2020
|
+
if (!choice?.message) {
|
|
2021
|
+
return historyItems;
|
|
2022
|
+
}
|
|
2023
|
+
// Check if this is a tool use response (SDK returns camelCase)
|
|
2024
|
+
if (choice.message.toolCalls && choice.message.toolCalls.length > 0) {
|
|
2025
|
+
// Don't add to history yet - will be added after tool execution
|
|
2026
|
+
return historyItems;
|
|
2027
|
+
}
|
|
2028
|
+
// Extract text content for non-tool responses
|
|
2029
|
+
if (choice.message.content) {
|
|
2030
|
+
historyItems.push({
|
|
2031
|
+
content: choice.message.content,
|
|
2032
|
+
role: exports.LlmMessageRole.Assistant,
|
|
2033
|
+
type: exports.LlmMessageType.Message,
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
return historyItems;
|
|
2037
|
+
}
|
|
2038
|
+
//
|
|
2039
|
+
// Error Classification
|
|
2040
|
+
//
|
|
2041
|
+
classifyError(error) {
|
|
2042
|
+
// Check if error has a status code (HTTP error)
|
|
2043
|
+
const errorWithStatus = error;
|
|
2044
|
+
const statusCode = errorWithStatus.status || errorWithStatus.statusCode;
|
|
2045
|
+
if (statusCode) {
|
|
2046
|
+
// Rate limit error
|
|
2047
|
+
if (statusCode === RATE_LIMIT_STATUS_CODE) {
|
|
2048
|
+
return {
|
|
2049
|
+
error,
|
|
2050
|
+
category: ErrorCategory.RateLimit,
|
|
2051
|
+
shouldRetry: false,
|
|
2052
|
+
suggestedDelayMs: 60000,
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
// Retryable errors (server errors, timeouts, etc.)
|
|
2056
|
+
if (RETRYABLE_STATUS_CODES.includes(statusCode)) {
|
|
2057
|
+
return {
|
|
2058
|
+
error,
|
|
2059
|
+
category: ErrorCategory.Retryable,
|
|
2060
|
+
shouldRetry: true,
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
// Client errors (4xx except 429) are unrecoverable
|
|
2064
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
2065
|
+
return {
|
|
2066
|
+
error,
|
|
2067
|
+
category: ErrorCategory.Unrecoverable,
|
|
2068
|
+
shouldRetry: false,
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
// Check error message for rate limit indicators
|
|
2073
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : "";
|
|
2074
|
+
if (errorMessage.includes("rate limit") ||
|
|
2075
|
+
errorMessage.includes("too many requests")) {
|
|
2076
|
+
return {
|
|
2077
|
+
error,
|
|
2078
|
+
category: ErrorCategory.RateLimit,
|
|
2079
|
+
shouldRetry: false,
|
|
2080
|
+
suggestedDelayMs: 60000,
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
// Unknown error - treat as potentially retryable
|
|
2084
|
+
return {
|
|
2085
|
+
error,
|
|
2086
|
+
category: ErrorCategory.Unknown,
|
|
2087
|
+
shouldRetry: true,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
//
|
|
2091
|
+
// Provider-Specific Features
|
|
2092
|
+
//
|
|
2093
|
+
isComplete(response) {
|
|
2094
|
+
const openRouterResponse = response;
|
|
2095
|
+
const choice = openRouterResponse.choices[0];
|
|
2096
|
+
// Complete if no tool calls (SDK returns camelCase)
|
|
2097
|
+
if (!choice?.message?.toolCalls?.length) {
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
hasStructuredOutput(response) {
|
|
2103
|
+
const openRouterResponse = response;
|
|
2104
|
+
const choice = openRouterResponse.choices[0];
|
|
2105
|
+
// SDK returns camelCase (toolCalls)
|
|
2106
|
+
if (!choice?.message?.toolCalls?.length) {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
// Check if the last tool call is structured_output
|
|
2110
|
+
const lastToolCall = choice.message.toolCalls[choice.message.toolCalls.length - 1];
|
|
2111
|
+
return lastToolCall?.function?.name === STRUCTURED_OUTPUT_TOOL_NAME;
|
|
2112
|
+
}
|
|
2113
|
+
extractStructuredOutput(response) {
|
|
2114
|
+
const openRouterResponse = response;
|
|
2115
|
+
const choice = openRouterResponse.choices[0];
|
|
2116
|
+
// SDK returns camelCase (toolCalls)
|
|
2117
|
+
if (!choice?.message?.toolCalls?.length) {
|
|
2118
|
+
return undefined;
|
|
2119
|
+
}
|
|
2120
|
+
const lastToolCall = choice.message.toolCalls[choice.message.toolCalls.length - 1];
|
|
2121
|
+
if (lastToolCall?.function?.name === STRUCTURED_OUTPUT_TOOL_NAME) {
|
|
2122
|
+
try {
|
|
2123
|
+
return JSON.parse(lastToolCall.function.arguments);
|
|
2124
|
+
}
|
|
2125
|
+
catch {
|
|
2126
|
+
return undefined;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return undefined;
|
|
2130
|
+
}
|
|
2131
|
+
//
|
|
2132
|
+
// Private Helpers
|
|
2133
|
+
//
|
|
2134
|
+
hasToolCalls(response) {
|
|
2135
|
+
const choice = response.choices[0];
|
|
2136
|
+
// SDK returns camelCase (toolCalls)
|
|
2137
|
+
return (choice?.message?.toolCalls?.length ?? 0) > 0;
|
|
2138
|
+
}
|
|
2139
|
+
extractContent(response) {
|
|
2140
|
+
// Check for structured output first
|
|
2141
|
+
if (this.hasStructuredOutput(response)) {
|
|
2142
|
+
return this.extractStructuredOutput(response);
|
|
2143
|
+
}
|
|
2144
|
+
const choice = response.choices[0];
|
|
2145
|
+
return choice?.message?.content ?? undefined;
|
|
2146
|
+
}
|
|
2147
|
+
convertMessagesToOpenRouter(messages, system) {
|
|
2148
|
+
const openRouterMessages = [];
|
|
2149
|
+
// Add system message if provided
|
|
2150
|
+
if (system) {
|
|
2151
|
+
openRouterMessages.push({
|
|
2152
|
+
role: "system",
|
|
2153
|
+
content: system,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
for (const msg of messages) {
|
|
2157
|
+
const message = msg;
|
|
2158
|
+
// Handle different message types
|
|
2159
|
+
if (message.role === "system") {
|
|
2160
|
+
openRouterMessages.push({
|
|
2161
|
+
role: "system",
|
|
2162
|
+
content: message.content,
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
else if (message.role === "user") {
|
|
2166
|
+
openRouterMessages.push({
|
|
2167
|
+
role: "user",
|
|
2168
|
+
content: message.content,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
else if (message.role === "assistant") {
|
|
2172
|
+
const assistantMsg = {
|
|
2173
|
+
role: "assistant",
|
|
2174
|
+
content: message.content || null,
|
|
2175
|
+
};
|
|
2176
|
+
// Include toolCalls if present (check both camelCase and snake_case for compatibility)
|
|
2177
|
+
if (message.toolCalls) {
|
|
2178
|
+
assistantMsg.toolCalls = message.toolCalls;
|
|
2179
|
+
}
|
|
2180
|
+
else if (message.tool_calls) {
|
|
2181
|
+
assistantMsg.toolCalls = message.tool_calls;
|
|
2182
|
+
}
|
|
2183
|
+
openRouterMessages.push(assistantMsg);
|
|
2184
|
+
}
|
|
2185
|
+
else if (message.role === "tool") {
|
|
2186
|
+
openRouterMessages.push({
|
|
2187
|
+
role: "tool",
|
|
2188
|
+
toolCallId: message.toolCallId || message.tool_call_id,
|
|
2189
|
+
content: message.content,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
else if (message.type === exports.LlmMessageType.Message) {
|
|
2193
|
+
// Handle internal message format
|
|
2194
|
+
const role = message.role?.toLowerCase();
|
|
2195
|
+
if (role === "assistant") {
|
|
2196
|
+
openRouterMessages.push({
|
|
2197
|
+
role: "assistant",
|
|
2198
|
+
content: message.content,
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
else {
|
|
2202
|
+
openRouterMessages.push({
|
|
2203
|
+
role: "user",
|
|
2204
|
+
content: message.content,
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return openRouterMessages;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
// Export singleton instance
|
|
2213
|
+
const openRouterAdapter = new OpenRouterAdapter();
|
|
2214
|
+
|
|
2215
|
+
const DEFAULT_TOOL_TYPE = "function";
|
|
2216
|
+
const log = core.log.lib({ lib: core.JAYPIE.LIB.LLM });
|
|
2217
|
+
function logToolMessage(message, context) {
|
|
2218
|
+
log.trace.var({ [context.name]: message });
|
|
2219
|
+
}
|
|
2220
|
+
class Toolkit {
|
|
2221
|
+
constructor(tools, options) {
|
|
2222
|
+
this._tools = tools;
|
|
2223
|
+
this._options = options || {};
|
|
2224
|
+
this.explain = this._options.explain ? true : false;
|
|
2225
|
+
this.log = this._options.log !== undefined ? this._options.log : true;
|
|
2226
|
+
}
|
|
2227
|
+
get tools() {
|
|
2228
|
+
return this._tools.map((tool) => {
|
|
2229
|
+
const toolCopy = { ...tool };
|
|
2230
|
+
delete toolCopy.call;
|
|
2231
|
+
delete toolCopy.message;
|
|
2232
|
+
if (this.explain && toolCopy.parameters?.type === "object") {
|
|
2233
|
+
if (toolCopy.parameters?.properties) {
|
|
2234
|
+
if (!toolCopy.parameters.properties.__Explanation) {
|
|
2235
|
+
toolCopy.parameters.properties.__Explanation = {
|
|
2236
|
+
type: "string",
|
|
2237
|
+
description: `Clearly state why the tool is being called and what larger question it helps answer. For example, "I am checking the location API because the user asked for nearby pizza and I need coordinates to proceed"`,
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
// Set default type if not provided
|
|
2243
|
+
if (!toolCopy.type) {
|
|
2244
|
+
toolCopy.type = DEFAULT_TOOL_TYPE;
|
|
2245
|
+
}
|
|
2246
|
+
return toolCopy;
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
async call({ name, arguments: args }) {
|
|
2250
|
+
const tool = this._tools.find((t) => t.name === name);
|
|
2251
|
+
if (!tool) {
|
|
2252
|
+
throw new Error(`Tool '${name}' not found`);
|
|
2253
|
+
}
|
|
2254
|
+
let parsedArgs;
|
|
2255
|
+
try {
|
|
2256
|
+
parsedArgs = JSON.parse(args);
|
|
2257
|
+
if (this.explain) {
|
|
2258
|
+
delete parsedArgs.__Explanation;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch {
|
|
2262
|
+
parsedArgs = args;
|
|
2263
|
+
}
|
|
2264
|
+
if (this.log !== false) {
|
|
2265
|
+
try {
|
|
2266
|
+
const context = {
|
|
2267
|
+
name,
|
|
2268
|
+
args: parsedArgs,
|
|
2269
|
+
};
|
|
2270
|
+
let message;
|
|
2271
|
+
if (this.explain) {
|
|
2272
|
+
context.explanation = parsedArgs.__Explanation;
|
|
2273
|
+
}
|
|
2274
|
+
if (tool.message) {
|
|
2275
|
+
if (typeof tool.message === "string") {
|
|
2276
|
+
log.trace("[Toolkit] Tool provided string message");
|
|
2277
|
+
message = tool.message;
|
|
2278
|
+
}
|
|
2279
|
+
else if (typeof tool.message === "function") {
|
|
2280
|
+
log.trace("[Toolkit] Tool provided function message");
|
|
2281
|
+
log.trace("[Toolkit] Resolving message result");
|
|
2282
|
+
message = await core.resolveValue(tool.message(parsedArgs, { name }));
|
|
2283
|
+
}
|
|
2284
|
+
else {
|
|
2285
|
+
log.warn("[Toolkit] Tool provided unknown message type");
|
|
2286
|
+
message = String(tool.message);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
else {
|
|
2290
|
+
log.trace("[Toolkit] Log tool call with default message");
|
|
2291
|
+
message = `${tool.name}:${JSON.stringify(parsedArgs)}`;
|
|
2292
|
+
}
|
|
2293
|
+
if (typeof this.log === "function") {
|
|
2294
|
+
log.trace("[Toolkit] Log tool call with custom logger");
|
|
2295
|
+
await core.resolveValue(this.log(message, context));
|
|
922
2296
|
}
|
|
923
2297
|
else {
|
|
924
|
-
log.trace("[
|
|
2298
|
+
log.trace("[Toolkit] Log tool call with default logger");
|
|
2299
|
+
logToolMessage(message, context);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
catch (error) {
|
|
2303
|
+
log.error("[Toolkit] Caught error during logToolCall");
|
|
2304
|
+
log.var({ error });
|
|
2305
|
+
log.debug("[Toolkit] Continuing...");
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return await core.resolveValue(tool.call(parsedArgs));
|
|
2309
|
+
}
|
|
2310
|
+
extend(tools, options = {}) {
|
|
2311
|
+
for (const tool of tools) {
|
|
2312
|
+
const existingIndex = this._tools.findIndex((t) => t.name === tool.name);
|
|
2313
|
+
if (existingIndex !== -1) {
|
|
2314
|
+
if (options.replace === false) {
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
if (options.warn !== false) {
|
|
2318
|
+
if (typeof this.log === "function") {
|
|
2319
|
+
this.log(`[Toolkit] Tool '${tool.name}' already exists, replacing with new tool`, { name: tool.name, args: {} });
|
|
2320
|
+
}
|
|
2321
|
+
else if (this.log) {
|
|
2322
|
+
log.warn(`[Toolkit] Tool '${tool.name}' already exists, replacing with new tool`);
|
|
2323
|
+
}
|
|
925
2324
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
2325
|
+
this._tools[existingIndex] = tool;
|
|
2326
|
+
}
|
|
2327
|
+
else {
|
|
2328
|
+
this._tools.push(tool);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (Object.prototype.hasOwnProperty.call(options, "log")) {
|
|
2332
|
+
this.log = options.log;
|
|
2333
|
+
}
|
|
2334
|
+
if (Object.prototype.hasOwnProperty.call(options, "explain")) {
|
|
2335
|
+
this.explain = options.explain;
|
|
2336
|
+
}
|
|
2337
|
+
return this;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
//
|
|
2342
|
+
//
|
|
2343
|
+
// Main
|
|
2344
|
+
//
|
|
2345
|
+
/**
|
|
2346
|
+
* HookRunner provides a centralized, consistent way to execute lifecycle hooks
|
|
2347
|
+
* during the LLM operate loop. It handles async resolution and provides error
|
|
2348
|
+
* isolation for hook execution.
|
|
2349
|
+
*/
|
|
2350
|
+
class HookRunner {
|
|
2351
|
+
/**
|
|
2352
|
+
* Execute the beforeEachModelRequest hook if defined
|
|
2353
|
+
*/
|
|
2354
|
+
async runBeforeModelRequest(hooks, context) {
|
|
2355
|
+
if (hooks?.beforeEachModelRequest) {
|
|
2356
|
+
await core.resolveValue(hooks.beforeEachModelRequest(context));
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Execute the afterEachModelResponse hook if defined
|
|
2361
|
+
*/
|
|
2362
|
+
async runAfterModelResponse(hooks, context) {
|
|
2363
|
+
if (hooks?.afterEachModelResponse) {
|
|
2364
|
+
await core.resolveValue(hooks.afterEachModelResponse(context));
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Execute the beforeEachTool hook if defined
|
|
2369
|
+
*/
|
|
2370
|
+
async runBeforeTool(hooks, context) {
|
|
2371
|
+
if (hooks?.beforeEachTool) {
|
|
2372
|
+
await core.resolveValue(hooks.beforeEachTool(context));
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Execute the afterEachTool hook if defined
|
|
2377
|
+
*/
|
|
2378
|
+
async runAfterTool(hooks, context) {
|
|
2379
|
+
if (hooks?.afterEachTool) {
|
|
2380
|
+
await core.resolveValue(hooks.afterEachTool(context));
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Execute the onToolError hook if defined
|
|
2385
|
+
*/
|
|
2386
|
+
async runOnToolError(hooks, context) {
|
|
2387
|
+
if (hooks?.onToolError) {
|
|
2388
|
+
await core.resolveValue(hooks.onToolError(context));
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Execute the onRetryableModelError hook if defined
|
|
2393
|
+
*/
|
|
2394
|
+
async runOnRetryableError(hooks, context) {
|
|
2395
|
+
if (hooks?.onRetryableModelError) {
|
|
2396
|
+
await core.resolveValue(hooks.onRetryableModelError(context));
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Execute the onUnrecoverableModelError hook if defined
|
|
2401
|
+
*/
|
|
2402
|
+
async runOnUnrecoverableError(hooks, context) {
|
|
2403
|
+
if (hooks?.onUnrecoverableModelError) {
|
|
2404
|
+
await core.resolveValue(hooks.onUnrecoverableModelError(context));
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
// Export singleton instance for convenience
|
|
2409
|
+
const hookRunner = new HookRunner();
|
|
2410
|
+
|
|
2411
|
+
//
|
|
2412
|
+
//
|
|
2413
|
+
// Main
|
|
2414
|
+
//
|
|
2415
|
+
/**
|
|
2416
|
+
* InputProcessor handles input normalization, placeholder substitution,
|
|
2417
|
+
* and history merging for the operate loop.
|
|
2418
|
+
*/
|
|
2419
|
+
class InputProcessor {
|
|
2420
|
+
/**
|
|
2421
|
+
* Process input with placeholders, history merging, and system message handling
|
|
2422
|
+
*
|
|
2423
|
+
* @param input - The raw input (string, message, or history)
|
|
2424
|
+
* @param options - The operate options containing data, history, system, etc.
|
|
2425
|
+
* @returns Processed input with all transformations applied
|
|
2426
|
+
*/
|
|
2427
|
+
process(input, options = {}) {
|
|
2428
|
+
// Convert input to history format with placeholder substitution
|
|
2429
|
+
let history = this.formatInputWithPlaceholders(input, options);
|
|
2430
|
+
// Process instructions with placeholders
|
|
2431
|
+
const instructions = this.processInstructions(options);
|
|
2432
|
+
// Process system prompt with placeholders
|
|
2433
|
+
const system = this.processSystem(options);
|
|
2434
|
+
// Merge with provided history
|
|
2435
|
+
if (options.history) {
|
|
2436
|
+
history = [...options.history, ...history];
|
|
2437
|
+
}
|
|
2438
|
+
// Handle system message prepending
|
|
2439
|
+
if (system) {
|
|
2440
|
+
history = this.prependSystemMessage(history, system);
|
|
2441
|
+
}
|
|
2442
|
+
return {
|
|
2443
|
+
history,
|
|
2444
|
+
instructions,
|
|
2445
|
+
system,
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Format input and apply placeholders if data is provided
|
|
2450
|
+
*/
|
|
2451
|
+
formatInputWithPlaceholders(input, options) {
|
|
2452
|
+
// Apply placeholders to input if data is provided and placeholders.input is not false
|
|
2453
|
+
if (options.data &&
|
|
2454
|
+
(options.placeholders?.input === undefined || options.placeholders?.input)) {
|
|
2455
|
+
return formatOperateInput(input, { data: options.data });
|
|
2456
|
+
}
|
|
2457
|
+
return formatOperateInput(input);
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Process instructions with placeholder substitution
|
|
2461
|
+
*/
|
|
2462
|
+
processInstructions(options) {
|
|
2463
|
+
if (!options.instructions) {
|
|
2464
|
+
return undefined;
|
|
2465
|
+
}
|
|
2466
|
+
// Apply placeholders to instructions if data is provided and placeholders.instructions is not false
|
|
2467
|
+
if (options.data &&
|
|
2468
|
+
(options.placeholders?.instructions === undefined ||
|
|
2469
|
+
options.placeholders?.instructions)) {
|
|
2470
|
+
return core.placeholders(options.instructions, options.data);
|
|
2471
|
+
}
|
|
2472
|
+
return options.instructions;
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* Process system prompt with placeholder substitution
|
|
2476
|
+
*/
|
|
2477
|
+
processSystem(options) {
|
|
2478
|
+
if (!options.system) {
|
|
2479
|
+
return undefined;
|
|
2480
|
+
}
|
|
2481
|
+
// Apply placeholders to system if data is provided and placeholders.system is not false
|
|
2482
|
+
if (options.data && options.placeholders?.system !== false) {
|
|
2483
|
+
return core.placeholders(options.system, options.data);
|
|
2484
|
+
}
|
|
2485
|
+
return options.system;
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Prepend system message to history, handling duplicates
|
|
2489
|
+
*/
|
|
2490
|
+
prependSystemMessage(history, systemContent) {
|
|
2491
|
+
// Create system message
|
|
2492
|
+
const systemMessage = {
|
|
2493
|
+
content: systemContent,
|
|
2494
|
+
role: exports.LlmMessageRole.System,
|
|
2495
|
+
type: exports.LlmMessageType.Message,
|
|
2496
|
+
};
|
|
2497
|
+
// Check if history starts with an identical system message
|
|
2498
|
+
const firstMessage = history[0];
|
|
2499
|
+
const isIdenticalSystemMessage = firstMessage?.type === exports.LlmMessageType.Message &&
|
|
2500
|
+
firstMessage?.role === exports.LlmMessageRole.System &&
|
|
2501
|
+
firstMessage?.content === systemContent;
|
|
2502
|
+
// If identical, return as-is
|
|
2503
|
+
if (isIdenticalSystemMessage) {
|
|
2504
|
+
return history;
|
|
2505
|
+
}
|
|
2506
|
+
// Remove any existing system message from the beginning
|
|
2507
|
+
if (firstMessage?.type === exports.LlmMessageType.Message &&
|
|
2508
|
+
firstMessage?.role === exports.LlmMessageRole.System) {
|
|
2509
|
+
return [systemMessage, ...history.slice(1)];
|
|
2510
|
+
}
|
|
2511
|
+
// Prepend new system message
|
|
2512
|
+
return [systemMessage, ...history];
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
// Export singleton instance for convenience
|
|
2516
|
+
const inputProcessor = new InputProcessor();
|
|
2517
|
+
|
|
2518
|
+
//
|
|
2519
|
+
//
|
|
2520
|
+
// Main
|
|
2521
|
+
//
|
|
2522
|
+
/**
|
|
2523
|
+
* ResponseBuilder provides a fluent API for constructing LlmOperateResponse objects.
|
|
2524
|
+
* It standardizes response construction across providers.
|
|
2525
|
+
*/
|
|
2526
|
+
class ResponseBuilder {
|
|
2527
|
+
constructor(config) {
|
|
2528
|
+
this.response = {
|
|
2529
|
+
content: undefined,
|
|
2530
|
+
error: undefined,
|
|
2531
|
+
history: [],
|
|
2532
|
+
model: config.model,
|
|
2533
|
+
output: [],
|
|
2534
|
+
provider: config.provider,
|
|
2535
|
+
responses: [],
|
|
2536
|
+
status: LlmResponseStatus.InProgress,
|
|
2537
|
+
usage: [],
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Set the response content
|
|
2542
|
+
*/
|
|
2543
|
+
setContent(content) {
|
|
2544
|
+
this.response.content = content;
|
|
2545
|
+
return this;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Set the response status
|
|
2549
|
+
*/
|
|
2550
|
+
setStatus(status) {
|
|
2551
|
+
this.response.status = status;
|
|
2552
|
+
return this;
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Set an error on the response
|
|
2556
|
+
*/
|
|
2557
|
+
setError(error) {
|
|
2558
|
+
this.response.error = error;
|
|
2559
|
+
return this;
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Set the history
|
|
2563
|
+
*/
|
|
2564
|
+
setHistory(history) {
|
|
2565
|
+
this.response.history = history;
|
|
2566
|
+
return this;
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Append items to the history
|
|
2570
|
+
*/
|
|
2571
|
+
appendToHistory(...items) {
|
|
2572
|
+
this.response.history.push(...items);
|
|
2573
|
+
return this;
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Set the output
|
|
2577
|
+
*/
|
|
2578
|
+
setOutput(output) {
|
|
2579
|
+
this.response.output = output;
|
|
2580
|
+
return this;
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Append items to the output
|
|
2584
|
+
*/
|
|
2585
|
+
appendToOutput(...items) {
|
|
2586
|
+
this.response.output.push(...items);
|
|
2587
|
+
return this;
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Add a raw provider response
|
|
2591
|
+
*/
|
|
2592
|
+
addResponse(response) {
|
|
2593
|
+
this.response.responses.push(response);
|
|
2594
|
+
return this;
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Add a usage entry for a single API call
|
|
2598
|
+
*/
|
|
2599
|
+
addUsage(usage) {
|
|
2600
|
+
this.response.usage.push(usage);
|
|
2601
|
+
return this;
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Set the entire usage array
|
|
2605
|
+
*/
|
|
2606
|
+
setUsage(usage) {
|
|
2607
|
+
this.response.usage = usage;
|
|
2608
|
+
return this;
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Get the current usage array for modifications
|
|
2612
|
+
*/
|
|
2613
|
+
getUsage() {
|
|
2614
|
+
return this.response.usage;
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Get the current history for modifications
|
|
2618
|
+
*/
|
|
2619
|
+
getHistory() {
|
|
2620
|
+
return this.response.history;
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Get the current output for modifications
|
|
2624
|
+
*/
|
|
2625
|
+
getOutput() {
|
|
2626
|
+
return this.response.output;
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Mark response as completed
|
|
2630
|
+
*/
|
|
2631
|
+
complete() {
|
|
2632
|
+
this.response.status = LlmResponseStatus.Completed;
|
|
2633
|
+
return this;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Mark response as incomplete (e.g., max turns exceeded)
|
|
2637
|
+
*/
|
|
2638
|
+
incomplete() {
|
|
2639
|
+
this.response.status = LlmResponseStatus.Incomplete;
|
|
2640
|
+
return this;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Build and return the final response object
|
|
2644
|
+
*/
|
|
2645
|
+
build() {
|
|
2646
|
+
return { ...this.response };
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Factory function to create a new ResponseBuilder
|
|
2651
|
+
*/
|
|
2652
|
+
function createResponseBuilder(config) {
|
|
2653
|
+
return new ResponseBuilder(config);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
//
|
|
2657
|
+
//
|
|
2658
|
+
// Constants
|
|
2659
|
+
//
|
|
2660
|
+
const DEFAULT_INITIAL_DELAY_MS = 1000; // 1 second
|
|
2661
|
+
const DEFAULT_MAX_DELAY_MS = 32000; // 32 seconds
|
|
2662
|
+
const DEFAULT_BACKOFF_FACTOR = 2; // Exponential backoff multiplier
|
|
2663
|
+
const DEFAULT_MAX_RETRIES = 6;
|
|
2664
|
+
const MAX_RETRIES_ABSOLUTE_LIMIT = 72;
|
|
2665
|
+
//
|
|
2666
|
+
//
|
|
2667
|
+
// Main
|
|
2668
|
+
//
|
|
2669
|
+
/**
|
|
2670
|
+
* RetryPolicy encapsulates retry configuration and delay calculation
|
|
2671
|
+
* for the operate loop's retry logic.
|
|
2672
|
+
*/
|
|
2673
|
+
class RetryPolicy {
|
|
2674
|
+
constructor(config = {}) {
|
|
2675
|
+
this.initialDelayMs = config.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
2676
|
+
this.maxDelayMs = config.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
2677
|
+
this.backoffFactor = config.backoffFactor ?? DEFAULT_BACKOFF_FACTOR;
|
|
2678
|
+
this.maxRetries = Math.min(config.maxRetries ?? DEFAULT_MAX_RETRIES, MAX_RETRIES_ABSOLUTE_LIMIT);
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Calculate the delay for a given attempt number (0-indexed)
|
|
2682
|
+
*/
|
|
2683
|
+
getDelayForAttempt(attempt) {
|
|
2684
|
+
const delay = this.initialDelayMs * Math.pow(this.backoffFactor, attempt);
|
|
2685
|
+
return Math.min(delay, this.maxDelayMs);
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Check if another retry should be attempted
|
|
2689
|
+
*/
|
|
2690
|
+
shouldRetry(currentAttempt) {
|
|
2691
|
+
return currentAttempt < this.maxRetries;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
// Export a default policy instance
|
|
2695
|
+
const defaultRetryPolicy = new RetryPolicy();
|
|
2696
|
+
|
|
2697
|
+
//
|
|
2698
|
+
//
|
|
2699
|
+
// Main
|
|
2700
|
+
//
|
|
2701
|
+
/**
|
|
2702
|
+
* RetryExecutor handles the retry loop logic for LLM API calls.
|
|
2703
|
+
* It provides exponential backoff, error classification, and hook execution.
|
|
2704
|
+
*/
|
|
2705
|
+
class RetryExecutor {
|
|
2706
|
+
constructor(config) {
|
|
2707
|
+
this.policy = config.policy ?? defaultRetryPolicy;
|
|
2708
|
+
this.hookRunner = config.hookRunner ?? hookRunner;
|
|
2709
|
+
this.errorClassifier = config.errorClassifier;
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Execute an operation with retry logic
|
|
2713
|
+
*
|
|
2714
|
+
* @param operation - The async operation to execute
|
|
2715
|
+
* @param options - Execution options including context and hooks
|
|
2716
|
+
* @returns The result of the operation
|
|
2717
|
+
* @throws BadGatewayError if all retries are exhausted or error is not retryable
|
|
2718
|
+
*/
|
|
2719
|
+
async execute(operation, options) {
|
|
2720
|
+
let attempt = 0;
|
|
2721
|
+
while (true) {
|
|
2722
|
+
try {
|
|
2723
|
+
const result = await operation();
|
|
2724
|
+
if (attempt > 0) {
|
|
2725
|
+
log$1.debug(`API call succeeded after ${attempt} retries`);
|
|
933
2726
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
log.
|
|
2727
|
+
return result;
|
|
2728
|
+
}
|
|
2729
|
+
catch (error) {
|
|
2730
|
+
// Check if we've exhausted retries
|
|
2731
|
+
if (!this.policy.shouldRetry(attempt)) {
|
|
2732
|
+
log$1.error(`API call failed after ${this.policy.maxRetries} retries`);
|
|
2733
|
+
log$1.var({ error });
|
|
2734
|
+
await this.hookRunner.runOnUnrecoverableError(options.hooks, {
|
|
2735
|
+
input: options.context.input,
|
|
2736
|
+
options: options.context.options,
|
|
2737
|
+
providerRequest: options.context.providerRequest,
|
|
2738
|
+
error,
|
|
2739
|
+
});
|
|
2740
|
+
throw new errors.BadGatewayError();
|
|
941
2741
|
}
|
|
942
|
-
//
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
reasoning: currentResponse.usage.output_tokens_details?.reasoning_tokens ||
|
|
952
|
-
0,
|
|
953
|
-
provider: PROVIDER.OPENAI.NAME,
|
|
954
|
-
model: options?.model || PROVIDER.OPENAI.MODEL.DEFAULT,
|
|
2742
|
+
// Check if error is not retryable
|
|
2743
|
+
if (!this.errorClassifier.isRetryable(error)) {
|
|
2744
|
+
log$1.error("API call failed with non-retryable error");
|
|
2745
|
+
log$1.var({ error });
|
|
2746
|
+
await this.hookRunner.runOnUnrecoverableError(options.hooks, {
|
|
2747
|
+
input: options.context.input,
|
|
2748
|
+
options: options.context.options,
|
|
2749
|
+
providerRequest: options.context.providerRequest,
|
|
2750
|
+
error,
|
|
955
2751
|
});
|
|
2752
|
+
throw new errors.BadGatewayError();
|
|
956
2753
|
}
|
|
957
|
-
//
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
input,
|
|
962
|
-
options,
|
|
963
|
-
providerRequest: requestOptions,
|
|
964
|
-
providerResponse: currentResponse,
|
|
965
|
-
content: extractedContent || "",
|
|
966
|
-
usage: returnResponse.usage,
|
|
967
|
-
}));
|
|
2754
|
+
// Warn if this is an unknown error type
|
|
2755
|
+
if (!this.errorClassifier.isKnownError(error)) {
|
|
2756
|
+
log$1.warn("API returned unknown error type, will retry");
|
|
2757
|
+
log$1.var({ error });
|
|
968
2758
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
2759
|
+
const delay = this.policy.getDelayForAttempt(attempt);
|
|
2760
|
+
log$1.warn(`API call failed. Retrying in ${delay}ms...`);
|
|
2761
|
+
await this.hookRunner.runOnRetryableError(options.hooks, {
|
|
2762
|
+
input: options.context.input,
|
|
2763
|
+
options: options.context.options,
|
|
2764
|
+
providerRequest: options.context.providerRequest,
|
|
2765
|
+
error,
|
|
2766
|
+
});
|
|
2767
|
+
await core.sleep(delay);
|
|
2768
|
+
attempt++;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
//
|
|
2775
|
+
//
|
|
2776
|
+
// Constants
|
|
2777
|
+
//
|
|
2778
|
+
const ERROR = {
|
|
2779
|
+
BAD_FUNCTION_CALL: "Bad Function Call",
|
|
2780
|
+
};
|
|
2781
|
+
//
|
|
2782
|
+
//
|
|
2783
|
+
// Helpers
|
|
2784
|
+
//
|
|
2785
|
+
/**
|
|
2786
|
+
* Create an ErrorClassifier from a ProviderAdapter
|
|
2787
|
+
*/
|
|
2788
|
+
function createErrorClassifier(adapter) {
|
|
2789
|
+
return {
|
|
2790
|
+
isRetryable: (error) => adapter.isRetryableError(error),
|
|
2791
|
+
isKnownError: (error) => {
|
|
2792
|
+
const classified = adapter.classifyError(error);
|
|
2793
|
+
return classified.category !== "unknown";
|
|
2794
|
+
},
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
//
|
|
2798
|
+
//
|
|
2799
|
+
// Main
|
|
2800
|
+
//
|
|
2801
|
+
/**
|
|
2802
|
+
* OperateLoop implements the core multi-turn conversation loop.
|
|
2803
|
+
* It orchestrates provider adapters, retry logic, hook execution, and tool calling.
|
|
2804
|
+
*
|
|
2805
|
+
* This class uses Template Method + Strategy patterns:
|
|
2806
|
+
* - Template Method: The execute() method defines the algorithm skeleton
|
|
2807
|
+
* - Strategy: Provider adapters handle provider-specific operations
|
|
2808
|
+
*/
|
|
2809
|
+
class OperateLoop {
|
|
2810
|
+
constructor(config) {
|
|
2811
|
+
this.adapter = config.adapter;
|
|
2812
|
+
this.client = config.client;
|
|
2813
|
+
this.hookRunnerInstance = config.hookRunner ?? hookRunner;
|
|
2814
|
+
this.inputProcessorInstance = config.inputProcessor ?? inputProcessor;
|
|
2815
|
+
this.maxRetries = config.maxRetries ?? 6;
|
|
2816
|
+
this.retryPolicy = config.retryPolicy ?? defaultRetryPolicy;
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Execute the operate loop for multi-turn conversations with tool calling.
|
|
2820
|
+
*/
|
|
2821
|
+
async execute(input, options = {}) {
|
|
2822
|
+
// Initialize state
|
|
2823
|
+
const state = this.initializeState(input, options);
|
|
2824
|
+
const context = this.createContext(options);
|
|
2825
|
+
// Build initial request
|
|
2826
|
+
let request = this.buildInitialRequest(state, options);
|
|
2827
|
+
// Multi-turn loop
|
|
2828
|
+
while (state.currentTurn < state.maxTurns) {
|
|
2829
|
+
state.currentTurn++;
|
|
2830
|
+
// Execute one turn with retry logic
|
|
2831
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
2832
|
+
if (!shouldContinue) {
|
|
2833
|
+
break;
|
|
2834
|
+
}
|
|
2835
|
+
// Rebuild request with updated history for next turn
|
|
2836
|
+
request = {
|
|
2837
|
+
format: state.formattedFormat,
|
|
2838
|
+
instructions: options.instructions,
|
|
2839
|
+
messages: state.currentInput,
|
|
2840
|
+
model: options.model ?? this.adapter.defaultModel,
|
|
2841
|
+
providerOptions: options.providerOptions,
|
|
2842
|
+
system: options.system,
|
|
2843
|
+
tools: state.formattedTools,
|
|
2844
|
+
user: options.user,
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
return state.responseBuilder.build();
|
|
2848
|
+
}
|
|
2849
|
+
//
|
|
2850
|
+
// Private Methods
|
|
2851
|
+
//
|
|
2852
|
+
initializeState(input, options) {
|
|
2853
|
+
// Process input with placeholders
|
|
2854
|
+
const processedInput = this.inputProcessorInstance.process(input, options);
|
|
2855
|
+
// Determine max turns
|
|
2856
|
+
const maxTurns = maxTurnsFromOptions(options);
|
|
2857
|
+
// Initialize response builder
|
|
2858
|
+
const responseBuilderConfig = {
|
|
2859
|
+
model: options.model ?? this.adapter.defaultModel,
|
|
2860
|
+
provider: this.adapter.name,
|
|
2861
|
+
};
|
|
2862
|
+
const responseBuilder = createResponseBuilder(responseBuilderConfig);
|
|
2863
|
+
// Set initial history
|
|
2864
|
+
responseBuilder.setHistory([...processedInput.history]);
|
|
2865
|
+
// Get toolkit
|
|
2866
|
+
let toolkit;
|
|
2867
|
+
if (options.tools) {
|
|
2868
|
+
if (options.tools instanceof Toolkit) {
|
|
2869
|
+
toolkit = options.tools;
|
|
2870
|
+
}
|
|
2871
|
+
else if (Array.isArray(options.tools) && options.tools.length > 0) {
|
|
2872
|
+
const explain = options.explain ?? false;
|
|
2873
|
+
toolkit = new Toolkit(options.tools, { explain });
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
// Format output schema through adapter if provided
|
|
2877
|
+
let formattedFormat;
|
|
2878
|
+
if (options.format) {
|
|
2879
|
+
formattedFormat = this.adapter.formatOutputSchema(options.format);
|
|
2880
|
+
}
|
|
2881
|
+
// Format tools through adapter
|
|
2882
|
+
const formattedTools = toolkit
|
|
2883
|
+
? this.adapter.formatTools(toolkit, formattedFormat)
|
|
2884
|
+
: undefined;
|
|
2885
|
+
return {
|
|
2886
|
+
currentInput: processedInput.history,
|
|
2887
|
+
currentTurn: 0,
|
|
2888
|
+
formattedFormat,
|
|
2889
|
+
formattedTools,
|
|
2890
|
+
maxTurns,
|
|
2891
|
+
responseBuilder,
|
|
2892
|
+
toolkit,
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
createContext(options) {
|
|
2896
|
+
return {
|
|
2897
|
+
hooks: options.hooks ?? {},
|
|
2898
|
+
options,
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
buildInitialRequest(state, options) {
|
|
2902
|
+
return {
|
|
2903
|
+
format: state.formattedFormat,
|
|
2904
|
+
instructions: options.instructions,
|
|
2905
|
+
messages: state.currentInput,
|
|
2906
|
+
model: options.model ?? this.adapter.defaultModel,
|
|
2907
|
+
providerOptions: options.providerOptions,
|
|
2908
|
+
system: options.system,
|
|
2909
|
+
tools: state.formattedTools,
|
|
2910
|
+
user: options.user,
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
async executeOneTurn(request, state, context, options) {
|
|
2914
|
+
// Create error classifier from adapter
|
|
2915
|
+
const errorClassifier = createErrorClassifier(this.adapter);
|
|
2916
|
+
// Create retry executor for this turn
|
|
2917
|
+
const retryExecutor = new RetryExecutor({
|
|
2918
|
+
errorClassifier,
|
|
2919
|
+
hookRunner: this.hookRunnerInstance,
|
|
2920
|
+
policy: this.retryPolicy,
|
|
2921
|
+
});
|
|
2922
|
+
// Build provider-specific request
|
|
2923
|
+
const providerRequest = this.adapter.buildRequest(request);
|
|
2924
|
+
// Execute beforeEachModelRequest hook
|
|
2925
|
+
await this.hookRunnerInstance.runBeforeModelRequest(context.hooks, {
|
|
2926
|
+
input: state.currentInput,
|
|
2927
|
+
options,
|
|
2928
|
+
providerRequest,
|
|
2929
|
+
});
|
|
2930
|
+
// Execute with retry (RetryExecutor handles error hooks and throws appropriate errors)
|
|
2931
|
+
const response = await retryExecutor.execute(() => this.adapter.executeRequest(this.client, providerRequest), {
|
|
2932
|
+
context: {
|
|
2933
|
+
input: state.currentInput,
|
|
2934
|
+
options,
|
|
2935
|
+
providerRequest,
|
|
2936
|
+
},
|
|
2937
|
+
hooks: context.hooks,
|
|
2938
|
+
});
|
|
2939
|
+
// Parse response
|
|
2940
|
+
const parsed = this.adapter.parseResponse(response, options);
|
|
2941
|
+
// Track usage
|
|
2942
|
+
if (parsed.usage) {
|
|
2943
|
+
state.responseBuilder.addUsage(parsed.usage);
|
|
2944
|
+
}
|
|
2945
|
+
// Add raw response
|
|
2946
|
+
state.responseBuilder.addResponse(parsed.raw);
|
|
2947
|
+
// Execute afterEachModelResponse hook
|
|
2948
|
+
const currentUsage = state.responseBuilder.build().usage;
|
|
2949
|
+
await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
|
|
2950
|
+
content: parsed.content ?? "",
|
|
2951
|
+
input: state.currentInput,
|
|
2952
|
+
options,
|
|
2953
|
+
providerRequest,
|
|
2954
|
+
providerResponse: response,
|
|
2955
|
+
usage: currentUsage,
|
|
2956
|
+
});
|
|
2957
|
+
// Check for structured output (Anthropic magic tool pattern)
|
|
2958
|
+
if (this.adapter.hasStructuredOutput(response)) {
|
|
2959
|
+
const structuredOutput = this.adapter.extractStructuredOutput(response);
|
|
2960
|
+
if (structuredOutput) {
|
|
2961
|
+
state.responseBuilder.setContent(structuredOutput);
|
|
2962
|
+
state.responseBuilder.complete();
|
|
2963
|
+
return false; // Stop loop
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
// Handle tool calls
|
|
2967
|
+
if (parsed.hasToolCalls) {
|
|
2968
|
+
const toolCalls = this.adapter.extractToolCalls(response);
|
|
2969
|
+
if (toolCalls.length > 0 && state.toolkit && state.maxTurns > 1) {
|
|
2970
|
+
// Track updated provider request for tool results
|
|
2971
|
+
let currentProviderRequest = providerRequest;
|
|
2972
|
+
// Add all response output items to the request BEFORE processing tool calls
|
|
2973
|
+
// This is critical for OpenAI which requires reasoning items to be present
|
|
2974
|
+
// when function_call items reference them
|
|
2975
|
+
const responseItems = this.adapter.responseToHistoryItems(response);
|
|
2976
|
+
this.appendResponseItemsToRequest(currentProviderRequest, responseItems);
|
|
2977
|
+
// Process each tool call
|
|
2978
|
+
for (const toolCall of toolCalls) {
|
|
2979
|
+
try {
|
|
2980
|
+
// Execute beforeEachTool hook
|
|
2981
|
+
await this.hookRunnerInstance.runBeforeTool(context.hooks, {
|
|
2982
|
+
args: toolCall.arguments,
|
|
2983
|
+
toolName: toolCall.name,
|
|
2984
|
+
});
|
|
2985
|
+
// Call the tool
|
|
2986
|
+
log$1.trace(`[operate] Calling tool - ${toolCall.name}`);
|
|
2987
|
+
const result = await state.toolkit.call({
|
|
2988
|
+
arguments: toolCall.arguments,
|
|
2989
|
+
name: toolCall.name,
|
|
2990
|
+
});
|
|
2991
|
+
// Execute afterEachTool hook
|
|
2992
|
+
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|
|
2993
|
+
args: toolCall.arguments,
|
|
2994
|
+
result,
|
|
2995
|
+
toolName: toolCall.name,
|
|
2996
|
+
});
|
|
2997
|
+
// Format result and append to request
|
|
2998
|
+
const formattedResult = {
|
|
2999
|
+
callId: toolCall.callId,
|
|
3000
|
+
output: JSON.stringify(result),
|
|
3001
|
+
success: true,
|
|
3002
|
+
};
|
|
3003
|
+
// Update provider request with tool result
|
|
3004
|
+
currentProviderRequest = this.adapter.appendToolResult(currentProviderRequest, toolCall, formattedResult);
|
|
3005
|
+
// Sync state from updated request
|
|
3006
|
+
this.syncInputFromRequest(state, currentProviderRequest);
|
|
3007
|
+
// Add tool result to history
|
|
3008
|
+
const toolResultFormatted = this.adapter.formatToolResult(toolCall, formattedResult);
|
|
3009
|
+
state.responseBuilder.appendToHistory(toolResultFormatted);
|
|
3010
|
+
}
|
|
3011
|
+
catch (error) {
|
|
3012
|
+
// Execute onToolError hook
|
|
3013
|
+
await this.hookRunnerInstance.runOnToolError(context.hooks, {
|
|
3014
|
+
args: toolCall.arguments,
|
|
3015
|
+
error: error,
|
|
3016
|
+
toolName: toolCall.name,
|
|
3017
|
+
});
|
|
3018
|
+
// Set error on response
|
|
3019
|
+
const jaypieError = new errors.BadGatewayError();
|
|
3020
|
+
const detail = [
|
|
3021
|
+
`Error executing function call ${toolCall.name}.`,
|
|
3022
|
+
error.message,
|
|
3023
|
+
].join("\n");
|
|
3024
|
+
state.responseBuilder.setError({
|
|
3025
|
+
detail,
|
|
3026
|
+
status: jaypieError.status,
|
|
3027
|
+
title: ERROR.BAD_FUNCTION_CALL,
|
|
3028
|
+
});
|
|
3029
|
+
log$1.error(`Error executing function call ${toolCall.name}`);
|
|
3030
|
+
log$1.var({ error });
|
|
1083
3031
|
}
|
|
1084
3032
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
// This helps with test mocks that might not have the expected structure
|
|
1088
|
-
log.warn("Error processing response for function calls");
|
|
1089
|
-
log.var({ error });
|
|
1090
|
-
}
|
|
1091
|
-
// Set content using the shared extraction function
|
|
1092
|
-
returnResponse.content = extractContentFromResponse(currentResponse, options);
|
|
1093
|
-
// If there's no function call or we can't take another turn, exit the loop
|
|
1094
|
-
if (!hasFunctionCall || !enableMultipleTurns) {
|
|
1095
|
-
returnResponse.status = LlmResponseStatus.Completed;
|
|
1096
|
-
return returnResponse;
|
|
1097
|
-
}
|
|
1098
|
-
// If we've reached the maximum number of turns, exit the loop
|
|
1099
|
-
if (currentTurn >= maxTurns) {
|
|
3033
|
+
// Check if we've reached max turns
|
|
3034
|
+
if (state.currentTurn >= state.maxTurns) {
|
|
1100
3035
|
const error = new errors.TooManyRequestsError();
|
|
1101
|
-
const detail = `Model requested function call but exceeded ${maxTurns} turns`;
|
|
1102
|
-
log.warn(detail);
|
|
1103
|
-
|
|
1104
|
-
returnResponse.error = {
|
|
3036
|
+
const detail = `Model requested function call but exceeded ${state.maxTurns} turns`;
|
|
3037
|
+
log$1.warn(detail);
|
|
3038
|
+
state.responseBuilder.setError({
|
|
1105
3039
|
detail,
|
|
1106
3040
|
status: error.status,
|
|
1107
3041
|
title: error.title,
|
|
1108
|
-
};
|
|
1109
|
-
|
|
3042
|
+
});
|
|
3043
|
+
state.responseBuilder.incomplete();
|
|
3044
|
+
return false; // Stop loop
|
|
1110
3045
|
}
|
|
1111
|
-
// Continue to next turn
|
|
1112
|
-
|
|
3046
|
+
return true; // Continue to next turn
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
// No tool calls or no toolkit - we're done
|
|
3050
|
+
state.responseBuilder.setContent(parsed.content);
|
|
3051
|
+
state.responseBuilder.complete();
|
|
3052
|
+
// Add final history items
|
|
3053
|
+
const historyItems = this.adapter.responseToHistoryItems(parsed.raw);
|
|
3054
|
+
for (const item of historyItems) {
|
|
3055
|
+
state.responseBuilder.appendToHistory(item);
|
|
3056
|
+
}
|
|
3057
|
+
return false; // Stop loop
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Sync the current input state from the updated provider request.
|
|
3061
|
+
* This is necessary because appendToolResult modifies the provider-specific request,
|
|
3062
|
+
* and we need to keep our state in sync.
|
|
3063
|
+
*/
|
|
3064
|
+
syncInputFromRequest(state, updatedRequest) {
|
|
3065
|
+
// Extract input/messages from the updated request
|
|
3066
|
+
// This is provider-specific but follows common patterns
|
|
3067
|
+
const request = updatedRequest;
|
|
3068
|
+
if (Array.isArray(request.input)) {
|
|
3069
|
+
// OpenAI format
|
|
3070
|
+
state.currentInput = request.input;
|
|
3071
|
+
}
|
|
3072
|
+
else if (Array.isArray(request.messages)) {
|
|
3073
|
+
// Anthropic format
|
|
3074
|
+
state.currentInput = request.messages;
|
|
3075
|
+
}
|
|
3076
|
+
else if (Array.isArray(request.contents)) {
|
|
3077
|
+
// Gemini format - convert contents to history items
|
|
3078
|
+
state.currentInput = this.convertGeminiContentsToHistory(request.contents);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Convert Gemini contents format to internal history format.
|
|
3083
|
+
*/
|
|
3084
|
+
convertGeminiContentsToHistory(contents) {
|
|
3085
|
+
const history = [];
|
|
3086
|
+
for (const content of contents) {
|
|
3087
|
+
if (!content.parts)
|
|
3088
|
+
continue;
|
|
3089
|
+
for (const part of content.parts) {
|
|
3090
|
+
if (part.text && typeof part.text === "string") {
|
|
3091
|
+
// Regular text message
|
|
3092
|
+
history.push({
|
|
3093
|
+
role: content.role === "model"
|
|
3094
|
+
? exports.LlmMessageRole.Assistant
|
|
3095
|
+
: exports.LlmMessageRole.User,
|
|
3096
|
+
content: part.text,
|
|
3097
|
+
type: exports.LlmMessageType.Message,
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
else if (part.functionCall) {
|
|
3101
|
+
// Function call
|
|
3102
|
+
const fc = part.functionCall;
|
|
3103
|
+
history.push({
|
|
3104
|
+
type: exports.LlmMessageType.FunctionCall,
|
|
3105
|
+
name: fc.name || "",
|
|
3106
|
+
arguments: JSON.stringify(fc.args || {}),
|
|
3107
|
+
call_id: fc.id || "",
|
|
3108
|
+
id: fc.id || "",
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
else if (part.functionResponse) {
|
|
3112
|
+
// Function response
|
|
3113
|
+
const fr = part.functionResponse;
|
|
3114
|
+
// Store name in the object even though it's not part of LlmToolResult type
|
|
3115
|
+
// This allows round-trip conversion back to Gemini format
|
|
3116
|
+
history.push({
|
|
3117
|
+
type: exports.LlmMessageType.FunctionCallOutput,
|
|
3118
|
+
output: JSON.stringify(fr.response || {}),
|
|
3119
|
+
call_id: "",
|
|
3120
|
+
name: fr.name || "",
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return history;
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* Append response items to the provider request.
|
|
3129
|
+
* This adds all output items from a response (including reasoning, function_calls, etc.)
|
|
3130
|
+
* to the request's input/messages array.
|
|
3131
|
+
*
|
|
3132
|
+
* This is critical for OpenAI which requires reasoning items to be present
|
|
3133
|
+
* when function_call items reference them.
|
|
3134
|
+
*/
|
|
3135
|
+
appendResponseItemsToRequest(request, responseItems) {
|
|
3136
|
+
const requestObj = request;
|
|
3137
|
+
if (Array.isArray(requestObj.input)) {
|
|
3138
|
+
// OpenAI format
|
|
3139
|
+
requestObj.input.push(...responseItems);
|
|
3140
|
+
}
|
|
3141
|
+
else if (Array.isArray(requestObj.messages)) {
|
|
3142
|
+
// Anthropic format
|
|
3143
|
+
requestObj.messages.push(...responseItems);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
//
|
|
3148
|
+
//
|
|
3149
|
+
// Factory
|
|
3150
|
+
//
|
|
3151
|
+
/**
|
|
3152
|
+
* Create an OperateLoop instance with the specified configuration.
|
|
3153
|
+
*/
|
|
3154
|
+
function createOperateLoop(config) {
|
|
3155
|
+
return new OperateLoop(config);
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// Logger
|
|
3159
|
+
const getLogger$3 = () => core.log.lib({ lib: core.JAYPIE.LIB.LLM });
|
|
3160
|
+
// Client initialization
|
|
3161
|
+
async function initializeClient$3({ apiKey, } = {}) {
|
|
3162
|
+
const logger = getLogger$3();
|
|
3163
|
+
const resolvedApiKey = apiKey || (await aws.getEnvSecret("ANTHROPIC_API_KEY"));
|
|
3164
|
+
if (!resolvedApiKey) {
|
|
3165
|
+
throw new core.ConfigurationError("The application could not resolve the required API key: ANTHROPIC_API_KEY");
|
|
3166
|
+
}
|
|
3167
|
+
const client = new Anthropic({ apiKey: resolvedApiKey });
|
|
3168
|
+
logger.trace("Initialized Anthropic client");
|
|
3169
|
+
return client;
|
|
3170
|
+
}
|
|
3171
|
+
// Message formatting functions
|
|
3172
|
+
function formatSystemMessage$2(systemPrompt, { data, placeholders } = {}) {
|
|
3173
|
+
return placeholders?.system === false
|
|
3174
|
+
? systemPrompt
|
|
3175
|
+
: core.placeholders(systemPrompt, data);
|
|
3176
|
+
}
|
|
3177
|
+
function formatUserMessage$3(message, { data, placeholders } = {}) {
|
|
3178
|
+
const content = placeholders?.message === false
|
|
3179
|
+
? message
|
|
3180
|
+
: core.placeholders(message, data);
|
|
3181
|
+
return {
|
|
3182
|
+
role: PROVIDER.ANTHROPIC.ROLE.USER,
|
|
3183
|
+
content,
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
function prepareMessages$3(message, { data, placeholders } = {}) {
|
|
3187
|
+
const logger = getLogger$3();
|
|
3188
|
+
const messages = [];
|
|
3189
|
+
// Add user message (necessary for all requests)
|
|
3190
|
+
const userMessage = formatUserMessage$3(message, { data, placeholders });
|
|
3191
|
+
messages.push(userMessage);
|
|
3192
|
+
logger.trace(`User message: ${userMessage.content.length} characters`);
|
|
3193
|
+
return messages;
|
|
3194
|
+
}
|
|
3195
|
+
// Basic text completion
|
|
3196
|
+
async function createTextCompletion$1(client, messages, model, systemMessage) {
|
|
3197
|
+
core.log.trace("Using text output (unstructured)");
|
|
3198
|
+
const params = {
|
|
3199
|
+
model,
|
|
3200
|
+
messages,
|
|
3201
|
+
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
3202
|
+
};
|
|
3203
|
+
// Add system instruction if provided
|
|
3204
|
+
if (systemMessage) {
|
|
3205
|
+
params.system = systemMessage;
|
|
3206
|
+
core.log.trace(`System message: ${systemMessage.length} characters`);
|
|
3207
|
+
}
|
|
3208
|
+
const response = await client.messages.create(params);
|
|
3209
|
+
const firstContent = response.content[0];
|
|
3210
|
+
const text = firstContent && "text" in firstContent ? firstContent.text : "";
|
|
3211
|
+
core.log.trace(`Assistant reply: ${text.length} characters`);
|
|
3212
|
+
return text;
|
|
3213
|
+
}
|
|
3214
|
+
// Structured output completion
|
|
3215
|
+
async function createStructuredCompletion$1(client, messages, model, responseSchema, systemMessage) {
|
|
3216
|
+
core.log.trace("Using structured output");
|
|
3217
|
+
// Get the JSON schema for the response
|
|
3218
|
+
const schema = responseSchema instanceof v4.z.ZodType
|
|
3219
|
+
? responseSchema
|
|
3220
|
+
: naturalZodSchema(responseSchema);
|
|
3221
|
+
// Set system message with JSON instructions
|
|
3222
|
+
const defaultSystemPrompt = "You will be responding with structured JSON data. " +
|
|
3223
|
+
"Format your entire response as a valid JSON object with the following structure: " +
|
|
3224
|
+
JSON.stringify(v4.z.toJSONSchema(schema));
|
|
3225
|
+
const systemPrompt = systemMessage || defaultSystemPrompt;
|
|
3226
|
+
try {
|
|
3227
|
+
// Use standard Anthropic API to get response
|
|
3228
|
+
const params = {
|
|
3229
|
+
model,
|
|
3230
|
+
messages,
|
|
3231
|
+
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
3232
|
+
system: systemPrompt,
|
|
3233
|
+
};
|
|
3234
|
+
const response = await client.messages.create(params);
|
|
3235
|
+
// Extract text from response
|
|
3236
|
+
const firstContent = response.content[0];
|
|
3237
|
+
const responseText = firstContent && "text" in firstContent ? firstContent.text : "";
|
|
3238
|
+
// Find JSON in response
|
|
3239
|
+
const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/) ||
|
|
3240
|
+
responseText.match(/\{[\s\S]*\}/);
|
|
3241
|
+
if (jsonMatch) {
|
|
3242
|
+
try {
|
|
3243
|
+
// Parse the JSON response
|
|
3244
|
+
const jsonStr = jsonMatch[1] || jsonMatch[0];
|
|
3245
|
+
const result = JSON.parse(jsonStr);
|
|
3246
|
+
if (!schema.parse(result)) {
|
|
3247
|
+
throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
|
|
3248
|
+
}
|
|
3249
|
+
core.log.trace("Received structured response", { result });
|
|
3250
|
+
return result;
|
|
3251
|
+
}
|
|
3252
|
+
catch {
|
|
3253
|
+
throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
// If we can't extract JSON
|
|
3257
|
+
throw new Error("Failed to parse structured response from Anthropic");
|
|
3258
|
+
}
|
|
3259
|
+
catch (error) {
|
|
3260
|
+
core.log.error("Error creating structured completion", { error });
|
|
3261
|
+
throw error;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// Maps Jaypie roles to Anthropic roles
|
|
3266
|
+
({
|
|
3267
|
+
[exports.LlmMessageRole.User]: PROVIDER.ANTHROPIC.ROLE.USER,
|
|
3268
|
+
[exports.LlmMessageRole.System]: PROVIDER.ANTHROPIC.ROLE.SYSTEM,
|
|
3269
|
+
[exports.LlmMessageRole.Assistant]: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
3270
|
+
[exports.LlmMessageRole.Developer]: PROVIDER.ANTHROPIC.ROLE.SYSTEM,
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
// Main class implementation
|
|
3274
|
+
class AnthropicProvider {
|
|
3275
|
+
constructor(model = PROVIDER.ANTHROPIC.MODEL.DEFAULT, { apiKey } = {}) {
|
|
3276
|
+
this.log = getLogger$3();
|
|
3277
|
+
this.conversationHistory = [];
|
|
3278
|
+
this.model = model;
|
|
3279
|
+
this.apiKey = apiKey;
|
|
3280
|
+
}
|
|
3281
|
+
async getClient() {
|
|
3282
|
+
if (this._client) {
|
|
3283
|
+
return this._client;
|
|
3284
|
+
}
|
|
3285
|
+
this._client = await initializeClient$3({ apiKey: this.apiKey });
|
|
3286
|
+
return this._client;
|
|
3287
|
+
}
|
|
3288
|
+
async getOperateLoop() {
|
|
3289
|
+
if (this._operateLoop) {
|
|
3290
|
+
return this._operateLoop;
|
|
3291
|
+
}
|
|
3292
|
+
const client = await this.getClient();
|
|
3293
|
+
this._operateLoop = createOperateLoop({
|
|
3294
|
+
adapter: anthropicAdapter,
|
|
3295
|
+
client,
|
|
3296
|
+
});
|
|
3297
|
+
return this._operateLoop;
|
|
3298
|
+
}
|
|
3299
|
+
// Main send method
|
|
3300
|
+
async send(message, options) {
|
|
3301
|
+
const client = await this.getClient();
|
|
3302
|
+
const messages = prepareMessages$3(message, options || {});
|
|
3303
|
+
const modelToUse = options?.model || this.model;
|
|
3304
|
+
// Process system message if provided
|
|
3305
|
+
let systemMessage;
|
|
3306
|
+
if (options?.system) {
|
|
3307
|
+
systemMessage = formatSystemMessage$2(options.system, {
|
|
3308
|
+
data: options.data,
|
|
3309
|
+
placeholders: options.placeholders,
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
if (options?.response) {
|
|
3313
|
+
return createStructuredCompletion$1(client, messages, modelToUse, options.response, systemMessage);
|
|
3314
|
+
}
|
|
3315
|
+
return createTextCompletion$1(client, messages, modelToUse, systemMessage);
|
|
3316
|
+
}
|
|
3317
|
+
async operate(input, options = {}) {
|
|
3318
|
+
const operateLoop = await this.getOperateLoop();
|
|
3319
|
+
const mergedOptions = { ...options, model: options.model ?? this.model };
|
|
3320
|
+
// Create a merged history including both the tracked history and any explicitly provided history
|
|
3321
|
+
if (this.conversationHistory.length > 0) {
|
|
3322
|
+
// If options.history exists, merge with instance history, otherwise use instance history
|
|
3323
|
+
mergedOptions.history = options.history
|
|
3324
|
+
? [...this.conversationHistory, ...options.history]
|
|
3325
|
+
: [...this.conversationHistory];
|
|
3326
|
+
}
|
|
3327
|
+
// Execute operate loop
|
|
3328
|
+
const response = await operateLoop.execute(input, mergedOptions);
|
|
3329
|
+
// Update conversation history with the new history from the response
|
|
3330
|
+
if (response.history && response.history.length > 0) {
|
|
3331
|
+
this.conversationHistory = response.history;
|
|
3332
|
+
}
|
|
3333
|
+
return response;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// Logger
|
|
3338
|
+
const getLogger$2 = () => core.log.lib({ lib: core.JAYPIE.LIB.LLM });
|
|
3339
|
+
// Client initialization
|
|
3340
|
+
async function initializeClient$2({ apiKey, } = {}) {
|
|
3341
|
+
const logger = getLogger$2();
|
|
3342
|
+
const resolvedApiKey = apiKey || (await aws.getEnvSecret("GEMINI_API_KEY"));
|
|
3343
|
+
if (!resolvedApiKey) {
|
|
3344
|
+
throw new core.ConfigurationError("The application could not resolve the requested keys");
|
|
3345
|
+
}
|
|
3346
|
+
const client = new genai.GoogleGenAI({ apiKey: resolvedApiKey });
|
|
3347
|
+
logger.trace("Initialized Gemini client");
|
|
3348
|
+
return client;
|
|
3349
|
+
}
|
|
3350
|
+
function formatUserMessage$2(message, { data, placeholders } = {}) {
|
|
3351
|
+
const content = placeholders?.message === false
|
|
3352
|
+
? message
|
|
3353
|
+
: core.placeholders(message, data);
|
|
3354
|
+
return {
|
|
3355
|
+
role: "user",
|
|
3356
|
+
content,
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
function prepareMessages$2(message, { data, placeholders } = {}) {
|
|
3360
|
+
const logger = getLogger$2();
|
|
3361
|
+
const messages = [];
|
|
3362
|
+
let systemInstruction;
|
|
3363
|
+
// Note: Gemini handles system prompts differently via systemInstruction config
|
|
3364
|
+
// This function is kept for compatibility but system prompts should be passed
|
|
3365
|
+
// via the systemInstruction parameter in generateContent
|
|
3366
|
+
const userMessage = formatUserMessage$2(message, { data, placeholders });
|
|
3367
|
+
messages.push(userMessage);
|
|
3368
|
+
logger.trace(`User message: ${userMessage.content?.length} characters`);
|
|
3369
|
+
return { messages, systemInstruction };
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
class GeminiProvider {
|
|
3373
|
+
constructor(model = PROVIDER.GEMINI.MODEL.DEFAULT, { apiKey } = {}) {
|
|
3374
|
+
this.log = getLogger$2();
|
|
3375
|
+
this.conversationHistory = [];
|
|
3376
|
+
this.model = model;
|
|
3377
|
+
this.apiKey = apiKey;
|
|
3378
|
+
}
|
|
3379
|
+
async getClient() {
|
|
3380
|
+
if (this._client) {
|
|
3381
|
+
return this._client;
|
|
3382
|
+
}
|
|
3383
|
+
this._client = await initializeClient$2({ apiKey: this.apiKey });
|
|
3384
|
+
return this._client;
|
|
3385
|
+
}
|
|
3386
|
+
async getOperateLoop() {
|
|
3387
|
+
if (this._operateLoop) {
|
|
3388
|
+
return this._operateLoop;
|
|
3389
|
+
}
|
|
3390
|
+
const client = await this.getClient();
|
|
3391
|
+
this._operateLoop = createOperateLoop({
|
|
3392
|
+
adapter: geminiAdapter,
|
|
3393
|
+
client,
|
|
3394
|
+
});
|
|
3395
|
+
return this._operateLoop;
|
|
3396
|
+
}
|
|
3397
|
+
async send(message, options) {
|
|
3398
|
+
const client = await this.getClient();
|
|
3399
|
+
const { messages } = prepareMessages$2(message, options);
|
|
3400
|
+
const modelToUse = options?.model || this.model;
|
|
3401
|
+
// Build the request config
|
|
3402
|
+
const config = {};
|
|
3403
|
+
if (options?.system) {
|
|
3404
|
+
config.systemInstruction = options.system;
|
|
3405
|
+
}
|
|
3406
|
+
// Handle structured output via responseSchema
|
|
3407
|
+
if (options?.response) {
|
|
3408
|
+
config.responseMimeType = "application/json";
|
|
3409
|
+
// Convert the response schema to JSON schema format
|
|
3410
|
+
// Note: For simple send() calls, we'll use Gemini's native JSON response
|
|
3411
|
+
}
|
|
3412
|
+
const response = await client.models.generateContent({
|
|
3413
|
+
model: modelToUse,
|
|
3414
|
+
contents: messages.map((m) => ({
|
|
3415
|
+
role: m.role,
|
|
3416
|
+
parts: [{ text: m.content }],
|
|
3417
|
+
})),
|
|
3418
|
+
config: Object.keys(config).length > 0 ? config : undefined,
|
|
3419
|
+
});
|
|
3420
|
+
const text = response.text;
|
|
3421
|
+
this.log.trace(`Assistant reply: ${text?.length || 0} characters`);
|
|
3422
|
+
// If structured output was requested, try to parse the response
|
|
3423
|
+
if (options?.response && text) {
|
|
3424
|
+
try {
|
|
3425
|
+
return JSON.parse(text);
|
|
1113
3426
|
}
|
|
1114
|
-
catch
|
|
1115
|
-
|
|
1116
|
-
if (retryCount >= maxRetries) {
|
|
1117
|
-
log.error(`OpenAI API call failed after ${maxRetries} retries`);
|
|
1118
|
-
log.var({ error });
|
|
1119
|
-
// Execute onUnrecoverableModelError hook if defined
|
|
1120
|
-
if (options.hooks?.onUnrecoverableModelError) {
|
|
1121
|
-
await core.resolveValue(options.hooks.onUnrecoverableModelError({
|
|
1122
|
-
input,
|
|
1123
|
-
options,
|
|
1124
|
-
providerRequest: requestOptions,
|
|
1125
|
-
error,
|
|
1126
|
-
}));
|
|
1127
|
-
}
|
|
1128
|
-
throw new errors.BadGatewayError();
|
|
1129
|
-
}
|
|
1130
|
-
// Check if the error is not retryable
|
|
1131
|
-
let isNotRetryable = false;
|
|
1132
|
-
for (const notRetryableError of NOT_RETRYABLE_ERRORS) {
|
|
1133
|
-
if (error instanceof notRetryableError) {
|
|
1134
|
-
isNotRetryable = true;
|
|
1135
|
-
break;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (isNotRetryable) {
|
|
1139
|
-
log.error("OpenAI API call failed with non-retryable error");
|
|
1140
|
-
log.var({ error });
|
|
1141
|
-
// Execute onUnrecoverableModelError hook if defined
|
|
1142
|
-
if (options.hooks?.onUnrecoverableModelError) {
|
|
1143
|
-
await core.resolveValue(options.hooks.onUnrecoverableModelError({
|
|
1144
|
-
input,
|
|
1145
|
-
options,
|
|
1146
|
-
providerRequest: requestOptions,
|
|
1147
|
-
error,
|
|
1148
|
-
}));
|
|
1149
|
-
}
|
|
1150
|
-
throw new errors.BadGatewayError();
|
|
1151
|
-
}
|
|
1152
|
-
// Warn if this error is not in our known retryable errors
|
|
1153
|
-
let isUnknownError = true;
|
|
1154
|
-
for (const retryableError of RETRYABLE_ERRORS) {
|
|
1155
|
-
if (error instanceof retryableError) {
|
|
1156
|
-
isUnknownError = false;
|
|
1157
|
-
break;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
if (isUnknownError) {
|
|
1161
|
-
log.warn("OpenAI API returned unknown error");
|
|
1162
|
-
log.var({ error });
|
|
1163
|
-
}
|
|
1164
|
-
// Log the error and retry
|
|
1165
|
-
log.warn(`OpenAI API call failed. Retrying in ${retryDelay}ms...`);
|
|
1166
|
-
// Execute onRetryableModelError hook if defined
|
|
1167
|
-
if (options.hooks?.onRetryableModelError) {
|
|
1168
|
-
await core.resolveValue(options.hooks.onRetryableModelError({
|
|
1169
|
-
input,
|
|
1170
|
-
options,
|
|
1171
|
-
providerRequest: requestOptions,
|
|
1172
|
-
error,
|
|
1173
|
-
}));
|
|
1174
|
-
}
|
|
1175
|
-
// Wait before retrying
|
|
1176
|
-
await core.sleep(retryDelay);
|
|
1177
|
-
// Increase retry count and delay for next attempt (exponential backoff)
|
|
1178
|
-
retryCount++;
|
|
1179
|
-
retryDelay = Math.min(retryDelay * RETRY_BACKOFF_FACTOR, MAX_RETRY_DELAY_MS);
|
|
3427
|
+
catch {
|
|
3428
|
+
return text || "";
|
|
1180
3429
|
}
|
|
1181
3430
|
}
|
|
3431
|
+
return text || "";
|
|
3432
|
+
}
|
|
3433
|
+
async operate(input, options = {}) {
|
|
3434
|
+
const operateLoop = await this.getOperateLoop();
|
|
3435
|
+
const mergedOptions = { ...options, model: options.model ?? this.model };
|
|
3436
|
+
// Create a merged history including both the tracked history and any explicitly provided history
|
|
3437
|
+
if (this.conversationHistory.length > 0) {
|
|
3438
|
+
// If options.history exists, merge with instance history, otherwise use instance history
|
|
3439
|
+
mergedOptions.history = options.history
|
|
3440
|
+
? [...this.conversationHistory, ...options.history]
|
|
3441
|
+
: [...this.conversationHistory];
|
|
3442
|
+
}
|
|
3443
|
+
// Execute operate loop
|
|
3444
|
+
const response = await operateLoop.execute(input, mergedOptions);
|
|
3445
|
+
// Update conversation history with the new history from the response
|
|
3446
|
+
if (response.history && response.history.length > 0) {
|
|
3447
|
+
this.conversationHistory = response.history;
|
|
3448
|
+
}
|
|
3449
|
+
return response;
|
|
1182
3450
|
}
|
|
1183
|
-
// * All possible paths should return a response; getting here is an error
|
|
1184
|
-
// The main loop is `currentTurn < maxTurns` and `currentTurn >= maxTurns` within the loop returns
|
|
1185
|
-
log.warn("This should never happen");
|
|
1186
|
-
returnResponse.status = LlmResponseStatus.Incomplete;
|
|
1187
|
-
// Always return the full LlmOperateResponse object for consistency
|
|
1188
|
-
return returnResponse;
|
|
1189
3451
|
}
|
|
1190
3452
|
|
|
1191
3453
|
// Logger
|
|
@@ -1233,7 +3495,7 @@ function prepareMessages$1(message, { system, data, placeholders } = {}) {
|
|
|
1233
3495
|
return messages;
|
|
1234
3496
|
}
|
|
1235
3497
|
// Completion requests
|
|
1236
|
-
async function createStructuredCompletion
|
|
3498
|
+
async function createStructuredCompletion(client, { messages, responseSchema, model, }) {
|
|
1237
3499
|
const logger = getLogger$1();
|
|
1238
3500
|
logger.trace("Using structured output");
|
|
1239
3501
|
const zodSchema = responseSchema instanceof v4.z.ZodType
|
|
@@ -1256,7 +3518,7 @@ async function createStructuredCompletion$1(client, { messages, responseSchema,
|
|
|
1256
3518
|
checks.shift();
|
|
1257
3519
|
}
|
|
1258
3520
|
responseFormat.json_schema.schema = jsonSchema;
|
|
1259
|
-
const completion = await client.
|
|
3521
|
+
const completion = await client.chat.completions.parse({
|
|
1260
3522
|
messages,
|
|
1261
3523
|
model,
|
|
1262
3524
|
response_format: responseFormat,
|
|
@@ -1264,7 +3526,7 @@ async function createStructuredCompletion$1(client, { messages, responseSchema,
|
|
|
1264
3526
|
logger.var({ assistantReply: completion.choices[0].message.parsed });
|
|
1265
3527
|
return completion.choices[0].message.parsed;
|
|
1266
3528
|
}
|
|
1267
|
-
async function createTextCompletion
|
|
3529
|
+
async function createTextCompletion(client, { messages, model, }) {
|
|
1268
3530
|
const logger = getLogger$1();
|
|
1269
3531
|
logger.trace("Using text output (unstructured)");
|
|
1270
3532
|
const completion = await client.chat.completions.create({
|
|
@@ -1289,35 +3551,45 @@ class OpenAiProvider {
|
|
|
1289
3551
|
this._client = await initializeClient$1({ apiKey: this.apiKey });
|
|
1290
3552
|
return this._client;
|
|
1291
3553
|
}
|
|
3554
|
+
async getOperateLoop() {
|
|
3555
|
+
if (this._operateLoop) {
|
|
3556
|
+
return this._operateLoop;
|
|
3557
|
+
}
|
|
3558
|
+
const client = await this.getClient();
|
|
3559
|
+
this._operateLoop = createOperateLoop({
|
|
3560
|
+
adapter: openAiAdapter,
|
|
3561
|
+
client,
|
|
3562
|
+
});
|
|
3563
|
+
return this._operateLoop;
|
|
3564
|
+
}
|
|
1292
3565
|
async send(message, options) {
|
|
1293
3566
|
const client = await this.getClient();
|
|
1294
3567
|
const messages = prepareMessages$1(message, options || {});
|
|
1295
3568
|
const modelToUse = options?.model || this.model;
|
|
1296
3569
|
if (options?.response) {
|
|
1297
|
-
return createStructuredCompletion
|
|
3570
|
+
return createStructuredCompletion(client, {
|
|
1298
3571
|
messages,
|
|
1299
3572
|
responseSchema: options.response,
|
|
1300
3573
|
model: modelToUse,
|
|
1301
3574
|
});
|
|
1302
3575
|
}
|
|
1303
|
-
return createTextCompletion
|
|
3576
|
+
return createTextCompletion(client, {
|
|
1304
3577
|
messages,
|
|
1305
3578
|
model: modelToUse,
|
|
1306
3579
|
});
|
|
1307
3580
|
}
|
|
1308
3581
|
async operate(input, options = {}) {
|
|
1309
|
-
const
|
|
1310
|
-
|
|
3582
|
+
const operateLoop = await this.getOperateLoop();
|
|
3583
|
+
const mergedOptions = { ...options, model: options.model ?? this.model };
|
|
1311
3584
|
// Create a merged history including both the tracked history and any explicitly provided history
|
|
1312
|
-
const mergedOptions = { ...options };
|
|
1313
3585
|
if (this.conversationHistory.length > 0) {
|
|
1314
3586
|
// If options.history exists, merge with instance history, otherwise use instance history
|
|
1315
3587
|
mergedOptions.history = options.history
|
|
1316
3588
|
? [...this.conversationHistory, ...options.history]
|
|
1317
3589
|
: [...this.conversationHistory];
|
|
1318
3590
|
}
|
|
1319
|
-
//
|
|
1320
|
-
const response = await
|
|
3591
|
+
// Execute operate loop
|
|
3592
|
+
const response = await operateLoop.execute(input, mergedOptions);
|
|
1321
3593
|
// Update conversation history with the new history from the response
|
|
1322
3594
|
if (response.history && response.history.length > 0) {
|
|
1323
3595
|
this.conversationHistory = response.history;
|
|
@@ -1326,384 +3598,57 @@ class OpenAiProvider {
|
|
|
1326
3598
|
}
|
|
1327
3599
|
}
|
|
1328
3600
|
|
|
1329
|
-
// Handle placeholder logic
|
|
1330
|
-
// Convert string input to array format if needed
|
|
1331
|
-
// Apply placeholders to fields if data is provided and placeholders.* is undefined or true
|
|
1332
|
-
function handleInputAndPlaceholders(input, options) {
|
|
1333
|
-
let history = formatOperateInput(input);
|
|
1334
|
-
let llmInstructions;
|
|
1335
|
-
let systemPrompt;
|
|
1336
|
-
if (options?.data &&
|
|
1337
|
-
(options.placeholders?.input === undefined || options.placeholders?.input)) {
|
|
1338
|
-
history = formatOperateInput(input, {
|
|
1339
|
-
data: options?.data,
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
if (options?.instructions) {
|
|
1343
|
-
llmInstructions =
|
|
1344
|
-
options.data && options.placeholders?.instructions !== false
|
|
1345
|
-
? core.placeholders(options.instructions, options.data)
|
|
1346
|
-
: options.instructions;
|
|
1347
|
-
}
|
|
1348
|
-
if (options?.system) {
|
|
1349
|
-
systemPrompt =
|
|
1350
|
-
options.data && options.placeholders?.system !== false
|
|
1351
|
-
? core.placeholders(options.system, options.data)
|
|
1352
|
-
: options.system;
|
|
1353
|
-
}
|
|
1354
|
-
return { history, systemPrompt, llmInstructions };
|
|
1355
|
-
}
|
|
1356
|
-
function updateUsage(usage, totalUsage) {
|
|
1357
|
-
totalUsage.input += usage.input_tokens;
|
|
1358
|
-
totalUsage.output += usage.output_tokens;
|
|
1359
|
-
totalUsage.total += usage.input_tokens + usage.output_tokens;
|
|
1360
|
-
}
|
|
1361
|
-
function handleMaxTurns(maxTurns, history, inputMessages, response, totalUsage) {
|
|
1362
|
-
const error = new errors.TooManyRequestsError();
|
|
1363
|
-
const detail = `Model requested function call but exceeded ${maxTurns} turns`;
|
|
1364
|
-
log.warn(detail);
|
|
1365
|
-
return {
|
|
1366
|
-
//model: model,
|
|
1367
|
-
//provider: PROVIDER.ANTHROPIC,
|
|
1368
|
-
error: {
|
|
1369
|
-
detail,
|
|
1370
|
-
status: error.status,
|
|
1371
|
-
title: error.title,
|
|
1372
|
-
},
|
|
1373
|
-
history,
|
|
1374
|
-
output: inputMessages.slice(-1),
|
|
1375
|
-
responses: response.content,
|
|
1376
|
-
status: LlmResponseStatus.Incomplete,
|
|
1377
|
-
usage: [totalUsage],
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
function handleOutputSchema(format) {
|
|
1381
|
-
let schema;
|
|
1382
|
-
if (format) {
|
|
1383
|
-
// Check if format is a JsonObject with type "json_schema"
|
|
1384
|
-
if (typeof format === "object" &&
|
|
1385
|
-
format !== null &&
|
|
1386
|
-
!Array.isArray(format) &&
|
|
1387
|
-
format.type === "json_schema") {
|
|
1388
|
-
// Direct pass-through for JsonObject with type "json_schema"
|
|
1389
|
-
schema = structuredClone(format);
|
|
1390
|
-
schema.type = "object"; // Validator does not recognise "json_schema" as a type
|
|
1391
|
-
}
|
|
1392
|
-
else {
|
|
1393
|
-
// Convert NaturalSchema to JSON schema through Zod
|
|
1394
|
-
const zodSchema = format instanceof v4.z.ZodType
|
|
1395
|
-
? format
|
|
1396
|
-
: naturalZodSchema(format);
|
|
1397
|
-
schema = v4.z.toJSONSchema(zodSchema);
|
|
1398
|
-
}
|
|
1399
|
-
if (schema.$schema) {
|
|
1400
|
-
delete schema.$schema; // Hack to fix issue with validator
|
|
1401
|
-
}
|
|
1402
|
-
return schema;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
// Register tools and process them to work with Anthropic
|
|
1406
|
-
function bundleTools(tools, explain, schema) {
|
|
1407
|
-
let toolkit;
|
|
1408
|
-
let processedTools = [];
|
|
1409
|
-
if (tools instanceof Toolkit) {
|
|
1410
|
-
toolkit = tools;
|
|
1411
|
-
}
|
|
1412
|
-
else if (Array.isArray(tools)) {
|
|
1413
|
-
toolkit = new Toolkit(tools, { explain });
|
|
1414
|
-
}
|
|
1415
|
-
if (toolkit) {
|
|
1416
|
-
toolkit.tools.forEach((tool) => {
|
|
1417
|
-
processedTools.push({
|
|
1418
|
-
...tool,
|
|
1419
|
-
input_schema: {
|
|
1420
|
-
...tool.parameters,
|
|
1421
|
-
type: "object",
|
|
1422
|
-
},
|
|
1423
|
-
type: "custom",
|
|
1424
|
-
});
|
|
1425
|
-
delete processedTools[processedTools.length - 1].parameters;
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
if (schema) {
|
|
1429
|
-
processedTools.push({
|
|
1430
|
-
name: "structured_output",
|
|
1431
|
-
description: "Output a structured JSON object, " +
|
|
1432
|
-
"use this before your final response to give structured outputs to the user",
|
|
1433
|
-
input_schema: schema,
|
|
1434
|
-
type: "custom",
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
return { processedTools, toolkit };
|
|
1438
|
-
}
|
|
1439
|
-
// Handles individual tool calls. Returns true for break, false for continue.
|
|
1440
|
-
async function callTool(inputMessages, response, hooks, toolkit) {
|
|
1441
|
-
inputMessages.push({
|
|
1442
|
-
role: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
1443
|
-
content: response.content,
|
|
1444
|
-
});
|
|
1445
|
-
// Get the tool use
|
|
1446
|
-
const toolUse = response.content[response.content.length - 1];
|
|
1447
|
-
// If the tool use is structured output (magic tool), break
|
|
1448
|
-
if (toolUse.name === "structured_output") {
|
|
1449
|
-
return true;
|
|
1450
|
-
}
|
|
1451
|
-
if (hooks?.beforeEachTool) {
|
|
1452
|
-
await hooks.beforeEachTool({
|
|
1453
|
-
toolName: toolUse.name,
|
|
1454
|
-
args: JSON.stringify(toolUse.input),
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
let result;
|
|
1458
|
-
try {
|
|
1459
|
-
result = await toolkit?.call({
|
|
1460
|
-
name: toolUse.name,
|
|
1461
|
-
arguments: JSON.stringify(toolUse.input),
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
catch (error) {
|
|
1465
|
-
if (hooks?.onToolError) {
|
|
1466
|
-
await hooks.onToolError({
|
|
1467
|
-
error: error,
|
|
1468
|
-
toolName: toolUse.name,
|
|
1469
|
-
args: JSON.stringify(toolUse.input),
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
throw error;
|
|
1473
|
-
}
|
|
1474
|
-
if (hooks?.afterEachTool) {
|
|
1475
|
-
await hooks.afterEachTool({
|
|
1476
|
-
result,
|
|
1477
|
-
toolName: toolUse.name,
|
|
1478
|
-
args: JSON.stringify(toolUse.input),
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
inputMessages.push({
|
|
1482
|
-
role: PROVIDER.ANTHROPIC.ROLE.USER,
|
|
1483
|
-
content: [
|
|
1484
|
-
{
|
|
1485
|
-
type: "tool_result",
|
|
1486
|
-
content: JSON.stringify(result),
|
|
1487
|
-
tool_use_id: toolUse.id,
|
|
1488
|
-
},
|
|
1489
|
-
],
|
|
1490
|
-
});
|
|
1491
|
-
return false;
|
|
1492
|
-
}
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
// Main
|
|
1496
|
-
//
|
|
1497
|
-
async function operate(input, options = {}, context = {
|
|
1498
|
-
client: new Anthropic.Anthropic(),
|
|
1499
|
-
}) {
|
|
1500
|
-
// Set model
|
|
1501
|
-
const model = options?.model || PROVIDER.ANTHROPIC.MODEL.DEFAULT;
|
|
1502
|
-
let schema = handleOutputSchema(options.format);
|
|
1503
|
-
let { processedTools, toolkit } = bundleTools(options.tools, options.explain, schema);
|
|
1504
|
-
let { history, systemPrompt, llmInstructions } = handleInputAndPlaceholders(input, options);
|
|
1505
|
-
// If history is provided, merge it with the input
|
|
1506
|
-
if (options.history) {
|
|
1507
|
-
history = [...options.history, ...history];
|
|
1508
|
-
}
|
|
1509
|
-
// Avoid Anthropic error by removing type property
|
|
1510
|
-
const inputMessages = structuredClone(history);
|
|
1511
|
-
inputMessages.forEach((message) => {
|
|
1512
|
-
delete message.type;
|
|
1513
|
-
});
|
|
1514
|
-
// Add instruction to the input message
|
|
1515
|
-
if (llmInstructions) {
|
|
1516
|
-
inputMessages[inputMessages.length - 1].content += "\n\n" + llmInstructions;
|
|
1517
|
-
}
|
|
1518
|
-
// Setup usage tracking
|
|
1519
|
-
let totalUsage = {
|
|
1520
|
-
input: 0,
|
|
1521
|
-
output: 0,
|
|
1522
|
-
reasoning: 0,
|
|
1523
|
-
total: 0,
|
|
1524
|
-
};
|
|
1525
|
-
// Determine max turns from options
|
|
1526
|
-
const maxTurns = maxTurnsFromOptions(options);
|
|
1527
|
-
const enableMultipleTurns = maxTurns > 1;
|
|
1528
|
-
let currentTurn = 0;
|
|
1529
|
-
let response;
|
|
1530
|
-
while (true) {
|
|
1531
|
-
// Loop for tool use
|
|
1532
|
-
response = await context.client.messages.create({
|
|
1533
|
-
model: model,
|
|
1534
|
-
system: systemPrompt,
|
|
1535
|
-
messages: inputMessages,
|
|
1536
|
-
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
1537
|
-
stream: false,
|
|
1538
|
-
tools: processedTools,
|
|
1539
|
-
tool_choice: processedTools.length > 0
|
|
1540
|
-
? { type: schema ? "any" : "auto" }
|
|
1541
|
-
: undefined,
|
|
1542
|
-
...options?.providerOptions,
|
|
1543
|
-
});
|
|
1544
|
-
// Update usage
|
|
1545
|
-
updateUsage(response.usage, totalUsage);
|
|
1546
|
-
// If the response is not a tool use, break
|
|
1547
|
-
if (response.stop_reason !== "tool_use") {
|
|
1548
|
-
break;
|
|
1549
|
-
}
|
|
1550
|
-
const breakLoop = await callTool(inputMessages, response, options.hooks, toolkit);
|
|
1551
|
-
if (breakLoop) {
|
|
1552
|
-
break;
|
|
1553
|
-
}
|
|
1554
|
-
// Handle turn limit
|
|
1555
|
-
if (!enableMultipleTurns || currentTurn >= maxTurns) {
|
|
1556
|
-
return handleMaxTurns(maxTurns, history, inputMessages, response, totalUsage);
|
|
1557
|
-
}
|
|
1558
|
-
currentTurn++;
|
|
1559
|
-
}
|
|
1560
|
-
let jsonResult;
|
|
1561
|
-
if (schema) {
|
|
1562
|
-
const validator = new ZSchema({});
|
|
1563
|
-
jsonResult = response.content[response.content.length - 1].input;
|
|
1564
|
-
if (!validator.validate(jsonResult, schema)) {
|
|
1565
|
-
throw new Error("Model returned invalid JSON");
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
history.push({
|
|
1569
|
-
content: schema
|
|
1570
|
-
? JSON.stringify(jsonResult)
|
|
1571
|
-
: response.content[0].text,
|
|
1572
|
-
role: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
1573
|
-
type: exports.LlmMessageType.Message,
|
|
1574
|
-
});
|
|
1575
|
-
return {
|
|
1576
|
-
//model: model,
|
|
1577
|
-
//provider: PROVIDER.ANTHROPIC,
|
|
1578
|
-
content: schema
|
|
1579
|
-
? jsonResult
|
|
1580
|
-
: response.content[0].text,
|
|
1581
|
-
responses: [response],
|
|
1582
|
-
output: history.slice(-1),
|
|
1583
|
-
history,
|
|
1584
|
-
status: LlmResponseStatus.Completed,
|
|
1585
|
-
usage: [totalUsage],
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
3601
|
// Logger
|
|
1590
3602
|
const getLogger = () => core.log.lib({ lib: core.JAYPIE.LIB.LLM });
|
|
1591
3603
|
// Client initialization
|
|
1592
3604
|
async function initializeClient({ apiKey, } = {}) {
|
|
1593
3605
|
const logger = getLogger();
|
|
1594
|
-
const resolvedApiKey = apiKey || (await aws.getEnvSecret("
|
|
3606
|
+
const resolvedApiKey = apiKey || (await aws.getEnvSecret("OPENROUTER_API_KEY"));
|
|
1595
3607
|
if (!resolvedApiKey) {
|
|
1596
|
-
throw new core.ConfigurationError("The application could not resolve the
|
|
3608
|
+
throw new core.ConfigurationError("The application could not resolve the requested keys");
|
|
1597
3609
|
}
|
|
1598
|
-
const client = new
|
|
1599
|
-
logger.trace("Initialized
|
|
3610
|
+
const client = new sdk.OpenRouter({ apiKey: resolvedApiKey });
|
|
3611
|
+
logger.trace("Initialized OpenRouter client");
|
|
1600
3612
|
return client;
|
|
1601
3613
|
}
|
|
1602
|
-
//
|
|
3614
|
+
// Get default model from environment or constants
|
|
3615
|
+
function getDefaultModel() {
|
|
3616
|
+
return process.env.OPENROUTER_MODEL || PROVIDER.OPENROUTER.MODEL.DEFAULT;
|
|
3617
|
+
}
|
|
1603
3618
|
function formatSystemMessage(systemPrompt, { data, placeholders } = {}) {
|
|
1604
|
-
|
|
3619
|
+
const content = placeholders?.system === false
|
|
1605
3620
|
? systemPrompt
|
|
1606
3621
|
: core.placeholders(systemPrompt, data);
|
|
3622
|
+
return {
|
|
3623
|
+
role: "system",
|
|
3624
|
+
content,
|
|
3625
|
+
};
|
|
1607
3626
|
}
|
|
1608
3627
|
function formatUserMessage(message, { data, placeholders } = {}) {
|
|
1609
3628
|
const content = placeholders?.message === false
|
|
1610
3629
|
? message
|
|
1611
3630
|
: core.placeholders(message, data);
|
|
1612
3631
|
return {
|
|
1613
|
-
role:
|
|
3632
|
+
role: "user",
|
|
1614
3633
|
content,
|
|
1615
3634
|
};
|
|
1616
3635
|
}
|
|
1617
|
-
function prepareMessages(message, { data, placeholders } = {}) {
|
|
3636
|
+
function prepareMessages(message, { system, data, placeholders } = {}) {
|
|
1618
3637
|
const logger = getLogger();
|
|
1619
3638
|
const messages = [];
|
|
1620
|
-
|
|
3639
|
+
if (system) {
|
|
3640
|
+
const systemMessage = formatSystemMessage(system, { data, placeholders });
|
|
3641
|
+
messages.push(systemMessage);
|
|
3642
|
+
logger.trace(`System message: ${systemMessage.content?.length} characters`);
|
|
3643
|
+
}
|
|
1621
3644
|
const userMessage = formatUserMessage(message, { data, placeholders });
|
|
1622
3645
|
messages.push(userMessage);
|
|
1623
|
-
logger.trace(`User message: ${userMessage.content
|
|
3646
|
+
logger.trace(`User message: ${userMessage.content?.length} characters`);
|
|
1624
3647
|
return messages;
|
|
1625
3648
|
}
|
|
1626
|
-
// Basic text completion
|
|
1627
|
-
async function createTextCompletion(client, messages, model, systemMessage) {
|
|
1628
|
-
core.log.trace("Using text output (unstructured)");
|
|
1629
|
-
const params = {
|
|
1630
|
-
model,
|
|
1631
|
-
messages,
|
|
1632
|
-
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
1633
|
-
};
|
|
1634
|
-
// Add system instruction if provided
|
|
1635
|
-
if (systemMessage) {
|
|
1636
|
-
params.system = systemMessage;
|
|
1637
|
-
core.log.trace(`System message: ${systemMessage.length} characters`);
|
|
1638
|
-
}
|
|
1639
|
-
const response = await client.messages.create(params);
|
|
1640
|
-
const firstContent = response.content[0];
|
|
1641
|
-
const text = firstContent && "text" in firstContent ? firstContent.text : "";
|
|
1642
|
-
core.log.trace(`Assistant reply: ${text.length} characters`);
|
|
1643
|
-
return text;
|
|
1644
|
-
}
|
|
1645
|
-
// Structured output completion
|
|
1646
|
-
async function createStructuredCompletion(client, messages, model, responseSchema, systemMessage) {
|
|
1647
|
-
core.log.trace("Using structured output");
|
|
1648
|
-
// Get the JSON schema for the response
|
|
1649
|
-
const schema = responseSchema instanceof v4.z.ZodType
|
|
1650
|
-
? responseSchema
|
|
1651
|
-
: naturalZodSchema(responseSchema);
|
|
1652
|
-
// Set system message with JSON instructions
|
|
1653
|
-
const defaultSystemPrompt = "You will be responding with structured JSON data. " +
|
|
1654
|
-
"Format your entire response as a valid JSON object with the following structure: " +
|
|
1655
|
-
JSON.stringify(v4.z.toJSONSchema(schema));
|
|
1656
|
-
const systemPrompt = systemMessage || defaultSystemPrompt;
|
|
1657
|
-
try {
|
|
1658
|
-
// Use standard Anthropic API to get response
|
|
1659
|
-
const params = {
|
|
1660
|
-
model,
|
|
1661
|
-
messages,
|
|
1662
|
-
max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
1663
|
-
system: systemPrompt,
|
|
1664
|
-
};
|
|
1665
|
-
const response = await client.messages.create(params);
|
|
1666
|
-
// Extract text from response
|
|
1667
|
-
const firstContent = response.content[0];
|
|
1668
|
-
const responseText = firstContent && "text" in firstContent ? firstContent.text : "";
|
|
1669
|
-
// Find JSON in response
|
|
1670
|
-
const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/) ||
|
|
1671
|
-
responseText.match(/\{[\s\S]*\}/);
|
|
1672
|
-
if (jsonMatch) {
|
|
1673
|
-
try {
|
|
1674
|
-
// Parse the JSON response
|
|
1675
|
-
const jsonStr = jsonMatch[1] || jsonMatch[0];
|
|
1676
|
-
const result = JSON.parse(jsonStr);
|
|
1677
|
-
if (!schema.parse(result)) {
|
|
1678
|
-
throw new Error(`JSON response from Anthropic does not match schema: ${responseText}`);
|
|
1679
|
-
}
|
|
1680
|
-
core.log.trace("Received structured response", { result });
|
|
1681
|
-
return result;
|
|
1682
|
-
}
|
|
1683
|
-
catch {
|
|
1684
|
-
throw new Error(`Failed to parse JSON response from Anthropic: ${responseText}`);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
// If we can't extract JSON
|
|
1688
|
-
throw new Error("Failed to parse structured response from Anthropic");
|
|
1689
|
-
}
|
|
1690
|
-
catch (error) {
|
|
1691
|
-
core.log.error("Error creating structured completion", { error });
|
|
1692
|
-
throw error;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// Maps Jaypie roles to Anthropic roles
|
|
1697
|
-
({
|
|
1698
|
-
[exports.LlmMessageRole.User]: PROVIDER.ANTHROPIC.ROLE.USER,
|
|
1699
|
-
[exports.LlmMessageRole.System]: PROVIDER.ANTHROPIC.ROLE.SYSTEM,
|
|
1700
|
-
[exports.LlmMessageRole.Assistant]: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
1701
|
-
[exports.LlmMessageRole.Developer]: PROVIDER.ANTHROPIC.ROLE.SYSTEM,
|
|
1702
|
-
});
|
|
1703
3649
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
constructor(model = PROVIDER.ANTHROPIC.MODEL.DEFAULT, { apiKey } = {}) {
|
|
3650
|
+
class OpenRouterProvider {
|
|
3651
|
+
constructor(model = getDefaultModel(), { apiKey } = {}) {
|
|
1707
3652
|
this.log = getLogger();
|
|
1708
3653
|
this.conversationHistory = [];
|
|
1709
3654
|
this.model = model;
|
|
@@ -1716,37 +3661,60 @@ class AnthropicProvider {
|
|
|
1716
3661
|
this._client = await initializeClient({ apiKey: this.apiKey });
|
|
1717
3662
|
return this._client;
|
|
1718
3663
|
}
|
|
1719
|
-
|
|
3664
|
+
async getOperateLoop() {
|
|
3665
|
+
if (this._operateLoop) {
|
|
3666
|
+
return this._operateLoop;
|
|
3667
|
+
}
|
|
3668
|
+
const client = await this.getClient();
|
|
3669
|
+
this._operateLoop = createOperateLoop({
|
|
3670
|
+
adapter: openRouterAdapter,
|
|
3671
|
+
client,
|
|
3672
|
+
});
|
|
3673
|
+
return this._operateLoop;
|
|
3674
|
+
}
|
|
1720
3675
|
async send(message, options) {
|
|
1721
3676
|
const client = await this.getClient();
|
|
1722
|
-
const messages = prepareMessages(message, options
|
|
3677
|
+
const messages = prepareMessages(message, options);
|
|
1723
3678
|
const modelToUse = options?.model || this.model;
|
|
1724
|
-
//
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
3679
|
+
// Build the request
|
|
3680
|
+
const response = await client.chat.send({
|
|
3681
|
+
model: modelToUse,
|
|
3682
|
+
messages: messages,
|
|
3683
|
+
});
|
|
3684
|
+
const rawContent = response.choices?.[0]?.message?.content;
|
|
3685
|
+
// Extract text content - content could be string or array of content items
|
|
3686
|
+
const content = typeof rawContent === "string"
|
|
3687
|
+
? rawContent
|
|
3688
|
+
: Array.isArray(rawContent)
|
|
3689
|
+
? rawContent
|
|
3690
|
+
.filter((item) => item.type === "text")
|
|
3691
|
+
.map((item) => item.text)
|
|
3692
|
+
.join("")
|
|
3693
|
+
: "";
|
|
3694
|
+
this.log.trace(`Assistant reply: ${content?.length || 0} characters`);
|
|
3695
|
+
// If structured output was requested, try to parse the response
|
|
3696
|
+
if (options?.response && content) {
|
|
3697
|
+
try {
|
|
3698
|
+
return JSON.parse(content);
|
|
3699
|
+
}
|
|
3700
|
+
catch {
|
|
3701
|
+
return content || "";
|
|
3702
|
+
}
|
|
1734
3703
|
}
|
|
1735
|
-
return
|
|
3704
|
+
return content || "";
|
|
1736
3705
|
}
|
|
1737
3706
|
async operate(input, options = {}) {
|
|
1738
|
-
const
|
|
1739
|
-
|
|
3707
|
+
const operateLoop = await this.getOperateLoop();
|
|
3708
|
+
const mergedOptions = { ...options, model: options.model ?? this.model };
|
|
1740
3709
|
// Create a merged history including both the tracked history and any explicitly provided history
|
|
1741
|
-
const mergedOptions = { ...options };
|
|
1742
3710
|
if (this.conversationHistory.length > 0) {
|
|
1743
3711
|
// If options.history exists, merge with instance history, otherwise use instance history
|
|
1744
3712
|
mergedOptions.history = options.history
|
|
1745
3713
|
? [...this.conversationHistory, ...options.history]
|
|
1746
3714
|
: [...this.conversationHistory];
|
|
1747
3715
|
}
|
|
1748
|
-
//
|
|
1749
|
-
const response = await
|
|
3716
|
+
// Execute operate loop
|
|
3717
|
+
const response = await operateLoop.execute(input, mergedOptions);
|
|
1750
3718
|
// Update conversation history with the new history from the response
|
|
1751
3719
|
if (response.history && response.history.length > 0) {
|
|
1752
3720
|
this.conversationHistory = response.history;
|
|
@@ -1793,12 +3761,20 @@ class Llm {
|
|
|
1793
3761
|
createProvider(providerName, options = {}) {
|
|
1794
3762
|
const { apiKey, model } = options;
|
|
1795
3763
|
switch (providerName) {
|
|
3764
|
+
case PROVIDER.ANTHROPIC.NAME:
|
|
3765
|
+
return new AnthropicProvider(model || PROVIDER.ANTHROPIC.MODEL.DEFAULT, { apiKey });
|
|
3766
|
+
case PROVIDER.GEMINI.NAME:
|
|
3767
|
+
return new GeminiProvider(model || PROVIDER.GEMINI.MODEL.DEFAULT, {
|
|
3768
|
+
apiKey,
|
|
3769
|
+
});
|
|
1796
3770
|
case PROVIDER.OPENAI.NAME:
|
|
1797
3771
|
return new OpenAiProvider(model || PROVIDER.OPENAI.MODEL.DEFAULT, {
|
|
1798
3772
|
apiKey,
|
|
1799
3773
|
});
|
|
1800
|
-
case PROVIDER.
|
|
1801
|
-
return new
|
|
3774
|
+
case PROVIDER.OPENROUTER.NAME:
|
|
3775
|
+
return new OpenRouterProvider(model || PROVIDER.OPENROUTER.MODEL.DEFAULT, {
|
|
3776
|
+
apiKey,
|
|
3777
|
+
});
|
|
1802
3778
|
default:
|
|
1803
3779
|
throw new errors.ConfigurationError(`Unsupported provider: ${providerName}`);
|
|
1804
3780
|
}
|
|
@@ -1940,11 +3916,11 @@ const roll = {
|
|
|
1940
3916
|
let total = 0;
|
|
1941
3917
|
const parsedNumber = tryParseNumber(number, {
|
|
1942
3918
|
defaultValue: 1,
|
|
1943
|
-
warnFunction: log.warn,
|
|
3919
|
+
warnFunction: log$1.warn,
|
|
1944
3920
|
});
|
|
1945
3921
|
const parsedSides = tryParseNumber(sides, {
|
|
1946
3922
|
defaultValue: 6,
|
|
1947
|
-
warnFunction: log.warn,
|
|
3923
|
+
warnFunction: log$1.warn,
|
|
1948
3924
|
});
|
|
1949
3925
|
for (let i = 0; i < parsedNumber; i++) {
|
|
1950
3926
|
const rollValue = rng({ min: 1, max: parsedSides, integer: true });
|
|
@@ -2119,9 +4095,22 @@ class JaypieToolkit extends Toolkit {
|
|
|
2119
4095
|
}
|
|
2120
4096
|
const toolkit = new JaypieToolkit(tools);
|
|
2121
4097
|
|
|
4098
|
+
//
|
|
4099
|
+
//
|
|
4100
|
+
// Role Mapping
|
|
4101
|
+
//
|
|
4102
|
+
({
|
|
4103
|
+
[exports.LlmMessageRole.User]: "user",
|
|
4104
|
+
[exports.LlmMessageRole.System]: "user", // Gemini doesn't have system role in contents
|
|
4105
|
+
[exports.LlmMessageRole.Assistant]: "model",
|
|
4106
|
+
[exports.LlmMessageRole.Developer]: "user",
|
|
4107
|
+
});
|
|
4108
|
+
|
|
4109
|
+
exports.GeminiProvider = GeminiProvider;
|
|
2122
4110
|
exports.JaypieToolkit = JaypieToolkit;
|
|
2123
4111
|
exports.LLM = constants;
|
|
2124
4112
|
exports.Llm = Llm;
|
|
4113
|
+
exports.OpenRouterProvider = OpenRouterProvider;
|
|
2125
4114
|
exports.Toolkit = Toolkit;
|
|
2126
4115
|
exports.toolkit = toolkit;
|
|
2127
4116
|
exports.tools = tools;
|