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