Adds 40 new tests across 5 test files that document critical bugs and
fill coverage gaps in the routstr-core codebase:
- test_emergency_refund_integrity.py (5 tests):
Documents the try/except/pass vulnerability in emergency refund paths
(base.py:3643-3653 and base.py:4607-4617) where DB store failures
silently lose minted tokens. Verifies store_cashu_transaction catches
all exceptions and send_token mints before DB persistence.
- test_zero_cost_fallback.py (6 tests):
Documents the hardcoded zero-cost fallback (base.py:1012-1030) where
exceptions from adjust_payment_for_tokens() result in total_msats=0,
giving users free service with permanently reserved balances.
- test_db_and_payout_resilience.py (8 tests):
Confirms store_cashu_transaction_with_retry was reverted (#600→#604).
Documents the fee payout pay-then-reset crash window and wallet
caching mechanism.
- test_coverage_middleware.py (11 tests):
Fills middleware.py coverage gap (was 38%) — tests LoggingMiddleware,
_should_log filters, request_id_context, and middleware exports.
- test_coverage_payment_helpers.py (10 tests):
Fills payment/helpers.py coverage gap (was 52%) — tests
check_token_balance, estimate_tokens, create_error_response,
and image token calculation helpers.
All tests pass against current main (830 passed, 13 skipped).
PPQ.AI (BYOK) requests were billed at ~5% of their true cost because
_resolve_usd_cost fell through to usage.cost (a small BYOK routing fee)
instead of using cost_details.upstream_inference_cost (the real inference
cost). The proxy operator absorbed the inference cost.
The fix adds a BYOK-specific branch in _resolve_usd_cost: when is_byok is
true and cost_details.upstream_inference_cost is present, bill
upstream_inference_cost + byok_fee — what PPQ actually deducts from the
balance. Non-BYOK providers (e.g. OpenRouter) are unaffected because their
usage.cost already equals upstream_inference_cost.
Regression tests mirror the live glm-5.2-fast request from GitHub issue #615,
asserting the corrected billing (940,274 msats vs the old 45,202 msats — a
20.8× undercharge).
Closes#615
The prepare_request_body override in OpenRouterUpstreamProvider injected
provider.require_parameters=true on tool-use requests. This is no longer
needed. Removed the override, the now-unused json import, and the
corresponding test suite.
The X-Cashu refund endpoint raised 404 "Refund not found" when the
"in" transaction existed with a request_id but the "out" (refund)
transaction had not been written yet. This is a timing race: the
endpoint is polled while the upstream request is still in flight,
before send_refund() has minted and stored the refund token.
The 404 was indistinguishable from a genuinely-missing refund, so
clients had no signal that retrying would succeed — leading to
stranded refunds when clients gave up.
Replace the third 404 branch with 425 Too Early + Retry-After: 2.
The two earlier 404 branches (no "in" row, no request_id) remain 404
since those genuinely mean no refund will ever exist.
Also adds debug logging on all three not-found/pending branches so
the race is no longer invisible to operators (middleware does not log
request headers).
Adds unit tests for the new 425 pending path and the no-request_id
404 path.
Refs: refund-race-condition
The bare-tail tie-break ranked candidates by prompt price alone, so two
entries sharing a tail where one is cheaper on prompt but far dearer on
completion could resolve to the entry that undercharges output-heavy
traffic — contradicting the "highest-priced wins for money safety" promise.
Rank by the combined prompt + completion per-token cost instead, so the
choice stays deterministic and money-safe whichever way traffic leans. As
before there are zero bare-tail collisions in the live feed, so this changes
no resolved price today; it only governs the latent case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generic provider treated any Venice-style model_spec.pricing as
authoritative after only a None check, so a native both-zero price served
the model free, a negative one credited the caller on every request, and a
non-numeric string threw while parsing — the outer catch then dropped the
provider's entire catalog.
Coerce both native prices through the resolver's _as_float and reject
absent / non-numeric / negative / both-zero values, falling through to the
shared litellm→OpenRouter→fail-closed chain instead. This extends the same
money-safety guard the litellm and OpenRouter rungs already apply to the
native source, and keeps one malformed entry from emptying the catalog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route the Authorization: Bearer cashu… redemption failures through the
same failure taxonomy the X-Cashu path already uses (token_already_spent /
invalid_token / mint_error / cashu_error, carried in the error "type"),
with matching statuses, so both redemption paths agree on the
classification for a given failure class instead of introducing a third
parallel code.
- "already spent" -> token_already_spent (400)
- fee/melt failures -> mint_error (422)
- invalid/undecodable token -> invalid_token (400, was 401)
- other expected wallet errors -> cashu_error (400)
- unexpected faults still -> internal_error (500)
Also align the msats<=0 defense-in-depth envelope and note it is now
practically unreachable (credit_balance rejects non-positive amounts).
- Anchor the broad invalid/decode buckets to co-occur with "token" so
internal faults (e.g. "Invalid isoformat string") fall through to 500
instead of masquerading as a 401 token error.
- Map the "estimate fees"/"exceed token amount" wallet errors to the
specific "too small to cover swap fees" message instead of the generic
bucket.
- Stop the X-Cashu redemption path from interpolating raw mint/exception
text into mint_error/cashu_error responses (both duplicated blocks).
- Add tests for the fee-gap mapping and the anchored gate.
The generic provider computed modality as "text->text" for any vision model
and "text" otherwise, discarding the source's own modality and mislabelling
image-capable models. Carry OpenRouter's modality string through verbatim, and
when a source doesn't supply one, derive it from the captured input/output
modalities in the same "in->out" shape (e.g. "text+image->text").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a bare model id matched several OpenRouter entries by tail, the first feed
entry won, making the resolved price depend on feed order and risking an
undercharge if a cheaper reseller happened to sort first. Pick the
highest-priced candidate instead: deterministic and money-safe, since
undercharging is the hazard. An exact id match still wins ahead of any tail
match. Measured against the live feed there are zero bare-tail collisions
today, so this only governs the latent case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
litellm's max_tokens is the completion cap (it equals max_output_tokens for
~94% of models), not the context window, so falling back to it overstated the
context as the output limit. Take context from max_input_tokens alone; a model
that reports none falls through to the id-based estimate downstream, which is
honest about being a guess rather than mislabelling the output cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A litellm cost entry that lists a model but prices both tokens at 0 (free
moderation/rerank tiers) was resolved as a real price, importing the model
enabled and served for free. Reject a both-zero (or negative) litellm hit so
the caller falls through to the next source or fails closed, mirroring the
_has_valid_pricing check the OpenRouter feed already applies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A generic (OpenAI-compatible) upstream whose /models response omits pricing
was silently defaulting to $0.001/M tokens and a 4096 context window. For a
provider like DeepSeek that reports no price, this undercharged real usage by
~280x — a direct money leak — while presenting a plausible-looking price.
Resolve each model through trust-ordered sources instead: the provider's
native schema (Venice's model_spec) first, then litellm's bundled cost map
(curated list prices), then the OpenRouter feed. Capture the richer metadata
those sources carry (cache rates, modalities, max output tokens, context)
rather than only price and context. When no source knows the model, import it
disabled with a warning rather than invent a number, so an operator can price
it before it serves traffic.
Context has no trustworthy source of last resort, but it is not a billing
input, so a model priced without a reported context window falls back to an
id-based estimate. The whole source-incomplete fallback (price chain +
context estimate) lives in one pricing_resolver module so it can later be
hoisted into the base provider unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11000 is nutshell's generic, unregistered TransactionError covering many
unrelated failures, so trusting the code alone made any 11000 retryable.
Gate the generic code on the 'not enough inputs' detail text; keep trusting
the registered 11005 (TransactionUnbalanced) on the code alone. Behavior is
unchanged for every existing case; a bare non-shortfall 11000 now surfaces
immediately instead of burning the retry budget.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>