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