llmist 17.1.0 → 17.3.0
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/index.cjs +204 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -12
- package/dist/index.d.ts +24 -12
- package/dist/index.js +204 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4660,6 +4660,9 @@ var init_create_gadget = __esm({
|
|
|
4660
4660
|
});
|
|
4661
4661
|
|
|
4662
4662
|
// src/gadgets/output-viewer.ts
|
|
4663
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
4664
|
+
return count === 1 ? singular : plural;
|
|
4665
|
+
}
|
|
4663
4666
|
function applyPattern(lines, pattern) {
|
|
4664
4667
|
const regex = new RegExp(pattern.regex);
|
|
4665
4668
|
if (!pattern.include) {
|
|
@@ -4684,80 +4687,169 @@ function applyPatterns(lines, patterns) {
|
|
|
4684
4687
|
}
|
|
4685
4688
|
return result;
|
|
4686
4689
|
}
|
|
4687
|
-
function
|
|
4690
|
+
function parseLimitWindow(limit) {
|
|
4688
4691
|
const trimmed = limit.trim();
|
|
4689
4692
|
if (trimmed.endsWith("-") && !trimmed.startsWith("-")) {
|
|
4690
4693
|
const n = parseInt(trimmed.slice(0, -1), 10);
|
|
4691
|
-
if (!isNaN(n) && n > 0) {
|
|
4692
|
-
return
|
|
4694
|
+
if (!Number.isNaN(n) && n > 0) {
|
|
4695
|
+
return { kind: "first", count: n };
|
|
4693
4696
|
}
|
|
4694
4697
|
}
|
|
4695
4698
|
if (trimmed.startsWith("-") && !trimmed.includes("-", 1)) {
|
|
4696
4699
|
const n = parseInt(trimmed, 10);
|
|
4697
|
-
if (!isNaN(n) && n < 0) {
|
|
4698
|
-
return
|
|
4700
|
+
if (!Number.isNaN(n) && n < 0) {
|
|
4701
|
+
return { kind: "last", count: Math.abs(n) };
|
|
4699
4702
|
}
|
|
4700
4703
|
}
|
|
4701
4704
|
const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/);
|
|
4702
4705
|
if (rangeMatch) {
|
|
4703
4706
|
const start = parseInt(rangeMatch[1], 10);
|
|
4704
4707
|
const end = parseInt(rangeMatch[2], 10);
|
|
4705
|
-
if (!isNaN(start) && !isNaN(end) && start > 0 && end >= start) {
|
|
4706
|
-
return
|
|
4708
|
+
if (!Number.isNaN(start) && !Number.isNaN(end) && start > 0 && end >= start) {
|
|
4709
|
+
return { kind: "range", start, end };
|
|
4707
4710
|
}
|
|
4708
4711
|
}
|
|
4709
|
-
return
|
|
4712
|
+
return null;
|
|
4713
|
+
}
|
|
4714
|
+
function applyLineLimit(lines, limit) {
|
|
4715
|
+
const window = parseLimitWindow(limit);
|
|
4716
|
+
if (!window) return lines;
|
|
4717
|
+
switch (window.kind) {
|
|
4718
|
+
case "first":
|
|
4719
|
+
return lines.slice(0, window.count);
|
|
4720
|
+
case "last":
|
|
4721
|
+
return lines.slice(-window.count);
|
|
4722
|
+
case "range":
|
|
4723
|
+
return lines.slice(window.start - 1, window.end);
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
function applyCharacterLimit(content, limit, maxOutputChars) {
|
|
4727
|
+
const total = content.length;
|
|
4728
|
+
if (total === 0) {
|
|
4729
|
+
return { text: "", start: 0, end: 0, total: 0, truncatedBySize: false, hasMoreAfter: false };
|
|
4730
|
+
}
|
|
4731
|
+
let startIndex = 0;
|
|
4732
|
+
let endExclusive = total;
|
|
4733
|
+
const window = limit ? parseLimitWindow(limit) : null;
|
|
4734
|
+
if (window) {
|
|
4735
|
+
switch (window.kind) {
|
|
4736
|
+
case "first":
|
|
4737
|
+
endExclusive = Math.min(window.count, total);
|
|
4738
|
+
break;
|
|
4739
|
+
case "last":
|
|
4740
|
+
startIndex = Math.max(0, total - window.count);
|
|
4741
|
+
break;
|
|
4742
|
+
case "range":
|
|
4743
|
+
startIndex = Math.min(window.start - 1, total);
|
|
4744
|
+
endExclusive = Math.min(window.end, total);
|
|
4745
|
+
break;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
let text3 = content.slice(startIndex, endExclusive);
|
|
4749
|
+
let truncatedBySize = false;
|
|
4750
|
+
if (text3.length > maxOutputChars) {
|
|
4751
|
+
text3 = window?.kind === "last" ? text3.slice(-maxOutputChars) : text3.slice(0, maxOutputChars);
|
|
4752
|
+
if (window?.kind === "last") {
|
|
4753
|
+
startIndex = endExclusive - text3.length;
|
|
4754
|
+
}
|
|
4755
|
+
truncatedBySize = true;
|
|
4756
|
+
}
|
|
4757
|
+
return {
|
|
4758
|
+
text: text3,
|
|
4759
|
+
start: text3.length === 0 ? 0 : startIndex + 1,
|
|
4760
|
+
end: text3.length === 0 ? 0 : startIndex + text3.length,
|
|
4761
|
+
total,
|
|
4762
|
+
truncatedBySize,
|
|
4763
|
+
hasMoreAfter: startIndex + text3.length < total
|
|
4764
|
+
};
|
|
4765
|
+
}
|
|
4766
|
+
function buildCharacterRangeHint(start, total) {
|
|
4767
|
+
if (total <= 0 || start > total) return null;
|
|
4768
|
+
const end = Math.min(total, start + CHARACTER_HINT_WINDOW - 1);
|
|
4769
|
+
return `${start}-${end}`;
|
|
4770
|
+
}
|
|
4771
|
+
function buildCharacterModeSuggestion(stored, opts = {}) {
|
|
4772
|
+
const hint = buildCharacterRangeHint(opts.start ?? 1, stored.charCount);
|
|
4773
|
+
const action = opts.removePatterns ? "Remove patterns and then try" : "Try";
|
|
4774
|
+
const lineLabel = pluralize(stored.lineCount, "line");
|
|
4775
|
+
return `This output is dense (${stored.lineCount.toLocaleString()} ${lineLabel}; longest line ${stored.maxLineLength.toLocaleString()} chars). ${action} mode: "character"` + (hint ? `, limit: "${hint}"` : "") + ".";
|
|
4776
|
+
}
|
|
4777
|
+
function shouldSuggestCharacterMode(stored, maxOutputChars = DEFAULT_MAX_OUTPUT_CHARS) {
|
|
4778
|
+
return stored.lineCount <= 3 && (stored.maxLineLength > maxOutputChars || stored.maxLineLength >= DENSE_LINE_THRESHOLD);
|
|
4710
4779
|
}
|
|
4711
4780
|
function createGadgetOutputViewer(store, maxOutputChars = DEFAULT_MAX_OUTPUT_CHARS) {
|
|
4712
4781
|
return createGadget({
|
|
4713
4782
|
name: "GadgetOutputViewer",
|
|
4714
|
-
description:
|
|
4783
|
+
description: 'View stored output from gadgets that returned too much data. Use mode "line" for grep-like filtering and mode "character" for raw chunked browsing when the output is dense or effectively single-line. Patterns work only in line mode.',
|
|
4715
4784
|
schema: import_zod.z.object({
|
|
4716
4785
|
id: import_zod.z.string().describe("ID of the stored output (from the truncation message)"),
|
|
4786
|
+
mode: import_zod.z.enum(["line", "character"]).default("line").describe(
|
|
4787
|
+
'Browse by "line" (supports patterns) or by "character" (raw windows for dense output).'
|
|
4788
|
+
),
|
|
4717
4789
|
patterns: import_zod.z.array(patternSchema).optional().describe(
|
|
4718
|
-
|
|
4790
|
+
'Line-mode filter patterns applied in order (like piping through grep). Not supported in mode "character".'
|
|
4719
4791
|
),
|
|
4720
4792
|
limit: import_zod.z.string().optional().describe(
|
|
4721
|
-
"
|
|
4793
|
+
`Pagination window. In mode "line" it is a line range; in mode "character" it is a character range. Formats: "100-" (first 100), "-25" (last 25), "50-100" (inclusive range).`
|
|
4722
4794
|
)
|
|
4723
4795
|
}),
|
|
4724
4796
|
examples: [
|
|
4725
4797
|
{
|
|
4726
4798
|
comment: "View first 50 lines of stored output",
|
|
4727
|
-
params: { id: "Search_abc12345", limit: "50-" }
|
|
4799
|
+
params: { id: "Search_abc12345", mode: "line", limit: "50-" }
|
|
4728
4800
|
},
|
|
4729
4801
|
{
|
|
4730
4802
|
comment: "Filter for error lines with context",
|
|
4731
4803
|
params: {
|
|
4732
4804
|
id: "Search_abc12345",
|
|
4805
|
+
mode: "line",
|
|
4733
4806
|
patterns: [{ regex: "error|Error|ERROR", include: true, before: 2, after: 5 }]
|
|
4734
4807
|
}
|
|
4735
4808
|
},
|
|
4736
4809
|
{
|
|
4737
|
-
comment: "Exclude blank lines, then show first 100",
|
|
4810
|
+
comment: "Exclude blank lines, then show first 100 lines",
|
|
4738
4811
|
params: {
|
|
4739
4812
|
id: "Search_abc12345",
|
|
4813
|
+
mode: "line",
|
|
4740
4814
|
patterns: [{ regex: "^\\s*$", include: false, before: 0, after: 0 }],
|
|
4741
4815
|
limit: "100-"
|
|
4742
4816
|
}
|
|
4743
4817
|
},
|
|
4744
4818
|
{
|
|
4745
|
-
comment: "
|
|
4819
|
+
comment: "Browse the raw output by character window when line mode is too dense",
|
|
4746
4820
|
params: {
|
|
4747
4821
|
id: "Search_abc12345",
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
{ regex: "test|spec", include: false, before: 0, after: 0 }
|
|
4751
|
-
],
|
|
4752
|
-
limit: "50-"
|
|
4822
|
+
mode: "character",
|
|
4823
|
+
limit: "1-2000"
|
|
4753
4824
|
}
|
|
4754
4825
|
}
|
|
4755
4826
|
],
|
|
4756
|
-
execute: ({ id, patterns, limit }) => {
|
|
4827
|
+
execute: ({ id, mode, patterns, limit }) => {
|
|
4757
4828
|
const stored = store.get(id);
|
|
4758
4829
|
if (!stored) {
|
|
4759
4830
|
return `Error: No stored output with id "${id}". Available IDs: ${store.getIds().join(", ") || "(none)"}`;
|
|
4760
4831
|
}
|
|
4832
|
+
const suggestCharacterMode = shouldSuggestCharacterMode(stored, maxOutputChars);
|
|
4833
|
+
if (mode === "character") {
|
|
4834
|
+
if (patterns && patterns.length > 0) {
|
|
4835
|
+
return 'Error: patterns are only supported in mode "line". Remove patterns or switch back to mode: "line".';
|
|
4836
|
+
}
|
|
4837
|
+
const window = applyCharacterLimit(stored.content, limit, maxOutputChars);
|
|
4838
|
+
if (window.total === 0) {
|
|
4839
|
+
return "[Mode: character | Output is empty]";
|
|
4840
|
+
}
|
|
4841
|
+
const header2 = [
|
|
4842
|
+
`[Mode: character | Showing chars ${window.start.toLocaleString()}-${window.end.toLocaleString()} of ${window.total.toLocaleString()}${window.truncatedBySize ? " (truncated due to viewer size limit)" : ""}]`
|
|
4843
|
+
];
|
|
4844
|
+
if (window.hasMoreAfter) {
|
|
4845
|
+
const nextRange = buildCharacterRangeHint(window.end + 1, window.total);
|
|
4846
|
+
if (nextRange) {
|
|
4847
|
+
header2.push(`[Next chunk: mode: "character", limit: "${nextRange}"]`);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
return `${header2.join("\n")}
|
|
4851
|
+
${window.text}`;
|
|
4852
|
+
}
|
|
4761
4853
|
let lines = stored.content.split("\n");
|
|
4762
4854
|
if (patterns && patterns.length > 0) {
|
|
4763
4855
|
lines = applyPatterns(
|
|
@@ -4773,55 +4865,77 @@ function createGadgetOutputViewer(store, maxOutputChars = DEFAULT_MAX_OUTPUT_CHA
|
|
|
4773
4865
|
if (limit) {
|
|
4774
4866
|
lines = applyLineLimit(lines, limit);
|
|
4775
4867
|
}
|
|
4776
|
-
let output = lines.join("\n");
|
|
4777
4868
|
const totalLines = stored.lineCount;
|
|
4869
|
+
const totalLineLabel = pluralize(totalLines, "line");
|
|
4778
4870
|
const returnedLines = lines.length;
|
|
4779
4871
|
if (returnedLines === 0) {
|
|
4780
|
-
|
|
4872
|
+
const base = `No lines matched the filters. Original output had ${totalLines.toLocaleString()} lines.`;
|
|
4873
|
+
if (!suggestCharacterMode) return base;
|
|
4874
|
+
return `${base} ${buildCharacterModeSuggestion(stored, {
|
|
4875
|
+
removePatterns: Boolean(patterns && patterns.length > 0)
|
|
4876
|
+
})}`;
|
|
4781
4877
|
}
|
|
4878
|
+
let output = lines.join("\n");
|
|
4782
4879
|
let truncatedBySize = false;
|
|
4783
4880
|
let linesIncluded = returnedLines;
|
|
4881
|
+
let clippedFirstLine = false;
|
|
4784
4882
|
if (output.length > maxOutputChars) {
|
|
4785
4883
|
truncatedBySize = true;
|
|
4786
4884
|
let truncatedOutput = "";
|
|
4787
4885
|
linesIncluded = 0;
|
|
4788
4886
|
for (const line of lines) {
|
|
4789
|
-
|
|
4790
|
-
|
|
4887
|
+
const addition = linesIncluded === 0 ? line : `
|
|
4888
|
+
${line}`;
|
|
4889
|
+
if (truncatedOutput.length + addition.length > maxOutputChars) break;
|
|
4890
|
+
truncatedOutput += addition;
|
|
4791
4891
|
linesIncluded++;
|
|
4792
4892
|
}
|
|
4893
|
+
if (linesIncluded === 0) {
|
|
4894
|
+
clippedFirstLine = true;
|
|
4895
|
+
linesIncluded = 1;
|
|
4896
|
+
truncatedOutput = lines[0].slice(0, maxOutputChars);
|
|
4897
|
+
}
|
|
4793
4898
|
output = truncatedOutput;
|
|
4794
4899
|
}
|
|
4795
4900
|
let header;
|
|
4796
|
-
if (
|
|
4901
|
+
if (clippedFirstLine) {
|
|
4902
|
+
header = `[Mode: line | Showing 1 partial line of ${totalLines.toLocaleString()} ${totalLineLabel} (the selected line exceeds the viewer size limit)]
|
|
4903
|
+
`;
|
|
4904
|
+
} else if (truncatedBySize) {
|
|
4797
4905
|
const remainingLines = returnedLines - linesIncluded;
|
|
4798
|
-
header = `[Showing ${linesIncluded} of ${totalLines}
|
|
4799
|
-
[... ${remainingLines.toLocaleString()} more
|
|
4906
|
+
header = `[Mode: line | Showing ${linesIncluded.toLocaleString()} of ${totalLines.toLocaleString()} ${totalLineLabel} (truncated due to size limit)]
|
|
4907
|
+
[... ${remainingLines.toLocaleString()} more ${pluralize(remainingLines, "line")}. Use limit parameter to paginate, e.g., limit: "${linesIncluded + 1}-${linesIncluded + 200}"]
|
|
4800
4908
|
`;
|
|
4801
4909
|
} else if (returnedLines < totalLines) {
|
|
4802
|
-
header = `[Showing ${returnedLines} of ${totalLines}
|
|
4910
|
+
header = `[Mode: line | Showing ${returnedLines.toLocaleString()} of ${totalLines.toLocaleString()} ${totalLineLabel}]
|
|
4803
4911
|
`;
|
|
4804
4912
|
} else {
|
|
4805
|
-
header = `[Showing all ${totalLines}
|
|
4913
|
+
header = `[Mode: line | Showing all ${totalLines.toLocaleString()} ${totalLineLabel}]
|
|
4806
4914
|
`;
|
|
4807
4915
|
}
|
|
4808
|
-
|
|
4916
|
+
const footer = suggestCharacterMode || clippedFirstLine ? `
|
|
4917
|
+
[Tip: ${buildCharacterModeSuggestion(stored, {
|
|
4918
|
+
removePatterns: Boolean(patterns && patterns.length > 0)
|
|
4919
|
+
})}]` : "";
|
|
4920
|
+
return header + output + footer;
|
|
4809
4921
|
}
|
|
4810
4922
|
});
|
|
4811
4923
|
}
|
|
4812
|
-
var import_zod,
|
|
4924
|
+
var import_zod, DEFAULT_MAX_OUTPUT_CHARS, CHARACTER_HINT_WINDOW, DENSE_LINE_THRESHOLD, patternSchema;
|
|
4813
4925
|
var init_output_viewer = __esm({
|
|
4814
4926
|
"src/gadgets/output-viewer.ts"() {
|
|
4815
4927
|
"use strict";
|
|
4816
4928
|
import_zod = require("zod");
|
|
4817
4929
|
init_create_gadget();
|
|
4930
|
+
DEFAULT_MAX_OUTPUT_CHARS = 76800;
|
|
4931
|
+
CHARACTER_HINT_WINDOW = 2e3;
|
|
4932
|
+
DENSE_LINE_THRESHOLD = 4e3;
|
|
4818
4933
|
patternSchema = import_zod.z.object({
|
|
4819
4934
|
regex: import_zod.z.string().describe("Regular expression to match"),
|
|
4820
4935
|
include: import_zod.z.boolean().default(true).describe("true = keep matching lines, false = exclude matching lines"),
|
|
4821
4936
|
before: import_zod.z.number().int().min(0).default(0).describe("Context lines before each match (like grep -B)"),
|
|
4822
4937
|
after: import_zod.z.number().int().min(0).default(0).describe("Context lines after each match (like grep -A)")
|
|
4823
4938
|
});
|
|
4824
|
-
DEFAULT_MAX_OUTPUT_CHARS = 76800;
|
|
4825
4939
|
}
|
|
4826
4940
|
});
|
|
4827
4941
|
|
|
@@ -4843,12 +4957,15 @@ var init_gadget_output_store = __esm({
|
|
|
4843
4957
|
store(gadgetName, content) {
|
|
4844
4958
|
const id = this.generateId(gadgetName);
|
|
4845
4959
|
const encoder = new TextEncoder();
|
|
4960
|
+
const lines = content.split("\n");
|
|
4846
4961
|
const stored = {
|
|
4847
4962
|
id,
|
|
4848
4963
|
gadgetName,
|
|
4849
4964
|
content,
|
|
4965
|
+
charCount: content.length,
|
|
4850
4966
|
byteSize: encoder.encode(content).length,
|
|
4851
|
-
lineCount:
|
|
4967
|
+
lineCount: lines.length,
|
|
4968
|
+
maxLineLength: lines.reduce((max, line) => Math.max(max, line.length), 0),
|
|
4852
4969
|
timestamp: /* @__PURE__ */ new Date()
|
|
4853
4970
|
};
|
|
4854
4971
|
this.outputs.set(id, stored);
|
|
@@ -4952,16 +5069,20 @@ var init_output_limit_manager = __esm({
|
|
|
4952
5069
|
}
|
|
4953
5070
|
if (result.length > this.charLimit) {
|
|
4954
5071
|
const id = this.outputStore.store(ctx.gadgetName, result);
|
|
4955
|
-
const
|
|
4956
|
-
const
|
|
5072
|
+
const stored = this.outputStore.get(id);
|
|
5073
|
+
const lines = stored?.lineCount ?? result.split("\n").length;
|
|
5074
|
+
const bytes = stored?.byteSize ?? new TextEncoder().encode(result).length;
|
|
5075
|
+
const denseSuggestion = stored && shouldSuggestCharacterMode(stored, this.charLimit) ? ` ${buildCharacterModeSuggestion(stored)}` : "";
|
|
4957
5076
|
this.logger.info("Gadget output exceeded limit, stored for browsing", {
|
|
4958
5077
|
gadgetName: ctx.gadgetName,
|
|
4959
5078
|
outputId: id,
|
|
4960
5079
|
bytes,
|
|
4961
5080
|
lines,
|
|
5081
|
+
charCount: stored?.charCount,
|
|
5082
|
+
maxLineLength: stored?.maxLineLength,
|
|
4962
5083
|
charLimit: this.charLimit
|
|
4963
5084
|
});
|
|
4964
|
-
return `[Gadget "${ctx.gadgetName}" returned too much data: ${bytes.toLocaleString()} bytes, ${lines.toLocaleString()} lines. Use GadgetOutputViewer with id "${id}" to read it]
|
|
5085
|
+
return `[Gadget "${ctx.gadgetName}" returned too much data: ${bytes.toLocaleString()} bytes, ${lines.toLocaleString()} lines. Use GadgetOutputViewer with id "${id}" to read it.]` + denseSuggestion;
|
|
4965
5086
|
}
|
|
4966
5087
|
return result;
|
|
4967
5088
|
};
|
|
@@ -9950,7 +10071,7 @@ var init_openai_compatible_provider = __esm({
|
|
|
9950
10071
|
inputTokens: chunk.usage.prompt_tokens,
|
|
9951
10072
|
outputTokens: chunk.usage.completion_tokens,
|
|
9952
10073
|
totalTokens: chunk.usage.total_tokens,
|
|
9953
|
-
cachedInputTokens: 0,
|
|
10074
|
+
cachedInputTokens: usageDetails?.prompt_tokens_details?.cached_tokens ?? 0,
|
|
9954
10075
|
reasoningTokens: usageDetails?.completion_tokens_details?.reasoning_tokens
|
|
9955
10076
|
} : void 0;
|
|
9956
10077
|
if (finishReason || usage) {
|
|
@@ -11931,7 +12052,7 @@ var init_openrouter = __esm({
|
|
|
11931
12052
|
high: "high",
|
|
11932
12053
|
maximum: "xhigh"
|
|
11933
12054
|
};
|
|
11934
|
-
OpenRouterProvider = class extends OpenAICompatibleProvider {
|
|
12055
|
+
OpenRouterProvider = class _OpenRouterProvider extends OpenAICompatibleProvider {
|
|
11935
12056
|
providerId = "openrouter";
|
|
11936
12057
|
providerAlias = "or";
|
|
11937
12058
|
constructor(client, config = {}) {
|
|
@@ -11941,8 +12062,10 @@ var init_openrouter = __esm({
|
|
|
11941
12062
|
return OPENROUTER_MODELS;
|
|
11942
12063
|
}
|
|
11943
12064
|
/**
|
|
11944
|
-
* Override buildApiRequest to inject reasoning parameters.
|
|
11945
|
-
* OpenRouter normalizes reasoning into the standard OpenAI format
|
|
12065
|
+
* Override buildApiRequest to inject reasoning parameters and cache_control breakpoints.
|
|
12066
|
+
* OpenRouter normalizes reasoning into the standard OpenAI format,
|
|
12067
|
+
* and supports cache_control on message content blocks for both
|
|
12068
|
+
* Anthropic Claude and Google Gemini models.
|
|
11946
12069
|
*/
|
|
11947
12070
|
buildApiRequest(options, descriptor, spec, messages) {
|
|
11948
12071
|
const request = super.buildApiRequest(options, descriptor, spec, messages);
|
|
@@ -11952,8 +12075,49 @@ var init_openrouter = __esm({
|
|
|
11952
12075
|
effort: OPENROUTER_EFFORT_MAP[options.reasoning.effort ?? "medium"]
|
|
11953
12076
|
};
|
|
11954
12077
|
}
|
|
12078
|
+
const cachingEnabled = options.caching?.enabled !== false;
|
|
12079
|
+
if (cachingEnabled) {
|
|
12080
|
+
this.injectCacheBreakpoints(request);
|
|
12081
|
+
}
|
|
11955
12082
|
return request;
|
|
11956
12083
|
}
|
|
12084
|
+
/** Minimal shape for messages in the already-built OpenAI-compatible request. */
|
|
12085
|
+
static CACHE_CONTROL = { type: "ephemeral" };
|
|
12086
|
+
/**
|
|
12087
|
+
* Add cache_control breakpoints to the last system message and last user message.
|
|
12088
|
+
* This enables OpenRouter's prompt caching for supported providers (Anthropic, Gemini).
|
|
12089
|
+
*
|
|
12090
|
+
* Operates on the already-built request object. We cast through `unknown` because
|
|
12091
|
+
* OpenAI's `ChatCompletionMessageParam` union is too narrow to assign content arrays
|
|
12092
|
+
* with the non-standard `cache_control` property.
|
|
12093
|
+
*/
|
|
12094
|
+
injectCacheBreakpoints(request) {
|
|
12095
|
+
const msgs = request.messages;
|
|
12096
|
+
let lastSystemIdx = -1;
|
|
12097
|
+
let lastUserIdx = -1;
|
|
12098
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
12099
|
+
if (msgs[i].role === "system") lastSystemIdx = i;
|
|
12100
|
+
if (msgs[i].role === "user") lastUserIdx = i;
|
|
12101
|
+
}
|
|
12102
|
+
if (lastSystemIdx >= 0) {
|
|
12103
|
+
msgs[lastSystemIdx].content = this.withCacheControl(msgs[lastSystemIdx].content);
|
|
12104
|
+
}
|
|
12105
|
+
if (lastUserIdx >= 0) {
|
|
12106
|
+
msgs[lastUserIdx].content = this.withCacheControl(msgs[lastUserIdx].content);
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
/**
|
|
12110
|
+
* Return a new content array with cache_control on the last block.
|
|
12111
|
+
* String content is promoted to a single-element text block array.
|
|
12112
|
+
*/
|
|
12113
|
+
withCacheControl(content) {
|
|
12114
|
+
if (typeof content === "string") {
|
|
12115
|
+
return [{ type: "text", text: content, cache_control: _OpenRouterProvider.CACHE_CONTROL }];
|
|
12116
|
+
}
|
|
12117
|
+
return content.map(
|
|
12118
|
+
(block, i) => i === content.length - 1 ? { ...block, cache_control: _OpenRouterProvider.CACHE_CONTROL } : block
|
|
12119
|
+
);
|
|
12120
|
+
}
|
|
11957
12121
|
/**
|
|
11958
12122
|
* Get custom headers for OpenRouter analytics.
|
|
11959
12123
|
*/
|