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