@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.
- package/dist/cjs/Llm.d.ts +5 -5
- package/dist/cjs/constants.d.ts +19 -17
- package/dist/cjs/index.cjs +692 -59
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/operate/OperateLoop.d.ts +2 -2
- package/dist/cjs/operate/StreamLoop.d.ts +2 -2
- package/dist/cjs/operate/adapters/GeminiAdapter.d.ts +2 -1
- package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +1 -1
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +12 -2
- package/dist/cjs/operate/input/InputProcessor.d.ts +3 -3
- package/dist/cjs/operate/response/ResponseBuilder.d.ts +14 -1
- package/dist/cjs/types/LlmOperateInput.guards.d.ts +17 -0
- package/dist/cjs/types/LlmProvider.interface.d.ts +48 -2
- package/dist/cjs/upload/detectMimeType.d.ts +16 -0
- package/dist/cjs/upload/extractPdfPages.d.ts +13 -0
- package/dist/cjs/upload/index.d.ts +5 -0
- package/dist/cjs/upload/loadLocalFile.d.ts +6 -0
- package/dist/cjs/upload/loadS3File.d.ts +7 -0
- package/dist/cjs/upload/resolveOperateInput.d.ts +12 -0
- package/dist/cjs/util/extractReasoning.d.ts +14 -0
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/esm/Llm.d.ts +5 -5
- package/dist/esm/constants.d.ts +19 -17
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +689 -61
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operate/OperateLoop.d.ts +2 -2
- package/dist/esm/operate/StreamLoop.d.ts +2 -2
- package/dist/esm/operate/adapters/GeminiAdapter.d.ts +2 -1
- package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +1 -1
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +12 -2
- package/dist/esm/operate/input/InputProcessor.d.ts +3 -3
- package/dist/esm/operate/response/ResponseBuilder.d.ts +14 -1
- package/dist/esm/types/LlmOperateInput.guards.d.ts +17 -0
- package/dist/esm/types/LlmProvider.interface.d.ts +48 -2
- package/dist/esm/upload/detectMimeType.d.ts +16 -0
- package/dist/esm/upload/extractPdfPages.d.ts +13 -0
- package/dist/esm/upload/index.d.ts +5 -0
- package/dist/esm/upload/loadLocalFile.d.ts +6 -0
- package/dist/esm/upload/loadS3File.d.ts +7 -0
- package/dist/esm/upload/resolveOperateInput.d.ts +12 -0
- package/dist/esm/util/extractReasoning.d.ts +14 -0
- package/dist/esm/util/index.d.ts +1 -0
- package/package.json +2 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -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: "
|
|
21
|
-
SMALL: "
|
|
22
|
-
LARGE: "
|
|
23
|
-
TINY: "
|
|
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-
|
|
39
|
-
|
|
40
|
-
|
|
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-
|
|
116
|
-
SMALL: "gpt-
|
|
117
|
-
LARGE: "gpt-
|
|
118
|
-
TINY: "gpt-
|
|
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
|
|
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
|
|
762
|
-
|
|
763
|
-
|
|
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:
|
|
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:
|
|
971
|
-
output:
|
|
972
|
-
reasoning: 0,
|
|
973
|
-
total:
|
|
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
|
|
1583
|
-
? message.content
|
|
1584
|
-
: JSON.stringify(message.content);
|
|
1744
|
+
const parts = this.convertContentToParts(message.content);
|
|
1585
1745
|
contents.push({
|
|
1586
1746
|
role,
|
|
1587
|
-
parts
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
2141
|
-
|
|
2142
|
-
openRouterRequest.tool_choice =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
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
|