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