diff --git a/docs/api/overview.md b/docs/api/overview.md index b5636509..92fedd7e 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -155,7 +155,7 @@ The response includes your change in the same header: X-Cashu: cashuA7k2mNp4... ``` -This is fully stateless—no session, no `/v1/balance/refund` call needed. However, **streaming does not work with `X-Cashu`** because the refund can only be calculated after the full response is generated. +This is fully stateless—no session, no `/v1/balance/refund` call needed. However, **streaming does not work with `X-Cashu`** because the refund can only be calculated after the full response is generated. If you lose the `X-Cashu` response header before claiming your change, you can reclaim the refund via `POST /v1/wallet/refund` by supplying the original payment token in the `x-cashu` header. ## Response Headers diff --git a/routstr/balance.py b/routstr/balance.py index e50bafc2..692be252 100644 --- a/routstr/balance.py +++ b/routstr/balance.py @@ -609,26 +609,6 @@ async def reset_child_key_spent( return {"success": True, "message": "Child key balance reset successfully."} -@router.get("/cashu-refund/{payment_token_hash}") -async def get_cashu_refund( - payment_token_hash: str, - session: AsyncSession = Depends(get_session), -) -> dict: - """Retrieve a stored Cashu refund token by the hash of the original payment token.""" - result = await session.get(CashuTransaction, payment_token_hash) - if result is None: - raise HTTPException(status_code=404, detail="Refund not found") - if result.swept: - raise HTTPException(status_code=410, detail="Refund has been swept") - result.collected = True - session.add(result) - await session.commit() - return { - "refund_token": result.token, - "amount": result.amount, - "unit": result.unit, - } - @router.api_route( "/{path:path}", diff --git a/routstr/upstream/base.py b/routstr/upstream/base.py index 627e7840..b2e763f7 100644 --- a/routstr/upstream/base.py +++ b/routstr/upstream/base.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import hashlib import json import traceback import uuid @@ -1891,7 +1890,6 @@ class BaseUpstreamProvider: max_cost_for_model: int, model_obj: Model, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> Response | StreamingResponse: """Dispatch /v1/messages via litellm for x-cashu payments. @@ -1913,7 +1911,6 @@ class BaseUpstreamProvider: max_cost_for_model, requested_model, mint, - payment_token_hash, request_id, ) @@ -1940,7 +1937,6 @@ class BaseUpstreamProvider: refund_amount, unit, mint, - payment_token_hash, request_id=request_id, ) response_headers["X-Cashu"] = refund_token @@ -2127,7 +2123,6 @@ class BaseUpstreamProvider: max_cost_for_model: int, requested_model: str | None, mint: str | None, - payment_token_hash: str | None, request_id: str | None, ) -> StreamingResponse: """Buffer a litellm stream end-to-end, compute cost, then replay. @@ -2231,7 +2226,6 @@ class BaseUpstreamProvider: refund_amount, unit, mint, - payment_token_hash, request_id=request_id, ) response_headers["X-Cashu"] = refund_token @@ -2996,7 +2990,6 @@ class BaseUpstreamProvider: amount: int, unit: str, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> str: """Create and send a refund token to the user. @@ -3005,7 +2998,6 @@ class BaseUpstreamProvider: amount: Refund amount unit: Unit of the refund (sat or msat) mint: Optional mint URL for the refund token - payment_token_hash: Optional SHA-256 hash of the original payment token for storage request_id: Optional HTTP request ID for tracking Returns: @@ -3097,7 +3089,6 @@ class BaseUpstreamProvider: unit: str, max_cost_for_model: int, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> StreamingResponse: """Handle streaming response for X-Cashu payment, calculating refund if needed. @@ -3108,7 +3099,6 @@ class BaseUpstreamProvider: amount: Payment amount received unit: Payment unit (sat or msat) max_cost_for_model: Maximum cost for the model - payment_token_hash: Optional hash of original payment token for refund storage Returns: StreamingResponse with refund token in header if applicable @@ -3199,7 +3189,6 @@ class BaseUpstreamProvider: refund_amount, unit, mint, - payment_token_hash, request_id=request_id, ) response_headers["X-Cashu"] = refund_token @@ -3278,7 +3267,6 @@ class BaseUpstreamProvider: unit: str, max_cost_for_model: int, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> Response: """Handle non-streaming response for X-Cashu payment, calculating refund if needed. @@ -3289,7 +3277,6 @@ class BaseUpstreamProvider: amount: Payment amount received unit: Payment unit (sat or msat) max_cost_for_model: Maximum cost for the model - payment_token_hash: Optional hash of original payment token for refund storage Returns: Response with refund token in header if applicable @@ -3359,7 +3346,6 @@ class BaseUpstreamProvider: refund_amount, unit, mint, - payment_token_hash, request_id=request_id, ) response_headers["X-Cashu"] = refund_token @@ -3431,7 +3417,6 @@ class BaseUpstreamProvider: unit: str, max_cost_for_model: int, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> StreamingResponse | Response: """Handle chat completion response for X-Cashu payment, detecting streaming vs non-streaming. @@ -3475,7 +3460,6 @@ class BaseUpstreamProvider: unit, max_cost_for_model, mint, - payment_token_hash, request_id=request_id, ) else: @@ -3486,7 +3470,6 @@ class BaseUpstreamProvider: unit, max_cost_for_model, mint, - payment_token_hash, request_id=request_id, ) @@ -3516,7 +3499,6 @@ class BaseUpstreamProvider: max_cost_for_model: int, model_obj: Model, mint: str | None = None, - payment_token_hash: str | None = None, ) -> Response | StreamingResponse: """Forward request paid with X-Cashu token to upstream service. @@ -3555,7 +3537,6 @@ class BaseUpstreamProvider: max_cost_for_model=max_cost_for_model, model_obj=model_obj, mint=mint, - payment_token_hash=payment_token_hash, request_id=getattr(request.state, "request_id", None), ) @@ -3625,7 +3606,6 @@ class BaseUpstreamProvider: amount, unit, mint, - payment_token_hash, request_id=getattr(request.state, "request_id", None), ) @@ -3675,7 +3655,6 @@ class BaseUpstreamProvider: unit, max_cost_for_model, mint, - payment_token_hash, request_id=getattr(request.state, "request_id", None), ) background_tasks = BackgroundTasks() @@ -3751,7 +3730,6 @@ class BaseUpstreamProvider: ) try: - payment_token_hash = hashlib.sha256(x_cashu_token.encode()).hexdigest() headers = dict(request.headers) amount, unit, mint = await recieve_token(x_cashu_token) headers = self.prepare_headers(dict(request.headers)) @@ -3784,7 +3762,6 @@ class BaseUpstreamProvider: max_cost_for_model, model_obj, mint, - payment_token_hash, ) except Exception as e: error_message = str(e) @@ -3844,7 +3821,6 @@ class BaseUpstreamProvider: max_cost_for_model: int, model_obj: Model, mint: str | None = None, - payment_token_hash: str | None = None, ) -> Response | StreamingResponse: """Forward Responses API request paid with X-Cashu token to upstream service. @@ -3920,7 +3896,6 @@ class BaseUpstreamProvider: amount, unit, mint, - payment_token_hash, request_id=getattr(request.state, "request_id", None), ) @@ -3965,7 +3940,6 @@ class BaseUpstreamProvider: unit, max_cost_for_model, mint, - payment_token_hash, request_id=getattr(request.state, "request_id", None), ) background_tasks = BackgroundTasks() @@ -4016,7 +3990,6 @@ class BaseUpstreamProvider: unit: str, max_cost_for_model: int, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> StreamingResponse | Response: """Handle Responses API completion response for X-Cashu payment. @@ -4061,7 +4034,6 @@ class BaseUpstreamProvider: unit, max_cost_for_model, mint, - payment_token_hash, request_id=request_id, ) else: @@ -4072,7 +4044,6 @@ class BaseUpstreamProvider: unit, max_cost_for_model, mint, - payment_token_hash, request_id=request_id, ) @@ -4100,7 +4071,6 @@ class BaseUpstreamProvider: unit: str, max_cost_for_model: int, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> StreamingResponse: """Handle streaming Responses API response for X-Cashu payment. @@ -4187,7 +4157,6 @@ class BaseUpstreamProvider: refund_amount, unit, mint, - payment_token_hash, request_id=request_id, ) response_headers["X-Cashu"] = refund_token @@ -4266,7 +4235,6 @@ class BaseUpstreamProvider: unit: str, max_cost_for_model: int, mint: str | None = None, - payment_token_hash: str | None = None, request_id: str | None = None, ) -> Response: """Handle non-streaming Responses API response for X-Cashu payment.""" @@ -4335,7 +4303,6 @@ class BaseUpstreamProvider: refund_amount, unit, mint, - payment_token_hash, request_id=request_id, ) response_headers["X-Cashu"] = refund_token @@ -4432,7 +4399,6 @@ class BaseUpstreamProvider: ) try: - payment_token_hash = hashlib.sha256(x_cashu_token.encode()).hexdigest() headers = dict(request.headers) amount, unit, mint = await recieve_token(x_cashu_token) headers = self.prepare_headers(dict(request.headers)) @@ -4465,7 +4431,6 @@ class BaseUpstreamProvider: max_cost_for_model, model_obj, mint, - payment_token_hash, ) except Exception as e: error_message = str(e) diff --git a/tests/unit/test_messages_litellm_dispatch.py b/tests/unit/test_messages_litellm_dispatch.py index b9c0a767..1e770d1b 100644 --- a/tests/unit/test_messages_litellm_dispatch.py +++ b/tests/unit/test_messages_litellm_dispatch.py @@ -693,7 +693,6 @@ async def test_x_cashu_non_streaming_dispatches_and_refunds_overpaid_amount() -> max_cost_for_model=10_000, model_obj=model, mint="https://mint.example", - payment_token_hash="hash123", request_id="req-1", ) @@ -972,7 +971,6 @@ async def test_forward_x_cashu_request_routes_messages_via_litellm() -> None: max_cost_for_model=10_000, model_obj=model, mint="https://mint", - payment_token_hash="h", ) mock_helper.assert_awaited_once() @@ -1041,7 +1039,6 @@ async def test_forward_x_cashu_request_handles_count_tokens_locally() -> None: max_cost_for_model=10_000, model_obj=model, mint="https://mint", - payment_token_hash="h", ) assert response.status_code == 200 diff --git a/tests/unit/test_x_cashu_cost_sats.py b/tests/unit/test_x_cashu_cost_sats.py index d37d7acc..0dc509cf 100644 --- a/tests/unit/test_x_cashu_cost_sats.py +++ b/tests/unit/test_x_cashu_cost_sats.py @@ -64,7 +64,6 @@ async def test_non_streaming_includes_cost_sats() -> None: unit="msat", max_cost_for_model=10000, mint=None, - payment_token_hash=None, ) body = json.loads(response.body) @@ -170,7 +169,6 @@ async def test_streaming_includes_cost_sats_in_usage_chunk() -> None: unit="msat", max_cost_for_model=10000, mint=None, - payment_token_hash=None, ) chunks = await _collect_streaming(response)