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