Skip to content

Commit 043f7be

Browse files
balogh.adam@icloud.comclaude
andcommitted
surface x402 payment-required details on LLM HTTP errors
Decodes the base64-encoded `payment-required` response header so chat failures report the actual x402 error (e.g. permit2_simulation_failed) instead of a bare status line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 516f877 commit 043f7be

1 file changed

Lines changed: 24 additions & 1 deletion

File tree

src/opengradient/client/llm.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""LLM chat and completion via TEE-verified execution with x402 payments."""
22

3+
import base64
34
import json
45
import logging
56
from dataclasses import dataclass
@@ -35,6 +36,7 @@
3536
_COMPLETION_ENDPOINT = "/v1/completions"
3637
_REQUEST_TIMEOUT = 60
3738

39+
3840
@dataclass(frozen=True)
3941
class _ChatParams:
4042
"""Bundles the common parameters for chat/completion requests."""
@@ -385,8 +387,9 @@ async def _request() -> TextGenerationOutput:
385387
headers=self._headers(params.x402_settlement_mode),
386388
timeout=_REQUEST_TIMEOUT,
387389
)
388-
response.raise_for_status()
389390
raw_body = await response.aread()
391+
if response.is_error:
392+
raise RuntimeError(_format_http_error(response, raw_body))
390393
result = json.loads(raw_body.decode())
391394

392395
choices = result.get("choices")
@@ -532,3 +535,23 @@ async def _parse_sse_response(self, response, tee) -> AsyncGenerator[StreamChunk
532535
chunk.tee_endpoint = tee.endpoint
533536
chunk.tee_payment_address = tee.payment_address
534537
yield chunk
538+
539+
540+
def _decode_payment_required(header_value: Optional[str]) -> str:
541+
"""Decode the base64-encoded JSON in the `payment-required` response header."""
542+
if not header_value:
543+
return "<missing>"
544+
try:
545+
decoded = base64.b64decode(header_value).decode("utf-8")
546+
return json.dumps(json.loads(decoded), indent=2)
547+
except Exception:
548+
return header_value
549+
550+
551+
def _format_http_error(response: httpx.Response, body: bytes) -> str:
552+
"""Build an error message that surfaces the x402 payment-required details."""
553+
return (
554+
f"HTTP {response.status_code} from {response.url}\n"
555+
f"Payment-Required: {_decode_payment_required(response.headers.get('payment-required'))}\n"
556+
f"Body: {body.decode(errors='replace')}"
557+
)

0 commit comments

Comments
 (0)