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