@jaypie/llm 1.2.1 → 1.2.3

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 (47) hide show
  1. package/dist/cjs/Llm.d.ts +5 -5
  2. package/dist/cjs/constants.d.ts +33 -77
  3. package/dist/cjs/index.cjs +725 -112
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/index.d.ts +3 -1
  6. package/dist/cjs/operate/OperateLoop.d.ts +2 -2
  7. package/dist/cjs/operate/StreamLoop.d.ts +2 -2
  8. package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +1 -1
  9. package/dist/cjs/operate/adapters/GeminiAdapter.d.ts +2 -1
  10. package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +1 -1
  11. package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +12 -2
  12. package/dist/cjs/operate/input/InputProcessor.d.ts +3 -3
  13. package/dist/cjs/operate/response/ResponseBuilder.d.ts +14 -1
  14. package/dist/cjs/types/LlmOperateInput.guards.d.ts +17 -0
  15. package/dist/cjs/types/LlmProvider.interface.d.ts +48 -2
  16. package/dist/cjs/upload/detectMimeType.d.ts +16 -0
  17. package/dist/cjs/upload/extractPdfPages.d.ts +13 -0
  18. package/dist/cjs/upload/index.d.ts +5 -0
  19. package/dist/cjs/upload/loadLocalFile.d.ts +6 -0
  20. package/dist/cjs/upload/loadS3File.d.ts +7 -0
  21. package/dist/cjs/upload/resolveOperateInput.d.ts +12 -0
  22. package/dist/cjs/util/extractReasoning.d.ts +14 -0
  23. package/dist/cjs/util/index.d.ts +1 -0
  24. package/dist/esm/Llm.d.ts +5 -5
  25. package/dist/esm/constants.d.ts +33 -77
  26. package/dist/esm/index.d.ts +3 -1
  27. package/dist/esm/index.js +722 -114
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/operate/OperateLoop.d.ts +2 -2
  30. package/dist/esm/operate/StreamLoop.d.ts +2 -2
  31. package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +1 -1
  32. package/dist/esm/operate/adapters/GeminiAdapter.d.ts +2 -1
  33. package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +1 -1
  34. package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +12 -2
  35. package/dist/esm/operate/input/InputProcessor.d.ts +3 -3
  36. package/dist/esm/operate/response/ResponseBuilder.d.ts +14 -1
  37. package/dist/esm/types/LlmOperateInput.guards.d.ts +17 -0
  38. package/dist/esm/types/LlmProvider.interface.d.ts +48 -2
  39. package/dist/esm/upload/detectMimeType.d.ts +16 -0
  40. package/dist/esm/upload/extractPdfPages.d.ts +13 -0
  41. package/dist/esm/upload/index.d.ts +5 -0
  42. package/dist/esm/upload/loadLocalFile.d.ts +6 -0
  43. package/dist/esm/upload/loadS3File.d.ts +7 -0
  44. package/dist/esm/upload/resolveOperateInput.d.ts +12 -0
  45. package/dist/esm/util/extractReasoning.d.ts +14 -0
  46. package/dist/esm/util/index.d.ts +1 -0
  47. package/package.json +2 -1
package/dist/esm/index.js CHANGED
@@ -5,7 +5,10 @@ import { log as log$2 } from '@jaypie/logger';
5
5
  import RandomLib from 'random';
6
6
  import { RateLimitError, APIConnectionError, APIConnectionTimeoutError, InternalServerError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, NotFoundError, PermissionDeniedError, UnprocessableEntityError, OpenAI } from 'openai';
7
7
  import { zodResponseFormat } from 'openai/helpers/zod';
8
- import { getEnvSecret } from '@jaypie/aws';
8
+ import { PDFDocument } from 'pdf-lib';
9
+ import { readFile } from 'fs/promises';
10
+ import { resolve } from 'path';
11
+ import { getS3FileBuffer, getEnvSecret } from '@jaypie/aws';
9
12
  import { fetchWeatherApi } from 'openmeteo';
10
13
 
11
14
  const PROVIDER = {
@@ -15,10 +18,10 @@ const PROVIDER = {
15
18
  // The model format is: provider/model-name (e.g., "openai/gpt-4", "anthropic/claude-3-opus")
16
19
  MODEL: {
17
20
  // Default uses env var OPENROUTER_MODEL if set, otherwise a reasonable default
18
- DEFAULT: "openai/gpt-4o",
19
- SMALL: "openai/gpt-4o-mini",
20
- LARGE: "anthropic/claude-3-opus",
21
- TINY: "openai/gpt-4o-mini",
21
+ DEFAULT: "z-ai/glm-4.7",
22
+ SMALL: "z-ai/glm-4.7",
23
+ LARGE: "z-ai/glm-4.7",
24
+ TINY: "z-ai/glm-4.7",
22
25
  },
23
26
  MODEL_MATCH_WORDS: ["openrouter"],
24
27
  NAME: "openrouter",
@@ -32,21 +35,10 @@ const PROVIDER = {
32
35
  GEMINI: {
33
36
  // https://ai.google.dev/gemini-api/docs/models
34
37
  MODEL: {
35
- // Jaypie Aliases
36
- DEFAULT: "gemini-2.5-flash",
37
- SMALL: "gemini-2.5-flash",
38
- LARGE: "gemini-2.5-pro",
39
- TINY: "gemini-2.0-flash-lite",
40
- // Gemini 2.5 Models
41
- GEMINI_2_5_PRO: "gemini-2.5-pro",
42
- GEMINI_2_5_FLASH: "gemini-2.5-flash",
43
- // Gemini 2.0 Models
44
- GEMINI_2_0_FLASH: "gemini-2.0-flash",
45
- GEMINI_2_0_FLASH_LITE: "gemini-2.0-flash-lite",
46
- // Gemini 1.5 Models (backward compatibility)
47
- GEMINI_1_5_PRO: "gemini-1.5-pro",
48
- GEMINI_1_5_FLASH: "gemini-1.5-flash",
49
- GEMINI_1_5_FLASH_8B: "gemini-1.5-flash-8b",
38
+ DEFAULT: "gemini-3-pro-preview",
39
+ LARGE: "gemini-3-pro-preview",
40
+ SMALL: "gemini-3-flash-preview",
41
+ TINY: "gemini-3-flash-preview",
50
42
  },
51
43
  MODEL_MATCH_WORDS: ["gemini", "google"],
52
44
  NAME: "gemini",
@@ -58,29 +50,10 @@ const PROVIDER = {
58
50
  ANTHROPIC: {
59
51
  // https://docs.anthropic.com/en/docs/about-claude/models/overview
60
52
  MODEL: {
61
- // Jaypie Aliases
62
- DEFAULT: "claude-opus-4-1",
63
- SMALL: "claude-sonnet-4-0",
64
- TINY: "claude-3-5-haiku-latest",
65
- LARGE: "claude-opus-4-1",
66
- // Latests
67
- CLAUDE_OPUS_4: "claude-opus-4-1",
68
- CLAUDE_SONNET_4: "claude-sonnet-4-0",
69
- CLAUDE_3_HAIKU: "claude-3-5-haiku-latest",
70
- CLAUDE_3_OPUS: "claude-3-opus-latest",
71
- CLAUDE_3_SONNET: "claude-3-7-sonnet-latest",
72
- // Specifics
73
- CLAUDE_OPUS_4_1: "claude-opus-4-1",
74
- CLAUDE_OPUS_4_0: "claude-opus-4-0",
75
- CLAUDE_SONNET_4_0: "claude-sonnet-4-0",
76
- CLAUDE_3_7_SONNET: "claude-3-7-sonnet-latest ",
77
- CLAUDE_3_5_SONNET: "claude-3-5-sonnet-latest",
78
- CLAUDE_3_5_HAIKU: "claude-3-5-haiku-latest",
79
- // _Note: Claude reversed the order of model name and version in 4_
80
- // Backward compatibility
81
- CLAUDE_HAIKU_3: "claude-3-5-haiku-latest",
82
- CLAUDE_OPUS_3: "claude-3-opus-latest",
83
- CLAUDE_SONNET_3: "claude-3-7-sonnet-latest",
53
+ DEFAULT: "claude-sonnet-4-5",
54
+ LARGE: "claude-opus-4-5",
55
+ SMALL: "claude-sonnet-4-5",
56
+ TINY: "claude-haiku-4-5",
84
57
  },
85
58
  MODEL_MATCH_WORDS: [
86
59
  "anthropic",
@@ -109,30 +82,10 @@ const PROVIDER = {
109
82
  OPENAI: {
110
83
  // https://platform.openai.com/docs/models
111
84
  MODEL: {
112
- // Jaypie Aliases
113
- DEFAULT: "gpt-5",
114
- SMALL: "gpt-5-mini",
115
- LARGE: "gpt-5",
116
- TINY: "gpt-5-nano",
117
- // OpenAI Official
118
- GPT_5: "gpt-5",
119
- GPT_5_MINI: "gpt-5-mini",
120
- GPT_5_NANO: "gpt-5-nano",
121
- GPT_4_1: "gpt-4.1",
122
- GPT_4_1_MINI: "gpt-4.1-mini",
123
- GPT_4_1_NANO: "gpt-4.1-nano",
124
- GPT_4: "gpt-4",
125
- GPT_4_O_MINI: "gpt-4o-mini",
126
- GPT_4_O: "gpt-4o",
127
- GPT_4_5: "gpt-4.5-preview",
128
- O1: "o1",
129
- O1_MINI: "o1-mini",
130
- O1_PRO: "o1-pro",
131
- O3_MINI: "o3-mini",
132
- O3_MINI_HIGH: "o3-mini-high",
133
- O3: "o3",
134
- O3_PRO: "o3-pro",
135
- O4_MINI: "o4-mini",
85
+ DEFAULT: "gpt-5.1",
86
+ LARGE: "gpt-5.1",
87
+ SMALL: "gpt-5.1-mini",
88
+ TINY: "gpt-5.1-nano",
136
89
  },
137
90
  MODEL_MATCH_WORDS: ["openai", "gpt", /^o\d/],
138
91
  NAME: "openai",
@@ -140,11 +93,55 @@ const PROVIDER = {
140
93
  };
141
94
  // Last: Defaults
142
95
  const DEFAULT = {
96
+ MODEL: {
97
+ BASE: PROVIDER.OPENAI.MODEL.DEFAULT,
98
+ LARGE: PROVIDER.OPENAI.MODEL.LARGE,
99
+ SMALL: PROVIDER.OPENAI.MODEL.SMALL,
100
+ TINY: PROVIDER.OPENAI.MODEL.TINY,
101
+ },
143
102
  PROVIDER: PROVIDER.OPENAI,
144
103
  };
104
+ // Only include "first class" models, not OpenRouter or other proxy services
105
+ const ALL = {
106
+ BASE: [
107
+ PROVIDER.ANTHROPIC.MODEL.DEFAULT,
108
+ PROVIDER.GEMINI.MODEL.DEFAULT,
109
+ PROVIDER.OPENAI.MODEL.DEFAULT,
110
+ ],
111
+ COMBINED: [
112
+ PROVIDER.ANTHROPIC.MODEL.DEFAULT,
113
+ PROVIDER.ANTHROPIC.MODEL.LARGE,
114
+ PROVIDER.ANTHROPIC.MODEL.SMALL,
115
+ PROVIDER.ANTHROPIC.MODEL.TINY,
116
+ PROVIDER.GEMINI.MODEL.DEFAULT,
117
+ PROVIDER.GEMINI.MODEL.LARGE,
118
+ PROVIDER.GEMINI.MODEL.SMALL,
119
+ PROVIDER.GEMINI.MODEL.TINY,
120
+ PROVIDER.OPENAI.MODEL.DEFAULT,
121
+ PROVIDER.OPENAI.MODEL.LARGE,
122
+ PROVIDER.OPENAI.MODEL.SMALL,
123
+ PROVIDER.OPENAI.MODEL.TINY,
124
+ ],
125
+ LARGE: [
126
+ PROVIDER.ANTHROPIC.MODEL.LARGE,
127
+ PROVIDER.GEMINI.MODEL.LARGE,
128
+ PROVIDER.OPENAI.MODEL.LARGE,
129
+ ],
130
+ SMALL: [
131
+ PROVIDER.ANTHROPIC.MODEL.SMALL,
132
+ PROVIDER.GEMINI.MODEL.SMALL,
133
+ PROVIDER.OPENAI.MODEL.SMALL,
134
+ ],
135
+ TINY: [
136
+ PROVIDER.ANTHROPIC.MODEL.TINY,
137
+ PROVIDER.GEMINI.MODEL.TINY,
138
+ PROVIDER.OPENAI.MODEL.TINY,
139
+ ],
140
+ };
145
141
 
146
142
  var constants = /*#__PURE__*/Object.freeze({
147
143
  __proto__: null,
144
+ ALL: ALL,
148
145
  DEFAULT: DEFAULT,
149
146
  PROVIDER: PROVIDER
150
147
  });
@@ -365,6 +362,76 @@ var LlmStreamChunkType;
365
362
  LlmStreamChunkType["ToolResult"] = "tool_result";
366
363
  })(LlmStreamChunkType || (LlmStreamChunkType = {}));
367
364
 
365
+ /**
366
+ * Type guard to check if an item is a dedicated reasoning item (OpenAI)
367
+ */
368
+ function isReasoningItem(item) {
369
+ return (typeof item === "object" &&
370
+ item !== null &&
371
+ item.type === "reasoning");
372
+ }
373
+ /**
374
+ * Type guard to check if an item is a thinking item (Anthropic)
375
+ */
376
+ function isThinkingItem(item) {
377
+ return (typeof item === "object" &&
378
+ item !== null &&
379
+ item.type === "thinking" &&
380
+ typeof item.thinking === "string");
381
+ }
382
+ /**
383
+ * Type guard to check if an item has a reasoning property
384
+ */
385
+ function hasReasoningProperty(item) {
386
+ return (typeof item === "object" &&
387
+ item !== null &&
388
+ typeof item.reasoning === "string" &&
389
+ item.reasoning !== "");
390
+ }
391
+ /**
392
+ * Extracts reasoning text from LLM history items.
393
+ *
394
+ * Reasoning items may have text in different locations depending on the provider:
395
+ * - OpenAI: `summary` array with objects containing `text` property (type: "reasoning")
396
+ * - OpenAI: `content` property on reasoning items (type: "reasoning")
397
+ * - Anthropic: `thinking` property on thinking items (type: "thinking")
398
+ * - OpenRouter/z-ai: `reasoning` property on message items
399
+ *
400
+ * @param history - The LLM history array to extract reasoning from
401
+ * @returns Array of reasoning text strings
402
+ */
403
+ function extractReasoning(history) {
404
+ const reasoningTexts = [];
405
+ for (const item of history) {
406
+ // Cast to unknown first for type checking
407
+ const entry = item;
408
+ // Handle dedicated reasoning items (OpenAI extended thinking)
409
+ if (isReasoningItem(entry)) {
410
+ // Handle summary array format
411
+ if (Array.isArray(entry.summary)) {
412
+ for (const summaryItem of entry.summary) {
413
+ if (summaryItem?.text && typeof summaryItem.text === "string") {
414
+ reasoningTexts.push(summaryItem.text);
415
+ }
416
+ }
417
+ }
418
+ // Handle direct content format
419
+ if (entry.content && typeof entry.content === "string") {
420
+ reasoningTexts.push(entry.content);
421
+ }
422
+ }
423
+ // Handle thinking items (Anthropic extended thinking)
424
+ if (isThinkingItem(entry)) {
425
+ reasoningTexts.push(entry.thinking);
426
+ }
427
+ // Handle reasoning property on message items (OpenRouter/z-ai format)
428
+ if (hasReasoningProperty(entry)) {
429
+ reasoningTexts.push(entry.reasoning);
430
+ }
431
+ }
432
+ return reasoningTexts;
433
+ }
434
+
368
435
  /**
369
436
  * Converts a string to a standardized LlmInputMessage
370
437
  * @param input - String to format
@@ -717,6 +784,73 @@ var ErrorCategory;
717
784
  // Constants
718
785
  //
719
786
  const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
787
+ // Regular expression to parse data URLs: data:mime/type;base64,data
788
+ const DATA_URL_REGEX = /^data:([^;]+);base64,(.+)$/;
789
+ /**
790
+ * Parse a data URL into its components
791
+ */
792
+ function parseDataUrl(dataUrl) {
793
+ const match = dataUrl.match(DATA_URL_REGEX);
794
+ if (!match)
795
+ return null;
796
+ return { mediaType: match[1], data: match[2] };
797
+ }
798
+ /**
799
+ * Convert standardized content items to Anthropic format
800
+ */
801
+ function convertContentToAnthropic(content) {
802
+ if (typeof content === "string") {
803
+ return content;
804
+ }
805
+ return content.map((item) => {
806
+ // Text content
807
+ if (item.type === LlmMessageType.InputText) {
808
+ return { type: "text", text: item.text };
809
+ }
810
+ // Image content
811
+ if (item.type === LlmMessageType.InputImage) {
812
+ const imageUrl = item.image_url || "";
813
+ const parsed = parseDataUrl(imageUrl);
814
+ if (parsed) {
815
+ return {
816
+ type: "image",
817
+ source: {
818
+ type: "base64",
819
+ media_type: parsed.mediaType,
820
+ data: parsed.data,
821
+ },
822
+ };
823
+ }
824
+ // Fallback for URL-based images (not base64)
825
+ return {
826
+ type: "image",
827
+ source: {
828
+ type: "url",
829
+ url: imageUrl,
830
+ },
831
+ };
832
+ }
833
+ // File/Document content (PDF, etc.)
834
+ if (item.type === LlmMessageType.InputFile) {
835
+ const fileData = typeof item.file_data === "string" ? item.file_data : "";
836
+ const parsed = parseDataUrl(fileData);
837
+ if (parsed) {
838
+ return {
839
+ type: "document",
840
+ source: {
841
+ type: "base64",
842
+ media_type: parsed.mediaType,
843
+ data: parsed.data,
844
+ },
845
+ };
846
+ }
847
+ // Fallback - return as text with the filename
848
+ return { type: "text", text: `[File: ${item.filename || "unknown"}]` };
849
+ }
850
+ // Unknown type - return as text
851
+ return { type: "text", text: JSON.stringify(item) };
852
+ });
853
+ }
720
854
  // Error names for classification (using string names since SDK is optional)
721
855
  const RETRYABLE_ERROR_NAMES = [
722
856
  "APIConnectionError",
@@ -748,7 +882,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
748
882
  // Request Building
749
883
  //
750
884
  buildRequest(request) {
751
- // Convert messages to Anthropic format (remove 'type' property)
885
+ // Convert messages to Anthropic format
752
886
  // Filter out system messages - Anthropic only accepts system as a top-level field
753
887
  const messages = request.messages
754
888
  .filter((msg) => {
@@ -756,9 +890,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
756
890
  return role !== "system";
757
891
  })
758
892
  .map((msg) => {
759
- const anthropicMsg = structuredClone(msg);
760
- delete anthropicMsg.type;
761
- return anthropicMsg;
893
+ const typedMsg = msg;
894
+ return {
895
+ role: typedMsg.role,
896
+ content: convertContentToAnthropic(typedMsg.content),
897
+ };
762
898
  });
763
899
  // Append instructions to last message if provided
764
900
  if (request.instructions && messages.length > 0) {
@@ -860,6 +996,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
860
996
  // Track usage for final chunk
861
997
  let inputTokens = 0;
862
998
  let outputTokens = 0;
999
+ let thinkingTokens = 0;
863
1000
  let model = streamRequest.model;
864
1001
  for await (const event of stream) {
865
1002
  if (event.type === "message_start") {
@@ -912,6 +1049,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
912
1049
  // Extract final usage
913
1050
  if (event.usage) {
914
1051
  outputTokens = event.usage.output_tokens;
1052
+ // Check for thinking tokens in extended thinking responses
1053
+ const extendedUsage = event.usage;
1054
+ if (extendedUsage.thinking_tokens) {
1055
+ thinkingTokens = extendedUsage.thinking_tokens;
1056
+ }
915
1057
  }
916
1058
  }
917
1059
  else if (event.type === "message_stop") {
@@ -922,7 +1064,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
922
1064
  {
923
1065
  input: inputTokens,
924
1066
  output: outputTokens,
925
- reasoning: 0,
1067
+ reasoning: thinkingTokens,
926
1068
  total: inputTokens + outputTokens,
927
1069
  provider: this.name,
928
1070
  model,
@@ -964,12 +1106,14 @@ class AnthropicAdapter extends BaseProviderAdapter {
964
1106
  }
965
1107
  extractUsage(response, model) {
966
1108
  const anthropicResponse = response;
1109
+ // Check for thinking tokens in the usage (extended thinking feature)
1110
+ // Anthropic includes thinking tokens in a separate field when enabled
1111
+ const usage = anthropicResponse.usage;
967
1112
  return {
968
- input: anthropicResponse.usage.input_tokens,
969
- output: anthropicResponse.usage.output_tokens,
970
- reasoning: 0, // Anthropic doesn't expose reasoning tokens
971
- total: anthropicResponse.usage.input_tokens +
972
- anthropicResponse.usage.output_tokens,
1113
+ input: usage.input_tokens,
1114
+ output: usage.output_tokens,
1115
+ reasoning: usage.thinking_tokens || 0,
1116
+ total: usage.input_tokens + usage.output_tokens,
973
1117
  provider: this.name,
974
1118
  model,
975
1119
  };
@@ -1010,6 +1154,15 @@ class AnthropicAdapter extends BaseProviderAdapter {
1010
1154
  // Don't add to history yet - will be added after tool execution
1011
1155
  return historyItems;
1012
1156
  }
1157
+ // Include thinking blocks for extended thinking support
1158
+ // Thinking blocks are preserved in history so extractReasoning can find them
1159
+ for (const block of anthropicResponse.content) {
1160
+ if (block.type === "thinking") {
1161
+ // Push raw block - types are loosely checked at runtime
1162
+ // This allows extractReasoning to access the thinking property
1163
+ historyItems.push(block);
1164
+ }
1165
+ }
1013
1166
  // Extract text content for non-tool responses
1014
1167
  const textBlock = anthropicResponse.content.find((block) => block.type === "text");
1015
1168
  if (textBlock) {
@@ -1577,28 +1730,31 @@ class GeminiAdapter extends BaseProviderAdapter {
1577
1730
  // Handle input/output messages
1578
1731
  if ("role" in message && "content" in message) {
1579
1732
  const role = this.mapRole(message.role);
1580
- const content = typeof message.content === "string"
1581
- ? message.content
1582
- : JSON.stringify(message.content);
1733
+ const parts = this.convertContentToParts(message.content);
1583
1734
  contents.push({
1584
1735
  role,
1585
- parts: [{ text: content }],
1736
+ parts,
1586
1737
  });
1587
1738
  }
1588
1739
  // Handle function call items
1589
1740
  else if ("type" in message &&
1590
1741
  message.type === LlmMessageType.FunctionCall) {
1742
+ // Build the function call part, including thoughtSignature if present
1743
+ // (required for Gemini 3 models)
1744
+ const functionCallPart = {
1745
+ functionCall: {
1746
+ name: message.name,
1747
+ args: JSON.parse(message.arguments || "{}"),
1748
+ id: message.call_id,
1749
+ },
1750
+ };
1751
+ // Preserve thoughtSignature for Gemini 3 models
1752
+ if (message.thoughtSignature) {
1753
+ functionCallPart.thoughtSignature = message.thoughtSignature;
1754
+ }
1591
1755
  contents.push({
1592
1756
  role: "model",
1593
- parts: [
1594
- {
1595
- functionCall: {
1596
- name: message.name,
1597
- args: JSON.parse(message.arguments || "{}"),
1598
- id: message.call_id,
1599
- },
1600
- },
1601
- ],
1757
+ parts: [functionCallPart],
1602
1758
  });
1603
1759
  }
1604
1760
  // Handle function call output items
@@ -1627,6 +1783,91 @@ class GeminiAdapter extends BaseProviderAdapter {
1627
1783
  }
1628
1784
  return contents;
1629
1785
  }
1786
+ convertContentToParts(content) {
1787
+ // Handle string content
1788
+ if (typeof content === "string") {
1789
+ return [{ text: content }];
1790
+ }
1791
+ // Handle array content
1792
+ if (Array.isArray(content)) {
1793
+ const parts = [];
1794
+ for (const item of content) {
1795
+ // Handle Gemini-native parts (already have inlineData, text, etc.)
1796
+ if (item.inlineData) {
1797
+ parts.push({ inlineData: item.inlineData });
1798
+ continue;
1799
+ }
1800
+ if (item.text && !item.type) {
1801
+ // Plain text part
1802
+ parts.push({ text: item.text });
1803
+ continue;
1804
+ }
1805
+ if (item.fileData) {
1806
+ parts.push({ fileData: item.fileData });
1807
+ continue;
1808
+ }
1809
+ // Handle standardized LlmInputContentText
1810
+ if (item.type === LlmMessageType.InputText) {
1811
+ parts.push({ text: item.text });
1812
+ continue;
1813
+ }
1814
+ // Handle standardized LlmInputContentImage
1815
+ if (item.type === LlmMessageType.InputImage) {
1816
+ const imageUrl = item.image_url;
1817
+ // Parse data URL format: data:image/png;base64,<data>
1818
+ if (imageUrl?.startsWith("data:")) {
1819
+ const match = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
1820
+ if (match) {
1821
+ parts.push({
1822
+ inlineData: {
1823
+ mimeType: match[1],
1824
+ data: match[2],
1825
+ },
1826
+ });
1827
+ continue;
1828
+ }
1829
+ }
1830
+ // If not a data URL, try to use as file URI
1831
+ if (imageUrl) {
1832
+ parts.push({
1833
+ fileData: {
1834
+ mimeType: "image/png",
1835
+ fileUri: imageUrl,
1836
+ },
1837
+ });
1838
+ }
1839
+ continue;
1840
+ }
1841
+ // Handle standardized LlmInputContentFile
1842
+ if (item.type === LlmMessageType.InputFile) {
1843
+ const fileData = item.file_data;
1844
+ // Parse data URL format: data:application/pdf;base64,<data>
1845
+ if (fileData?.startsWith("data:")) {
1846
+ const match = fileData.match(/^data:([^;]+);base64,(.+)$/);
1847
+ if (match) {
1848
+ parts.push({
1849
+ inlineData: {
1850
+ mimeType: match[1],
1851
+ data: match[2],
1852
+ },
1853
+ });
1854
+ continue;
1855
+ }
1856
+ }
1857
+ // If not a data URL, just add as text (fallback)
1858
+ if (typeof fileData === "string") {
1859
+ parts.push({ text: `[File: ${item.filename || "unknown"}]` });
1860
+ }
1861
+ continue;
1862
+ }
1863
+ // Fallback: stringify unknown content
1864
+ parts.push({ text: JSON.stringify(item) });
1865
+ }
1866
+ return parts;
1867
+ }
1868
+ // Fallback for other types
1869
+ return [{ text: JSON.stringify(content) }];
1870
+ }
1630
1871
  mapRole(role) {
1631
1872
  switch (role) {
1632
1873
  case LlmMessageRole.User:
@@ -1684,6 +1925,17 @@ class GeminiAdapter extends BaseProviderAdapter {
1684
1925
  // Export singleton instance
1685
1926
  const geminiAdapter = new GeminiAdapter();
1686
1927
 
1928
+ // Patterns for OpenAI reasoning models that support extended thinking
1929
+ const REASONING_MODEL_PATTERNS = [
1930
+ /^gpt-[5-9]/, // GPT-5 and above (gpt-5, gpt-5.1, gpt-5.2, gpt-6, etc.)
1931
+ /^o\d/, // O-series (o1, o3, o4, o5, etc.)
1932
+ ];
1933
+ /**
1934
+ * Check if a model is a reasoning model that supports extended thinking
1935
+ */
1936
+ function isReasoningModel(model) {
1937
+ return REASONING_MODEL_PATTERNS.some((pattern) => pattern.test(model));
1938
+ }
1687
1939
  //
1688
1940
  //
1689
1941
  // Constants
@@ -1722,8 +1974,9 @@ class OpenAiAdapter extends BaseProviderAdapter {
1722
1974
  // Request Building
1723
1975
  //
1724
1976
  buildRequest(request) {
1977
+ const model = request.model || this.defaultModel;
1725
1978
  const openaiRequest = {
1726
- model: request.model || this.defaultModel,
1979
+ model,
1727
1980
  input: request.messages,
1728
1981
  };
1729
1982
  if (request.user) {
@@ -1745,6 +1998,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
1745
1998
  format: request.format,
1746
1999
  };
1747
2000
  }
2001
+ // Enable reasoning summary for reasoning models (o1, o3, etc.)
2002
+ // This allows us to extract reasoning text from the response
2003
+ if (isReasoningModel(model)) {
2004
+ openaiRequest.reasoning = {
2005
+ summary: "auto",
2006
+ };
2007
+ }
1748
2008
  if (request.providerOptions) {
1749
2009
  Object.assign(openaiRequest, request.providerOptions);
1750
2010
  }
@@ -1868,7 +2128,8 @@ class OpenAiAdapter extends BaseProviderAdapter {
1868
2128
  if (response?.usage) {
1869
2129
  inputTokens = response.usage.input_tokens || 0;
1870
2130
  outputTokens = response.usage.output_tokens || 0;
1871
- reasoningTokens = response.usage.output_tokens_details?.reasoning_tokens || 0;
2131
+ reasoningTokens =
2132
+ response.usage.output_tokens_details?.reasoning_tokens || 0;
1872
2133
  }
1873
2134
  }
1874
2135
  else if (eventType === "response.done") {
@@ -2087,6 +2348,41 @@ const openAiAdapter = new OpenAiAdapter();
2087
2348
  // Constants
2088
2349
  //
2089
2350
  const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
2351
+ /**
2352
+ * Convert standardized content items to OpenRouter format
2353
+ * Note: OpenRouter does not support native file/image uploads.
2354
+ * Images and files are discarded with a warning.
2355
+ */
2356
+ function convertContentToOpenRouter(content) {
2357
+ if (typeof content === "string") {
2358
+ return content;
2359
+ }
2360
+ const parts = [];
2361
+ for (const item of content) {
2362
+ // Text content - pass through
2363
+ if (item.type === LlmMessageType.InputText) {
2364
+ parts.push({ type: "text", text: item.text });
2365
+ continue;
2366
+ }
2367
+ // Image content - warn and discard
2368
+ if (item.type === LlmMessageType.InputImage) {
2369
+ log$2.warn("OpenRouter does not support image uploads; image discarded");
2370
+ continue;
2371
+ }
2372
+ // File/Document content - warn and discard
2373
+ if (item.type === LlmMessageType.InputFile) {
2374
+ log$2.warn({ filename: item.filename }, "OpenRouter does not support file uploads; file discarded");
2375
+ continue;
2376
+ }
2377
+ // Unknown type - warn and skip
2378
+ log$2.warn({ item }, "Unknown content type for OpenRouter; discarded");
2379
+ }
2380
+ // If no text parts remain, return empty string to avoid empty array
2381
+ if (parts.length === 0) {
2382
+ return "";
2383
+ }
2384
+ return parts;
2385
+ }
2090
2386
  // OpenRouter SDK error types based on HTTP status codes
2091
2387
  const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
2092
2388
  const RATE_LIMIT_STATUS_CODE = 429;
@@ -2135,9 +2431,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2135
2431
  parameters: tool.parameters,
2136
2432
  },
2137
2433
  }));
2138
- // Determine tool choice based on whether structured output is requested
2139
- const hasStructuredOutput = request.tools.some((t) => t.name === STRUCTURED_OUTPUT_TOOL_NAME);
2140
- openRouterRequest.tool_choice = hasStructuredOutput ? "required" : "auto";
2434
+ // Use "auto" for tool_choice - many OpenRouter models don't support "required"
2435
+ // The structured_output tool prompt already emphasizes it must be called
2436
+ openRouterRequest.tool_choice = "auto";
2141
2437
  }
2142
2438
  if (request.providerOptions) {
2143
2439
  Object.assign(openRouterRequest, request.providerOptions);
@@ -2358,7 +2654,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2358
2654
  return {
2359
2655
  input: usage.promptTokens || usage.prompt_tokens || 0,
2360
2656
  output: usage.completionTokens || usage.completion_tokens || 0,
2361
- reasoning: 0, // OpenRouter doesn't expose reasoning tokens in standard format
2657
+ reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
2362
2658
  total: usage.totalTokens || usage.total_tokens || 0,
2363
2659
  provider: this.name,
2364
2660
  model,
@@ -2404,11 +2700,16 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2404
2700
  }
2405
2701
  // Extract text content for non-tool responses
2406
2702
  if (choice.message.content) {
2407
- historyItems.push({
2703
+ const historyItem = {
2408
2704
  content: choice.message.content,
2409
2705
  role: LlmMessageRole.Assistant,
2410
2706
  type: LlmMessageType.Message,
2411
- });
2707
+ };
2708
+ // Preserve reasoning if present (z-ai/glm models include this)
2709
+ if (choice.message.reasoning) {
2710
+ historyItem.reasoning = choice.message.reasoning;
2711
+ }
2712
+ historyItems.push(historyItem);
2412
2713
  }
2413
2714
  return historyItems;
2414
2715
  }
@@ -2542,7 +2843,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2542
2843
  else if (message.role === "user") {
2543
2844
  openRouterMessages.push({
2544
2845
  role: "user",
2545
- content: message.content,
2846
+ content: convertContentToOpenRouter(message.content),
2546
2847
  });
2547
2848
  }
2548
2849
  else if (message.role === "assistant") {
@@ -2578,7 +2879,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2578
2879
  else {
2579
2880
  openRouterMessages.push({
2580
2881
  role: "user",
2581
- content: message.content,
2882
+ content: convertContentToOpenRouter(message.content),
2582
2883
  });
2583
2884
  }
2584
2885
  }
@@ -2785,6 +3086,265 @@ class HookRunner {
2785
3086
  // Export singleton instance for convenience
2786
3087
  const hookRunner = new HookRunner();
2787
3088
 
3089
+ /**
3090
+ * Type guard to check if an item is an LlmOperateInputFile
3091
+ */
3092
+ function isLlmOperateInputFile(item) {
3093
+ return (typeof item === "object" &&
3094
+ item !== null &&
3095
+ "file" in item &&
3096
+ typeof item.file === "string");
3097
+ }
3098
+ /**
3099
+ * Type guard to check if an item is an LlmOperateInputImage
3100
+ */
3101
+ function isLlmOperateInputImage(item) {
3102
+ return (typeof item === "object" &&
3103
+ item !== null &&
3104
+ "image" in item &&
3105
+ typeof item.image === "string");
3106
+ }
3107
+ /**
3108
+ * Type guard to check if an item is an LlmOperateInputContent
3109
+ */
3110
+ function isLlmOperateInputContent(item) {
3111
+ return (typeof item === "string" ||
3112
+ isLlmOperateInputFile(item) ||
3113
+ isLlmOperateInputImage(item));
3114
+ }
3115
+ /**
3116
+ * Type guard to check if input is an LlmOperateInput array
3117
+ */
3118
+ function isLlmOperateInput(input) {
3119
+ return (Array.isArray(input) &&
3120
+ input.length > 0 &&
3121
+ input.every(isLlmOperateInputContent));
3122
+ }
3123
+
3124
+ /**
3125
+ * Image extensions that are auto-detected from file paths
3126
+ */
3127
+ const IMAGE_EXTENSIONS = new Set([
3128
+ "avif",
3129
+ "bmp",
3130
+ "gif",
3131
+ "ico",
3132
+ "jpeg",
3133
+ "jpg",
3134
+ "png",
3135
+ "svg",
3136
+ "tiff",
3137
+ "webp",
3138
+ ]);
3139
+ /**
3140
+ * Mapping of file extensions to MIME types
3141
+ */
3142
+ const EXTENSION_TO_MIME = {
3143
+ // Images
3144
+ avif: "image/avif",
3145
+ bmp: "image/bmp",
3146
+ gif: "image/gif",
3147
+ ico: "image/x-icon",
3148
+ jpeg: "image/jpeg",
3149
+ jpg: "image/jpeg",
3150
+ png: "image/png",
3151
+ svg: "image/svg+xml",
3152
+ tiff: "image/tiff",
3153
+ webp: "image/webp",
3154
+ // Documents
3155
+ pdf: "application/pdf",
3156
+ // Text
3157
+ json: "application/json",
3158
+ txt: "text/plain",
3159
+ };
3160
+ /**
3161
+ * Extract file extension from a path or filename
3162
+ */
3163
+ function getFileExtension(filePath) {
3164
+ const match = filePath.match(/\.(\w+)$/);
3165
+ return match ? match[1].toLowerCase() : undefined;
3166
+ }
3167
+ /**
3168
+ * Determine MIME type from file extension
3169
+ */
3170
+ function getMimeType(filePath) {
3171
+ const ext = getFileExtension(filePath);
3172
+ return ext ? EXTENSION_TO_MIME[ext] : undefined;
3173
+ }
3174
+ /**
3175
+ * Check if a file path has an image extension
3176
+ */
3177
+ function isImageExtension(filePath) {
3178
+ const ext = getFileExtension(filePath);
3179
+ return ext ? IMAGE_EXTENSIONS.has(ext) : false;
3180
+ }
3181
+ /**
3182
+ * Check if a file path has a PDF extension
3183
+ */
3184
+ function isPdfExtension(filePath) {
3185
+ const ext = getFileExtension(filePath);
3186
+ return ext === "pdf";
3187
+ }
3188
+
3189
+ /**
3190
+ * Extract specific pages from a PDF buffer
3191
+ * @param pdfBytes - Buffer containing the PDF data
3192
+ * @param pages - Array of page numbers to extract (1-indexed)
3193
+ * @returns Buffer containing a new PDF with only the specified pages
3194
+ */
3195
+ async function extractPdfPages(pdfBytes, pages) {
3196
+ const pdfDoc = await PDFDocument.load(pdfBytes);
3197
+ const totalPages = pdfDoc.getPageCount();
3198
+ // Validate page numbers
3199
+ for (const pageNum of pages) {
3200
+ if (pageNum < 1 || pageNum > totalPages) {
3201
+ throw new Error(`Page number ${pageNum} is out of range. Document has ${totalPages} pages.`);
3202
+ }
3203
+ }
3204
+ // Create a new PDF with only the specified pages
3205
+ const newPdfDoc = await PDFDocument.create();
3206
+ // Convert 1-indexed page numbers to 0-indexed for pdf-lib
3207
+ const pageIndices = pages.map((p) => p - 1);
3208
+ const copiedPages = await newPdfDoc.copyPages(pdfDoc, pageIndices);
3209
+ for (const page of copiedPages) {
3210
+ newPdfDoc.addPage(page);
3211
+ }
3212
+ const newPdfBytes = await newPdfDoc.save();
3213
+ return Buffer.from(newPdfBytes);
3214
+ }
3215
+
3216
+ /**
3217
+ * Load a file from the local filesystem
3218
+ * @param filePath - Path to the file (relative paths resolve from process.cwd())
3219
+ * @returns Buffer containing the file contents
3220
+ */
3221
+ async function loadLocalFile(filePath) {
3222
+ const absolutePath = resolve(process.cwd(), filePath);
3223
+ return readFile(absolutePath);
3224
+ }
3225
+
3226
+ /**
3227
+ * Load a file from S3
3228
+ * @param bucket - S3 bucket name
3229
+ * @param key - S3 object key
3230
+ * @returns Buffer containing the file contents
3231
+ */
3232
+ async function loadS3File(bucket, key) {
3233
+ return getS3FileBuffer({ bucket, key });
3234
+ }
3235
+
3236
+ /**
3237
+ * Load file data from source (provided data, S3, or local filesystem)
3238
+ */
3239
+ async function loadFileData(item) {
3240
+ // Get the path from either file or image property
3241
+ const path = "file" in item ? item.file : item.image;
3242
+ // Priority 1: Use provided data directly
3243
+ if (item.data) {
3244
+ return Buffer.from(item.data, "base64");
3245
+ }
3246
+ // Priority 2: Load from S3 (explicit bucket or CDK_ENV_BUCKET fallback)
3247
+ const bucket = item.bucket ?? process.env.CDK_ENV_BUCKET;
3248
+ if (bucket) {
3249
+ return loadS3File(bucket, path);
3250
+ }
3251
+ // Priority 3: Load from local filesystem
3252
+ return loadLocalFile(path);
3253
+ }
3254
+ /**
3255
+ * Resolve a file input to content ready for LLM
3256
+ */
3257
+ async function resolveFileInput(item) {
3258
+ let buffer = await loadFileData(item);
3259
+ const mimeType = getMimeType(item.file) || "application/octet-stream";
3260
+ const filename = item.file.split("/").pop() || item.file;
3261
+ // Handle PDF page extraction
3262
+ if (isPdfExtension(item.file) && item.pages && item.pages.length > 0) {
3263
+ buffer = await extractPdfPages(buffer, item.pages);
3264
+ }
3265
+ return {
3266
+ data: buffer.toString("base64"),
3267
+ filename,
3268
+ mimeType,
3269
+ };
3270
+ }
3271
+ /**
3272
+ * Resolve an image input to content ready for LLM
3273
+ */
3274
+ async function resolveImageInput(item) {
3275
+ const buffer = await loadFileData(item);
3276
+ const mimeType = getMimeType(item.image) || "image/png";
3277
+ const filename = item.image.split("/").pop() || item.image;
3278
+ return {
3279
+ data: buffer.toString("base64"),
3280
+ filename,
3281
+ mimeType,
3282
+ };
3283
+ }
3284
+ /**
3285
+ * Convert resolved content to LlmInputContent
3286
+ */
3287
+ function toInputContent(resolved, isImage) {
3288
+ if (isImage) {
3289
+ const imageContent = {
3290
+ image_url: `data:${resolved.mimeType};base64,${resolved.data}`,
3291
+ type: LlmMessageType.InputImage,
3292
+ };
3293
+ return imageContent;
3294
+ }
3295
+ const fileContent = {
3296
+ file_data: `data:${resolved.mimeType};base64,${resolved.data}`,
3297
+ filename: resolved.filename,
3298
+ type: LlmMessageType.InputFile,
3299
+ };
3300
+ return fileContent;
3301
+ }
3302
+ /**
3303
+ * Resolve a single content item
3304
+ */
3305
+ async function resolveContentItem(item) {
3306
+ // String becomes text content
3307
+ if (typeof item === "string") {
3308
+ const textContent = {
3309
+ text: item,
3310
+ type: LlmMessageType.InputText,
3311
+ };
3312
+ return textContent;
3313
+ }
3314
+ // Image input
3315
+ if (isLlmOperateInputImage(item)) {
3316
+ const resolved = await resolveImageInput(item);
3317
+ return toInputContent(resolved, true);
3318
+ }
3319
+ // File input
3320
+ if (isLlmOperateInputFile(item)) {
3321
+ const resolved = await resolveFileInput(item);
3322
+ // Check if the file is actually an image based on extension
3323
+ const isImage = isImageExtension(item.file);
3324
+ return toInputContent(resolved, isImage);
3325
+ }
3326
+ // Fallback - shouldn't reach here if types are correct
3327
+ throw new Error(`Unknown content item type: ${JSON.stringify(item)}`);
3328
+ }
3329
+ /**
3330
+ * Resolve LlmOperateInput to LlmInputMessage
3331
+ *
3332
+ * This function takes the simplified LlmOperateInput format and resolves
3333
+ * all files/images from their sources (provided data, S3, or local filesystem),
3334
+ * converting them to the standard LlmInputMessage format.
3335
+ *
3336
+ * @param input - LlmOperateInput array
3337
+ * @returns LlmInputMessage ready for LLM providers
3338
+ */
3339
+ async function resolveOperateInput(input) {
3340
+ const content = await Promise.all(input.map(resolveContentItem));
3341
+ return {
3342
+ content,
3343
+ role: LlmMessageRole.User,
3344
+ type: LlmMessageType.Message,
3345
+ };
3346
+ }
3347
+
2788
3348
  //
2789
3349
  //
2790
3350
  // Main
@@ -2797,13 +3357,18 @@ class InputProcessor {
2797
3357
  /**
2798
3358
  * Process input with placeholders, history merging, and system message handling
2799
3359
  *
2800
- * @param input - The raw input (string, message, or history)
3360
+ * @param input - The raw input (string, message, history, or LlmOperateInput)
2801
3361
  * @param options - The operate options containing data, history, system, etc.
2802
3362
  * @returns Processed input with all transformations applied
2803
3363
  */
2804
- process(input, options = {}) {
3364
+ async process(input, options = {}) {
3365
+ // Handle LlmOperateInput by resolving files first
3366
+ let resolvedInput = input;
3367
+ if (isLlmOperateInput(input)) {
3368
+ resolvedInput = await resolveOperateInput(input);
3369
+ }
2805
3370
  // Convert input to history format with placeholder substitution
2806
- let history = this.formatInputWithPlaceholders(input, options);
3371
+ let history = this.formatInputWithPlaceholders(resolvedInput, options);
2807
3372
  // Process instructions with placeholders
2808
3373
  const instructions = this.processInstructions(options);
2809
3374
  // Process system prompt with placeholders
@@ -2909,6 +3474,7 @@ class ResponseBuilder {
2909
3474
  model: config.model,
2910
3475
  output: [],
2911
3476
  provider: config.provider,
3477
+ reasoning: [],
2912
3478
  responses: [],
2913
3479
  status: LlmResponseStatus.InProgress,
2914
3480
  usage: [],
@@ -2963,6 +3529,26 @@ class ResponseBuilder {
2963
3529
  this.response.output.push(...items);
2964
3530
  return this;
2965
3531
  }
3532
+ /**
3533
+ * Set the reasoning array
3534
+ */
3535
+ setReasoning(reasoning) {
3536
+ this.response.reasoning = reasoning;
3537
+ return this;
3538
+ }
3539
+ /**
3540
+ * Append reasoning text items
3541
+ */
3542
+ appendToReasoning(...items) {
3543
+ this.response.reasoning.push(...items);
3544
+ return this;
3545
+ }
3546
+ /**
3547
+ * Get the current reasoning array
3548
+ */
3549
+ getReasoning() {
3550
+ return this.response.reasoning;
3551
+ }
2966
3552
  /**
2967
3553
  * Add a raw provider response
2968
3554
  */
@@ -3017,9 +3603,14 @@ class ResponseBuilder {
3017
3603
  return this;
3018
3604
  }
3019
3605
  /**
3020
- * Build and return the final response object
3606
+ * Build and return the final response object.
3607
+ * Automatically extracts reasoning from history if not already set.
3021
3608
  */
3022
3609
  build() {
3610
+ // Extract reasoning from history if not already populated
3611
+ if (this.response.reasoning.length === 0) {
3612
+ this.response.reasoning = extractReasoning(this.response.history);
3613
+ }
3023
3614
  return { ...this.response };
3024
3615
  }
3025
3616
  }
@@ -3200,7 +3791,7 @@ class OperateLoop {
3200
3791
  log$1.var({ "operate.input": input });
3201
3792
  log$1.var({ "operate.options": options });
3202
3793
  // Initialize state
3203
- const state = this.initializeState(input, options);
3794
+ const state = await this.initializeState(input, options);
3204
3795
  const context = this.createContext(options);
3205
3796
  // Build initial request
3206
3797
  let request = this.buildInitialRequest(state, options);
@@ -3229,9 +3820,9 @@ class OperateLoop {
3229
3820
  //
3230
3821
  // Private Methods
3231
3822
  //
3232
- initializeState(input, options) {
3823
+ async initializeState(input, options) {
3233
3824
  // Process input with placeholders
3234
- const processedInput = this.inputProcessorInstance.process(input, options);
3825
+ const processedInput = await this.inputProcessorInstance.process(input, options);
3235
3826
  // Determine max turns
3236
3827
  const maxTurns = maxTurnsFromOptions(options);
3237
3828
  // Initialize response builder
@@ -3259,9 +3850,22 @@ class OperateLoop {
3259
3850
  formattedFormat = this.adapter.formatOutputSchema(options.format);
3260
3851
  }
3261
3852
  // Format tools through adapter
3262
- const formattedTools = toolkit
3263
- ? this.adapter.formatTools(toolkit, formattedFormat)
3264
- : undefined;
3853
+ // If format is provided but no toolkit, create an empty toolkit
3854
+ // so that structured_output tool can be added for providers that need it
3855
+ // (Anthropic, OpenRouter use tool-based structured output)
3856
+ let formattedTools;
3857
+ if (toolkit) {
3858
+ formattedTools = this.adapter.formatTools(toolkit, formattedFormat);
3859
+ }
3860
+ else if (formattedFormat) {
3861
+ // Create empty toolkit just for structured output
3862
+ const emptyToolkit = new Toolkit([]);
3863
+ formattedTools = this.adapter.formatTools(emptyToolkit, formattedFormat);
3864
+ // Only include if there are tools (structured_output was added)
3865
+ if (formattedTools.length === 0) {
3866
+ formattedTools = undefined;
3867
+ }
3868
+ }
3265
3869
  return {
3266
3870
  currentInput: processedInput.history,
3267
3871
  currentTurn: 0,
@@ -3486,12 +4090,16 @@ class OperateLoop {
3486
4090
  else if (part.functionCall) {
3487
4091
  // Function call
3488
4092
  const fc = part.functionCall;
4093
+ // Preserve thoughtSignature for Gemini 3 models (required for tool calls)
4094
+ const thoughtSignature = part
4095
+ .thoughtSignature;
3489
4096
  history.push({
3490
4097
  type: LlmMessageType.FunctionCall,
3491
4098
  name: fc.name || "",
3492
4099
  arguments: JSON.stringify(fc.args || {}),
3493
4100
  call_id: fc.id || "",
3494
4101
  id: fc.id || "",
4102
+ ...(thoughtSignature && { thoughtSignature }),
3495
4103
  });
3496
4104
  }
3497
4105
  else if (part.functionResponse) {
@@ -3574,7 +4182,7 @@ class StreamLoop {
3574
4182
  throw new BadGatewayError(`Provider ${this.adapter.name} does not support streaming`);
3575
4183
  }
3576
4184
  // Initialize state
3577
- const state = this.initializeState(input, options);
4185
+ const state = await this.initializeState(input, options);
3578
4186
  const context = this.createContext(options);
3579
4187
  // Build initial request
3580
4188
  let request = this.buildInitialRequest(state, options);
@@ -3629,9 +4237,9 @@ class StreamLoop {
3629
4237
  //
3630
4238
  // Private Methods
3631
4239
  //
3632
- initializeState(input, options) {
4240
+ async initializeState(input, options) {
3633
4241
  // Process input with placeholders
3634
- const processedInput = this.inputProcessorInstance.process(input, options);
4242
+ const processedInput = await this.inputProcessorInstance.process(input, options);
3635
4243
  // Determine max turns
3636
4244
  const maxTurns = maxTurnsFromOptions(options);
3637
4245
  // Get toolkit
@@ -4972,5 +5580,5 @@ const toolkit = new JaypieToolkit(tools);
4972
5580
  [LlmMessageRole.Developer]: "user",
4973
5581
  });
4974
5582
 
4975
- export { GeminiProvider, JaypieToolkit, constants as LLM, Llm, LlmMessageRole, LlmMessageType, LlmStreamChunkType, OpenRouterProvider, Toolkit, toolkit, tools };
5583
+ export { GeminiProvider, JaypieToolkit, constants as LLM, Llm, LlmMessageRole, LlmMessageType, LlmStreamChunkType, OpenRouterProvider, Toolkit, extractReasoning, isLlmOperateInput, isLlmOperateInputContent, isLlmOperateInputFile, isLlmOperateInputImage, toolkit, tools };
4976
5584
  //# sourceMappingURL=index.js.map