Skip to content

Feat/prompt caching - #137

Open
YoussefFadh wants to merge 2 commits into
mainfrom
feat/prompt-caching
Open

Feat/prompt caching#137
YoussefFadh wants to merge 2 commits into
mainfrom
feat/prompt-caching

Conversation

@YoussefFadh

Copy link
Copy Markdown
Contributor

Add prompt caching to the sourcehunt hunt loop

Sourcehunt's hunt is a many-turn conversation, and every turn re-sent the whole context (system prompt, tool schemas, and the full running history) at full input price. That cost compounds fast on a deep hunt.

This change caches the repeated prefix so each turn reads it back from the model cache instead of paying to send it again. It's a transport-level hint only: the model still receives identical input, so it has no effect on what the hunt finds. It's opt-in and defaults off, so existing callers are unchanged, and it's inert on providers without caching. All four multi-turn loops (hunt, elaboration, exploitation, reverse-engineering) pick it up through arun.

Impact (measured before/after on our ground-truth CVEs, real Bedrock billing):

  • Hunt cost down 52–93% per CVE (roughly $79 → $29 combined)
  • Uncached input tokens down 96–99%, with 90%+ of each turn served from cache
  • Same vulnerabilities found as before, zero errors

Changes: one helper plus two optional params in AsyncLLMClient (clearwing/llm/native.py), the hunt loop opting in (clearwing/sourcehunt/hunter.py), and tests.

YoussefFadh and others added 2 commits August 10, 2026 17:42
The sourcehunt ReAct loop re-sends system + tool schemas + the full
growing message history on every turn (hunter.py:1452) and never prunes,
so the same large prefix is billed as fresh input tokens dozens of times
per deep hunt. Nothing set a cache directive today (native.py:573).

Add an opt-in, default-off cache policy to AsyncLLMClient.achat /
achat_stream (cache_prefix, prompt_cache_key), plumbed through
aask_text/aask_json and preserved across the reasoning/max_tokens retry
rebuilds. _mark_cache_prefix places one rolling cache_control="ephemeral"
breakpoint on the last message (a copy — caller objects are never
mutated, so markers can't accumulate past Anthropic's 4-breakpoint cap)
and NativeHunter.arun opts in with a per-hunt stable prompt_cache_key.

This is a transport/billing hint only: the model receives byte-identical
input, so findings are unchanged by construction. Inert on providers
without caching (genai_pyo3 maps the string per-provider). Cached-token
accounting was already wired (budget.py:389). Default-off means every
existing caller keeps sending identical requests.

Co-Authored-By: Claude <noreply@anthropic.com>
Reconstructing the last ChatMessage to attach cache_control dropped the
reasoning value paired with an assistant turn's thought_signature, so
Anthropic rejected the follow-up turn ("thinking blocks require one
reasoning value per thought signature"). A benchmark run surfaced this
on turn 2 of a deep hunt.

Mutate only the cache_control field in place instead: it is the one
field that is never part of the tokenized prompt, so toggling it is
invisible to the model and cannot drop reasoning_content,
thought_signatures, or raw_content_json. Clear stale markers on earlier
messages so the reused hunter history never exceeds one breakpoint.

Verified against the gateway: turn 1 writes cache, turn 2 reads it, no
400. Adds test_native_cache_prefix.py.

Co-Authored-By: Claude <noreply@anthropic.com>

@jorge-garcia-le jorge-garcia-le left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will require some extra works for fireworks it seems: https://docs.fireworks.ai/guides/prompt-caching

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.

2 participants