module

LLM::Cache

Constants

CACHE_DIR_PERMISSIONS = 448

0700, and entries below are written 0600 (see store). A cache entry is the model's answer about a specific chunk of the user's source, and the prompt it answers is that source — private code, on a box that may have other accounts. The default 0755/0644 published it to every local user. $NOIR_HOME/config.yaml is already 0600 for the same reason.

CACHE_FILE_PERMISSIONS = 384
CACHE_FILE_SUFFIX = ".json"

All cache entries are stored as <sha256>.json flat in cache_dir. The bulk operations below (clear, purge_older_than, stats) filter on this suffix so a stray .lock or user-dropped file in the directory is left alone.

CACHE_TMP_MARKER = ".tmp-"

Infix store stamps onto its in-flight temp file (see store). Kept as a constant so the writer and the sweepers below can't drift apart: a stranded temp file whose name the sweepers don't recognize is a file nothing can ever delete.

KEY_VERSION = "2"

The prompts themselves are part of every key. Bump this when a change outside them (adapter-side wrapping, how a reply is read) should stop older cached replies from being replayed.

Class methods

cache_dir
Source
clear
Source
disable
Source
disabled_by_env?
Source
enabled?
Source
ensure_dir
Source
fetch(key : String) : String | Nil
Source
hits

Responses served from disk this run, for the end-of-run usage line.

Source
key(provider : String, model : String, kind : String, format : String, payload : String) : String

Build a deterministic cache key from inputs

  • provider: "openai", "ollama", url, etc.
  • model: "gpt-4o", "llama3", etc.
  • kind: logical operation e.g. "FILTER", "ANALYZE", "BUNDLE_ANALYZE"
  • format: response_format string (e.g., "json" or JSON schema string)
  • payload: variable content (file list, source code, bundle, etc.)

The sampling settings are folded in too, so a reply sampled at another temperature or seed is not replayed.

Returns a hex-encoded SHA256 digest.

Source
logger=(logger : NoirLogger | Nil)

Where the swallowed cache failures below are reported.

They used to go to Crystal's global Log at debug level, which was wrong twice over: that logger's default backend writes to STDOUT, the stream carrying the -f json / -f sarif report, and nothing in Noir ever lowers its Info threshold, so the messages could not surface at all. A diagnostic that is simultaneously invisible and aimed at the report stream is the worst of both.

NoirLogger (STDERR, gated on --debug) is where every other Noir diagnostic goes. Nil until a run installs one, so a library caller constructing no logger stays silent exactly as before.

Source
path_for(key : String) : String
Source
purge_older_than(days : Int32) : DeleteOutcome
Source
stats
Source
store(key : String, content : String) : Bool

Write atomically: a partially-written file from a crash mid-write would parse as broken JSON on the next fetch, forcing a spurious fresh API call. By writing to a tmp sibling and renaming we either leave the previous (valid) entry in place or atomically publish the new one.

Source
tmp_entry?(name : String) : Bool

True for a temp file stranded by a crash between File.write and File.rename. These do not end in .json, so the bulk operations used to skip them entirely — clear could not remove them and stats did not count their bytes, leaving files that grew the cache directory with no way to reclaim them from the CLI.

Source

Nested types