Commit Graph
274 Commits
Author SHA1 Message Date
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
Jeroen UbbinkandClaude Opus 4.8 2a27fb239e fix(upstream): resolve generic provider pricing instead of fabricating it
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>
2026-07-03 14:42:33 +02:00
9qeklajcandGitHub b7fcf000af Merge pull request #577 from jeroenubbink/refactor/provider-identity
refactor(upstream): resolve a provider's own row by primary key
2026-07-01 22:11:29 +02:00
9qeklajc 5bedbd129f use file path 2026-07-01 17:08:28 +02:00
9qeklajc f588147b41 use slug base 2026-07-01 17:01:32 +02:00
9qeklajc 17bc949597 add test 2026-07-01 16:53:52 +02:00
9qeklajc 7705656016 resolve review comment 2026-07-01 16:40:31 +02:00
Jeroen UbbinkandClaude Opus 4.8 434283c58d refactor(upstream): resolve a provider's own row by primary key
A live upstream provider re-finds its own database row in two places —
PPQ.AI's insufficient-balance self-disable and the base
refresh_models_cache — with WHERE base_url == self.base_url AND
api_key == self.api_key. That uses a rotatable secret as a self-handle:
if the row's key rotates under a live object, it can no longer find
itself.

Carry the row's primary key on the instance as db_id, stamped centrally
by from_db_row via a _build_from_row construction hook that subclasses
override, and look the row up with session.get(UpstreamProviderRow,
db_id). This also closes a latent gap where providers built outside the
init path (auto-topup) never received db_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:53:31 +02:00
9qeklajc 0c7373675f solidify logic 2026-06-27 22:53:35 +02:00
9qeklajc a33ea5da07 update-pricing 2026-06-27 16:01:45 +02:00
9qeklajcandGitHub 38d7075652 Merge pull request #567 from Routstr/feat-prune-dead-api-keys
feat: prune dead zero-balance API keys with background janitor
2026-06-25 14:21:20 +02:00
9qeklajc 7b56dcb902 resolve review comments 2026-06-24 22:54:07 +02:00
9qeklajcandGitHub 2131cfc7f9 Merge pull request #566 from Routstr/fix-reservation-overrun-charge
fix: always charge balance on reservation overrun even after stale sweep
2026-06-24 22:38:28 +02:00
9qeklajcandGitHub cbd97e8e3a Merge pull request #549 from jeroenubbink/fix/swap-fee-retry
fix: retry foreign mint swaps with observed fees instead of trusting fee_reserve
2026-06-23 22:42:26 +02:00
Jeroen UbbinkandClaude Opus 4.8 e4b26dd27b fix: require shortfall text for generic 11000 melt errors
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>
2026-06-23 09:38:04 +02:00
9qeklajc 1e50afbd82 feat: prune dead zero-balance API keys with background janitor 2026-06-21 15:04:16 +02:00
9qeklajc 4d6e2cdf59 fix: always charge balance on reservation overrun even after stale sweep 2026-06-21 15:03:50 +02:00
9qeklajcandGitHub d079f45ba3 Merge pull request #564 from Routstr/fix-ratelimit-upstream-error-handling
better rate limit forwarding
2026-06-21 14:13:57 +02:00
9qeklajc af0796689f make openrouter default to provider supporting tool use 2026-06-20 23:55:03 +02:00
9qeklajc f408785409 better rate limit forwarding 2026-06-20 20:03:40 +02:00
9qeklajcandGitHub 90f1ba89e5 Merge pull request #563 from Routstr/fix-streaming-issue
consolidate streaming
2026-06-20 19:46:22 +02:00
9qeklajc 5a3774a414 consolidate streaming 2026-06-20 11:33:52 +02:00
9qeklajc b57f2d408e display cr as i 2026-06-20 00:57:11 +02:00
9qeklajcandGitHub c0a34a391f Merge pull request #558 from Routstr/display-balance-correctly
make sure balance is set correctly
2026-06-19 23:41:22 +02:00
9qeklajcandGitHub a97ea2995a Merge pull request #550 from jeroenubbink/fix/cached-token-overcharge
fix: bill cached input tokens at their real rates across vendor dialects
2026-06-19 23:41:04 +02:00
9qeklajc ff9c645bb1 make sure balance is set correctly 2026-06-19 11:20:56 +02:00
9qeklajcandGitHub d4318dcc07 Merge pull request #556 from jeroenubbink/fix/trusted-mint-input-fee
fix: deduct mint NUT-02 input fee when crediting trusted-mint topups
2026-06-18 21:48:24 +02:00
Jeroen UbbinkandClaude Opus 4.8 75ed865a5f fix: deduct input fee in swap_to_primary_mint same-mint shortcut
The same-mint shortcut in swap_to_primary_mint did a same-mint
split(include_fees=True) — which burns the mint's NUT-02 per-proof input
fee — but returned the full token amount, over-crediting the user (the
same bug already fixed for the trusted-mint receive path). It also
skipped DLEQ verification that the trusted path performs.

Extract the shared same-mint redeem into _redeem_same_mint (load mint,
verify DLEQ, split, credit amount - input_fees) and delegate from both
recieve_token and the shortcut, so the two paths can't drift again. This
shortcut is reachable when PRIMARY_MINT_URL is set outside CASHU_MINTS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:48:49 +02:00
Jeroen UbbinkandClaude Opus 4.8 7969a8da55 test: clarify mocked input-fee comment in trusted-mint test
The comment described "21 proofs @ 100 ppk" arithmetic, but the mocked
token has one proof and get_fees_for_proofs is hard-mocked to 3, so the
math wasn't exercised. Describe what the mock actually does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 12:26:44 +02:00
9qeklajc e9dced8148 rollback when token generation failed 2026-06-17 17:27:08 +02:00
Jeroen UbbinkandClaude Opus 4.8 ecd46975b4 fix: deduct mint NUT-02 input fee when crediting trusted-mint topups
recieve_token swaps the incoming proofs at the same mint with
include_fees=True (paying the mint's NUT-02 per-proof input fee) but credited
the full face value. On every topup from a fee-charging trusted mint, routstr
over-credited the user by the fee and its own wallet drifted toward insolvency.

Subtract get_fees_for_proofs(proofs) from the credited amount, mirroring the
foreign-mint swap path which already accounts for it. Adds a fee-charging
trusted-mint unit test (credited == face - input_fee).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:10:37 +02:00
9qeklajc 8f5f3d9738 resolve review comments 2026-06-13 23:40:39 +02:00
9qeklajc 355e3f19ef explicit cache 2026-06-13 23:40:39 +02:00
9qeklajc 439ac48216 update to all providers 2026-06-13 23:40:39 +02:00
9qeklajc ed6e0c0189 Merge main into fix-reset-reserve-balance
Resolve pay_for_request conflict: keep main's atomic child-key
balance_limit guard and post-rowcount-check ordering, and stamp
reserved_at on both billing and child reservations.
2026-06-12 21:08:53 +02:00
9qeklajc 861fda21b7 resolve review comments 2026-06-12 20:20:38 +02:00
9qeklajc 919dcf5535 make key reservation reset predictable 2026-06-12 19:45:39 +02:00
Jeroen UbbinkandClaude Fable 5 068fb3572f refactor: drop unused session parameter from calculate_cost
The session was needed when model pricing lived in the DB (73d3613) and has
been dead since pricing moved to the in-memory model map (0da08fb), yet every
caller was still obliged to supply one. get_x_cashu_cost even opened a DB
session per x-cashu request solely to feed it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:17:11 +02:00
Jeroen UbbinkandClaude Fable 5 eaf74edbba fix: bill cached input tokens at their real rates across vendor dialects
Cached prompt tokens were billed at the full input rate whenever a vendor's
usage dialect or cache pricing was unknown, overcharging DeepSeek topups
~5-10x on agentic workloads (hits are 10x cheaper upstream) and silently
mispricing OpenAI cached reads and Anthropic cache writes the same way.

Two root causes, two fixes:

- Usage dialects: DeepSeek reports prompt_cache_hit_tokens /
  prompt_cache_miss_tokens, which billing never parsed. Usage normalization
  now lives in payment/usage.py as a union parser over the known,
  non-colliding dialects (OpenAI prompt_tokens_details, Anthropic additive
  cache fields, DeepSeek hit/miss), producing one canonical NormalizedUsage.
  Providers expose it as an overridable BaseUpstreamProvider.normalize_usage
  hook — the escape hatch for future vendors whose fields genuinely
  conflict — and every settlement call site passes the provider's result
  through, so calculate_cost holds no vendor knowledge of its own.

- Cache rates: the OpenRouter model feed omits input_cache_read/-write for
  most DeepSeek models (and e.g. openai/gpt-4o), so billing fell back to the
  full input rate. Missing rates are now backfilled from litellm's bundled
  cost map before the provider fee is applied; the input-rate fallback
  remains only as the documented last resort.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:17:11 +02:00
Jeroen UbbinkandClaude Fable 5 eb6dc5189c test: pin the verbatim production error from issue #468 as retryable
The reported error carries cashu-py's "could not pay invoice" wrapper
around the mint detail; the classifier must still find the code and the
shortfall inside it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:08:38 +02:00
Jeroen UbbinkandClaude Fable 5 cbdbe4b899 test: cover all branches of the foreign-mint swap path
Every statement and branch of the topup path (recieve_token,
swap_to_primary_mint and its helpers, credit_balance) is now covered,
including the previously untested mint-failure/recovery block: recovery
must never credit proofs the wallet does not hold, and post-melt failures
must propagate unwrapped since the foreign proofs are already spent.

On testing internals: orchestration and side effects are tested only
through the public functions, with mocks at the external mint boundary
(the cashu Wallet object) — no internal code is ever mocked. The two pure
leaf helpers (_melt_insufficient_shortfall, _net_minted_amount) are tested
directly instead: their input spaces (seven error formats across mint
implementations, sat/msat conversions both ways) would cost ~30 lines of
quote/melt mock choreography per case through the swap loop, for the same
assertion a parametrize row makes. The same-mint branch of
_calculate_swap_amount is unreachable from public callers today; its test
documents the contract for the planned pre-flight fee estimation, which
will call it directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:08:38 +02:00
Jeroen UbbinkandClaude Fable 5 7870db9af1 fix: retry foreign mint swaps with observed fees instead of trusting fee_reserve
A foreign mint's melt fee_reserve is a non-binding estimate (NUT-05): the
mint may demand more on the real quote or reject the melt outright (e.g.
mint.cubabitcoin.org charging input fees beyond its quote). Instead of
padding the estimate with a safety buffer that strands funds at the
foreign mint on every swap, retry the mint-quote/melt-quote/melt cycle
(max 3 attempts) with the amount recomputed from the fees the mint
actually demands.

Melt failures are classified by the NUT-00 error code (11005 registered
TransactionUnbalanced as sent by cdk, 11000 nutshell's generic
TransactionError): fee-related rejections retry, others (e.g. 20004
Lightning payment failed) surface immediately. Nutshell's Provided/needed
amounts refine the retry step when present; otherwise shrink by 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:08:38 +02:00
Jeroen UbbinkandClaude Sonnet 4.6 28c4008892 fix: enforce balance_limit atomically on child key requests
The Python pre-check on balance_limit provided no concurrency guarantee —
two concurrent requests could both pass the check on stale in-memory state
and both proceed to reserve, exceeding the limit.

Add the balance_limit guard to the child key UPDATE's WHERE clause so the
enforcement is atomic. The HTTP 402 is raised without an explicit rollback:
since session.commit() was never called, the uncommitted billing key update
is discarded when the session context manager closes on exception exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 11:37:39 +02:00
9qeklajcandGitHub a16bc1220c Merge pull request #546 from Routstr/fix-parent-key-leak-to-child-key
do not leak parent key to child key
2026-06-07 15:27:32 +02:00
9qeklajc 68a7cd1dfb do not leak parent key to child key 2026-06-07 15:25:13 +02:00
9qeklajc d96dc20e86 secure endpoint 2026-06-07 15:18:49 +02:00
9qeklajc 9ef01acac9 Merge branch 'main' into pr-reactive-correction 2026-06-07 13:10:29 +02:00
9qeklajc 06c8a071a5 feat: reactive request-correction retry for recoverable upstream 400s 2026-06-07 13:00:19 +02:00