Skip to content

fix: cancellation turn-granularity, extension report gaps, and cache-aware cost pricing - #236

Merged
arunSunnyKVS merged 4 commits into
masterfrom
fix/cancellation-turn-granularity-and-extension-report-gaps
Aug 6, 2026
Merged

fix: cancellation turn-granularity, extension report gaps, and cache-aware cost pricing#236
arunSunnyKVS merged 4 commits into
masterfrom
fix/cancellation-turn-granularity-and-extension-report-gaps

Conversation

@arunSunnyKVS

@arunSunnyKVS arunSunnyKVS commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

Four bugs, surfaced while testing opfor run with Ctrl+C, the extension's Stop button, and comparing reported cost against actual billing.

Cancellation (1)AbortSignal cancellation was only checked between attacks/evaluators, never between turns within one multi-turn attack. A long-running attack (e.g. turns: 100) was atomic — Ctrl+C wouldn't stop it until every turn finished, contradicting the "finishes in-flight attack" promise in the CLI's own interrupt message.

Empty transcripts (2) — an attack interrupted before its first turn completed has no turns and an empty detail card, but the renderer emitted a transcript anyway: headed "1 turn", containing two blank bubbles. That reads as an exchange that happened and came back empty, rather than as nothing having run.

Extension reporting (3) — cancelled runs showed no token usage or cost at all. DomTarget.send() throws an Error tagged code: "OPFOR_STOP" on user cancel, which runAllBrowser didn't recognise as a clean stop condition, so it fell through to an unhandled throw and returned no report — silently dropping the run's token totals. Separately, cost was never plumbed through the extension even on successful runs.

Cost accuracy (4) — cost estimation charged every input token at the full input rate. But inputTokens is the inclusive total: it already contains the cached tokens that providers bill far more cheaply. The token counter was also discarding the cache split the AI SDK already reports. On a real 24-request run this overstated cost by 22%.

Solution

  1. Threaded the existing AbortSignal one level deeper, into runAttack()'s per-turn loop, so it's checked before every turn and breaks out the same way the existing early-stop-on-target-error path does — still calling finalize() so a partial transcript is judged and reported.
  2. Skip the transcript block and its toggle when neither the turns list nor the detail card has content. The judge's error message still renders, so the reason the attack produced nothing isn't lost with it.
  3. Added an OPFOR_STOP-tagged-error branch to runAllBrowser's catch block (recognised structurally via .code, since core can't import the extension's error class) so a user-cancelled run returns a proper partial report. Threaded tokenUsageByModel through orchestrator.js, aggregated it in popup.js via the already-bundled estimateRunCost, and wired the result into both the downloaded report and a new Cost stat card.
  4. Input is now divided across its cache tiers and each priced at its own published rate, reading the split from the AI SDK's provider-agnostic inputTokenDetails (so DeepSeek's prompt_tokens_details.cached_tokens and Anthropic's cache_read_input_tokens both work without provider-specific code).

Validation against real billing

The cost change was verified against LiteLLM's own spend logs for a 24-request run:

LiteLLM (actual billing) opfor report Delta
Requests 24 24 0
Input tokens 98,103 98,103 0
Cached tokens 29,696 29,696 0
Output tokens 32,269 32,269 0
Cost $0.057938723 $0.057938723 $0.000000000

Every one of the 24 requests agrees to 12 decimal places. The old formula reported $0.070749 for the same run — a 22.1% overstatement.

Changes

Cancellation

  • core/src/execute/attackRunner.tsrunAttack() accepts an optional signal, checked at the top of each turn
  • core/src/execute/runAgentLoop.ts, core/src/execute/mcpAttackDriver.ts — forward signal through to runAttack
  • core/src/execute/evaluatorLoop.ts — pass the already-available signal into both attack call sites

Reporting

  • core/src/report/render.ts — omit the transcript when there is no exchange to show
  • core/src/execute/runAllBrowser.ts — recognise OPFOR_STOP-tagged errors as a clean stop condition, same as TargetStopError
  • runners/extension/popup.js — derive detail from the last completed turn (aligning with core's buildReport.ts contract); aggregate tokenUsageByModel and derive cost; populate the new Cost stat
  • runners/extension/popup.html — add a statCost card next to the existing statTokens card
  • runners/extension/orchestrator.js — thread tokenUsageByModel through all three result paths (success, error, stopped)

Cost pricing

  • core/src/execute/tokenTracker.ts — capture the cache split instead of discarding it; derive the fresh count from inputTokens so the tiers always sum to it
  • core/src/pricing/estimateCost.ts — price each input tier at its own rate
  • core/src/pricing/types.ts — the "Not applied yet" doc comments, now applied

DocsREADME.md and docs/cli.md carried a "multi-turn runs are over-estimated" caveat that this PR makes false; replaced with current behaviour and the cases that still over-estimate. AGENTS.md gains the turn-granular cancellation contract and the inclusive-split invariant behind the cost maths.

Testscore/tests/attackRunner.test.ts, core/tests/pricing.test.ts, core/tests/tokenTracker.test.ts, and a new core/tests/render.test.ts (28 new).

Backward compatibility

  • Reported fields are unchanged by the pricing work — only the cost figure moves.
  • A run whose provider reports no cache split prices exactly as before (cacheRead/cacheWrite fall to 0, noCache absorbs the total, and the formula collapses to the old one).
  • A tier with no published rate falls back to the full input rate rather than to free, preserving the module's existing never-quietly-free stance.
  • A published rate of 0 is honoured (?? not ||) — DeepSeek genuinely publishes cw: 0, and there's a test pinning this.
  • A reported split that doesn't sum to inputTokens is not trusted: the fresh count is derived, and a split claiming more cached tokens than there was input is dropped entirely.

Review feedback

CodeRabbit raised four findings; one was valid and is fixed in c481c02:

  • Enforce the inclusive cache-split invariant (Major) — fixed. The transform trusted a reported noCacheTokens, so inputTokens: 100 + noCacheTokens: 100 + cacheRead: 50 produced a 150-token split against a 100-token call. The invariant was asserted in the docs and tests but never enforced. Two regression tests added.
  • Forward run_stopped to the extension popup (Minor) — skipped. The popup already learns the stop reason via a different channel: user-cancel goes through finalizeUserInterruption (explicit pause/cancel intent) and error paths persist a partial result carrying stopReason. Pre-existing gap, not introduced here.
  • Filter turns to those with an assistant response (Major) — skipped. DomTarget.send() pushes the user and assistant transcript entries atomically after extraction, or throws before pushing either, so no odd trailing user message is produced. The suggestion's second half would also drop the in-flight prompt, which is the most informative part of a cancelled run's transcript.
  • Import cost helpers from estimateCost.ts directly (Trivial) — not applicable. popup.js is extension code and cannot import from core/src/; every import there comes from ./dist/core.bundle.js, the esbuild bundle of core/src/browser.ts.

Issue

N/A

How to test

  1. npm run build
  2. CLI cancellation: run a target with turnMode: "multi" and a high turns count (e.g. 20+), Ctrl+C mid-attack, confirm it stops within one turn's latency instead of waiting for all turns.
  3. Extension: load the unpacked extension, start a run, click Stop mid-evaluator, download the report — confirm the cancelled evaluator shows no empty transcript, and the sidepanel + downloaded report both show token/cost stats.
  4. Cost: run any multi-turn assessment against a caching provider and compare the reported "Testing cost" against your provider's billing for the same window.
  5. npm test — 304 tests pass (28 new).
  6. npm run typecheck / npm run lint — clean.

Screenshots

N/A

Summary by CodeRabbit

  • New Features

    • Added support for stopping in-progress evaluations while preserving partial results and usage data.
    • Added run cost estimates and model-level token usage details, including cached and uncached input breakdowns.
    • Added provider-specific pricing for cached input tokens.
  • Bug Fixes

    • Stopped runs now display a clear status instead of an error.
    • Prevented cancelled runs from displaying fabricated empty results.
    • Improved report rendering when no transcript content is available.

The CLI's AbortSignal was only checked between attacks/evaluators, so a
long multi-turn attack (e.g. turns: 100) couldn't be interrupted until
every turn finished. The extension had a parallel gap: its own stop
signal wasn't recognized by runAllBrowser, so a cancelled run returned
no report at all — dropping token/cost data and leaving a fake blank
turn in the HTML report.

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

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds cancellation to attack execution, preserves partial stopped-run data, tracks cache-tier token usage, calculates tiered costs, and displays cost and final transcript details in the extension report.

Changes

Cancellation and cost reporting

Layer / File(s) Summary
Attack cancellation propagation
core/src/execute/attackRunner.ts, core/src/execute/mcpAttackDriver.ts, core/src/execute/runAgentLoop.ts, core/src/execute/evaluatorLoop.ts, core/tests/attackRunner.test.ts
Optional abort signals reach MCP and agent attacks. runAttack stops before the next turn and still finalizes. Tests cover pre-aborted, mid-run, and omitted signals.
Cache-aware token tracking and pricing
core/src/execute/tokenTracker.ts, core/src/pricing/estimateCost.ts, core/src/pricing/types.ts, core/tests/tokenTracker.test.ts, core/tests/pricing.test.ts
Token tracking separates uncached, cache-read, and cache-write input tokens. Cost estimation applies cache-tier prices and fallback rates. Tests cover parsing, propagation, balancing, zero rates, and fallback behavior.
Stopped-run handling and report presentation
core/src/execute/runAllBrowser.ts, runners/extension/orchestrator.js, runners/extension/popup.js, runners/extension/popup.html, core/src/report/render.ts, core/tests/render.test.ts
OPFOR_STOP records the stop reason, emits progress, and preserves partial results and token usage. The extension aggregates usage, calculates cost, displays the Cost statistic, and omits empty transcript content.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Extension as Extension orchestrator
  participant Browser as runAllBrowser
  participant Evaluator as evaluatorLoop
  participant Runner as runAttack
  participant Tracker as TokenTracker
  participant Popup as Extension popup
  Extension->>Browser: request evaluator run
  Browser->>Evaluator: run evaluators with AbortSignal
  Evaluator->>Runner: start MCP or agent attack
  Runner-->>Evaluator: stop before next turn and finalize
  Evaluator->>Tracker: record token usage
  Browser-->>Extension: run_stopped with partial results
  Extension->>Popup: provide token usage and evaluator results
  Popup->>Popup: calculate and display estimated cost
Loading

Possibly related PRs

Suggested reviewers: jithin23-kv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the PR’s three primary changes: cancellation behavior, extension reporting, and cache-aware cost pricing.
Description check ✅ Passed The description covers the problem, solution, changes, issue status, testing steps, validation results, compatibility, and screenshots status.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cancellation-turn-granularity-and-extension-report-gaps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
runners/extension/popup.js (1)

1423-1443: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep last limited to turns with an assistant response.

turnsForReport() outputs a turn when either the user or assistant entry exists. If an interrupted evaluator leaves an in-flight user message in raw.transcript, last can come from a turn whose response is empty, and turns can include that incomplete entry. Filter turns with a non-empty assistant response before selecting last and before returning turns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runners/extension/popup.js` around lines 1423 - 1443, Update the turn
handling in turnsForReport to retain only turns with a non-empty assistant
response, then select last from that filtered collection and return it as turns.
Preserve the existing fallback to empty prompt and response when no completed
turns remain.
🧹 Nitpick comments (1)
runners/extension/popup.js (1)

14-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import the cost helpers directly from their source module.

estimateRunCost and formatUsd are defined in core/src/pricing/estimateCost.ts, so import them from that file instead of re-exporting them through core/src/browser.ts and then bundling from ./dist/core.bundle.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runners/extension/popup.js` around lines 14 - 15, Update the imports for
estimateRunCost and formatUsd in the popup module to reference their defining
source module directly, core/src/pricing/estimateCost.ts, instead of importing
through core/src/browser.ts or ./dist/core.bundle.js. Keep both helper usages
unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/execute/runAllBrowser.ts`:
- Around line 217-222: Update the extension orchestrator’s event handling to add
a run_stopped branch alongside the existing attack_start and attack_done
branches. Forward the stopped event by calling broadcastProgress with its
reason/state so the popup displays the partial run reason emitted by
runAllBrowser.

---

Outside diff comments:
In `@runners/extension/popup.js`:
- Around line 1423-1443: Update the turn handling in turnsForReport to retain
only turns with a non-empty assistant response, then select last from that
filtered collection and return it as turns. Preserve the existing fallback to
empty prompt and response when no completed turns remain.

---

Nitpick comments:
In `@runners/extension/popup.js`:
- Around line 14-15: Update the imports for estimateRunCost and formatUsd in the
popup module to reference their defining source module directly,
core/src/pricing/estimateCost.ts, instead of importing through
core/src/browser.ts or ./dist/core.bundle.js. Keep both helper usages unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e5ddf7a0-b139-4175-a656-5bff67a48eb7

📥 Commits

Reviewing files that changed from the base of the PR and between a67cca9 and b6362ed.

📒 Files selected for processing (9)
  • core/src/execute/attackRunner.ts
  • core/src/execute/evaluatorLoop.ts
  • core/src/execute/mcpAttackDriver.ts
  • core/src/execute/runAgentLoop.ts
  • core/src/execute/runAllBrowser.ts
  • core/tests/attackRunner.test.ts
  • runners/extension/orchestrator.js
  • runners/extension/popup.html
  • runners/extension/popup.js

Comment on lines +217 to +222
if ((err as { code?: string })?.code === "OPFOR_STOP") {
stopReason = err instanceof Error ? err.message : "Run stopped by user.";
notify({ type: "run_stopped", reason: stopReason });
pushPartialResult(stopReason);
break evaluatorLoop;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline runners/extension/orchestrator.js --items all
rg -n -C 6 'run_stopped|onProgress|broadcastProgress|setRunStatus|finalizeUserInterruption' \
  runners/extension/orchestrator.js

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 12727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## runAllBrowser relevant section"
sed -n '180,235p' core/src/execute/runAllBrowser.ts

echo
echo "## extension run_stopped occurrences"
rg -n -C 8 'run_stopped|OPFOR_STOP|type === "attack_done"|emit\(|create.*Run|runAll\(' .

echo
echo "## executeAdaptiveRedTeamRun message send around stop"
sed -n '840,905p' runners/extension/orchestrator.js

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 50394


Forward run_stopped from core to the extension popup.

runAllBrowser emits run_stopped for user-cancel/error paths, but runners/extension/orchestrator.js only calls broadcastProgress() for attack_start and attack_done. Add a matching branch and broadcast the stopped state so the extension UI can show the partial run reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/runAllBrowser.ts` around lines 217 - 222, Update the
extension orchestrator’s event handling to add a run_stopped branch alongside
the existing attack_start and attack_done branches. Forward the stopped event by
calling broadcastProgress with its reason/state so the popup displays the
partial run reason emitted by runAllBrowser.

Cost estimation charged every input token at the full input rate, but
`inputTokens` is the inclusive total — it already contains the cached
tokens providers bill far more cheaply. The token counter was also
discarding the cache split the AI SDK already reports.

Input is now divided across its tiers and each priced at its own rate.
Validated against LiteLLM billing for a 24-request run: previously
reported $0.070749, actually billed $0.057939, now reports $0.057939 —
every request agreeing to 12 decimal places.

Reported fields are unchanged; only the cost figure moves. A run whose
provider reports no cache split prices exactly as before, and a tier with
no published rate falls back to the full input rate rather than to free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@arunSunnyKVS arunSunnyKVS changed the title fix: honor cancellation mid-attack and fix extension report gaps on stop fix: cancellation turn-granularity, extension report gaps, and cache-aware cost pricing Aug 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/execute/tokenTracker.ts`:
- Around line 99-124: Update the token usage transform around the
inputTokenDetails schema and its noCache calculation to always derive noCache
from inputTokens minus cacheRead and cacheWrite, ignoring any provider-supplied
noCacheTokens override. Validate and reject inputs where cacheRead plus
cacheWrite exceeds inputTokens, and add regression tests covering both the
override case and the over-limit case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b2540b77-5e97-4f72-bf8d-999e9b831573

📥 Commits

Reviewing files that changed from the base of the PR and between b6362ed and f0a92a1.

📒 Files selected for processing (5)
  • core/src/execute/tokenTracker.ts
  • core/src/pricing/estimateCost.ts
  • core/src/pricing/types.ts
  • core/tests/pricing.test.ts
  • core/tests/tokenTracker.test.ts

Comment thread core/src/execute/tokenTracker.ts
arunSunnyKVS and others added 2 commits August 6, 2026 16:06
The transform read `noCacheTokens` verbatim, so a provider reporting
inputTokens 100 with noCache 100 and cacheRead 50 produced a 150-token
split against a 100-token call — and estimateRunCost billed all 150.
The invariant was asserted in the docs and tests but never enforced.

Derive the fresh count from inputTokens instead. For a well-formed
provider the two agree (the AI SDK builds inputTokens as the sum), so
this costs nothing and makes the invariant hold by construction. A split
claiming more cached tokens than there was input can't be divided at all,
so it is dropped and the call prices at the full input rate.

Addresses CodeRabbit review on #236.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An attack interrupted before its first turn completed has no turns and an
empty detail card, but the renderer emitted a transcript anyway — headed
"1 turn" with two blank bubbles. That reads as an exchange that happened
and came back empty, rather than as nothing having run.

Skip the transcript and its toggle when there is no content on either the
turns list or the detail card. The judge's error message still renders, so
the reason the attack produced nothing is not lost with it.

Docs: the cache-aware pricing landed earlier in this PR made the
"multi-turn runs are over-estimated" caveat wrong in README.md and
docs/cli.md; replaced with what actually happens now and the cases that
still over-estimate. AGENTS.md gains the turn-granular cancellation
contract and the inclusive-split invariant behind the cost maths.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 127-129: Update the README pricing explanation to distinguish
cached-input reads from cache writes: describe cache reads as potentially much
cheaper than fresh input, while noting that cache writes may cost more,
including provider-specific rates. Preserve the existing explanation that
repeated prefixes are billed according to reported provider cache rates.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 964875dc-7abd-46a4-a2f1-25ce8d4492bc

📥 Commits

Reviewing files that changed from the base of the PR and between c481c02 and a14c581.

📒 Files selected for processing (5)
  • AGENTS.md
  • README.md
  • core/src/report/render.ts
  • core/tests/render.test.ts
  • docs/cli.md

Comment thread README.md
Comment on lines +127 to +129
Prices come from a snapshot of LiteLLM's public price map that ships with the package, so runs work offline and a report re-rendered later produces the same figure. Cached input is billed at the provider's cache rate — multi-turn attacks re-send the conversation each turn, and that repeated prefix is often ~100× cheaper than fresh text, so the figure tracks the real bill rather than a worst case. Caveats worth knowing:

- **Multi-turn runs read high.** Providers discount repeated context, and a multi-turn attack re-sends the conversation each turn — opfor prices every input token at full rate, so the real bill is usually lower.
- **Caching is only credited when it's reported.** A provider that doesn't break out cached tokens, or a model with no published cache rate, is charged at the full input rate — an over-estimate, chosen over quietly under-reporting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Anthropic cache write multipliers 1.25x 2x pricing

💡 Result:

Anthropic's prompt caching pricing is structured around a "write premium" paid when content is first stored, followed by a significant discount on subsequent reads [1][2]. These multipliers are applied relative to the model's base input token price [1][3]. The pricing multipliers for prompt caching are as follows: * 5-Minute Cache Write: 1.25x the base input price [1][2]. This tier keeps the cache valid for 5 minutes [1][3]. * 1-Hour Cache Write: 2x the base input price [1][2]. This tier keeps the cache valid for 1 hour [1][3]. * Cache Read (Hit): 0.1x the base input price [1][2]. This provides a 90% discount on standard input costs for any content retrieved from the cache [4][2]. Because cache reads cost only 10% of the standard input price, prompt caching generally becomes cost-effective after a small number of hits [1][4]. For the 5-minute cache (1.25x write), the investment is typically recovered after just one cache read [1][2]. For the 1-hour cache (2x write), it typically takes two cache reads to break even [1][2]. These multipliers stack with other pricing modifiers, such as the Batch API discount and data residency adjustments [3][5].

Citations:


🌐 Web query:

Anthropic pricing cache read 90% discount cache write multiplier

💡 Result:

Anthropic’s prompt caching pricing is based on a system of multipliers applied to the base input token rate [1][2][3]. Cache reads are charged at 0.1x (10%) of the base input price, effectively providing a 90% discount on those tokens compared to standard, uncached input [1][4][3]. Cache writes involve a premium because the content must be stored [1][2][5]. There are two tiers for cache writes, each with a different multiplier relative to the base input rate: 5-Minute Cache Write: 1.25x the base input price [1][6][3]. This tier is valid for 5 minutes and is generally more cost-effective for bursty or high-frequency interactions [3][5]. 1-Hour Cache Write: 2x the base input price [1][2][3]. This tier is valid for 1 hour and is intended for longer-running workflows where the cached content will be reused multiple times [4][3][5]. Because cache hits cost only 10% of the standard input rate, the upfront write premium is typically recovered quickly: the 5-minute cache write generally pays for itself after a single cache read, while the 1-hour cache write typically breaks even after two cache reads [1][2][4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'README relevant section:\n'
sed -n '120,136p' README.md | cat -n

printf '\nCache/tier terms and LiteLLM price map references:\n'
rg -n "cache|LiteLLM|cache-write|cache_read|cache_write|cache reads|cache writes|pricing|price map" README.md

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 2672


Distinguish cache reads from cache writes.

Line 127 says cached input is “often ~100× cheaper than fresh text,” but that describes cache reads. Cache writes can be pricier than fresh input, including Anthropic’s 5-minute cache-write rate at 1.25× base input pricing and the 1-hour cache-write rate at 2× base input pricing. Update this to describe cache-read and cache-write rates separately.

Proposed documentation change
- Cached input is billed at the provider's cache rate — multi-turn attacks re-send the conversation each turn, and that repeated prefix is often ~100× cheaper than fresh text, so the figure tracks the real bill rather than a worst case.
+ Cache-read and cache-write input are billed at their separate published rates. In multi-turn attacks, a repeated prefix can be much cheaper when it is reported as a cache read. Cache writes can cost more than fresh input.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Prices come from a snapshot of LiteLLM's public price map that ships with the package, so runs work offline and a report re-rendered later produces the same figure. Cached input is billed at the provider's cache rate — multi-turn attacks re-send the conversation each turn, and that repeated prefix is often ~100× cheaper than fresh text, so the figure tracks the real bill rather than a worst case. Caveats worth knowing:
- **Multi-turn runs read high.** Providers discount repeated context, and a multi-turn attack re-sends the conversation each turn — opfor prices every input token at full rate, so the real bill is usually lower.
- **Caching is only credited when it's reported.** A provider that doesn't break out cached tokens, or a model with no published cache rate, is charged at the full input rate — an over-estimate, chosen over quietly under-reporting.
Prices come from a snapshot of LiteLLM's public price map that ships with the package, so runs work offline and a report re-rendered later produces the same figure. Cache-read and cache-write input are billed at their separate published rates. In multi-turn attacks, a repeated prefix can be much cheaper when it is reported as a cache read. Cache writes can cost more than fresh input. Caveats worth knowing:
- **Caching is only credited when it's reported.** A provider that doesn't break out cached tokens, or a model with no published cache rate, is charged at the full input rate — an over-estimate, chosen over quietly under-reporting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 127 - 129, Update the README pricing explanation to
distinguish cached-input reads from cache writes: describe cache reads as
potentially much cheaper than fresh input, while noting that cache writes may
cost more, including provider-specific rates. Preserve the existing explanation
that repeated prefixes are billed according to reported provider cache rates.

@arunSunnyKVS
arunSunnyKVS merged commit 317eb27 into master Aug 6, 2026
8 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.

2 participants