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