@jaypie/llm 1.2.0 → 1.2.2

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 (45) hide show
  1. package/dist/cjs/Llm.d.ts +5 -5
  2. package/dist/cjs/constants.d.ts +19 -17
  3. package/dist/cjs/index.cjs +692 -59
  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/GeminiAdapter.d.ts +2 -1
  9. package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +1 -1
  10. package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +12 -2
  11. package/dist/cjs/operate/input/InputProcessor.d.ts +3 -3
  12. package/dist/cjs/operate/response/ResponseBuilder.d.ts +14 -1
  13. package/dist/cjs/types/LlmOperateInput.guards.d.ts +17 -0
  14. package/dist/cjs/types/LlmProvider.interface.d.ts +48 -2
  15. package/dist/cjs/upload/detectMimeType.d.ts +16 -0
  16. package/dist/cjs/upload/extractPdfPages.d.ts +13 -0
  17. package/dist/cjs/upload/index.d.ts +5 -0
  18. package/dist/cjs/upload/loadLocalFile.d.ts +6 -0
  19. package/dist/cjs/upload/loadS3File.d.ts +7 -0
  20. package/dist/cjs/upload/resolveOperateInput.d.ts +12 -0
  21. package/dist/cjs/util/extractReasoning.d.ts +14 -0
  22. package/dist/cjs/util/index.d.ts +1 -0
  23. package/dist/esm/Llm.d.ts +5 -5
  24. package/dist/esm/constants.d.ts +19 -17
  25. package/dist/esm/index.d.ts +3 -1
  26. package/dist/esm/index.js +689 -61
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/operate/OperateLoop.d.ts +2 -2
  29. package/dist/esm/operate/StreamLoop.d.ts +2 -2
  30. package/dist/esm/operate/adapters/GeminiAdapter.d.ts +2 -1
  31. package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +1 -1
  32. package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +12 -2
  33. package/dist/esm/operate/input/InputProcessor.d.ts +3 -3
  34. package/dist/esm/operate/response/ResponseBuilder.d.ts +14 -1
  35. package/dist/esm/types/LlmOperateInput.guards.d.ts +17 -0
  36. package/dist/esm/types/LlmProvider.interface.d.ts +48 -2
  37. package/dist/esm/upload/detectMimeType.d.ts +16 -0
  38. package/dist/esm/upload/extractPdfPages.d.ts +13 -0
  39. package/dist/esm/upload/index.d.ts +5 -0
  40. package/dist/esm/upload/loadLocalFile.d.ts +6 -0
  41. package/dist/esm/upload/loadS3File.d.ts +7 -0
  42. package/dist/esm/upload/resolveOperateInput.d.ts +12 -0
  43. package/dist/esm/util/extractReasoning.d.ts +14 -0
  44. package/dist/esm/util/index.d.ts +1 -0
  45. package/package.json +7 -6
@@ -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",
@@ -35,20 +38,23 @@ const PROVIDER = {
35
38
  // https://ai.google.dev/gemini-api/docs/models
36
39
  MODEL: {
37
40
  // Jaypie Aliases
38
- DEFAULT: "gemini-2.5-flash",
39
- SMALL: "gemini-2.5-flash",
40
- LARGE: "gemini-2.5-pro",
41
+ DEFAULT: "gemini-3-pro-preview",
42
+ LARGE: "gemini-3-pro-preview",
43
+ SMALL: "gemini-3-flash-preview",
41
44
  TINY: "gemini-2.0-flash-lite",
45
+ // Gemini 3 Models
46
+ GEMINI_3_FLASH_PREVIEW: "gemini-3-flash-preview",
47
+ GEMINI_3_PRO_PREVIEW: "gemini-3-pro-preview",
42
48
  // Gemini 2.5 Models
43
- GEMINI_2_5_PRO: "gemini-2.5-pro",
44
49
  GEMINI_2_5_FLASH: "gemini-2.5-flash",
50
+ GEMINI_2_5_PRO: "gemini-2.5-pro",
45
51
  // Gemini 2.0 Models
46
52
  GEMINI_2_0_FLASH: "gemini-2.0-flash",
47
53
  GEMINI_2_0_FLASH_LITE: "gemini-2.0-flash-lite",
48
54
  // Gemini 1.5 Models (backward compatibility)
49
- GEMINI_1_5_PRO: "gemini-1.5-pro",
50
55
  GEMINI_1_5_FLASH: "gemini-1.5-flash",
51
56
  GEMINI_1_5_FLASH_8B: "gemini-1.5-flash-8b",
57
+ GEMINI_1_5_PRO: "gemini-1.5-pro",
52
58
  },
53
59
  MODEL_MATCH_WORDS: ["gemini", "google"],
54
60
  NAME: "gemini",
@@ -112,10 +118,10 @@ const PROVIDER = {
112
118
  // https://platform.openai.com/docs/models
113
119
  MODEL: {
114
120
  // Jaypie Aliases
115
- DEFAULT: "gpt-5",
116
- SMALL: "gpt-5-mini",
117
- LARGE: "gpt-5",
118
- TINY: "gpt-5-nano",
121
+ DEFAULT: "gpt-4.1",
122
+ SMALL: "gpt-4.1-mini",
123
+ LARGE: "gpt-4.1",
124
+ TINY: "gpt-4.1-nano",
119
125
  // OpenAI Official
120
126
  GPT_5: "gpt-5",
121
127
  GPT_5_MINI: "gpt-5-mini",
@@ -367,6 +373,76 @@ exports.LlmStreamChunkType = void 0;
367
373
  LlmStreamChunkType["ToolResult"] = "tool_result";
368
374
  })(exports.LlmStreamChunkType || (exports.LlmStreamChunkType = {}));
369
375
 
376
+ /**
377
+ * Type guard to check if an item is a dedicated reasoning item (OpenAI)
378
+ */
379
+ function isReasoningItem(item) {
380
+ return (typeof item === "object" &&
381
+ item !== null &&
382
+ item.type === "reasoning");
383
+ }
384
+ /**
385
+ * Type guard to check if an item is a thinking item (Anthropic)
386
+ */
387
+ function isThinkingItem(item) {
388
+ return (typeof item === "object" &&
389
+ item !== null &&
390
+ item.type === "thinking" &&
391
+ typeof item.thinking === "string");
392
+ }
393
+ /**
394
+ * Type guard to check if an item has a reasoning property
395
+ */
396
+ function hasReasoningProperty(item) {
397
+ return (typeof item === "object" &&
398
+ item !== null &&
399
+ typeof item.reasoning === "string" &&
400
+ item.reasoning !== "");
401
+ }
402
+ /**
403
+ * Extracts reasoning text from LLM history items.
404
+ *
405
+ * Reasoning items may have text in different locations depending on the provider:
406
+ * - OpenAI: `summary` array with objects containing `text` property (type: "reasoning")
407
+ * - OpenAI: `content` property on reasoning items (type: "reasoning")
408
+ * - Anthropic: `thinking` property on thinking items (type: "thinking")
409
+ * - OpenRouter/z-ai: `reasoning` property on message items
410
+ *
411
+ * @param history - The LLM history array to extract reasoning from
412
+ * @returns Array of reasoning text strings
413
+ */
414
+ function extractReasoning(history) {
415
+ const reasoningTexts = [];
416
+ for (const item of history) {
417
+ // Cast to unknown first for type checking
418
+ const entry = item;
419
+ // Handle dedicated reasoning items (OpenAI extended thinking)
420
+ if (isReasoningItem(entry)) {
421
+ // Handle summary array format
422
+ if (Array.isArray(entry.summary)) {
423
+ for (const summaryItem of entry.summary) {
424
+ if (summaryItem?.text && typeof summaryItem.text === "string") {
425
+ reasoningTexts.push(summaryItem.text);
426
+ }
427
+ }
428
+ }
429
+ // Handle direct content format
430
+ if (entry.content && typeof entry.content === "string") {
431
+ reasoningTexts.push(entry.content);
432
+ }
433
+ }
434
+ // Handle thinking items (Anthropic extended thinking)
435
+ if (isThinkingItem(entry)) {
436
+ reasoningTexts.push(entry.thinking);
437
+ }
438
+ // Handle reasoning property on message items (OpenRouter/z-ai format)
439
+ if (hasReasoningProperty(entry)) {
440
+ reasoningTexts.push(entry.reasoning);
441
+ }
442
+ }
443
+ return reasoningTexts;
444
+ }
445
+
370
446
  /**
371
447
  * Converts a string to a standardized LlmInputMessage
372
448
  * @param input - String to format
@@ -719,6 +795,73 @@ var ErrorCategory;
719
795
  // Constants
720
796
  //
721
797
  const STRUCTURED_OUTPUT_TOOL_NAME$2 = "structured_output";
798
+ // Regular expression to parse data URLs: data:mime/type;base64,data
799
+ const DATA_URL_REGEX = /^data:([^;]+);base64,(.+)$/;
800
+ /**
801
+ * Parse a data URL into its components
802
+ */
803
+ function parseDataUrl(dataUrl) {
804
+ const match = dataUrl.match(DATA_URL_REGEX);
805
+ if (!match)
806
+ return null;
807
+ return { mediaType: match[1], data: match[2] };
808
+ }
809
+ /**
810
+ * Convert standardized content items to Anthropic format
811
+ */
812
+ function convertContentToAnthropic(content) {
813
+ if (typeof content === "string") {
814
+ return content;
815
+ }
816
+ return content.map((item) => {
817
+ // Text content
818
+ if (item.type === exports.LlmMessageType.InputText) {
819
+ return { type: "text", text: item.text };
820
+ }
821
+ // Image content
822
+ if (item.type === exports.LlmMessageType.InputImage) {
823
+ const imageUrl = item.image_url || "";
824
+ const parsed = parseDataUrl(imageUrl);
825
+ if (parsed) {
826
+ return {
827
+ type: "image",
828
+ source: {
829
+ type: "base64",
830
+ media_type: parsed.mediaType,
831
+ data: parsed.data,
832
+ },
833
+ };
834
+ }
835
+ // Fallback for URL-based images (not base64)
836
+ return {
837
+ type: "image",
838
+ source: {
839
+ type: "url",
840
+ url: imageUrl,
841
+ },
842
+ };
843
+ }
844
+ // File/Document content (PDF, etc.)
845
+ if (item.type === exports.LlmMessageType.InputFile) {
846
+ const fileData = typeof item.file_data === "string" ? item.file_data : "";
847
+ const parsed = parseDataUrl(fileData);
848
+ if (parsed) {
849
+ return {
850
+ type: "document",
851
+ source: {
852
+ type: "base64",
853
+ media_type: parsed.mediaType,
854
+ data: parsed.data,
855
+ },
856
+ };
857
+ }
858
+ // Fallback - return as text with the filename
859
+ return { type: "text", text: `[File: ${item.filename || "unknown"}]` };
860
+ }
861
+ // Unknown type - return as text
862
+ return { type: "text", text: JSON.stringify(item) };
863
+ });
864
+ }
722
865
  // Error names for classification (using string names since SDK is optional)
723
866
  const RETRYABLE_ERROR_NAMES = [
724
867
  "APIConnectionError",
@@ -750,7 +893,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
750
893
  // Request Building
751
894
  //
752
895
  buildRequest(request) {
753
- // Convert messages to Anthropic format (remove 'type' property)
896
+ // Convert messages to Anthropic format
754
897
  // Filter out system messages - Anthropic only accepts system as a top-level field
755
898
  const messages = request.messages
756
899
  .filter((msg) => {
@@ -758,9 +901,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
758
901
  return role !== "system";
759
902
  })
760
903
  .map((msg) => {
761
- const anthropicMsg = structuredClone(msg);
762
- delete anthropicMsg.type;
763
- return anthropicMsg;
904
+ const typedMsg = msg;
905
+ return {
906
+ role: typedMsg.role,
907
+ content: convertContentToAnthropic(typedMsg.content),
908
+ };
764
909
  });
765
910
  // Append instructions to last message if provided
766
911
  if (request.instructions && messages.length > 0) {
@@ -862,6 +1007,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
862
1007
  // Track usage for final chunk
863
1008
  let inputTokens = 0;
864
1009
  let outputTokens = 0;
1010
+ let thinkingTokens = 0;
865
1011
  let model = streamRequest.model;
866
1012
  for await (const event of stream) {
867
1013
  if (event.type === "message_start") {
@@ -914,6 +1060,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
914
1060
  // Extract final usage
915
1061
  if (event.usage) {
916
1062
  outputTokens = event.usage.output_tokens;
1063
+ // Check for thinking tokens in extended thinking responses
1064
+ const extendedUsage = event.usage;
1065
+ if (extendedUsage.thinking_tokens) {
1066
+ thinkingTokens = extendedUsage.thinking_tokens;
1067
+ }
917
1068
  }
918
1069
  }
919
1070
  else if (event.type === "message_stop") {
@@ -924,7 +1075,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
924
1075
  {
925
1076
  input: inputTokens,
926
1077
  output: outputTokens,
927
- reasoning: 0,
1078
+ reasoning: thinkingTokens,
928
1079
  total: inputTokens + outputTokens,
929
1080
  provider: this.name,
930
1081
  model,
@@ -966,12 +1117,14 @@ class AnthropicAdapter extends BaseProviderAdapter {
966
1117
  }
967
1118
  extractUsage(response, model) {
968
1119
  const anthropicResponse = response;
1120
+ // Check for thinking tokens in the usage (extended thinking feature)
1121
+ // Anthropic includes thinking tokens in a separate field when enabled
1122
+ const usage = anthropicResponse.usage;
969
1123
  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,
1124
+ input: usage.input_tokens,
1125
+ output: usage.output_tokens,
1126
+ reasoning: usage.thinking_tokens || 0,
1127
+ total: usage.input_tokens + usage.output_tokens,
975
1128
  provider: this.name,
976
1129
  model,
977
1130
  };
@@ -1012,6 +1165,15 @@ class AnthropicAdapter extends BaseProviderAdapter {
1012
1165
  // Don't add to history yet - will be added after tool execution
1013
1166
  return historyItems;
1014
1167
  }
1168
+ // Include thinking blocks for extended thinking support
1169
+ // Thinking blocks are preserved in history so extractReasoning can find them
1170
+ for (const block of anthropicResponse.content) {
1171
+ if (block.type === "thinking") {
1172
+ // Push raw block - types are loosely checked at runtime
1173
+ // This allows extractReasoning to access the thinking property
1174
+ historyItems.push(block);
1175
+ }
1176
+ }
1015
1177
  // Extract text content for non-tool responses
1016
1178
  const textBlock = anthropicResponse.content.find((block) => block.type === "text");
1017
1179
  if (textBlock) {
@@ -1579,28 +1741,31 @@ class GeminiAdapter extends BaseProviderAdapter {
1579
1741
  // Handle input/output messages
1580
1742
  if ("role" in message && "content" in message) {
1581
1743
  const role = this.mapRole(message.role);
1582
- const content = typeof message.content === "string"
1583
- ? message.content
1584
- : JSON.stringify(message.content);
1744
+ const parts = this.convertContentToParts(message.content);
1585
1745
  contents.push({
1586
1746
  role,
1587
- parts: [{ text: content }],
1747
+ parts,
1588
1748
  });
1589
1749
  }
1590
1750
  // Handle function call items
1591
1751
  else if ("type" in message &&
1592
1752
  message.type === exports.LlmMessageType.FunctionCall) {
1753
+ // Build the function call part, including thoughtSignature if present
1754
+ // (required for Gemini 3 models)
1755
+ const functionCallPart = {
1756
+ functionCall: {
1757
+ name: message.name,
1758
+ args: JSON.parse(message.arguments || "{}"),
1759
+ id: message.call_id,
1760
+ },
1761
+ };
1762
+ // Preserve thoughtSignature for Gemini 3 models
1763
+ if (message.thoughtSignature) {
1764
+ functionCallPart.thoughtSignature = message.thoughtSignature;
1765
+ }
1593
1766
  contents.push({
1594
1767
  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
- ],
1768
+ parts: [functionCallPart],
1604
1769
  });
1605
1770
  }
1606
1771
  // Handle function call output items
@@ -1629,6 +1794,91 @@ class GeminiAdapter extends BaseProviderAdapter {
1629
1794
  }
1630
1795
  return contents;
1631
1796
  }
1797
+ convertContentToParts(content) {
1798
+ // Handle string content
1799
+ if (typeof content === "string") {
1800
+ return [{ text: content }];
1801
+ }
1802
+ // Handle array content
1803
+ if (Array.isArray(content)) {
1804
+ const parts = [];
1805
+ for (const item of content) {
1806
+ // Handle Gemini-native parts (already have inlineData, text, etc.)
1807
+ if (item.inlineData) {
1808
+ parts.push({ inlineData: item.inlineData });
1809
+ continue;
1810
+ }
1811
+ if (item.text && !item.type) {
1812
+ // Plain text part
1813
+ parts.push({ text: item.text });
1814
+ continue;
1815
+ }
1816
+ if (item.fileData) {
1817
+ parts.push({ fileData: item.fileData });
1818
+ continue;
1819
+ }
1820
+ // Handle standardized LlmInputContentText
1821
+ if (item.type === exports.LlmMessageType.InputText) {
1822
+ parts.push({ text: item.text });
1823
+ continue;
1824
+ }
1825
+ // Handle standardized LlmInputContentImage
1826
+ if (item.type === exports.LlmMessageType.InputImage) {
1827
+ const imageUrl = item.image_url;
1828
+ // Parse data URL format: data:image/png;base64,<data>
1829
+ if (imageUrl?.startsWith("data:")) {
1830
+ const match = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
1831
+ if (match) {
1832
+ parts.push({
1833
+ inlineData: {
1834
+ mimeType: match[1],
1835
+ data: match[2],
1836
+ },
1837
+ });
1838
+ continue;
1839
+ }
1840
+ }
1841
+ // If not a data URL, try to use as file URI
1842
+ if (imageUrl) {
1843
+ parts.push({
1844
+ fileData: {
1845
+ mimeType: "image/png",
1846
+ fileUri: imageUrl,
1847
+ },
1848
+ });
1849
+ }
1850
+ continue;
1851
+ }
1852
+ // Handle standardized LlmInputContentFile
1853
+ if (item.type === exports.LlmMessageType.InputFile) {
1854
+ const fileData = item.file_data;
1855
+ // Parse data URL format: data:application/pdf;base64,<data>
1856
+ if (fileData?.startsWith("data:")) {
1857
+ const match = fileData.match(/^data:([^;]+);base64,(.+)$/);
1858
+ if (match) {
1859
+ parts.push({
1860
+ inlineData: {
1861
+ mimeType: match[1],
1862
+ data: match[2],
1863
+ },
1864
+ });
1865
+ continue;
1866
+ }
1867
+ }
1868
+ // If not a data URL, just add as text (fallback)
1869
+ if (typeof fileData === "string") {
1870
+ parts.push({ text: `[File: ${item.filename || "unknown"}]` });
1871
+ }
1872
+ continue;
1873
+ }
1874
+ // Fallback: stringify unknown content
1875
+ parts.push({ text: JSON.stringify(item) });
1876
+ }
1877
+ return parts;
1878
+ }
1879
+ // Fallback for other types
1880
+ return [{ text: JSON.stringify(content) }];
1881
+ }
1632
1882
  mapRole(role) {
1633
1883
  switch (role) {
1634
1884
  case exports.LlmMessageRole.User:
@@ -1686,6 +1936,28 @@ class GeminiAdapter extends BaseProviderAdapter {
1686
1936
  // Export singleton instance
1687
1937
  const geminiAdapter = new GeminiAdapter();
1688
1938
 
1939
+ // OpenAI reasoning models that support extended thinking
1940
+ const REASONING_MODELS = [
1941
+ // GPT-5 series
1942
+ PROVIDER.OPENAI.MODEL.GPT_5,
1943
+ PROVIDER.OPENAI.MODEL.GPT_5_MINI,
1944
+ PROVIDER.OPENAI.MODEL.GPT_5_NANO,
1945
+ // O-series
1946
+ PROVIDER.OPENAI.MODEL.O1,
1947
+ PROVIDER.OPENAI.MODEL.O1_MINI,
1948
+ PROVIDER.OPENAI.MODEL.O1_PRO,
1949
+ PROVIDER.OPENAI.MODEL.O3,
1950
+ PROVIDER.OPENAI.MODEL.O3_MINI,
1951
+ PROVIDER.OPENAI.MODEL.O3_MINI_HIGH,
1952
+ PROVIDER.OPENAI.MODEL.O3_PRO,
1953
+ PROVIDER.OPENAI.MODEL.O4_MINI,
1954
+ ];
1955
+ /**
1956
+ * Check if a model is a reasoning model that supports extended thinking
1957
+ */
1958
+ function isReasoningModel(model) {
1959
+ return REASONING_MODELS.some((reasoningModel) => model === reasoningModel || model.startsWith(`${reasoningModel}-`));
1960
+ }
1689
1961
  //
1690
1962
  //
1691
1963
  // Constants
@@ -1724,8 +1996,9 @@ class OpenAiAdapter extends BaseProviderAdapter {
1724
1996
  // Request Building
1725
1997
  //
1726
1998
  buildRequest(request) {
1999
+ const model = request.model || this.defaultModel;
1727
2000
  const openaiRequest = {
1728
- model: request.model || this.defaultModel,
2001
+ model,
1729
2002
  input: request.messages,
1730
2003
  };
1731
2004
  if (request.user) {
@@ -1747,6 +2020,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
1747
2020
  format: request.format,
1748
2021
  };
1749
2022
  }
2023
+ // Enable reasoning summary for reasoning models (o1, o3, etc.)
2024
+ // This allows us to extract reasoning text from the response
2025
+ if (isReasoningModel(model)) {
2026
+ openaiRequest.reasoning = {
2027
+ summary: "auto",
2028
+ };
2029
+ }
1750
2030
  if (request.providerOptions) {
1751
2031
  Object.assign(openaiRequest, request.providerOptions);
1752
2032
  }
@@ -1870,7 +2150,8 @@ class OpenAiAdapter extends BaseProviderAdapter {
1870
2150
  if (response?.usage) {
1871
2151
  inputTokens = response.usage.input_tokens || 0;
1872
2152
  outputTokens = response.usage.output_tokens || 0;
1873
- reasoningTokens = response.usage.output_tokens_details?.reasoning_tokens || 0;
2153
+ reasoningTokens =
2154
+ response.usage.output_tokens_details?.reasoning_tokens || 0;
1874
2155
  }
1875
2156
  }
1876
2157
  else if (eventType === "response.done") {
@@ -2089,6 +2370,41 @@ const openAiAdapter = new OpenAiAdapter();
2089
2370
  // Constants
2090
2371
  //
2091
2372
  const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
2373
+ /**
2374
+ * Convert standardized content items to OpenRouter format
2375
+ * Note: OpenRouter does not support native file/image uploads.
2376
+ * Images and files are discarded with a warning.
2377
+ */
2378
+ function convertContentToOpenRouter(content) {
2379
+ if (typeof content === "string") {
2380
+ return content;
2381
+ }
2382
+ const parts = [];
2383
+ for (const item of content) {
2384
+ // Text content - pass through
2385
+ if (item.type === exports.LlmMessageType.InputText) {
2386
+ parts.push({ type: "text", text: item.text });
2387
+ continue;
2388
+ }
2389
+ // Image content - warn and discard
2390
+ if (item.type === exports.LlmMessageType.InputImage) {
2391
+ logger.log.warn("OpenRouter does not support image uploads; image discarded");
2392
+ continue;
2393
+ }
2394
+ // File/Document content - warn and discard
2395
+ if (item.type === exports.LlmMessageType.InputFile) {
2396
+ logger.log.warn({ filename: item.filename }, "OpenRouter does not support file uploads; file discarded");
2397
+ continue;
2398
+ }
2399
+ // Unknown type - warn and skip
2400
+ logger.log.warn({ item }, "Unknown content type for OpenRouter; discarded");
2401
+ }
2402
+ // If no text parts remain, return empty string to avoid empty array
2403
+ if (parts.length === 0) {
2404
+ return "";
2405
+ }
2406
+ return parts;
2407
+ }
2092
2408
  // OpenRouter SDK error types based on HTTP status codes
2093
2409
  const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
2094
2410
  const RATE_LIMIT_STATUS_CODE = 429;
@@ -2137,9 +2453,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2137
2453
  parameters: tool.parameters,
2138
2454
  },
2139
2455
  }));
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";
2456
+ // Use "auto" for tool_choice - many OpenRouter models don't support "required"
2457
+ // The structured_output tool prompt already emphasizes it must be called
2458
+ openRouterRequest.tool_choice = "auto";
2143
2459
  }
2144
2460
  if (request.providerOptions) {
2145
2461
  Object.assign(openRouterRequest, request.providerOptions);
@@ -2360,7 +2676,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2360
2676
  return {
2361
2677
  input: usage.promptTokens || usage.prompt_tokens || 0,
2362
2678
  output: usage.completionTokens || usage.completion_tokens || 0,
2363
- reasoning: 0, // OpenRouter doesn't expose reasoning tokens in standard format
2679
+ reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
2364
2680
  total: usage.totalTokens || usage.total_tokens || 0,
2365
2681
  provider: this.name,
2366
2682
  model,
@@ -2406,11 +2722,16 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2406
2722
  }
2407
2723
  // Extract text content for non-tool responses
2408
2724
  if (choice.message.content) {
2409
- historyItems.push({
2725
+ const historyItem = {
2410
2726
  content: choice.message.content,
2411
2727
  role: exports.LlmMessageRole.Assistant,
2412
2728
  type: exports.LlmMessageType.Message,
2413
- });
2729
+ };
2730
+ // Preserve reasoning if present (z-ai/glm models include this)
2731
+ if (choice.message.reasoning) {
2732
+ historyItem.reasoning = choice.message.reasoning;
2733
+ }
2734
+ historyItems.push(historyItem);
2414
2735
  }
2415
2736
  return historyItems;
2416
2737
  }
@@ -2544,7 +2865,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2544
2865
  else if (message.role === "user") {
2545
2866
  openRouterMessages.push({
2546
2867
  role: "user",
2547
- content: message.content,
2868
+ content: convertContentToOpenRouter(message.content),
2548
2869
  });
2549
2870
  }
2550
2871
  else if (message.role === "assistant") {
@@ -2580,7 +2901,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
2580
2901
  else {
2581
2902
  openRouterMessages.push({
2582
2903
  role: "user",
2583
- content: message.content,
2904
+ content: convertContentToOpenRouter(message.content),
2584
2905
  });
2585
2906
  }
2586
2907
  }
@@ -2787,6 +3108,265 @@ class HookRunner {
2787
3108
  // Export singleton instance for convenience
2788
3109
  const hookRunner = new HookRunner();
2789
3110
 
3111
+ /**
3112
+ * Type guard to check if an item is an LlmOperateInputFile
3113
+ */
3114
+ function isLlmOperateInputFile(item) {
3115
+ return (typeof item === "object" &&
3116
+ item !== null &&
3117
+ "file" in item &&
3118
+ typeof item.file === "string");
3119
+ }
3120
+ /**
3121
+ * Type guard to check if an item is an LlmOperateInputImage
3122
+ */
3123
+ function isLlmOperateInputImage(item) {
3124
+ return (typeof item === "object" &&
3125
+ item !== null &&
3126
+ "image" in item &&
3127
+ typeof item.image === "string");
3128
+ }
3129
+ /**
3130
+ * Type guard to check if an item is an LlmOperateInputContent
3131
+ */
3132
+ function isLlmOperateInputContent(item) {
3133
+ return (typeof item === "string" ||
3134
+ isLlmOperateInputFile(item) ||
3135
+ isLlmOperateInputImage(item));
3136
+ }
3137
+ /**
3138
+ * Type guard to check if input is an LlmOperateInput array
3139
+ */
3140
+ function isLlmOperateInput(input) {
3141
+ return (Array.isArray(input) &&
3142
+ input.length > 0 &&
3143
+ input.every(isLlmOperateInputContent));
3144
+ }
3145
+
3146
+ /**
3147
+ * Image extensions that are auto-detected from file paths
3148
+ */
3149
+ const IMAGE_EXTENSIONS = new Set([
3150
+ "avif",
3151
+ "bmp",
3152
+ "gif",
3153
+ "ico",
3154
+ "jpeg",
3155
+ "jpg",
3156
+ "png",
3157
+ "svg",
3158
+ "tiff",
3159
+ "webp",
3160
+ ]);
3161
+ /**
3162
+ * Mapping of file extensions to MIME types
3163
+ */
3164
+ const EXTENSION_TO_MIME = {
3165
+ // Images
3166
+ avif: "image/avif",
3167
+ bmp: "image/bmp",
3168
+ gif: "image/gif",
3169
+ ico: "image/x-icon",
3170
+ jpeg: "image/jpeg",
3171
+ jpg: "image/jpeg",
3172
+ png: "image/png",
3173
+ svg: "image/svg+xml",
3174
+ tiff: "image/tiff",
3175
+ webp: "image/webp",
3176
+ // Documents
3177
+ pdf: "application/pdf",
3178
+ // Text
3179
+ json: "application/json",
3180
+ txt: "text/plain",
3181
+ };
3182
+ /**
3183
+ * Extract file extension from a path or filename
3184
+ */
3185
+ function getFileExtension(filePath) {
3186
+ const match = filePath.match(/\.(\w+)$/);
3187
+ return match ? match[1].toLowerCase() : undefined;
3188
+ }
3189
+ /**
3190
+ * Determine MIME type from file extension
3191
+ */
3192
+ function getMimeType(filePath) {
3193
+ const ext = getFileExtension(filePath);
3194
+ return ext ? EXTENSION_TO_MIME[ext] : undefined;
3195
+ }
3196
+ /**
3197
+ * Check if a file path has an image extension
3198
+ */
3199
+ function isImageExtension(filePath) {
3200
+ const ext = getFileExtension(filePath);
3201
+ return ext ? IMAGE_EXTENSIONS.has(ext) : false;
3202
+ }
3203
+ /**
3204
+ * Check if a file path has a PDF extension
3205
+ */
3206
+ function isPdfExtension(filePath) {
3207
+ const ext = getFileExtension(filePath);
3208
+ return ext === "pdf";
3209
+ }
3210
+
3211
+ /**
3212
+ * Extract specific pages from a PDF buffer
3213
+ * @param pdfBytes - Buffer containing the PDF data
3214
+ * @param pages - Array of page numbers to extract (1-indexed)
3215
+ * @returns Buffer containing a new PDF with only the specified pages
3216
+ */
3217
+ async function extractPdfPages(pdfBytes, pages) {
3218
+ const pdfDoc = await pdfLib.PDFDocument.load(pdfBytes);
3219
+ const totalPages = pdfDoc.getPageCount();
3220
+ // Validate page numbers
3221
+ for (const pageNum of pages) {
3222
+ if (pageNum < 1 || pageNum > totalPages) {
3223
+ throw new Error(`Page number ${pageNum} is out of range. Document has ${totalPages} pages.`);
3224
+ }
3225
+ }
3226
+ // Create a new PDF with only the specified pages
3227
+ const newPdfDoc = await pdfLib.PDFDocument.create();
3228
+ // Convert 1-indexed page numbers to 0-indexed for pdf-lib
3229
+ const pageIndices = pages.map((p) => p - 1);
3230
+ const copiedPages = await newPdfDoc.copyPages(pdfDoc, pageIndices);
3231
+ for (const page of copiedPages) {
3232
+ newPdfDoc.addPage(page);
3233
+ }
3234
+ const newPdfBytes = await newPdfDoc.save();
3235
+ return Buffer.from(newPdfBytes);
3236
+ }
3237
+
3238
+ /**
3239
+ * Load a file from the local filesystem
3240
+ * @param filePath - Path to the file (relative paths resolve from process.cwd())
3241
+ * @returns Buffer containing the file contents
3242
+ */
3243
+ async function loadLocalFile(filePath) {
3244
+ const absolutePath = path.resolve(process.cwd(), filePath);
3245
+ return promises.readFile(absolutePath);
3246
+ }
3247
+
3248
+ /**
3249
+ * Load a file from S3
3250
+ * @param bucket - S3 bucket name
3251
+ * @param key - S3 object key
3252
+ * @returns Buffer containing the file contents
3253
+ */
3254
+ async function loadS3File(bucket, key) {
3255
+ return aws.getS3FileBuffer({ bucket, key });
3256
+ }
3257
+
3258
+ /**
3259
+ * Load file data from source (provided data, S3, or local filesystem)
3260
+ */
3261
+ async function loadFileData(item) {
3262
+ // Get the path from either file or image property
3263
+ const path = "file" in item ? item.file : item.image;
3264
+ // Priority 1: Use provided data directly
3265
+ if (item.data) {
3266
+ return Buffer.from(item.data, "base64");
3267
+ }
3268
+ // Priority 2: Load from S3 (explicit bucket or CDK_ENV_BUCKET fallback)
3269
+ const bucket = item.bucket ?? process.env.CDK_ENV_BUCKET;
3270
+ if (bucket) {
3271
+ return loadS3File(bucket, path);
3272
+ }
3273
+ // Priority 3: Load from local filesystem
3274
+ return loadLocalFile(path);
3275
+ }
3276
+ /**
3277
+ * Resolve a file input to content ready for LLM
3278
+ */
3279
+ async function resolveFileInput(item) {
3280
+ let buffer = await loadFileData(item);
3281
+ const mimeType = getMimeType(item.file) || "application/octet-stream";
3282
+ const filename = item.file.split("/").pop() || item.file;
3283
+ // Handle PDF page extraction
3284
+ if (isPdfExtension(item.file) && item.pages && item.pages.length > 0) {
3285
+ buffer = await extractPdfPages(buffer, item.pages);
3286
+ }
3287
+ return {
3288
+ data: buffer.toString("base64"),
3289
+ filename,
3290
+ mimeType,
3291
+ };
3292
+ }
3293
+ /**
3294
+ * Resolve an image input to content ready for LLM
3295
+ */
3296
+ async function resolveImageInput(item) {
3297
+ const buffer = await loadFileData(item);
3298
+ const mimeType = getMimeType(item.image) || "image/png";
3299
+ const filename = item.image.split("/").pop() || item.image;
3300
+ return {
3301
+ data: buffer.toString("base64"),
3302
+ filename,
3303
+ mimeType,
3304
+ };
3305
+ }
3306
+ /**
3307
+ * Convert resolved content to LlmInputContent
3308
+ */
3309
+ function toInputContent(resolved, isImage) {
3310
+ if (isImage) {
3311
+ const imageContent = {
3312
+ image_url: `data:${resolved.mimeType};base64,${resolved.data}`,
3313
+ type: exports.LlmMessageType.InputImage,
3314
+ };
3315
+ return imageContent;
3316
+ }
3317
+ const fileContent = {
3318
+ file_data: `data:${resolved.mimeType};base64,${resolved.data}`,
3319
+ filename: resolved.filename,
3320
+ type: exports.LlmMessageType.InputFile,
3321
+ };
3322
+ return fileContent;
3323
+ }
3324
+ /**
3325
+ * Resolve a single content item
3326
+ */
3327
+ async function resolveContentItem(item) {
3328
+ // String becomes text content
3329
+ if (typeof item === "string") {
3330
+ const textContent = {
3331
+ text: item,
3332
+ type: exports.LlmMessageType.InputText,
3333
+ };
3334
+ return textContent;
3335
+ }
3336
+ // Image input
3337
+ if (isLlmOperateInputImage(item)) {
3338
+ const resolved = await resolveImageInput(item);
3339
+ return toInputContent(resolved, true);
3340
+ }
3341
+ // File input
3342
+ if (isLlmOperateInputFile(item)) {
3343
+ const resolved = await resolveFileInput(item);
3344
+ // Check if the file is actually an image based on extension
3345
+ const isImage = isImageExtension(item.file);
3346
+ return toInputContent(resolved, isImage);
3347
+ }
3348
+ // Fallback - shouldn't reach here if types are correct
3349
+ throw new Error(`Unknown content item type: ${JSON.stringify(item)}`);
3350
+ }
3351
+ /**
3352
+ * Resolve LlmOperateInput to LlmInputMessage
3353
+ *
3354
+ * This function takes the simplified LlmOperateInput format and resolves
3355
+ * all files/images from their sources (provided data, S3, or local filesystem),
3356
+ * converting them to the standard LlmInputMessage format.
3357
+ *
3358
+ * @param input - LlmOperateInput array
3359
+ * @returns LlmInputMessage ready for LLM providers
3360
+ */
3361
+ async function resolveOperateInput(input) {
3362
+ const content = await Promise.all(input.map(resolveContentItem));
3363
+ return {
3364
+ content,
3365
+ role: exports.LlmMessageRole.User,
3366
+ type: exports.LlmMessageType.Message,
3367
+ };
3368
+ }
3369
+
2790
3370
  //
2791
3371
  //
2792
3372
  // Main
@@ -2799,13 +3379,18 @@ class InputProcessor {
2799
3379
  /**
2800
3380
  * Process input with placeholders, history merging, and system message handling
2801
3381
  *
2802
- * @param input - The raw input (string, message, or history)
3382
+ * @param input - The raw input (string, message, history, or LlmOperateInput)
2803
3383
  * @param options - The operate options containing data, history, system, etc.
2804
3384
  * @returns Processed input with all transformations applied
2805
3385
  */
2806
- process(input, options = {}) {
3386
+ async process(input, options = {}) {
3387
+ // Handle LlmOperateInput by resolving files first
3388
+ let resolvedInput = input;
3389
+ if (isLlmOperateInput(input)) {
3390
+ resolvedInput = await resolveOperateInput(input);
3391
+ }
2807
3392
  // Convert input to history format with placeholder substitution
2808
- let history = this.formatInputWithPlaceholders(input, options);
3393
+ let history = this.formatInputWithPlaceholders(resolvedInput, options);
2809
3394
  // Process instructions with placeholders
2810
3395
  const instructions = this.processInstructions(options);
2811
3396
  // Process system prompt with placeholders
@@ -2911,6 +3496,7 @@ class ResponseBuilder {
2911
3496
  model: config.model,
2912
3497
  output: [],
2913
3498
  provider: config.provider,
3499
+ reasoning: [],
2914
3500
  responses: [],
2915
3501
  status: LlmResponseStatus.InProgress,
2916
3502
  usage: [],
@@ -2965,6 +3551,26 @@ class ResponseBuilder {
2965
3551
  this.response.output.push(...items);
2966
3552
  return this;
2967
3553
  }
3554
+ /**
3555
+ * Set the reasoning array
3556
+ */
3557
+ setReasoning(reasoning) {
3558
+ this.response.reasoning = reasoning;
3559
+ return this;
3560
+ }
3561
+ /**
3562
+ * Append reasoning text items
3563
+ */
3564
+ appendToReasoning(...items) {
3565
+ this.response.reasoning.push(...items);
3566
+ return this;
3567
+ }
3568
+ /**
3569
+ * Get the current reasoning array
3570
+ */
3571
+ getReasoning() {
3572
+ return this.response.reasoning;
3573
+ }
2968
3574
  /**
2969
3575
  * Add a raw provider response
2970
3576
  */
@@ -3019,9 +3625,14 @@ class ResponseBuilder {
3019
3625
  return this;
3020
3626
  }
3021
3627
  /**
3022
- * Build and return the final response object
3628
+ * Build and return the final response object.
3629
+ * Automatically extracts reasoning from history if not already set.
3023
3630
  */
3024
3631
  build() {
3632
+ // Extract reasoning from history if not already populated
3633
+ if (this.response.reasoning.length === 0) {
3634
+ this.response.reasoning = extractReasoning(this.response.history);
3635
+ }
3025
3636
  return { ...this.response };
3026
3637
  }
3027
3638
  }
@@ -3202,7 +3813,7 @@ class OperateLoop {
3202
3813
  log$1.var({ "operate.input": input });
3203
3814
  log$1.var({ "operate.options": options });
3204
3815
  // Initialize state
3205
- const state = this.initializeState(input, options);
3816
+ const state = await this.initializeState(input, options);
3206
3817
  const context = this.createContext(options);
3207
3818
  // Build initial request
3208
3819
  let request = this.buildInitialRequest(state, options);
@@ -3231,9 +3842,9 @@ class OperateLoop {
3231
3842
  //
3232
3843
  // Private Methods
3233
3844
  //
3234
- initializeState(input, options) {
3845
+ async initializeState(input, options) {
3235
3846
  // Process input with placeholders
3236
- const processedInput = this.inputProcessorInstance.process(input, options);
3847
+ const processedInput = await this.inputProcessorInstance.process(input, options);
3237
3848
  // Determine max turns
3238
3849
  const maxTurns = maxTurnsFromOptions(options);
3239
3850
  // Initialize response builder
@@ -3261,9 +3872,22 @@ class OperateLoop {
3261
3872
  formattedFormat = this.adapter.formatOutputSchema(options.format);
3262
3873
  }
3263
3874
  // Format tools through adapter
3264
- const formattedTools = toolkit
3265
- ? this.adapter.formatTools(toolkit, formattedFormat)
3266
- : undefined;
3875
+ // If format is provided but no toolkit, create an empty toolkit
3876
+ // so that structured_output tool can be added for providers that need it
3877
+ // (Anthropic, OpenRouter use tool-based structured output)
3878
+ let formattedTools;
3879
+ if (toolkit) {
3880
+ formattedTools = this.adapter.formatTools(toolkit, formattedFormat);
3881
+ }
3882
+ else if (formattedFormat) {
3883
+ // Create empty toolkit just for structured output
3884
+ const emptyToolkit = new Toolkit([]);
3885
+ formattedTools = this.adapter.formatTools(emptyToolkit, formattedFormat);
3886
+ // Only include if there are tools (structured_output was added)
3887
+ if (formattedTools.length === 0) {
3888
+ formattedTools = undefined;
3889
+ }
3890
+ }
3267
3891
  return {
3268
3892
  currentInput: processedInput.history,
3269
3893
  currentTurn: 0,
@@ -3488,12 +4112,16 @@ class OperateLoop {
3488
4112
  else if (part.functionCall) {
3489
4113
  // Function call
3490
4114
  const fc = part.functionCall;
4115
+ // Preserve thoughtSignature for Gemini 3 models (required for tool calls)
4116
+ const thoughtSignature = part
4117
+ .thoughtSignature;
3491
4118
  history.push({
3492
4119
  type: exports.LlmMessageType.FunctionCall,
3493
4120
  name: fc.name || "",
3494
4121
  arguments: JSON.stringify(fc.args || {}),
3495
4122
  call_id: fc.id || "",
3496
4123
  id: fc.id || "",
4124
+ ...(thoughtSignature && { thoughtSignature }),
3497
4125
  });
3498
4126
  }
3499
4127
  else if (part.functionResponse) {
@@ -3576,7 +4204,7 @@ class StreamLoop {
3576
4204
  throw new errors.BadGatewayError(`Provider ${this.adapter.name} does not support streaming`);
3577
4205
  }
3578
4206
  // Initialize state
3579
- const state = this.initializeState(input, options);
4207
+ const state = await this.initializeState(input, options);
3580
4208
  const context = this.createContext(options);
3581
4209
  // Build initial request
3582
4210
  let request = this.buildInitialRequest(state, options);
@@ -3631,9 +4259,9 @@ class StreamLoop {
3631
4259
  //
3632
4260
  // Private Methods
3633
4261
  //
3634
- initializeState(input, options) {
4262
+ async initializeState(input, options) {
3635
4263
  // Process input with placeholders
3636
- const processedInput = this.inputProcessorInstance.process(input, options);
4264
+ const processedInput = await this.inputProcessorInstance.process(input, options);
3637
4265
  // Determine max turns
3638
4266
  const maxTurns = maxTurnsFromOptions(options);
3639
4267
  // Get toolkit
@@ -4980,6 +5608,11 @@ exports.LLM = constants;
4980
5608
  exports.Llm = Llm;
4981
5609
  exports.OpenRouterProvider = OpenRouterProvider;
4982
5610
  exports.Toolkit = Toolkit;
5611
+ exports.extractReasoning = extractReasoning;
5612
+ exports.isLlmOperateInput = isLlmOperateInput;
5613
+ exports.isLlmOperateInputContent = isLlmOperateInputContent;
5614
+ exports.isLlmOperateInputFile = isLlmOperateInputFile;
5615
+ exports.isLlmOperateInputImage = isLlmOperateInputImage;
4983
5616
  exports.toolkit = toolkit;
4984
5617
  exports.tools = tools;
4985
5618
  //# sourceMappingURL=index.cjs.map