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 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 applyLineLimit(lines, limit) {
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 lines.slice(0, n);
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 lines.slice(n);
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 lines.slice(start - 1, end);
4708
+ if (!Number.isNaN(start) && !Number.isNaN(end) && start > 0 && end >= start) {
4709
+ return { kind: "range", start, end };
4707
4710
  }
4708
4711
  }
4709
- return lines;
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: "View stored output from gadgets that returned too much data. Use patterns to filter lines (like grep) and limit to control output size. Patterns are applied first in order, then the limit is applied to the result.",
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
- "Filter patterns applied in order (like piping through grep). Each pattern can include or exclude lines with optional before/after context."
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
- "Line range to return after filtering. Formats: '100-' (first 100), '-25' (last 25), '50-100' (lines 50-100)"
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: "Chain filters: find TODOs, exclude tests, limit to 50 lines",
4819
+ comment: "Browse the raw output by character window when line mode is too dense",
4746
4820
  params: {
4747
4821
  id: "Search_abc12345",
4748
- patterns: [
4749
- { regex: "TODO", include: true, before: 1, after: 1 },
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
- return `No lines matched the filters. Original output had ${totalLines} lines.`;
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
- if (truncatedOutput.length + line.length + 1 > maxOutputChars) break;
4790
- truncatedOutput += line + "\n";
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 (truncatedBySize) {
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} lines (truncated due to size limit)]
4799
- [... ${remainingLines.toLocaleString()} more lines. Use limit parameter to paginate, e.g., limit: "${linesIncluded + 1}-${linesIncluded + 200}"]
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} lines]
4910
+ header = `[Mode: line | Showing ${returnedLines.toLocaleString()} of ${totalLines.toLocaleString()} ${totalLineLabel}]
4803
4911
  `;
4804
4912
  } else {
4805
- header = `[Showing all ${totalLines} lines]
4913
+ header = `[Mode: line | Showing all ${totalLines.toLocaleString()} ${totalLineLabel}]
4806
4914
  `;
4807
4915
  }
4808
- return header + output;
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, patternSchema, DEFAULT_MAX_OUTPUT_CHARS;
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: content.split("\n").length,
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 lines = result.split("\n").length;
4956
- const bytes = new TextEncoder().encode(result).length;
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
  */