@jaypie/llm 1.1.29 → 1.1.30

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