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