Commit Graph
1524 Commits
Author SHA1 Message Date
redshift 5dd3cbbc22 fix: route Tinfoil attestation directly 2026-06-22 13:40:32 +08:00
redshift 55fc25b4de fix: preserve provider EHBP target headers 2026-06-22 12:08:03 +08:00
redshift 36ed3ec9f0 fix: validate Tinfoil EHBP enclave URL overrides 2026-06-22 12:04:06 +08:00
redshift 2e350e082b feat: add Tinfoil direct blind-upstream integration
Add TinfoilUpstreamProvider that uses inference.tinfoil.sh as a direct
EHBP upstream. Routstr acts as a blind relay: it forwards the opaque
encrypted body to the Tinfoil enclave without ever seeing plaintext,
and bills from the X-Tinfoil-Usage-Metrics response header.

- New routstr/upstream/tinfoil.py: fetches models from public
  GET /v1/models, parses Tinfoil pricing into standard Model/Pricing
  schema, supports_ehbp=True, proxies /attestation to atc.tinfoil.sh
- Updated routstr/upstream/ehbp.py:
  - parse_tinfoil_usage_metrics() parses prompt=N,completion=N header
  - _resolve_ehbp_target_url() honors X-Tinfoil-Enclave-Url from SDK
  - _strip_proxy_headers() removes proxy-only headers before forwarding
  - _compute_ehbp_actual_cost() converts usage to msats via calculate_cost
  - forward_ehbp_request() finalizes with exact token cost when usage
    header is present (non-streaming), falls back to max-cost otherwise
  - forward_ehbp_x_cashu_request() computes refund from actual cost
    when usage is available
- Updated routstr/proxy.py: forward /attestation and /.well-known/ paths
  without model/cost/auth lookups
- Updated routstr/upstream/__init__.py and helpers.py: register and
  auto-seed Tinfoil provider from TINFOIL_API_KEY env var
- Updated .env.example with TINFOIL_API_KEY
- 22 new unit tests in tests/unit/test_tinfoil_integration.py
- Updated docs/tinfoil-direct-integration.md and docs/ehbp-proxy-support.md
  with implementation status and billing behavior table
2026-06-21 13:16:01 +08:00
redshift 24b90af6e6 refactor: move EHBP logic to dedicated module with explicit opt-in
- Add supports_ehbp + get_ehbp_forwarding_target hooks to BaseUpstreamProvider,
  keeping base.py minimal (no large forwarding methods)
- Create routstr/upstream/ehbp.py with forward_ehbp_request and
  forward_ehbp_x_cashu_request helpers, plus max-cost finalization
- PPQAIUpstreamProvider: set supports_ehbp = True, implement target to
  /private/v1/... with X-Private-Model header
- proxy.py: filter to EHBP-capable providers, route to ehbp helpers
- Fix bearer EHBP payment: reserve upfront, finalize max cost on success
- Fix X-Cashu EHBP: refund full token on failure, refund excess on success
- Update docs/ehbp-proxy-support.md to reflect new architecture
2026-06-20 17:18:53 +08:00
redshift b9bf0ea23b Merge branch 'main' of https://github.com/Routstr/routstr-core 2026-06-20 11:07:05 +08: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
9qeklajcandGitHub 5b50a78d95 Merge pull request #559 from Routstr/fix-provider-models-view
collapse when focus change
2026-06-19 14:33:58 +02:00
9qeklajc ccab5e4216 collapse when focus change 2026-06-19 11:53:30 +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
9qeklajcandGitHub 830c299130 Merge pull request #557 from Routstr/rollback-token-generation-when-request-failed
rollback when token generation failed
2026-06-17 22:17:55 +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
redshift 11fc826bfb Merge branch 'main' of https://github.com/Routstr/routstr-core 2026-06-15 15:43:55 +08: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
9qeklajcandGitHub bf05c96dfc Merge pull request #552 from Routstr/fix-reset-reserve-balance
Fix reset reserve balance
2026-06-12 21:52: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
9qeklajcandGitHub 412bfe479c Merge pull request #547 from jeroenubbink/fix/child-key-balance-limit-atomic
fix: enforce balance_limit atomically on child key requests
2026-06-12 20:23:25 +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
redshift 757d4400af Merge branch 'main' of https://github.com/Routstr/routstr-core 2026-06-12 10:18:22 +08:00
Jeroen UbbinkandClaude Fable 5 cbc424e8e7 build: type-check the entire repo in make targets, matching CI
CI runs 'uv run mypy .' while the Makefile only checked routstr/, so test
files could pass locally and fail the pipeline. lint, type-check and
ci-lint now check everything; --ignore-missing-imports is dropped since
the CI invocation passes without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:22:28 +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 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
9qeklajcandGitHub 5d3e687876 Merge pull request #545 from Routstr/secure-test-endpoint
secure endpoint
2026-06-07 15:25:53 +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
9qeklajcandGitHub 934afaa091 Merge pull request #544 from Routstr/pr-reactive-correction
feat: reactive request-correction retry for recoverable upstream 400s
2026-06-07 15:09:18 +02:00
9qeklajc 9ef01acac9 Merge branch 'main' into pr-reactive-correction 2026-06-07 13:10:29 +02:00
9qeklajcandGitHub cbd38c15fe Merge pull request #543 from Routstr/pr-sse-parser
fix: buffer-based SSE parser for all supported providers
2026-06-07 13:06:16 +02:00
9qeklajc 06c8a071a5 feat: reactive request-correction retry for recoverable upstream 400s 2026-06-07 13:00:19 +02:00
9qeklajc 9e9c2bde57 fix: buffer-based SSE parser for all supported providers
Replace the fragile `re.split(b"data: ")` streaming parser with a
buffered, event-delimited parser in both the chat-completion and
Responses-API streamers. Events are accumulated until the SSE blank-line
delimiter, so parsing is independent of network chunk boundaries.

Fixes:
- OpenRouter `: OPENROUTER PROCESSING` keepalive comments no longer leak
  to clients as `data: : ...` (the `Unexpected token ':'` client crash).
- JSON payloads split across TCP reads are reassembled before parsing.
- CRLF framing (Gemini native alt=sse) handled.
- Combined content+usage chunks (Gemini thinking models over the
  OpenAI-compat endpoint) forward content once and still report usage in
  the cost trailer, instead of dropping the assistant message.
- Multi-line non-JSON `data` blocks are re-prefixed per line so they stay
  valid SSE framing for the client.

Adds tests/unit/test_streaming_sse_providers.py driving the real
generator against per-provider on-the-wire framing, and switches the
integration token-mint fallback to secrets.token_hex to kill a
PRNG/clock collision flake.
2026-06-07 12:58:54 +02:00
9qeklajcandGitHub 222fd6ed45 Merge pull request #542 from Routstr/fix-model-provider-field-response
make sure model provider field naming is correct
2026-06-07 11:32:01 +02:00
9qeklajc 4abd751f5f make sure model provider field naming is correct 2026-06-07 11:01:53 +02:00
9qeklajcandGitHub 8f89171db1 Merge pull request #541 from jeroenubbink/fix/lightning-invoice-key-constraints
fix: persist and propagate key constraints from Lightning invoices
2026-06-07 10:51:29 +02:00
9qeklajcandGitHub 5dc9d60bec Merge pull request #540 from jeroenubbink/fix/child-key-atomic-balance-deduction
fix: make child key balance deduction atomic
2026-06-04 10:09:26 +02:00
redshift 766d59d666 Merge branch 'main' of https://github.com/Routstr/routstr-core 2026-06-04 16:08:28 +08:00
redshift 98ddd37853 2 weeks 2026-06-04 16:07:23 +08:00
Jeroen UbbinkandClaude Sonnet 4.6 feb76bc89d fix: persist and propagate key constraints from Lightning invoices
LightningInvoice had no columns for balance_limit, balance_limit_reset,
or validity_date. SQLModel silently dropped these constructor kwargs, so
create_api_key_from_invoice always produced an unconstrained key.

Add the three columns to LightningInvoice with a migration, and wire them
through to the ApiKey in create_api_key_from_invoice, matching the pattern
already used in the child key creation path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:58:40 +02:00
Jeroen UbbinkandClaude Sonnet 4.6 3b7f96560b fix: make child key balance deduction atomic
The previous in-memory deduction (key.balance -= cost) was a read-modify-
write on stale state, allowing two concurrent create_child_key() calls to
both pass the balance check and both succeed, effectively charging the
parent only once for two child keys.

Replace with an atomic UPDATE ... WHERE balance - reserved_balance >= cost
and check rowcount, matching the pattern already used in pay_for_request.
Also adds a concurrent integration test that reproduces the race and
confirms the fix holds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:02:12 +02:00
9qeklajcandGitHub 23e206c93a Merge pull request #538 from Routstr/revert-sse-refactoring
revert sse refactoring
2026-06-02 14:00:48 +02:00
9qeklajc 3e0ca0daf9 revert sse refactoring 2026-06-02 13:58:43 +02:00