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