Commit Graph
337 Commits
Author SHA1 Message Date
Jeroen UbbinkandClaude Fable 5 02cd2cfeec test: fix concurrent mock leak in overrun finalization tests
Two tests entered patch("routstr.auth.calculate_cost", ...) inside
concurrently gathered tasks. Interleaved patch exits restore in the
wrong order, leaving the mock permanently installed for every later
test in the session. Hoist the patch around the gather.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:09:41 +02:00
Jeroen UbbinkandClaude Fable 5 0aebfc6dbe fix: bill the serving provider's fee on the USD-cost path
The USD-cost path (and the litellm pricing fallback) resolved the
provider fee via get_provider_for_model(model_id)[0] — the best-ranked
provider for the alias, not the one that served. Settlement callers in
the upstream handlers now pass their own provider_fee through
adjust_payment_for_tokens / get_x_cashu_cost into calculate_cost; the
string-derived fallback remains for callers without a serving provider.
Configured model pricing is unaffected (the fee is already baked into
cached pricing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:09:41 +02:00
Jeroen UbbinkandClaude Fable 5 b76fa17f81 fix: thread the served model into x-cashu settlement pricing
X-Cashu handlers do not rewrite the upstream's echoed model string, so
get_x_cashu_cost previously priced whatever wire name the upstream
reported — the most collapse-prone alias lookup of all. The routed Model
is now threaded from forward_x_cashu_request through the chat and
Responses handler chains (and the litellm messages path) into
get_x_cashu_cost, so cost and refund are computed from the model that
actually served.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:09:41 +02:00
Jeroen UbbinkandClaude Fable 5 ed8ac914f9 fix: thread the served model into bearer settlement pricing
Settlement previously re-derived pricing from the response's model string
through the alias map, which resolves to the best-ranked candidate for
that alias — not necessarily the provider/model that actually served the
request. adjust_payment_for_tokens and calculate_cost now accept the
routed Model and bill its pricing directly; the string lookup remains as
a warning fallback for callers without routed identity (e.g. the generic
streaming finalizer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:09:41 +02:00
9qeklajcandGitHub 99724cc5f5 Merge pull request #620 from Routstr/fix/fee-payout-crash-guard
fix: guard fee payouts against crash double-payments
2026-07-22 00:41:04 +02:00
9qeklajc 22a94a68a4 test: extract passing coverage tests from #619 2026-07-18 15:58:54 +02:00
9qeklajc be5e323c68 test: cover payout restart and migration safety 2026-07-18 14:13:20 +02:00
9qeklajc f6d1a41728 fix: checkpoint fee payouts before sending 2026-07-18 14:08:41 +02:00
9qeklajcandGitHub 3035292d2a Merge pull request #575 from Routstr/ehbp-proxy-refactor
EHBP proxy support, Tinfoil direct integration, rate limiting, wallet fixes and more
2026-07-17 20:22:14 +02:00
redshift 01c01fe8ad fix: bill PPQ.AI BYOK upstream_inference_cost + BYOK fee
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
2026-07-16 17:01:55 +08:00
redshift 892aed61cc Fix mypy: move type: ignore onto ASGITransport line 2026-07-16 16:43:52 +08:00
redshift 57fef44ce7 Merge branch 'main' of https://github.com/Routstr/routstr-core into ehbp-proxy-refactor 2026-07-13 11:14:15 +08:00
9qeklajc 9460e24f21 fix: persist outbound Cashu tokens before sending 2026-07-13 00:07:00 +02:00
9qeklajcandGitHub b88c6d84fa Merge pull request #608 from Routstr/fix/refund-sweep-test-coverage
test: cover periodic refund sweep behavior
2026-07-12 23:06:51 +02:00
9qeklajcandGitHub 238beb3e52 Merge pull request #605 from Routstr/fix/admin-withdraw-transaction-audit
fix: record admin withdrawals as outgoing transactions
2026-07-12 15:05:20 +02:00
9qeklajcandGitHub dc833d9f5f Merge pull request #602 from Routstr/fix/auto-topup-transaction-recovery
fix: make auto-topup tokens recoverable
2026-07-12 14:57:03 +02:00
9qeklajc 31e7ff8fbd test: cover periodic refund sweep behavior 2026-07-12 14:54:26 +02:00
9qeklajc a1850dfa55 fix: record admin withdrawals as outgoing transactions 2026-07-12 14:49:51 +02:00
9qeklajcandGitHub 73f52ef1fd Revert "Retry Cashu transaction storage on transient DB failures" 2026-07-12 14:47:30 +02:00
9qeklajc 110ec5da5d test: exercise refund sweep state transitions 2026-07-12 14:46:19 +02:00
9qeklajc a5cee796c9 fix: make auto-topup tokens recoverable 2026-07-12 14:45:17 +02:00
9qeklajc 07c15de106 better retry logic 2026-07-12 14:12:29 +02:00
9qeklajc aea24d23da fix: retry Cashu transaction storage 2026-07-12 13:29:09 +02:00
9qeklajc 057a752b1e fix cost breakdown calculation 2026-07-12 13:07:05 +02:00
9qeklajc 8f087bf07a Merge branch 'main' into fix/provider-scoped-model-overrides 2026-07-10 22:00:25 +02:00
redshift b81c5add6a test: satisfy mypy usage parser assertions 2026-07-10 23:40:37 +08:00
redshift 02109616b9 fix(ehbp): compare resolved upstream model identities 2026-07-10 15:16:21 +08:00
redshift 956a1ac3e1 fix(ehbp): harden model comparison against casing & date-versioned aliases
- 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
2026-07-10 09:58:35 +05:30
redshift 4287f038cf feat(ehbp): extract & use model name from Tinfoil usage metrics header
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.
2026-07-10 09:57:10 +05:30
redshift 82fd2c08a7 Merge branch 'main' of https://github.com/Routstr/routstr-core into ehbp-proxy-refactor 2026-07-10 12:12:51 +08:00
9qeklajc a1118a510d Merge branch 'main' into fix-payout 2026-07-08 22:40:03 +02:00
redshift ebb2267844 Remove require_parameters injection from OpenRouter provider
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.
2026-07-08 17:01:52 +05:30
9qeklajcandGitHub 806771df13 Merge pull request #591 from Routstr/fix/refund-race-condition
fix(wallet): return 425 when refund is pending, not 404
2026-07-07 16:27:00 +02:00
redshift 0f7d3d2f86 fix(wallet): return 425 when refund is pending, not 404
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
2026-07-07 22:00:21 +08:00
9qeklajcandGitHub 3c1b6d5f17 Merge pull request #578 from Routstr/token-error-code
fix: specific, sanitized errors for failed Cashu token redemption
2026-07-07 15:59:49 +02:00
redshift a9d5a52b32 fix(wallet): return 425 when refund is pending, not 404
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
2026-07-07 21:49:32 +08:00
9qeklajcandGitHub 97172641f8 Merge pull request #581 from jeroenubbink/fix/generic-provider-pricing
Resolve generic provider pricing instead of fabricating it
2026-07-07 14:38:32 +02:00
9qeklajc 46bbba7027 resolve reivews 2026-07-07 14:29:39 +02:00
9qeklajc edfb0f127c fix provider-scoped model overrides 2026-07-07 13:59:23 +02:00
Jeroen UbbinkandClaude Opus 4.8 2baea4149e fix(upstream): break OpenRouter bare-tail ties on combined token cost
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>
2026-07-07 12:25:09 +02:00
Jeroen UbbinkandClaude Opus 4.8 64d9460711 fix(upstream): validate native model_spec prices before trusting them
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>
2026-07-07 12:24:01 +02:00
redshift bc6d0af6d6 Merge branch 'main' of github.com-red:Routstr/routstr-core into ehbp-proxy-refactor 2026-07-07 13:47:06 +05:30
9qeklajc 6097193442 fix payout 2026-07-05 23:35:10 +02:00
9qeklajc e2aa02307b explicit code for error report & update doc 2026-07-05 23:05:53 +02:00
9qeklajc 66bc1260a4 refactor: align bearer redemption errors with shared X-Cashu taxonomy
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).
2026-07-05 17:14:30 +02:00
9qeklajc 9dc4c979b8 fix: tighten redemption error mapping and stop X-Cashu raw-text leak
- 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.
2026-07-05 16:53:54 +02:00
Jeroen UbbinkandClaude Opus 4.8 ec0fd1143b fix(upstream): preserve source modality instead of flattening it
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>
2026-07-05 15:40:57 +02:00
Jeroen UbbinkandClaude Opus 4.8 ae9748db02 fix(upstream): break OpenRouter bare-tail ties by highest price
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>
2026-07-05 15:40:43 +02:00
Jeroen UbbinkandClaude Opus 4.8 ccee76b31e fix(upstream): source litellm context from max_input_tokens only
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>
2026-07-05 15:39:41 +02:00
Jeroen UbbinkandClaude Opus 4.8 9f04525c82 fix(upstream): reject unusable litellm prices instead of resolving them
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>
2026-07-05 15:39:03 +02:00