Conversation
- Guard CliBackend _cache/_tokens with a lock on the opt-in parallel replay path (SKILLOPT_SLEEP_WORKERS>1) so a concurrent miss cannot corrupt state or lose the token cost metric; the model call stays outside the lock so parallel workers still overlap. - Redact report.json (redact_secrets) before staging. - Redact harvest --output and --json exports (_redact_deep). - Add tests for CliBackend caching/thread-safety.
|
Please add boundary-level tests for both harvest output/file and |
Address maintainer review on microsoft#251: - Use redact_secrets (mapping-key aware) instead of _redact_deep for harvest --output/--json and handoff exports, so nested api_key/token mappings are redacted (not just bare string leaves). - Redact report_md before writing it alongside report.json. - Add boundary tests (nested api_key/token + report.md).
Address maintainer review on microsoft#251 (thread-safety): - Add _cache_get/_cache_pop/_cache_pop_if locked helpers and route the Pi and OpenCode _cached_call overrides through them (they previously read/pop the cache outside the lock). - tokens_used() now reads _tokens under the lock. - Popping a failed entry is conditional (_cache_pop_if): a failed caller only drops its own empty value, never another worker's just-stored success. - Add tests: barrier-forced overlapping misses stay consistent, and pop-if does not delete a successful entry.
Address maintainer review on microsoft#251 (last thread-safety item): - Record each model call's token delta on the calling thread (thread-local), so parallel replay_one() charges its own cost instead of a before/after global total that an overlapping worker inflates. - replay_one reads backend.token_delta() (falling back to the text-length heuristic for backends that don't track tokens). - Add tests for call-local and thread-isolated token deltas.
Address maintainer review on microsoft#251 (deepen thread-safety): - _cached_call no longer caches empty (transient-failure) results and prefers a concurrently cached success, so an empty/duplicate cannot clobber or delete another worker's successful entry. - Add a Pi subclass-level concurrency test (barrier-forced empty-vs-success) asserting the success survives.
|
Yifan Yang (@Yif-Yang) — thank you for the thorough review and guidance! I've addressed the feedback:
Thanks again for the detailed review! |
|
Yifan Yang (@Yif-Yang) — thank you for the careful review and guidance. In fact I've done almost all of these submissions through DSH, which is exactly why I have a bold idea. Across your recent PR reviews I noticed that you consistently apply a set of quality baselines: fail-closed handling, structure-aware and boundary-consistent redaction, thread-safety with call-local accounting, validating against real contracts, PR hygiene, and so on. I'd like to distill that into a reusable |
|
Thanks — several original races and the
Please add boundary-level stdout coverage for mapping-key secrets, a same-thread miss→hit regression, a barrier-forced same-key concurrent-miss test that checks paid-call accounting, and a |
- Redefine _redact_deep to delegate to the key-aware redact_secrets walker so
{"api_key": "x"} is scrubbed at every boundary (--json, digests/snapshot
files, gate_trials, extra, display), not just the --json link.
- Reset _thread_local.delta on cache hit so a later hit doesn't reuse the
previous call's delta.
- Charge every real call's tokens on a concurrent miss (the dedup worker used
to be free, undercounting).
- Add DualBackend.token_delta() so replay_one() reads the target's call cost.
- Regressions: cache-hit delta reset, barrier-forced concurrent charge,
DualBackend token_delta, key-aware _redact_deep.
- The barrier-forced concurrency tests waited 5s for all workers to reach the barrier; under a slow/loaded CI that can break the barrier mid-test and turn a pass into a spurious failure. Raise the wait to 15s (no semantic change).
|
Already addressed the review feedback and updated this branch (#251):
Also added a comment on |
Document that token_delta() is target-only by design (replay drives the target), that the optimizer only appears in replay via the rare model-judge fallback (rule/exact/answer tasks are scored locally, 0 tokens), and that the aggregate tokens_used() still counts both sub-backends so the total is not undercounted.
|
Thanks — the original cache-hit, concurrent-call, boundary-redaction, and |
…daction test - Set the thread-local delta in every attempt_with_tools override that charged _tokens directly (Claude CLI, OpenCode, Codex, Cursor), so replay_one() reads real call-local usage instead of falling back to a response-length estimate. - Update the stale test_redact_deep_loses_mapping_key_context to assert redaction (it was asserting the old leak bug). - Add tool-replay regressions: attempt_with_tools sets call-local delta, and the dual-backend path surfaces the target's delta.
|
Thanks for the re-review. Addressed both blockers: the stale est_redact_deep_loses_mapping_key_context now asserts redaction (was asserting the leak), and every �ttempt_with_tools override that charged _tokens now sets the call-local thread delta (Claude CLI, OpenCode, Codex, Cursor), so |
- Add CliBackend._record_cost(prompt, response) as the single path to charge an inference's token cost (aggregate _tokens under _lock + call-local _thread_local.delta), so no path under- or over-counts. - Route _cached_call miss, all attempt_with_tools overrides (Claude/OpenCode/Codex/Cursor), and reflect through it; the OpenCode error path keeps its prompt-only charge. - No behavior change: identical delta model, just centralized — removing the duplicated len//4 accounting that caused the microsoft#251-class bugs to recur.
|
Thanks — the tool-aware paths now set a call-local delta, but two accounting issues remain. |
|
Thanks to Teacher Yifan for the careful review, please wait a moment. |
…unting helper Address the review: all token charging now routes through one locked helper (_record_cost), and the call-local delta is reset on every no-call / early-return path (_reset_call_delta), so a reused worker never reports a previous call's token count — covering the OpenCode disabled-tool-replay early return, cache hits, and the start of every tool-aware path. Add regressions: barrier-forced concurrent _record_cost (no lost updates) and same-thread prior-call -> disabled-tool-replay delta reset.
- Add _record_delta(delta) and route the Azure/OpenCode real-usage accounting through it, so those backends also set the call-local _thread_local.delta — the last accounting path that did not (replay_one() saw 0/stale and fell back to a length estimate for these backends).
|
Thanks for the re-review - both accounting issues are resolved (commits 3ef661, c8ff7f8, 8a17aa6):
|
|
Thanks — the previous early-return and locking issues are fixed, but one accounting blocker remains on
Minimal reproduction with a fake chat-completions response reporting 10 prompt + 20 completion tokens, a 400-character prompt, and a 40-character response: A two-attempt reproduction (7 tokens on an empty response, then 11 on success) similarly reports: Thus both aggregate cost and Please establish one owner for accounting: preferably have Please add fake-client regressions for both Also, |
A backend that self-reports provider usage (AzureOpenAI/AzureResponses) must not be charged twice: _cached_call/_reflect now skip the len//4 estimate when the call already recorded its own exact usage (via a _charged_in_call marker), so a 30-token provider usage is charged once as 30, not as the ~110 estimate. - AzureOpenAI/AzureResponses _call accumulate usage across every paid attempt and record _record_delta(total) once; the call-local delta equals that increment. - OpenCodeCliBackend.attempt_with_tools error path routes prompt-only cost through _record_delta (no manual _tokens / _thread_local.delta update). - The _call str return contract is preserved, so llm_miner/rollout/slow_update keep working and their Azure cost stays accounted. - Added fake-client regressions for both Azure backends (single call, empty-retry + success accumulation, cache-hit no-charge) + OpenCode error-path.
0712e44 to
c72b1b4
Compare
Move _charged_in_call from a shared instance attribute to _thread_local.charged_in_call so parallel workers can never read another worker's marker; the marker already gets reset to False before every _call on the current thread.
|
Yifan Yang (@Yif-Yang) — addressed the remaining accounting blocker on Summary:
Added fake-client regressions ( |
|
Thanks for the single-owner accounting fix. Re-reviewing Offline fake-client reproduction through
Please finalize the accumulated usage on every exhausted-retry exit (as the Responses path already does), while distinguishing genuinely unavailable usage from known zero/known partial usage. Add an empty-paid-response -> terminal-error regression, including the cache/aggregate delta checks. The full shipped suite passes (1521 passed, 9 skipped); this added negative-path reproduction fails. |
AzureOpenAI._call recorded usage_total only when last_exc was 'empty-response'; if the final attempt raised (after a paid empty response), it returned without _record_delta, so _cached_call substituted its len//4 estimate and dropped the known paid usage. Now always record the accumulated usage + set the marker on any exhausted-retry exit (usage_total 0 = genuinely no paid usage). Regression: empty-paid-response -> terminal-error keeps the exact 7 tokens.
|
Yifan Yang (@Yif-Yang) — fixed on |
|
Thanks for There is still a separate backward-compatibility regression in I reproduced this offline with a minimal The independent matrix covers direct/wrapped ( Please introduce the call-local accounting contract compatibly, migrate supported implementations/wrappers or provide an appropriate compatibility path, and add these existing-backend regressions alongside the parallel-replay tests. Do not simply restore unconditional shared-total subtraction for concurrent CliBackend calls, since that would reintroduce the race this PR is meant to solve. Accurate reported per-attempt cost must not silently become a different estimate just because an existing backend lacks the new optional method. |
…()-only backends replay_one no longer substitutes a text-length estimate when a backend implements only the older tokens_used() contract (no token_delta): it reports the same-thread before/after total difference instead, and DualBackend does the same for a target that lacks token_delta (snapshotted in attempt/attempt_with_tools). CliBackend and DualBackend-with-modern-target keep the thread-safe per-call delta, so the shared total is still never reduced by subtraction for concurrent CliBackend calls. Added existing-backend regressions (direct + DualBackend, tokens_used-only).
|
Yifan Yang (@Yif-Yang) — fixed on
|
A replay served from the backend cache has no new model call: _cached_call() resets the per-thread delta to zero and the aggregate ledger is unchanged. replay_one() was still treating any zero as 'backend does not track tokens' and substituting a text-length estimate, so ReplayResult.tokens disagreed with the backend ledger (102 vs 0 in reproduction). With token_delta() present, the returned cost is authoritative - including a known zero cache hit. Only backends without the per-call interface fall back to before/after totals and the length estimate.
|
Yifan Yang (@Yif-Yang) Follow-up to With |
|
Re-reviewed The cache/accounting/export selection, including the independent regressions, passes 35 tests on Linux/Python 3.11. I am not continuing to treat those earlier reproductions as unfixed. One useful follow-up is to clarify the concurrency contract for legacy cumulative-only targets. The new inline note about a shared Official CI for this exact head is still awaiting maintainer approval. The local results resolve the cited accounting regressions, not the final full-review/CI merge gate. |
The cumulative-only fallback in DualBackend.token_delta() is sequential-only: `_target_tokens_before` is shared instance state, so two workers overlapping in one DualBackend overwrite each other's baseline. That is not the same guarantee as a target that reports a thread-local per-call delta, and it is not the same as a bare legacy backend (whose before/after snapshot replay_one takes on the worker's own thread). replay_batch()'s docstring now lists the four supported combinations, the DualBackend constructor note and its token_delta() fallback say which branch is sequential-only, and replay_one()'s comment no longer implies that any token_delta()-bearing backend is thread-safe. Tests: a regression for the modern path - replay_batch(workers=4) through a DualBackend over a target with a thread-local delta must produce exactly the same per-task costs as the sequential run, with the tasks spread far enough apart in size that the check is not vacuous.
|
Yifan Yang (@Yif-Yang) — addressed on Documentation of the supported combinations:
Regression for the modern DualBackend parallel path:
Verification on Linux / Python 3.11, on the merge with
Nothing here claims thread-safety for the cumulative-only fallback, and no test asserts it. |
…ckends The first version of this contract filed a bare backend with only the older tokens_used() contract under "safe to share". It is not: replay_one differences a counter that the other workers are also spending, so an overlapping worker's tokens land in this task. Two workers against a backend where every attempt costs 37 report [74, 37]. Only a thread-local per-call delta is safe to share; every cumulative-total shape over-counts when workers overlap. The docstring now says that and names the failure direction - budgets see more spend than actually happened - rather than implying that legacy backends are fine. The parallel regression did not bite either: with no real overlap, a backend that had lost its thread-locality still produced costs identical to the sequential run. The fake target now blocks on a barrier until every worker is in flight, so the assertion fails when that locality goes away. Verified by substituting a cumulative delta (test fails) and by five consecutive runs with the real one (test passes).
|
Yifan Yang (@Yif-Yang) — correcting myself on
So the contract now reads: only a thread-local per-call delta is safe to share — The regression was weaker than I claimed as well. It compared a parallel run against a sequential one, but with no real overlap a backend that had lost its thread-locality still produced costs identical to sequential, so it would not have caught the regression it was written for. The fake target now blocks on a barrier until every worker is in flight. Verified both ways: substituting a cumulative delta fails the test ( Re-verified on Linux / Python 3.11 (scratch branch, deleted afterwards; workflow file not part of this PR):
If you would rather have the guard than the documentation — |
|
Thank you for working through the concurrent accounting and export-hardening feedback. Re-reviewed The remaining unknown-versus-zero usage case still needs a fix. Both Independent offline reproduction using the SDK's own Both SDK usage fields are optional. The regression passes twice on main and fails twice on this head; these are fake-client calls, with no provider access. Please track whether usage is actually available separately from its numeric value. Preserve authoritative zero for a reported zero/cache hit, preserve known partial retry usage, and retain estimated accounting for successful attempts without usage metadata. Add both API variants to the regression suite. Upstream CI is still awaiting approval; the passing shipped suite does not cover this case. |
Sleep-cycle hardening.
tokens_used()contract keeps its real per-attempt cost.Parallel-replay accounting contract
Only a thread-local per-call delta is safe to share across workers:
CliBackend, and aDualBackendwhose target definestoken_delta(). Everything built on a cumulative total is sequential-only and over-counts when workers overlap - the bare legacy backend as much as theDualBackendfallback, becausereplay_onedifferences a counter the other workers are also spending. Measured with a barrier-forced overlap, where every attempt really costs 37 tokens:replay_batch()'s docstring now lists the supported combinations,DualBackend's constructor andtoken_delta()say which branch is sequential-only, andreplay_one()no longer reads as "anything with atoken_delta()is thread-safe". A regression covers the modern path: the fake target blocks on a barrier until every worker is in flight, so the test fails if that call-locality is lost - verified by substituting a cumulative delta (it fails) and by five consecutive clean runs with the real one.Test plan
tests/test_cli_backend_cache.py(20 passed),tests/test_azure_usage_accounting.py(8 passed),tests/test_export_redaction.py(3 passed)action_requiredpending maintainer approval