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