Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@

All notable changes to blockrun-llm will be documented in this file.

## 1.7.1 — 2026-07-16

### Fixed

- **The settlement header was read under a name no gateway sends.** Both
gateways emit `PAYMENT-RESPONSE` (the x402 v2 spec name) — `blockrun` at 36
call sites, `blockrun-sol` at 25 — and neither emits `X-PAYMENT-RESPONSE`
even once. The SDK read only the legacy name, in four hand-rolled places, so
`_last_settlement` decoded nothing against production: no tx hash, no
settlement on any paid call. The sidecar hit this exact bug and fixed it in
blockrun-litellm 0.6.0, live-verified against a real paid call; the SDK half
was never done. Both names now go through one helper
(`tx_log.read_settlement_header`) so they can't drift apart again. The legacy
name stays accepted for other facilitators.

- **The paid-request error no longer claims your money is gone.**
`"API error after payment"` reads as *funds are lost*, which is usually false
— a real image-edit 500 was reported as lost USDC by two readers before
anyone checked the gateway. It now reports only what the settlement header
proves: a tx hash means SETTLED and is named; absence means unknown.

Absence is **not** reported as "payment likely not taken", which the first cut
of this change did. That trades a false alarm for a false all-clear, and the
all-clear lands on precisely the wrong requests: Solana's paid chat path
settles *in parallel* with the upstream call and re-raises immediately
(`logChargedButFailed(...); throw primaryError`), so a request the gateway
logs as `CHARGED BUT REQUEST FAILED — refund manually` answers *before*
settlement lands, and therefore carries no header at all. Absence and "you
were charged" co-occur systematically on the one path where it costs money.
Base settles after the upstream call and does match the optimistic reading,
but a set of headers doesn't tell the SDK which gateway produced it. So the
wording names the usual case without asserting it, and points at wallet
history.

Gated on `tx_hash`, never the header's `success` field: the gateways hard-code
`success: true` even when settle didn't land, so older clients don't surface a
spurious error. A tx hash is the only field that means money moved — the same
field the gateways gate their own revenue accounting on.

## 1.7.0 — 2026-07-15

### Added
Expand Down
2 changes: 1 addition & 1 deletion blockrun_llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
)
from .tx_log import TransactionLogger, decode_settlement_header, format_row

__version__ = "1.7.0"
__version__ = "1.7.1"
__all__ = [
"LLMClient",
"AsyncLLMClient",
Expand Down
34 changes: 18 additions & 16 deletions blockrun_llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@
chunk_usage_dict,
)
from .router import route as route_request
from .tx_log import TransactionLogger, decode_settlement_header, _resolve_log_dir
from .tx_log import (
TransactionLogger,
decode_settlement_header,
paid_request_error_prefix,
read_settlement_header,
_resolve_log_dir,
)
from .x402 import create_payment_payload, parse_payment_required, extract_payment_details
from .validation import (
validate_private_key,
Expand Down Expand Up @@ -310,7 +316,7 @@ def __init__(
self._model_pricing_cache: Optional[Dict[str, Dict[str, float]]] = None

# Opt-in transaction log + last on-chain settlement payload. The
# settlement is populated from X-PAYMENT-RESPONSE on every paid retry
# settlement is populated from PAYMENT-RESPONSE on every paid retry
# and cleared right before save_to_cache fires so it can't bleed
# across calls when logging is disabled.
log_dir = _resolve_log_dir(transaction_log)
Expand All @@ -327,9 +333,7 @@ def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, An
``save_to_cache``. ``None`` when the facilitator didn't include a
settlement header — older facilitators / cached free responses.
"""
header = response.headers.get("x-payment-response") or response.headers.get(
"X-PAYMENT-RESPONSE"
)
header = read_settlement_header(response.headers)
settlement = decode_settlement_header(header)
self._last_settlement = settlement
return settlement
Expand Down Expand Up @@ -1050,7 +1054,7 @@ def _raise_stream_error(response: httpx.Response, *, after_payment: bool) -> Non
error_body = response.json()
except Exception:
error_body = {"error": "Stream request failed"}
prefix = "API error after payment" if after_payment else "API error"
prefix = paid_request_error_prefix(response.headers) if after_payment else "API error"
raise APIError(
f"{prefix}: {response.status_code}",
response.status_code,
Expand Down Expand Up @@ -1199,7 +1203,7 @@ def _handle_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down Expand Up @@ -1366,7 +1370,7 @@ def _handle_payment_and_retry_raw(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down Expand Up @@ -1504,7 +1508,7 @@ def _handle_get_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down Expand Up @@ -2082,7 +2086,7 @@ def _log_transaction(
"""Append one row to the project-local transaction log, if enabled.

Pulls the on-chain settlement out of ``self._last_settlement``
(captured from ``X-PAYMENT-RESPONSE`` on the paid retry) and
(captured from ``PAYMENT-RESPONSE`` on the paid retry) and
consumes it — so a subsequent free / cached call right after a
paid one cannot reuse stale tx fields. No-op when the logger is
disabled; never raises (best-effort logging by design)."""
Expand Down Expand Up @@ -2271,9 +2275,7 @@ def __init__(

def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, Any]]:
"""Async-client twin of :meth:`LLMClient._capture_settlement`."""
header = response.headers.get("x-payment-response") or response.headers.get(
"X-PAYMENT-RESPONSE"
)
header = read_settlement_header(response.headers)
settlement = decode_settlement_header(header)
self._last_settlement = settlement
return settlement
Expand Down Expand Up @@ -2765,7 +2767,7 @@ async def _handle_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down Expand Up @@ -2920,7 +2922,7 @@ async def _handle_payment_and_retry_raw(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down Expand Up @@ -3044,7 +3046,7 @@ async def _handle_get_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
validate_private_key,
validate_resource_url,
)
from .tx_log import paid_request_error_prefix


# Load environment variables
Expand Down Expand Up @@ -365,7 +366,7 @@ def _handle_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
validate_api_url,
sanitize_error_response,
)
from .tx_log import paid_request_error_prefix

load_dotenv()

Expand Down Expand Up @@ -238,7 +239,7 @@ def _handle_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/phone.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
extract_payment_details,
parse_payment_required,
)
from .tx_log import paid_request_error_prefix

load_dotenv()

Expand Down Expand Up @@ -300,7 +301,7 @@ def _unwrap(response: httpx.Response, *, after_payment: bool = False) -> Dict[st
error_body = response.json()
except Exception:
error_body = {"error": "Request failed"}
prefix = "API error after payment" if after_payment else "API error"
prefix = paid_request_error_prefix(response.headers) if after_payment else "API error"
raise APIError(
f"{prefix}: {response.status_code}",
response.status_code,
Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/portrait.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
sanitize_error_response,
validate_resource_url,
)
from .tx_log import paid_request_error_prefix

load_dotenv()

Expand Down Expand Up @@ -276,7 +277,7 @@ def _handle_payment_and_retry(
)

if retry.status_code != 200:
self._raise_api_error(retry, "Enrollment failed after payment")
self._raise_api_error(retry, f"Enrollment: {paid_request_error_prefix(retry.headers)}")

return PortraitEnrollment(**retry.json())

Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/price.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
validate_api_url,
sanitize_error_response,
)
from .tx_log import paid_request_error_prefix


load_dotenv()
Expand Down Expand Up @@ -285,7 +286,7 @@ def _pay_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry.status_code}",
f"{paid_request_error_prefix(retry.headers)}: {retry.status_code}",
retry.status_code,
sanitize_error_response(error_body),
)
Expand Down
5 changes: 4 additions & 1 deletion blockrun_llm/realface.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
sanitize_error_response,
validate_resource_url,
)
from .tx_log import paid_request_error_prefix

load_dotenv()

Expand Down Expand Up @@ -443,7 +444,9 @@ def _handle_payment_and_retry(
)

if retry.status_code != 200:
self._raise_api_error(retry, "RealFace enrollment failed after payment")
self._raise_api_error(
retry, f"RealFace enrollment: {paid_request_error_prefix(retry.headers)}"
)

return RealFaceEnrollment(**retry.json())

Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
validate_api_url,
sanitize_error_response,
)
from .tx_log import paid_request_error_prefix

load_dotenv()

Expand Down Expand Up @@ -378,7 +379,7 @@ def _handle_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry_response.status_code}",
f"{paid_request_error_prefix(retry_response.headers)}: {retry_response.status_code}",
retry_response.status_code,
sanitize_error_response(error_body),
)
Expand Down
3 changes: 2 additions & 1 deletion blockrun_llm/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
validate_api_url,
sanitize_error_response,
)
from .tx_log import paid_request_error_prefix


load_dotenv()
Expand Down Expand Up @@ -195,7 +196,7 @@ def _handle_payment_and_retry(
except Exception:
error_body = {"error": "Request failed"}
raise APIError(
f"API error after payment: {retry.status_code}",
f"{paid_request_error_prefix(retry.headers)}: {retry.status_code}",
retry.status_code,
sanitize_error_response(error_body),
)
Expand Down
Loading
Loading