Skip to content

feat: v0.6 — shadow mode, cache.stats(), server tools in Anthropic streams - #9

Merged
mateusands merged 6 commits into
mainfrom
feat/v0.6
Sep 28, 2026
Merged

mateusands merged 6 commits into
mainfrom
feat/v0.6

Conversation

@mateusands

Copy link
Copy Markdown
Owner

Summary

Area Change
Shadow mode shadow: true on the cache or per call: every request is looked up, but the model is always called and its answer returned. Lookup events are flagged shadow, misses are still stored, and onShadow receives each hit that would have been served next to the fresh answer. It lives in find(), the single place a hit becomes served, so wrap, create with and without streaming and the .stream() helpers all get it. Metrics: llm_cache_shadow_lookups_total, never counted as tokens saved.
cache.stats() Exact counts, expired entries, hits, sizes, and the top namespaces and models. The CLI's stats uses it.
Server tools Anthropic streams with web search, web fetch or tool search are stored and replayed. Tool input is assembled from input_json_delta like the SDK does; stop_details and container are carried over.
Behaviour change Anthropic answers that ran code or carry a container are no longer stored, streamed or not: the container expires, so a replay would return a dead id.

onShadow is the only hook that receives response content, and the README says so next to the options.

Testing

  • 238 tests from a clean install. Lint, typecheck, build and the consumer check pass. The integration suite is also green on pgvector 0.7.4 / PG17 and 0.8.0 / PG13.
  • Guards checked by breaking them on purpose:
    • disabling shadow in find() breaks the shadow tests on every path;
    • letting shadow count as tokens saved;
    • dropping the container rule;
    • accepting malformed tool input;
    • accepting code execution.
  • Real OpenAI in shadow mode: 3 model calls, and onShadow reported the exact hit and the semantic hit (0.961) with the stored answer next to the fresh one.

Not validated

  • Server tool streams against the real Anthropic API (no key). They are tested with the SDK's MessageStream and typed events.
  • Whether the API returns container when code execution is only offered. The rule refuses any container, so the answer doesn't change the behaviour.

The numbers the CLI's stats command prints are now available from code: exact counts, expired entries, hits, sizes and the top namespaces and models. It checks the table first so a missing one says to run migrate(), and the CLI now goes through it instead of a parallel internal call.
With shadow: true every request is looked up but never served: the model is always called, lookup events are flagged shadow, misses are still stored, and onShadow receives each hit that would have been served next to the fresh answer. It lives in find(), the single place a hit becomes a served answer, so wrap, streams and the stream() helpers all get it. Shadow lookups have their own Prometheus counter and never count as tokens saved.
…arch

Server tool calls and their results are stored and replayed like the SDK accumulates them: tool input is assembled from input_json_delta and parsed when the block stops (an empty buffer is {}), result blocks arrive whole, and stop_details and container are carried over. Answers that ran code or carry a container are no longer stored on either path, since the container they point at expires.
…a stream

The assembler merged only numeric usage fields from message_delta, so server_tool_use and output_tokens_details kept their message_start values: a replayed web-search answer reported 0 web search requests instead of 1. It now merges every non-null field, as MessageStream does.
get() returns null in shadow mode, set() ignores shadow, and onShadow only fires when the fresh answer could itself be stored. Also fixes a shadow test that asserted the wrong cache's call count.
Adds the would-have-hit ratio and shadow lookups by result, and has the metrics demo send 10% of its traffic in shadow mode so both have data (checked by examples/metrics/verify.ts, now 19 checks). Also mounts the dashboard folder instead of the file, since a single-file bind mount kept serving the old copy after an edit, and gives the similarity bars a fixed color instead of the default red threshold.
@mateusands
mateusands merged commit 1fe648d into main Sep 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant