Skip to content

techniques: ctx.add_cost is per-step (ContextVar) — no more lost or double-counted dollars under gather - #38

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/manual-cost-contextvar
Open

techniques: ctx.add_cost is per-step (ContextVar) — no more lost or double-counted dollars under gather#38
ChrisW09 wants to merge 1 commit into
mainfrom
fix/manual-cost-contextvar

Conversation

@ChrisW09

Copy link
Copy Markdown
Collaborator

Fixes #12.

Context._manual_cost was shared instance state, reset at the start of every ctx.run and read at the end — so overlapping steps corrupted each other. Verified failures: asyncio.gather(ctx.run("ret_a"), ctx.run("ret_b")) where A adds $0.10 and B adds $0.01 reported a total of $0.02 (A's dollars vanished, B's double-attributed); ctx.add_cost in the compose body was wiped by the next step's reset or dropped after the last one. The module docstring explicitly invites this control flow ("branches, cascades, loops — the topology is entirely yours").

Fix: the manual-cost sink is now a ContextVar set/reset around each technique call — the exact isolation pattern llm._usage_sink already uses (asyncio tasks copy their context, so gathered steps get independent sinks; nested ctx.run restores the outer sink on exit). add_cost called outside any step (compose body) charges the run's total_cost directly, so dollars are never silently dropped.

Regression tests: concurrent-stages attribution (exact per-stage and total assertions) and compose-body accrual. The existing manual-cost test (test_per_technique_and_manual_cost_flow_to_stage_report) passes unchanged.

Test: full cafe-core suite — 112 passed (1 pre-existing failure = #4, fix in PR #34).

🤖 Generated with Claude Code

_manual_cost was plain instance state shared by every step: a compose
that gathers two ctx.run calls had stage B's reset wipe stage A's
accrued dollars, and both steps read the same residual — costs vanished
or were double-counted (verified: $0.11 of add_cost reported as $0.02).
A compose-body add_cost was likewise wiped by the next ctx.run's reset,
or dropped entirely after the last one.

The manual-cost sink is now a ContextVar set around each technique call
— the same isolation pattern the LLM usage sink already uses, so
gathered steps (tasks copy their context) each accrue their own costs.
add_cost outside any step charges the run's total_cost directly instead
of being dropped.

Fixes #12

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

techniques: ctx.add_cost uses shared instance state — costs lost or double-counted under concurrent ctx.run, dropped at compose level

1 participant