Upstream
Problem
Anthropic documents that a refusal which happens before any output is not billed upstream: stop_reason=refusal, empty content, and output_tokens=0. The response may still contain non-zero input-token usage for rate-limit/accounting purposes.
LMM currently detects the refusal in apps/api-go/relay/channel/claude/relay-claude.go, but maybeMarkClaudeRefusal only writes ContextKeyAdminRejectReason. The returned usage then reaches apps/api-go/service/text_quota.go; textQuotaSummary.hasBillableUsage() treats non-zero input tokens as billable, so ratio/fixed/tiered settlement can still charge the user.
This is the same underlying gap fixed by upstream #7552.
Why not directly cherry-pick #7552
Do not mechanically copy the upstream patch yet:
- #7552 is still open and changes billing policy through a new global opt-in setting.
- The opt-in exists because Anthropic official endpoints do not charge these refusals, while a third-party Claude-compatible relay may still charge them. Applying a global exemption blindly could create under-billing.
- LMM has additional billing paths (
tiered_expr, quota saturation/composition, tool-call surcharges and custom pre-consume settlement), so the exemption must be placed before all final quota composition paths rather than patched only around the upstream call sites.
Proposed LMM-native implementation
Introduce a narrow settlement marker, e.g. ContextKeyBillingExemptReason=claude_refusal_no_output, only when all of these are true:
- final upstream response is a Claude refusal;
- no content block/output was produced;
- upstream usage exists;
output_tokens == 0;
- the selected policy says this channel/provider follows Anthropic no-charge refusal semantics.
Keep the original upstream response/usage returned to the client unchanged. Only the billing/logging view should become zero-cost.
For streaming, track whether any content_block_start has occurred before the refusal. For non-streaming, use len(content) == 0. Cover the Claude-native, Chat Completions -> Claude, and Claude -> Responses paths that share the adaptor.
At settlement time, the exemption should be resolved before ordinary ratio billing, fixed-price billing, tiered_expr, and tool-surcharge composition. It should:
- settle final quota to 0;
- fully refund any pre-consumed quota;
- skip tiered expression settlement for this request;
- record request count normally but not token/quota consumption;
- retain the admin-only refusal reason;
- add a machine-readable public/admin log field such as
billing_exempt_reason=claude_refusal_no_output;
- never mutate the usage body sent to the client.
Policy decision required
Prefer provider/channel capability over a single unconditional global switch if LMM can distinguish Anthropic-billed channels from third-party Claude-compatible relays. If that distinction is not reliable yet, use an explicit default-off setting as upstream #7552 does.
Do not make client identity, group, /fast, price lock, OAuth mode, or admin-AI state influence this decision.
Acceptance criteria
Verification target
At minimum run focused Claude relay tests, service settlement tests (including SQLite full pre-consume -> settle flow), tiered-billing regression tests, go vet on touched packages, and the existing CI billing-safety/release qualification jobs.
Upstream
e772bed48afaf94b434594e09aeb27fe4ebaea5amain@9ce3d78226fb50a38775e6e9d294d5f16771ed59Problem
Anthropic documents that a refusal which happens before any output is not billed upstream:
stop_reason=refusal, emptycontent, andoutput_tokens=0. The response may still contain non-zero input-token usage for rate-limit/accounting purposes.LMM currently detects the refusal in
apps/api-go/relay/channel/claude/relay-claude.go, butmaybeMarkClaudeRefusalonly writesContextKeyAdminRejectReason. The returned usage then reachesapps/api-go/service/text_quota.go;textQuotaSummary.hasBillableUsage()treats non-zero input tokens as billable, so ratio/fixed/tiered settlement can still charge the user.This is the same underlying gap fixed by upstream #7552.
Why not directly cherry-pick #7552
Do not mechanically copy the upstream patch yet:
tiered_expr, quota saturation/composition, tool-call surcharges and custom pre-consume settlement), so the exemption must be placed before all final quota composition paths rather than patched only around the upstream call sites.Proposed LMM-native implementation
Introduce a narrow settlement marker, e.g.
ContextKeyBillingExemptReason=claude_refusal_no_output, only when all of these are true:output_tokens == 0;Keep the original upstream response/usage returned to the client unchanged. Only the billing/logging view should become zero-cost.
For streaming, track whether any
content_block_starthas occurred before the refusal. For non-streaming, uselen(content) == 0. Cover the Claude-native, Chat Completions -> Claude, and Claude -> Responses paths that share the adaptor.At settlement time, the exemption should be resolved before ordinary ratio billing, fixed-price billing,
tiered_expr, and tool-surcharge composition. It should:billing_exempt_reason=claude_refusal_no_output;Policy decision required
Prefer provider/channel capability over a single unconditional global switch if LMM can distinguish Anthropic-billed channels from third-party Claude-compatible relays. If that distinction is not reliable yet, use an explicit default-off setting as upstream #7552 does.
Do not make client identity, group,
/fast, price lock, OAuth mode, or admin-AI state influence this decision.Acceptance criteria
content=[],stop_reason=refusal, usage present,output_tokens=0) settles to quota 0 when the no-charge policy is enabled/applicable.tiered_exprdoes not reintroduce a non-zero charge.output_tokens>0remains billable./fast/OAuth/admin-AI behavior is unchanged.Verification target
At minimum run focused Claude relay tests,
servicesettlement tests (including SQLite full pre-consume -> settle flow), tiered-billing regression tests,go veton touched packages, and the existing CI billing-safety/release qualification jobs.