Repository navigation
fix(usage): fold repair-round usage into refusal responses on macOS 27 (#516) - #518
Arthur-Ficial wants to merge 4 commits into
Conversation
#516) On macOS 27, when a repair round completes before the model throws a refusal, the repair round's runtime-reported usage was stored in usageRounds but all five refusal paths ignored it, under-reporting prompt and completion tokens. This adds TokenUsage.refusalBase() to select the runtime sum (macOS 27) or the counted fallback (macOS 26) and wires it into every refusal site in Handlers.swift and ResponsesHandlers.swift. Drafted by the apfel bug-solver routine. Needs review + local test run by @franzenzenhofer on a Mac with Apple Intelligence before merging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjhuKcS1iPkKapmj84Zhah
The v1.14.0 release on main stamped the [Unreleased] section while this branch was in flight. Git's auto-merge placed the new Fixed entry inside [1.14.0] instead of [Unreleased]. Moved it back so changelog-gate passes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjhuKcS1iPkKapmj84Zhah
Swift does not allow `await` on the right side of a non-assignment operator. Split `priorCompletionTokens + await TokenCounter...count()` into a separate let binding in all four affected sites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjhuKcS1iPkKapmj84Zhah
|
The This PR's code changes compile cleanly - the earlier I don't have the means to re-run CI, and this failure needs the CI workflow or Cheers, Arthur Generated by Claude Code |
Draft PR - needs @franzenzenhofer review and local test run before merging.
Fixes #516 (item 1).
Summary
Root cause
On macOS 27, when a tool-policy repair round completes and then the subsequent model call throws a refusal, the repair round's runtime-reported usage (stored in
usageRounds) was silently dropped by all five refusal paths. Those paths usedpromptTokens.resolve() + promptAdjustmentfor prompt tokens and counted only the refusal text for completion tokens. On macOS 26 this is correct becausepromptAdjustmentaccounts for the discarded round's overhead - but on macOS 27,promptAdjustmentstays 0 (the runtime reports usage instead of counting), so the refusal'susageobject under-reports both prompt and completion tokens.The fix
Added
TokenUsage.refusalBase(rounds:countedPromptTokens:)toApfelCore: whenusageRoundsis non-empty (macOS 27), it returns the sum's prompt and completion tokens; otherwise it falls back to the counted prompt figure with zero prior completion tokens (macOS 26 path, unchanged). All five refusal sites now call this helper:Handlers.swift: tool-call handler non-streaming refusal, tool-call handler streaming refusal, non-streaming handler refusal, streaming handler inline refusal (also includeslastRoundUsagelike the truncated-content path does)ResponsesHandlers.swift:/v1/responsesrefusalThe
refusalNonStreamingResponseandrefusalStreamingResponsehelpers gain apriorCompletionTokens: Int = 0parameter so they add the repair round's completion tokens to the counted refusal text.Test coverage
Tests/apfelTests/TokenUsageTests.swift:refusalBase with no rounds falls back to counted prompt tokens(macOS 26 path)refusalBase with one round uses reported prompt and completion tokens(macOS 27, one round before refusal)refusalBase with repair round sums both rounds(macOS 27, initial + repair before refusal)refusalBase ignores countedPromptTokens when rounds are presentTokenUsage.sumtests still cover the underlying summing.Test plan
swift build -c releaseswift run apfel-testsmake installand manual smoke testWhat I verified (static only)
usageRoundsscope in each handler.usageRounds + (lastRoundUsage.map { [$0] } ?? [])- the streaming refusal now matches that pattern.usageRoundsto fold.refusalBaseis a pure static function on aSendablestruct.Sources/Core/TokenUsage.swift,Sources/Handlers.swift,Sources/ResponsesHandlers.swift,Tests/apfelTests/TokenUsageTests.swift, andCHANGELOG.md- no touches to release infrastructure, guardrails, CI, or dependencies.priorCompletionTokens: Int = 0ensures the two call sites without repair rounds (structured handlers) remain unchanged if ever converted to use the helpers.What I did NOT verify
make teston a Mac with Apple Intelligence. I cannot run FoundationModels code. If the fix does not actually resolve the reported behaviour, that is on me to refine - ping me in a review comment.Item #2 (counted-path unit test for #176 guard)
Item #2 from the issue (explicit counted-path unit test for
TokenCounter.count(entries:)includingtoolDefinitions) requires FoundationModels types (Transcript.Entry,Transcript.ToolDefinition) which are unavailable in the pure test runner and cannot be written from this cloud environment. This needs to be authored on a Mac. ThefallbackCount(entries:)method atSources/TokenCounter.swift:187is private and tied to the FoundationModelsTranscripttype, so the test belongs in the integration suite or a new macOS-only unit test target.Anything that seemed suspicious
Nothing - straightforward follow-up from the native-usage review. Issue filed by the repo owner.
cc @franzenzenhofer - draft stays draft until you review, test locally, and mark ready.
Generated by the apfel bug-solver routine.
https://claude.ai/code/session_01WjhuKcS1iPkKapmj84Zhah
Generated by Claude Code