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
- Case-insensitive comparison of served model vs forwarded_model_id
(matches get_model_instance's lowercasing semantics)
- Suppress spurious mismatch when get_model_instance resolves back to
the same model (e.g. date-versioned glm-5-2-20260415 -> glm-5-2)
- Preserve unknown-model warning only when lookup returns None
- Tests: case-insensitive match + date-versioned alias resolution
Tinfoil PR #385 added model=<name> to the X-Tinfoil-Usage-Metrics
header/trailer. This commit uses that field for accurate billing.
Changes in routstr/upstream/ehbp.py:
- parse_tinfoil_usage_metrics(): extract the model= field as a string
alongside the existing token counts (previously silently discarded
because int() failed on it).
- _build_cost_info(): accept optional actual_model parameter propagated
through to callers when a real mismatch is detected.
- _compute_ehbp_actual_cost(): compare the served model against
model_obj.forwarded_model_id (the expected upstream ID) rather than
model_obj.id (the client-facing alias). This prevents spurious
mismatches when a node runner maps e.g. tinfoil-glm-5-2 -> glm-5-2
and the header correctly reports glm-5-2. On a genuine mismatch
(failover to a different upstream model), look up the actual model's
pricing via get_model_instance() (forwarded_model_id values are
registered as routable aliases in the global model map).
- forward_ehbp_request() / forward_ehbp_x_cashu_request(): use the
actual served model for payment finalization and logging when a
mismatch is detected.
Tests: 6 new scenarios (alias match, real mismatch with alias, unknown
model fallback, old-format compat, cache token details + model), plus
forwarded_model_id set on all existing mock model objects to keep them
passing. All 49 Tinfoil/EHBP unit tests pass.
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 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>