diff --git a/blockrun_llm/__init__.py b/blockrun_llm/__init__.py index 14a4f3f..97e06e0 100644 --- a/blockrun_llm/__init__.py +++ b/blockrun_llm/__init__.py @@ -54,252 +54,256 @@ - Solana (USDC): Use SolanaLLMClient (pip install blockrun-llm[solana]) """ +from __future__ import annotations + +from .anthropic_client import AnthropicClient +from .cache import ( + clear_cache, + export_cost_log_csv, + export_cost_log_json, + get_cost_log_summary, +) from .client import ( - LLMClient, AsyncLLMClient, - list_models, + LLMClient, + async_testnet_client, list_image_models, + list_models, testnet_client, - async_testnet_client, ) -from .anthropic_client import AnthropicClient -from .solana_client import AsyncSolanaLLMClient, SolanaLLMClient from .image import ImageClient from .music import MusicClient -from .speech import SpeechClient -from .video import VideoClient +from .phone import PhoneClient from .portrait import PortraitClient +from .price import PriceClient from .realface import RealFaceClient -from .voice import VoiceClient -from .phone import PhoneClient -from .surf import SurfClient +from .rpc import NETWORK_ALIASES, SUPPORTED_NETWORKS, RpcClient from .search import SearchClient -from .price import PriceClient -from .rpc import RpcClient, SUPPORTED_NETWORKS, NETWORK_ALIASES +from .solana_client import AsyncSolanaLLMClient, SolanaLLMClient +from .solana_wallet import ( + create_solana_wallet, + format_solana_wallet_migration_notice, + generate_solana_qr_ascii, + get_or_create_solana_wallet, + get_solana_public_key, + get_solana_usdc_balance, + import_solana_wallet, + list_discovered_solana_wallets, + load_solana_wallet, + open_solana_wallet_qr, + scan_solana_wallets, + setup_agent_solana_wallet, +) +from .speech import SpeechClient +from .surf import SurfClient +from .tx_log import TransactionLogger, decode_settlement_header, format_row from .types import ( - ChatMessage, - ChatResponse, - ChatCompletionChunk, + APIError, + AudioModel, + AudioTrack, ChatChunkChoice, ChatChunkDelta, - ChatChunkToolCall, ChatChunkFunctionCall, - Model, - APIError, - PaymentError, - SpendLimitError, - ImageResponse, + ChatChunkToolCall, + ChatCompletionChunk, + ChatMessage, + ChatResponse, ImageData, ImageModel, + ImageResponse, + Model, # Music / Audio types MusicResponse, - AudioTrack, - AudioModel, - # Speech (TTS / sound effects) types - SpeechResponse, - SpeechAudio, - # Video types - VideoResponse, - VideoClip, - VideoModel, + NewsSearchSource, + PaymentError, # Virtual Portrait types PortraitEnrollment, - PortraitUsage, - PortraitSettlement, PortraitList, PortraitListItem, + PortraitSettlement, + PortraitUsage, + PriceBar, + PriceHistoryResponse, + # Pyth market data types + PricePoint, + RealFaceEnrollment, # RealFace types RealFaceInit, - RealFaceStatus, - RealFaceEnrollment, RealFaceList, RealFaceListItem, - # Live Search types - SearchParameters, - WebSearchSource, - XSearchSource, - NewsSearchSource, - RssSearchSource, + RealFaceStatus, # Smart routing types RoutingDecision, - SmartChatResponse, + RpcError, + # Multi-chain RPC types + RpcResponse, + RssSearchSource, + # Live Search types + SearchParameters, # Standalone search SearchResult, - # Pyth market data types - PricePoint, - PriceBar, - PriceHistoryResponse, + SmartChatResponse, + SpeechAudio, + # Speech (TTS / sound effects) types + SpeechResponse, + SpendLimitError, SymbolListResponse, - # Multi-chain RPC types - RpcResponse, - RpcError, + VideoClip, + VideoModel, + # Video types + VideoResponse, + WebSearchSource, + XSearchSource, ) +from .video import VideoClient +from .voice import VoiceClient from .wallet import ( - setup_agent_wallet, # Entry point for agents (auto-creates wallet) - status, # One-command verification - get_or_create_wallet, - get_wallet_address, - format_wallet_created_message, - format_needs_funding_message, - format_funding_message_compact, + WALLET_DIR, + WALLET_FILE, format_error_message, + format_funding_message_compact, + format_needs_funding_message, + format_wallet_created_message, + format_wallet_migration_notice, generate_wallet_qr_ascii, - get_payment_links, get_eip681_uri, - save_wallet_qr, - open_wallet_qr, + get_or_create_wallet, + get_payment_links, + get_wallet_address, + import_wallet, + list_discovered_wallets, load_wallet, + open_wallet_qr, + save_wallet_qr, scan_wallets, - list_discovered_wallets, - import_wallet, - format_wallet_migration_notice, - create_wallet as generate_wallet, # User-friendly alias - WALLET_FILE, - WALLET_DIR, -) -from .solana_wallet import ( - setup_agent_solana_wallet, - get_solana_usdc_balance, - generate_solana_qr_ascii, - open_solana_wallet_qr, - get_or_create_solana_wallet, - create_solana_wallet, - load_solana_wallet, - scan_solana_wallets, - list_discovered_solana_wallets, - import_solana_wallet, - format_solana_wallet_migration_notice, - get_solana_public_key, + setup_agent_wallet, # Entry point for agents (auto-creates wallet) + status, # One-command verification ) -from .cache import ( - clear_cache, - export_cost_log_csv, - export_cost_log_json, - get_cost_log_summary, +from .wallet import ( + create_wallet as generate_wallet, # User-friendly alias ) -from .tx_log import TransactionLogger, decode_settlement_header, format_row __version__ = "1.9.0" __all__ = [ - "LLMClient", - "AsyncLLMClient", + "NETWORK_ALIASES", + "SUPPORTED_NETWORKS", + "WALLET_DIR", + "WALLET_FILE", + "APIError", "AnthropicClient", - "SolanaLLMClient", + "AsyncLLMClient", "AsyncSolanaLLMClient", - # Testnet convenience functions - "testnet_client", - "async_testnet_client", - # Entry point for agents (auto-creates wallet) - "setup_agent_wallet", - "status", - # Standalone functions (no wallet required) - "list_models", - "list_image_models", - "ImageClient", - "MusicClient", - "SpeechClient", - "VideoClient", - "PortraitClient", - "RealFaceClient", - "VoiceClient", - "PhoneClient", - "SurfClient", - "SearchClient", - "PriceClient", - "RpcClient", - "SUPPORTED_NETWORKS", - "NETWORK_ALIASES", - "ChatMessage", - "ChatResponse", - "ChatCompletionChunk", + "AudioModel", + "AudioTrack", "ChatChunkChoice", "ChatChunkDelta", - "ChatChunkToolCall", "ChatChunkFunctionCall", - "Model", - "APIError", - "PaymentError", - "SpendLimitError", - "ImageResponse", + "ChatChunkToolCall", + "ChatCompletionChunk", + "ChatMessage", + "ChatResponse", + "ImageClient", "ImageData", "ImageModel", + "ImageResponse", + "LLMClient", + "Model", + "MusicClient", "MusicResponse", - "AudioTrack", - "AudioModel", - "SpeechResponse", - "SpeechAudio", - "VideoResponse", - "VideoClip", - "VideoModel", + "NewsSearchSource", + "PaymentError", + "PhoneClient", + "PortraitClient", "PortraitEnrollment", - "PortraitUsage", - "PortraitSettlement", "PortraitList", "PortraitListItem", - "RealFaceInit", - "RealFaceStatus", + "PortraitSettlement", + "PortraitUsage", + "PriceBar", + "PriceClient", + "PriceHistoryResponse", + # Pyth market data types + "PricePoint", + "RealFaceClient", "RealFaceEnrollment", + "RealFaceInit", "RealFaceList", "RealFaceListItem", - # Live Search types - "SearchParameters", - "WebSearchSource", - "XSearchSource", - "NewsSearchSource", - "RssSearchSource", + "RealFaceStatus", # Smart routing types "RoutingDecision", - "SmartChatResponse", + "RpcClient", + "RpcError", + # Multi-chain RPC types + "RpcResponse", + "RssSearchSource", + "SearchClient", + # Live Search types + "SearchParameters", # Standalone search "SearchResult", - # Pyth market data types - "PricePoint", - "PriceBar", - "PriceHistoryResponse", + "SmartChatResponse", + "SolanaLLMClient", + "SpeechAudio", + "SpeechClient", + "SpeechResponse", + "SpendLimitError", + "SurfClient", "SymbolListResponse", - # Multi-chain RPC types - "RpcResponse", - "RpcError", - # Wallet utilities - "get_or_create_wallet", - "get_wallet_address", - "generate_wallet", - "format_wallet_created_message", - "format_needs_funding_message", - "format_funding_message_compact", + # Per-transaction log (opt-in, project-local ./log/) + "TransactionLogger", + "VideoClient", + "VideoClip", + "VideoModel", + "VideoResponse", + "VoiceClient", + "WebSearchSource", + "XSearchSource", + "async_testnet_client", + # Cache + billing utilities + "clear_cache", + "create_solana_wallet", + "decode_settlement_header", + "export_cost_log_csv", + "export_cost_log_json", "format_error_message", + "format_funding_message_compact", + "format_needs_funding_message", + "format_row", + "format_solana_wallet_migration_notice", + "format_wallet_created_message", + "format_wallet_migration_notice", + "generate_solana_qr_ascii", + "generate_wallet", "generate_wallet_qr_ascii", - "get_payment_links", + "get_cost_log_summary", "get_eip681_uri", - "save_wallet_qr", - "open_wallet_qr", + "get_or_create_solana_wallet", + # Wallet utilities + "get_or_create_wallet", + "get_payment_links", + "get_solana_public_key", + "get_solana_usdc_balance", + "get_wallet_address", + "import_solana_wallet", + "import_wallet", + "list_discovered_solana_wallets", + "list_discovered_wallets", + "list_image_models", + # Standalone functions (no wallet required) + "list_models", + "load_solana_wallet", "load_wallet", + "open_solana_wallet_qr", + "open_wallet_qr", + "save_wallet_qr", + "scan_solana_wallets", "scan_wallets", - "list_discovered_wallets", - "import_wallet", - "format_wallet_migration_notice", - "WALLET_FILE", - "WALLET_DIR", # Solana wallet utilities "setup_agent_solana_wallet", - "get_solana_usdc_balance", - "generate_solana_qr_ascii", - "open_solana_wallet_qr", - "get_or_create_solana_wallet", - "create_solana_wallet", - "load_solana_wallet", - "scan_solana_wallets", - "list_discovered_solana_wallets", - "import_solana_wallet", - "format_solana_wallet_migration_notice", - "get_solana_public_key", - # Cache + billing utilities - "clear_cache", - "get_cost_log_summary", - "export_cost_log_csv", - "export_cost_log_json", - # Per-transaction log (opt-in, project-local ./log/) - "TransactionLogger", - "decode_settlement_header", - "format_row", + # Entry point for agents (auto-creates wallet) + "setup_agent_wallet", + "status", + # Testnet convenience functions + "testnet_client", ] diff --git a/blockrun_llm/anthropic_client.py b/blockrun_llm/anthropic_client.py index b0b418b..180cdc8 100644 --- a/blockrun_llm/anthropic_client.py +++ b/blockrun_llm/anthropic_client.py @@ -16,16 +16,17 @@ print(response.content[0].text) """ +from __future__ import annotations + import os -from typing import Optional import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from .validation import validate_api_url, validate_private_key from .wallet import load_wallet -from .validation import validate_private_key, validate_api_url -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -39,7 +40,7 @@ class _BlockRunX402Transport(httpx.BaseTransport): """Custom httpx transport that intercepts 402 responses and signs x402 payments.""" def __init__( - self, account: Account, api_url: str, base_transport: Optional[httpx.BaseTransport] = None + self, account: Account, api_url: str, base_transport: httpx.BaseTransport | None = None ): self._account = account self._api_url = api_url @@ -126,8 +127,8 @@ class AnthropicClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_CHAT_TIMEOUT, **kwargs, ): diff --git a/blockrun_llm/billing.py b/blockrun_llm/billing.py index 6199e3c..d5dfb56 100644 --- a/blockrun_llm/billing.py +++ b/blockrun_llm/billing.py @@ -26,7 +26,6 @@ import argparse import sys -from typing import List, Optional from .cache import ( COST_LOG_PATH, @@ -174,7 +173,7 @@ def build_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) try: diff --git a/blockrun_llm/cache.py b/blockrun_llm/cache.py index a64ae4f..2f9f079 100644 --- a/blockrun_llm/cache.py +++ b/blockrun_llm/cache.py @@ -20,13 +20,13 @@ import json import re import time +from collections.abc import Iterator from datetime import datetime, timezone from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, Union - +from typing import Any # Default TTL in seconds per endpoint pattern -DEFAULT_TTL: Dict[str, int] = { +DEFAULT_TTL: dict[str, int] = { # X/Twitter data — cache 1 hour (followers/tweets don't change every minute) "/v1/partner/": 3600, # Prediction markets — cache 30 minutes @@ -68,7 +68,7 @@ def _get_ttl(endpoint: str) -> int: return 3600 -def _cache_key(endpoint: str, body: Dict[str, Any]) -> str: +def _cache_key(endpoint: str, body: dict[str, Any]) -> str: """Generate a deterministic cache key from endpoint + request body.""" key_data = json.dumps({"endpoint": endpoint, "body": body}, sort_keys=True) return hashlib.sha256(key_data.encode()).hexdigest()[:16] @@ -79,7 +79,7 @@ def _cache_path(key: str) -> Path: return CACHE_DIR / f"{key}.json" -def get_cached(endpoint: str, body: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def get_cached(endpoint: str, body: dict[str, Any]) -> dict[str, Any] | None: """ Check if a cached response exists and is still fresh. @@ -109,7 +109,7 @@ def get_cached(endpoint: str, body: Dict[str, Any]) -> Optional[Dict[str, Any]]: return None -def _readable_filename(endpoint: str, body: Dict[str, Any]) -> str: +def _readable_filename(endpoint: str, body: dict[str, Any]) -> str: """ Generate a human-readable filename from endpoint + request body. """ @@ -141,14 +141,14 @@ def _readable_filename(endpoint: str, body: Dict[str, Any]) -> str: def save_to_cache( endpoint: str, - body: Dict[str, Any], - response: Dict[str, Any], + body: dict[str, Any], + response: dict[str, Any], cost_usd: float = 0.0, *, - model: Optional[str] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, - client_kind: Optional[str] = None, + model: str | None = None, + wallet: str | None = None, + network: str | None = None, + client_kind: str | None = None, ) -> None: """ Save a paid API response locally. @@ -190,8 +190,8 @@ def save_to_cache( def _save_readable( endpoint: str, - body: Dict[str, Any], - response: Dict[str, Any], + body: dict[str, Any], + response: dict[str, Any], cost_usd: float, ) -> None: """Save a human-readable JSON file to ~/.blockrun/data/.""" @@ -214,10 +214,10 @@ def _append_cost_log( endpoint: str, cost_usd: float, *, - model: Optional[str] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, - client_kind: Optional[str] = None, + model: str | None = None, + wallet: str | None = None, + network: str | None = None, + client_kind: str | None = None, ) -> None: """Append one JSONL row to ``~/.blockrun/cost_log.jsonl``. @@ -234,7 +234,7 @@ def _append_cost_log( try: COST_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) with open(COST_LOG_PATH, "a") as f: - entry: Dict[str, Any] = { + entry: dict[str, Any] = { "ts": time.time(), "endpoint": endpoint, "cost_usd": cost_usd, @@ -268,7 +268,7 @@ def clear_cache() -> int: # --------------------------------------------------------------------------- -def _parse_date(value: Optional[str]) -> Optional[float]: +def _parse_date(value: str | None) -> float | None: """Parse a YYYY-MM-DD or ISO 8601 date into a unix timestamp. Bare dates (YYYY-MM-DD) anchor to UTC midnight. Returns ``None`` when @@ -292,11 +292,11 @@ def _parse_date(value: Optional[str]) -> Optional[float]: def _iter_cost_log( *, - from_ts: Optional[float] = None, - to_ts: Optional[float] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, -) -> Iterator[Dict[str, Any]]: + from_ts: float | None = None, + to_ts: float | None = None, + wallet: str | None = None, + network: str | None = None, +) -> Iterator[dict[str, Any]]: """Yield cost-log entries that match the optional filters.""" if not COST_LOG_PATH.exists(): return @@ -325,7 +325,7 @@ def _iter_cost_log( yield entry -def _group_key(entry: Dict[str, Any], group_by: str) -> str: +def _group_key(entry: dict[str, Any], group_by: str) -> str: """Compute the bucket key for a given grouping field.""" if group_by == "day": ts = entry.get("ts") @@ -345,12 +345,12 @@ def _group_key(entry: Dict[str, Any], group_by: str) -> str: def get_cost_log_summary( *, - from_date: Optional[str] = None, - to_date: Optional[str] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, + from_date: str | None = None, + to_date: str | None = None, + wallet: str | None = None, + network: str | None = None, group_by: str = "endpoint", -) -> Dict[str, Any]: +) -> dict[str, Any]: """Read the cost log and return an aggregated summary. Args: @@ -377,7 +377,7 @@ def get_cost_log_summary( total = 0.0 calls = 0 - groups: Dict[str, Dict[str, Any]] = {} + groups: dict[str, dict[str, Any]] = {} for entry in _iter_cost_log(from_ts=from_ts, to_ts=to_ts, wallet=wallet, network=network): cost = float(entry.get("cost_usd") or 0.0) @@ -388,7 +388,7 @@ def get_cost_log_summary( slot["calls"] += 1 slot["cost_usd"] += cost - result: Dict[str, Any] = { + result: dict[str, Any] = { "from_date": from_date, "to_date": to_date, "total_usd": total, @@ -420,7 +420,7 @@ def get_cost_log_summary( ) -def _entry_to_record(entry: Dict[str, Any]) -> Dict[str, Any]: +def _entry_to_record(entry: dict[str, Any]) -> dict[str, Any]: """Normalize one raw cost-log entry into the export record shape.""" ts = entry.get("ts") ts_iso = ( @@ -441,11 +441,11 @@ def _entry_to_record(entry: Dict[str, Any]) -> Dict[str, Any]: def _filtered_records( *, - from_date: Optional[str], - to_date: Optional[str], - wallet: Optional[str], - network: Optional[str], -) -> List[Dict[str, Any]]: + from_date: str | None, + to_date: str | None, + wallet: str | None, + network: str | None, +) -> list[dict[str, Any]]: from_ts = _parse_date(from_date) to_ts = _parse_date(to_date) return [ @@ -455,12 +455,12 @@ def _filtered_records( def export_cost_log_csv( - output_path: Optional[Union[str, Path]] = None, + output_path: str | Path | None = None, *, - from_date: Optional[str] = None, - to_date: Optional[str] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, + from_date: str | None = None, + to_date: str | None = None, + wallet: str | None = None, + network: str | None = None, ) -> str: """Render filtered cost-log entries as CSV. @@ -487,12 +487,12 @@ def export_cost_log_csv( def export_cost_log_json( - output_path: Optional[Union[str, Path]] = None, + output_path: str | Path | None = None, *, - from_date: Optional[str] = None, - to_date: Optional[str] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, + from_date: str | None = None, + to_date: str | None = None, + wallet: str | None = None, + network: str | None = None, ) -> str: """Render filtered cost-log entries as a JSON array of records. diff --git a/blockrun_llm/client.py b/blockrun_llm/client.py index c3ef64f..1379b29 100644 --- a/blockrun_llm/client.py +++ b/blockrun_llm/client.py @@ -37,39 +37,42 @@ print(result.choices[0].message.content) """ +from __future__ import annotations + +import json as _json import os import re import sys -import json as _json -from typing import AsyncIterator, Iterator, List, Dict, Any, Optional, Tuple, Union +from collections.abc import AsyncIterator, Iterator +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from .router import route as route_request +from .tx_log import ( + TransactionLogger, + _resolve_log_dir, + decode_settlement_header, + paid_request_error_prefix, + read_settlement_header, +) from .types import ( - ChatResponse, + APIError, ChatCompletionChunk, + ChatResponse, ImageResponse, - APIError, PaymentError, RoutingDecision, - SmartChatResponse, RoutingProfile, SearchResult, - stream_choice_content, - stream_choice_finish_reason, + SmartChatResponse, chunk_meta, chunk_usage_dict, + stream_choice_content, + stream_choice_finish_reason, ) -from .router import route as route_request -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 ( check_spend_limits, resolve_spend_limit, @@ -83,6 +86,7 @@ validate_temperature, validate_top_p, ) +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required # Load environment variables load_dotenv() @@ -106,7 +110,7 @@ def _get_user_agent() -> str: # ============================================================================= -def list_models(api_url: str = "https://blockrun.ai/api") -> List[Dict[str, Any]]: +def list_models(api_url: str = "https://blockrun.ai/api") -> list[dict[str, Any]]: """ List available LLM models with pricing (no wallet required). @@ -138,7 +142,7 @@ def list_models(api_url: str = "https://blockrun.ai/api") -> List[Dict[str, Any] return data.get("models", []) -def list_image_models(api_url: str = "https://blockrun.ai/api") -> List[Dict[str, Any]]: +def list_image_models(api_url: str = "https://blockrun.ai/api") -> list[dict[str, Any]]: """ List available image generation models without requiring a wallet. @@ -208,9 +212,7 @@ def _should_fallback(exc: Exception) -> bool: return True if isinstance(exc, httpx.NetworkError): return True - if isinstance(exc, APIError) and exc.status_code in (502, 503, 504, 522, 524): - return True - return False + return bool(isinstance(exc, APIError) and exc.status_code in (502, 503, 504, 522, 524)) # The gateway states the output-token ceiling it actually quoted in the 402's @@ -233,7 +235,7 @@ def _should_fallback(exc: Exception) -> bool: _DESCRIPTION_SCAN_LIMIT = 512 -def _warn_if_clamped(body: Dict[str, Any], resource_description: Optional[str]) -> None: +def _warn_if_clamped(body: dict[str, Any], resource_description: str | None) -> None: """Warn when the gateway quoted fewer output tokens than the caller asked for. An over-ceiling ``max_tokens`` is not rejected. The gateway silently clamps @@ -279,7 +281,7 @@ def _warn_if_clamped(body: Dict[str, Any], resource_description: Optional[str]) return -def _enforce_spend_limits(client: Any, cost_usd: float, model: Optional[str] = None) -> None: +def _enforce_spend_limits(client: Any, cost_usd: float, model: str | None = None) -> None: """Refuse a quote that breaches a limit the caller configured, before the paid request is sent. @@ -352,13 +354,13 @@ class LLMClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_CHAT_TIMEOUT, search_timeout: float = 300.0, - transaction_log: Union[bool, str, "os.PathLike[str]", None] = None, - max_cost_per_call: Optional[float] = None, - max_session_cost: Optional[float] = None, + transaction_log: bool | str | os.PathLike[str] | None = None, + max_cost_per_call: float | None = None, + max_session_cost: float | None = None, ): """ Initialize the BlockRun LLM client. @@ -442,19 +444,19 @@ def __init__( self._last_call_cost: float = 0.0 # Model pricing cache for smart routing - self._model_pricing_cache: Optional[Dict[str, Dict[str, float]]] = None + self._model_pricing_cache: dict[str, dict[str, float]] | None = None # Opt-in transaction log + last on-chain settlement payload. The # 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) - self._tx_logger: Optional[TransactionLogger] = ( + self._tx_logger: TransactionLogger | None = ( TransactionLogger(log_dir) if log_dir is not None else None ) - self._last_settlement: Optional[Dict[str, Any]] = None + self._last_settlement: dict[str, Any] | None = None - def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, Any]]: + def _capture_settlement(self, response: httpx.Response) -> dict[str, Any] | None: """Decode the x402 settlement header on a successful paid response. Returns the decoded settlement dict (also stashed on @@ -467,7 +469,7 @@ def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, An self._last_settlement = settlement return settlement - def _get_model_pricing(self) -> Dict[str, Dict[str, float]]: + def _get_model_pricing(self) -> dict[str, dict[str, float]]: """ Get model pricing for smart routing. @@ -484,7 +486,7 @@ def _get_model_pricing(self) -> Dict[str, Dict[str, float]]: return self._model_pricing_cache models = self.list_models() - pricing: Dict[str, Dict[str, float]] = {} + pricing: dict[str, dict[str, float]] = {} for model in models: model_id = model.get("id", "") block = model.get("pricing") or {} @@ -505,9 +507,9 @@ def smart_chat( self, prompt: str, *, - system: Optional[str] = None, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, + system: str | None = None, + max_tokens: int | None = None, + temperature: float | None = None, routing_profile: RoutingProfile = "auto", ) -> SmartChatResponse: """ @@ -573,7 +575,7 @@ def smart_chat( routing=RoutingDecision(**decision), ) - def get_spending(self) -> Dict[str, Any]: + def get_spending(self) -> dict[str, Any]: """ Get current session spending. @@ -594,14 +596,14 @@ def chat( model: str, prompt: str, *, - system: Optional[str] = None, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, - search: Optional[bool] = None, - search_parameters: Optional[Dict[str, Any]] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, + system: str | None = None, + max_tokens: int | None = None, + temperature: float | None = None, + search: bool | None = None, + search_parameters: dict[str, Any] | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, **extra: Any, ) -> str: """ @@ -634,7 +636,7 @@ def chat( search=True # Enable live search ) """ - messages: List[Dict[str, str]] = [] + messages: list[dict[str, str]] = [] if system: messages.append({"role": "system", "content": system}) @@ -659,18 +661,18 @@ def chat( def chat_completion( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], *, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, - top_p: Optional[float] = None, - search: Optional[bool] = None, - search_parameters: Optional[Dict[str, Any]] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, + max_tokens: int | None = None, + temperature: float | None = None, + top_p: float | None = None, + search: bool | None = None, + search_parameters: dict[str, Any] | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, **extra: Any, ) -> ChatResponse: """ @@ -748,7 +750,7 @@ def chat_completion( validate_top_p(top_p) # Build request body - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "messages": messages, "max_tokens": max_tokens or self.DEFAULT_MAX_TOKENS, @@ -788,7 +790,7 @@ def chat_completion( # network errors). Default behavior — single attempt — is preserved # when fallback_models is None or empty. attempts = [model, *(fallback_models or [])] - last_exc: Optional[Exception] = None + last_exc: Exception | None = None for i, attempt_model in enumerate(attempts): body["model"] = attempt_model try: @@ -814,18 +816,18 @@ def chat_completion( def chat_completion_stream( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], *, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, - top_p: Optional[float] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - search: Optional[bool] = None, - search_parameters: Optional[Dict[str, Any]] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, + max_tokens: int | None = None, + temperature: float | None = None, + top_p: float | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + search: bool | None = None, + search_parameters: dict[str, Any] | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, **extra: Any, ) -> Iterator[ChatCompletionChunk]: """ @@ -871,7 +873,7 @@ def chat_completion_stream( validate_temperature(temperature) validate_top_p(top_p) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "messages": messages, "stream": True, @@ -900,7 +902,7 @@ def chat_completion_stream( body.setdefault(k, v) attempts = [model, *(fallback_models or [])] - last_exc: Optional[Exception] = None + last_exc: Exception | None = None for i, attempt_model in enumerate(attempts): body["model"] = attempt_model @@ -938,7 +940,7 @@ def chat_completion_stream( def _stream_with_payment( self, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], ) -> Iterator[ChatCompletionChunk]: """ Run the 402 → sign → retry dance, then yield SSE chunks. @@ -955,7 +957,7 @@ def _stream_with_payment( timeout = self.search_timeout if is_search else self.timeout # ----- Phase 1: probe (no payment header) ----- - payment_headers: Optional[Dict[str, str]] = None + payment_headers: dict[str, str] | None = None cost_usd = 0.0 backoffs = self._STREAM_5XX_BACKOFFS @@ -997,10 +999,10 @@ def _stream_with_payment( def _stream_paid_phase( self, url: str, - body: Dict[str, Any], - payment_headers: Dict[str, str], + body: dict[str, Any], + payment_headers: dict[str, str], cost_usd: float, - timeout: Optional[float], + timeout: float | None, ) -> Iterator[ChatCompletionChunk]: """Phase 2 of :meth:`_stream_with_payment`: the paid, already-settled leg.""" backoffs = self._STREAM_5XX_BACKOFFS @@ -1029,7 +1031,7 @@ def _stream_paid_phase( def _iter_and_archive( self, response: httpx.Response, - body: Dict[str, Any], + body: dict[str, Any], cost_usd: float, *, streaming: bool = True, @@ -1039,12 +1041,12 @@ def _iter_and_archive( ``chat.completion`` response so paid streaming calls show up in ``~/.blockrun/cost_log.jsonl`` and ``~/.blockrun/data/`` the same way non-stream paid calls do.""" - assembled_id: Optional[str] = None - assembled_model: Optional[str] = None + assembled_id: str | None = None + assembled_model: str | None = None assembled_created: int = 0 - content_parts: List[str] = [] - finish_reason: Optional[str] = None - usage_dict: Optional[Dict[str, Any]] = None + content_parts: list[str] = [] + finish_reason: str | None = None + usage_dict: dict[str, Any] | None = None for chunk in self._iter_sse_chunks(response): if chunk.choices: @@ -1078,7 +1080,7 @@ def _iter_and_archive( if cost_usd > 0: from .cache import save_to_cache - response_data: Dict[str, Any] = { + response_data: dict[str, Any] = { "id": assembled_id or "stream", "object": "chat.completion", "created": assembled_created or int(__import__("time").time()), @@ -1138,9 +1140,9 @@ def _iter_sse_chunks(response: httpx.Response) -> Iterator[ChatCompletionChunk]: def _sign_payment_from_response( self, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - ) -> Tuple[Dict[str, str], float]: + ) -> tuple[dict[str, str], float]: """ Extract a 402's payment requirements, sign locally, and return ``(headers_with_PAYMENT_SIGNATURE, cost_usd)``. @@ -1150,7 +1152,7 @@ def _sign_payment_from_response( which lets the streaming path open an SSE connection for the retry. """ payment_header = response.headers.get("payment-required") - price_info: Dict[str, Any] = {} + price_info: dict[str, Any] = {} if not payment_header: try: resp_body = response.json() @@ -1219,7 +1221,7 @@ def _raise_stream_error(response: httpx.Response, *, after_payment: bool) -> Non sanitize_error_response(error_body), ) - def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> ChatResponse: + def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> ChatResponse: """ Make a request with automatic x402 payment handling. @@ -1273,7 +1275,7 @@ def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> ChatResp def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> ChatResponse: """ @@ -1409,7 +1411,7 @@ def _handle_payment_and_retry( return chat_response - def _request_with_payment_raw(self, endpoint: str, body: Dict[str, Any]) -> Dict[str, Any]: + def _request_with_payment_raw(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]: """ Make a request with automatic x402 payment handling, returning raw JSON. @@ -1469,9 +1471,9 @@ def _request_with_payment_raw(self, endpoint: str, body: Dict[str, Any]) -> Dict def _handle_payment_and_retry_raw( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle 402 response for raw endpoints: parse requirements, sign payment, retry.""" payment_header = response.headers.get("payment-required") price_info = {} @@ -1558,8 +1560,8 @@ def _handle_payment_and_retry_raw( return retry_response.json() def _get_with_payment_raw( - self, endpoint: str, params: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, endpoint: str, params: dict[str, Any] | None = None + ) -> dict[str, Any]: """ GET with automatic x402 payment handling, returning raw JSON. @@ -1612,9 +1614,9 @@ def _get_with_payment_raw( def _handle_get_payment_and_retry( self, url: str, - params: Optional[Dict[str, Any]], + params: dict[str, Any] | None, response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle 402 response for GET endpoints: parse requirements, sign payment, retry with GET.""" payment_header = response.headers.get("payment-required") price_info = {} @@ -1700,10 +1702,10 @@ def _handle_get_payment_and_retry( def image_edit( self, prompt: str, - image: Union[str, List[str]], + image: str | list[str], *, model: str = "openai/gpt-image-2", - mask: Optional[str] = None, + mask: str | None = None, size: str = "1024x1024", n: int = 1, ) -> ImageResponse: @@ -1727,7 +1729,7 @@ def image_edit( Returns: ImageResponse with edited image URLs """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "prompt": prompt, "image": image, @@ -1744,10 +1746,10 @@ def search( self, query: str, *, - sources: Optional[List[str]] = None, + sources: list[str] | None = None, max_results: int = 10, - from_date: Optional[str] = None, - to_date: Optional[str] = None, + from_date: str | None = None, + to_date: str | None = None, ) -> SearchResult: """ Standalone search (web, X/Twitter, news). @@ -1762,7 +1764,7 @@ def search( Returns: SearchResult with summary and citations """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "query": query, "max_results": max_results, } @@ -1778,7 +1780,7 @@ def search( # ── Exa Web Search (Powered by Exa) ───────────────────────────────────── - def exa(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: + def exa(self, path: str, body: dict[str, Any]) -> dict[str, Any]: """Generic Exa endpoint proxy via x402 USDC on Base. Args: @@ -1791,7 +1793,7 @@ def exa(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: """ return self._request_with_payment_raw(f"/v1/exa/{path}", body) - def exa_search(self, query: str, **kwargs: Any) -> Dict[str, Any]: + def exa_search(self, query: str, **kwargs: Any) -> dict[str, Any]: """Neural and keyword web search via Exa ($0.01/request, Base USDC). Args: @@ -1804,7 +1806,7 @@ def exa_search(self, query: str, **kwargs: Any) -> Dict[str, Any]: """ return self._request_with_payment_raw("/v1/exa/search", {"query": query, **kwargs}) - def exa_find_similar(self, url: str, **kwargs: Any) -> Dict[str, Any]: + def exa_find_similar(self, url: str, **kwargs: Any) -> dict[str, Any]: """Find pages semantically similar to a given URL via Exa ($0.01/request, Base USDC). @@ -1818,7 +1820,7 @@ def exa_find_similar(self, url: str, **kwargs: Any) -> Dict[str, Any]: """ return self._request_with_payment_raw("/v1/exa/find-similar", {"url": url, **kwargs}) - def exa_contents(self, urls: List[str], **kwargs: Any) -> Dict[str, Any]: + def exa_contents(self, urls: list[str], **kwargs: Any) -> dict[str, Any]: """Extract full text content from URLs via Exa ($0.002/URL, Base USDC). Args: @@ -1831,7 +1833,7 @@ def exa_contents(self, urls: List[str], **kwargs: Any) -> Dict[str, Any]: """ return self._request_with_payment_raw("/v1/exa/contents", {"urls": urls, **kwargs}) - def exa_answer(self, query: str, **kwargs: Any) -> Dict[str, Any]: + def exa_answer(self, query: str, **kwargs: Any) -> dict[str, Any]: """AI-generated answer grounded in live web search via Exa ($0.01/request, Base USDC). @@ -1847,7 +1849,7 @@ def exa_answer(self, query: str, **kwargs: Any) -> Dict[str, Any]: # ── Prediction Markets (Powered by Predexon) ──────────────────────────── - def pm(self, path: str, **params: Any) -> Dict[str, Any]: + def pm(self, path: str, **params: Any) -> dict[str, Any]: """ Query Predexon prediction market data (GET endpoints). @@ -1875,7 +1877,7 @@ def pm(self, path: str, **params: Any) -> Dict[str, Any]: """ return self._get_with_payment_raw(f"/v1/pm/{path}", params or None) - def pm_query(self, path: str, query: Dict[str, Any]) -> Dict[str, Any]: + def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: """ Structured query for Predexon prediction market data (POST endpoints). @@ -1901,7 +1903,7 @@ def pm_query(self, path: str, query: Dict[str, Any]) -> Dict[str, Any]: # Thin wrappers over pm() / pm_query() for the most common v2 endpoints. # All accept arbitrary keyword filters that are forwarded as query params. - def pm_markets(self, **params: Any) -> Dict[str, Any]: + def pm_markets(self, **params: Any) -> dict[str, Any]: """List canonical cross-venue markets (Predexon v2). Filter with venue=, status=, category=, league=, event_id=, @@ -1909,92 +1911,92 @@ def pm_markets(self, **params: Any) -> Dict[str, Any]: """ return self.pm("markets", **params) - def pm_listings(self, **params: Any) -> Dict[str, Any]: + def pm_listings(self, **params: Any) -> dict[str, Any]: """List venue-native executable listings flattened across canonical markets (Predexon v2). Tier 1 ($0.001/call).""" return self.pm("markets/listings", **params) - def pm_outcome(self, predexon_id: str) -> Dict[str, Any]: + def pm_outcome(self, predexon_id: str) -> dict[str, Any]: """Resolve a canonical Predexon outcome ID to its market context and venue listings (Predexon v2). Tier 1 ($0.001/call).""" return self.pm(f"outcomes/{predexon_id}") - def pm_polymarket_markets(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call). For high-volume traversal use ``pm_polymarket_markets_keyset()``. """ return self.pm("polymarket/markets", **params) - def pm_polymarket_events(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_events(self, **params: Any) -> dict[str, Any]: """List Polymarket events (Predexon v2). Tier 1 ($0.001/call). For high-volume traversal use ``pm_polymarket_events_keyset()``. """ return self.pm("polymarket/events", **params) - def pm_polymarket_markets_keyset(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_markets_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket markets with cursor-based keyset pagination (use pagination_key=). Tier 1 ($0.001/call).""" return self.pm("polymarket/markets/keyset", **params) - def pm_polymarket_events_keyset(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_events_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket events with cursor-based keyset pagination (use pagination_key=). Tier 1 ($0.001/call).""" return self.pm("polymarket/events/keyset", **params) - def pm_polymarket_positions(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_positions(self, **params: Any) -> dict[str, Any]: """Polymarket open positions (per-wallet, market-level PnL). Tier 1 ($0.001/call).""" return self.pm("polymarket/positions", **params) - def pm_polymarket_trades(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_trades(self, **params: Any) -> dict[str, Any]: """Recent Polymarket trades (token, side, shares, price, tx_hash). Tier 1 ($0.001/call).""" return self.pm("polymarket/trades", **params) - def pm_polymarket_leaderboard(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_leaderboard(self, **params: Any) -> dict[str, Any]: """Polymarket trader leaderboard (rank by window, sort_by). Tier 1 ($0.001/call).""" return self.pm("polymarket/leaderboard", **params) - def pm_kalshi_markets(self, **params: Any) -> Dict[str, Any]: + def pm_kalshi_markets(self, **params: Any) -> dict[str, Any]: """List Kalshi markets (CFTC-regulated event contracts). Tier 1 ($0.001/call).""" return self.pm("kalshi/markets", **params) - def pm_limitless_markets(self, **params: Any) -> Dict[str, Any]: + def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: """List Limitless markets (binary AMM-style outcomes). Tier 1 ($0.001/call).""" return self.pm("limitless/markets", **params) - def pm_sports_categories(self) -> Dict[str, Any]: + def pm_sports_categories(self) -> dict[str, Any]: """List available sports categories. Tier 1 ($0.001/call).""" return self.pm("sports/categories") - def pm_sports_markets(self, **params: Any) -> Dict[str, Any]: + def pm_sports_markets(self, **params: Any) -> dict[str, Any]: """List sports markets grouped by game. Filter with league=, sport_type=, status=, venue=. Tier 1 ($0.001/call).""" return self.pm("sports/markets", **params) - def pm_wallet_identity(self, wallet: str) -> Dict[str, Any]: + def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: """Fetch identity + profile metadata for one wallet (ENS, Twitter, portfolio, etc.). Tier 2 ($0.005/call).""" return self.pm(f"polymarket/wallet/identity/{wallet}") - def pm_wallet_identities(self, addresses: List[str]) -> Dict[str, Any]: + def pm_wallet_identities(self, addresses: list[str]) -> dict[str, Any]: """Bulk identity lookup for up to 200 wallet addresses (POST). Tier 2 ($0.005/call).""" return self.pm_query("polymarket/wallet/identities", {"addresses": addresses}) - def pm_wallet_cluster(self, address: str) -> Dict[str, Any]: + def pm_wallet_cluster(self, address: str) -> dict[str, Any]: """Discover wallets connected to a seed address via on-chain transfers and identity proofs. Tier 2 ($0.005/call).""" return self.pm(f"polymarket/wallet/{address}/cluster") # ── DefiLlama (DeFi protocols / TVL / yields / prices) ────────────────── - def defi(self, path: str, **params: Any) -> Dict[str, Any]: + def defi(self, path: str, **params: Any) -> dict[str, Any]: """ Query DefiLlama DeFi data (GET passthrough). Powered by DefiLlama. @@ -2014,23 +2016,23 @@ def defi(self, path: str, **params: Any) -> Dict[str, Any]: """ return self._get_with_payment_raw(f"/v1/defillama/{path}", params or None) - def defi_protocols(self) -> Dict[str, Any]: + def defi_protocols(self) -> dict[str, Any]: """All DeFi protocols with TVL ($0.005/call).""" return self.defi("protocols") - def defi_protocol(self, slug: str) -> Dict[str, Any]: + def defi_protocol(self, slug: str) -> dict[str, Any]: """Single protocol details + historical TVL ($0.005/call).""" return self.defi(f"protocol/{slug}") - def defi_chains(self) -> Dict[str, Any]: + def defi_chains(self) -> dict[str, Any]: """Current TVL of every chain ($0.005/call).""" return self.defi("chains") - def defi_yields(self, **params: Any) -> Dict[str, Any]: + def defi_yields(self, **params: Any) -> dict[str, Any]: """Yield pools with APY/TVL ($0.005/call).""" return self.defi("yields", **params) - def defi_prices(self, coins: Union[List[str], str]) -> Dict[str, Any]: + def defi_prices(self, coins: list[str] | str) -> dict[str, Any]: """Token price lookup ($0.001/call). Args: @@ -2047,9 +2049,9 @@ def dex( path: str, *, method: str = "GET", - body: Optional[Dict[str, Any]] = None, + body: dict[str, Any] | None = None, **params: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Query the 0x Swap / Gasless APIs (free — no x402 payment; BlockRun takes an on-chain affiliate fee on executed swaps instead). @@ -2074,41 +2076,41 @@ def dex( return self._request_with_payment_raw(endpoint, body or {}) return self._get_with_payment_raw(endpoint, params or None) - def dex_price(self, **params: Any) -> Dict[str, Any]: + def dex_price(self, **params: Any) -> dict[str, Any]: """Indicative Permit2 swap price — no commitment (free).""" return self.dex("price", **params) - def dex_quote(self, **params: Any) -> Dict[str, Any]: + def dex_quote(self, **params: Any) -> dict[str, Any]: """Firm Permit2 swap quote with permit2.eip712 + tx data (free).""" return self.dex("quote", **params) - def dex_gasless_price(self, **params: Any) -> Dict[str, Any]: + def dex_gasless_price(self, **params: Any) -> dict[str, Any]: """Gasless indicative price quote (free).""" return self.dex("gasless/price", **params) - def dex_gasless_quote(self, **params: Any) -> Dict[str, Any]: + def dex_gasless_quote(self, **params: Any) -> dict[str, Any]: """Gasless firm quote — returns trade.eip712 to sign (free).""" return self.dex("gasless/quote", **params) - def dex_gasless_submit(self, body: Dict[str, Any]) -> Dict[str, Any]: + def dex_gasless_submit(self, body: dict[str, Any]) -> dict[str, Any]: """Submit a signed gasless trade; the 0x relayer pays gas (free).""" return self.dex("gasless/submit", method="POST", body=body) - def dex_gasless_status(self, trade_hash: str) -> Dict[str, Any]: + def dex_gasless_status(self, trade_hash: str) -> dict[str, Any]: """Poll a gasless trade's status by tradeHash (free).""" return self.dex(f"gasless/status/{trade_hash}") - def dex_chains(self) -> Dict[str, Any]: + def dex_chains(self) -> dict[str, Any]: """Chains where the Swap API is supported (free).""" return self.dex("swap/chains") - def dex_gasless_chains(self) -> Dict[str, Any]: + def dex_gasless_chains(self) -> dict[str, Any]: """Chains where the Gasless API is supported (free).""" return self.dex("gasless/chains") # ── Modal Sandbox (pay-per-call cloud compute) ─────────────────────────── - def modal(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def modal(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]: """ Call the Modal sandbox compute API (POST passthrough). @@ -2119,7 +2121,7 @@ def modal(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, A """ return self._request_with_payment_raw(f"/v1/modal/{path}", body or {}) - def modal_sandbox_create(self, **body: Any) -> Dict[str, Any]: + def modal_sandbox_create(self, **body: Any) -> dict[str, Any]: """Create a sandboxed compute environment ($0.01 CPU / $0.05 GPU). Common fields: image ("python:3.11"), gpu (optional GPU type), @@ -2128,22 +2130,22 @@ def modal_sandbox_create(self, **body: Any) -> Dict[str, Any]: return self.modal("sandbox/create", body) def modal_sandbox_exec( - self, sandbox_id: str, command: List[str], **body: Any - ) -> Dict[str, Any]: + self, sandbox_id: str, command: list[str], **body: Any + ) -> dict[str, Any]: """Execute a command in a sandbox; returns stdout/stderr ($0.001).""" return self.modal("sandbox/exec", {"sandbox_id": sandbox_id, "command": command, **body}) - def modal_sandbox_status(self, sandbox_id: str) -> Dict[str, Any]: + def modal_sandbox_status(self, sandbox_id: str) -> dict[str, Any]: """Check a sandbox's status ($0.001).""" return self.modal("sandbox/status", {"sandbox_id": sandbox_id}) - def modal_sandbox_terminate(self, sandbox_id: str) -> Dict[str, Any]: + def modal_sandbox_terminate(self, sandbox_id: str) -> dict[str, Any]: """Terminate a sandbox ($0.001).""" return self.modal("sandbox/terminate", {"sandbox_id": sandbox_id}) # ── Coinbase Onramp ────────────────────────────────────────────────────── - def onramp(self, address: str) -> Dict[str, Any]: + def onramp(self, address: str) -> dict[str, Any]: """Mint a one-time Coinbase Onramp link to fund a wallet with fiat (FREE). Opens the door to buying Base USDC with a card or bank (60+ fiat @@ -2176,7 +2178,7 @@ def onramp(self, address: str) -> Dict[str, Any]: raise APIError("gateway returned no onramp url", 0, None) return data - def list_models(self) -> List[Dict[str, Any]]: + def list_models(self) -> list[dict[str, Any]]: """ List available LLM models with pricing. @@ -2198,7 +2200,7 @@ def list_models(self) -> List[Dict[str, Any]]: return response.json().get("data", []) - def list_image_models(self) -> List[Dict[str, Any]]: + def list_image_models(self) -> list[dict[str, Any]]: """ List available image generation models with pricing. @@ -2213,7 +2215,7 @@ def list_image_models(self) -> List[Dict[str, Any]]: """ return [m for m in self.list_models() if "image" in (m.get("categories") or [])] - def list_all_models(self) -> List[Dict[str, Any]]: + def list_all_models(self) -> list[dict[str, Any]]: """ List all available models (chat, image, music, etc.) with pricing. @@ -2244,7 +2246,7 @@ def is_testnet(self) -> bool: """Check if client is configured for testnet.""" return "testnet.blockrun.ai" in self.api_url - def _billing_meta(self) -> Dict[str, Optional[str]]: + def _billing_meta(self) -> dict[str, str | None]: """Return billing metadata (wallet / network / client_kind) for the cost log. Used by ``save_to_cache`` call sites.""" return { @@ -2256,7 +2258,7 @@ def _billing_meta(self) -> Dict[str, Optional[str]]: def _log_transaction( self, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], response: Any, cost_usd: float, ) -> None: @@ -2378,13 +2380,13 @@ class AsyncLLMClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_CHAT_TIMEOUT, search_timeout: float = 300.0, - transaction_log: Union[bool, str, "os.PathLike[str]", None] = None, - max_cost_per_call: Optional[float] = None, - max_session_cost: Optional[float] = None, + transaction_log: bool | str | os.PathLike[str] | None = None, + max_cost_per_call: float | None = None, + max_session_cost: float | None = None, ): """ Initialize the async BlockRun LLM client. @@ -2458,12 +2460,12 @@ def __init__( self._max_session_cost = resolve_spend_limit(max_session_cost, "BLOCKRUN_MAX_SESSION_COST") log_dir = _resolve_log_dir(transaction_log) - self._tx_logger: Optional[TransactionLogger] = ( + self._tx_logger: TransactionLogger | None = ( TransactionLogger(log_dir) if log_dir is not None else None ) - self._last_settlement: Optional[Dict[str, Any]] = None + self._last_settlement: dict[str, Any] | None = None - def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, Any]]: + def _capture_settlement(self, response: httpx.Response) -> dict[str, Any] | None: """Async-client twin of :meth:`LLMClient._capture_settlement`.""" header = read_settlement_header(response.headers) settlement = decode_settlement_header(header) @@ -2475,18 +2477,18 @@ async def chat( model: str, prompt: str, *, - system: Optional[str] = None, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, - search: Optional[bool] = None, - search_parameters: Optional[Dict[str, Any]] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, + system: str | None = None, + max_tokens: int | None = None, + temperature: float | None = None, + search: bool | None = None, + search_parameters: dict[str, Any] | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, **extra: Any, ) -> str: """Async 1-line chat interface with optional xAI Live Search.""" - messages: List[Dict[str, str]] = [] + messages: list[dict[str, str]] = [] if system: messages.append({"role": "system", "content": system}) @@ -2511,18 +2513,18 @@ async def chat( async def chat_completion( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], *, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, - top_p: Optional[float] = None, - search: Optional[bool] = None, - search_parameters: Optional[Dict[str, Any]] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, + max_tokens: int | None = None, + temperature: float | None = None, + top_p: float | None = None, + search: bool | None = None, + search_parameters: dict[str, Any] | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, **extra: Any, ) -> ChatResponse: """Async full chat completion interface with optional xAI Live Search and tool calling.""" @@ -2532,7 +2534,7 @@ async def chat_completion( validate_temperature(temperature) validate_top_p(top_p) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "messages": messages, "max_tokens": max_tokens or self.DEFAULT_MAX_TOKENS, @@ -2570,7 +2572,7 @@ async def chat_completion( # Walk [model, *fallback_models] on retriable errors. See sync # chat_completion() above for the rationale. attempts = [model, *(fallback_models or [])] - last_exc: Optional[Exception] = None + last_exc: Exception | None = None for i, attempt_model in enumerate(attempts): body["model"] = attempt_model try: @@ -2595,18 +2597,18 @@ async def chat_completion( async def chat_completion_stream( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], *, - max_tokens: Optional[int] = None, - temperature: Optional[float] = None, - top_p: Optional[float] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - search: Optional[bool] = None, - search_parameters: Optional[Dict[str, Any]] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, + max_tokens: int | None = None, + temperature: float | None = None, + top_p: float | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + search: bool | None = None, + search_parameters: dict[str, Any] | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, **extra: Any, ) -> AsyncIterator[ChatCompletionChunk]: """ @@ -2619,7 +2621,7 @@ async def chat_completion_stream( validate_temperature(temperature) validate_top_p(top_p) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "messages": messages, "stream": True, @@ -2648,7 +2650,7 @@ async def chat_completion_stream( body.setdefault(k, v) attempts = [model, *(fallback_models or [])] - last_exc: Optional[Exception] = None + last_exc: Exception | None = None for i, attempt_model in enumerate(attempts): body["model"] = attempt_model @@ -2684,7 +2686,7 @@ async def chat_completion_stream( async def _stream_with_payment( self, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], ) -> AsyncIterator[ChatCompletionChunk]: """Async version of LLMClient._stream_with_payment. @@ -2702,7 +2704,7 @@ async def _stream_with_payment( statuses_5xx = LLMClient._STREAM_5XX_STATUSES # ----- Phase 1: probe (no payment header) ----- - payment_headers: Optional[Dict[str, str]] = None + payment_headers: dict[str, str] | None = None cost_usd = 0.0 for attempt in range(len(backoffs) + 1): @@ -2747,10 +2749,10 @@ async def _stream_with_payment( async def _astream_paid_phase( self, url: str, - body: Dict[str, Any], - payment_headers: Dict[str, str], + body: dict[str, Any], + payment_headers: dict[str, str], cost_usd: float, - timeout: Optional[float], + timeout: float | None, ) -> AsyncIterator[ChatCompletionChunk]: """Phase 2 of the async stream: the paid, already-settled leg.""" backoffs = LLMClient._STREAM_5XX_BACKOFFS @@ -2784,7 +2786,7 @@ async def _astream_paid_phase( async def _aiter_and_archive( self, response: httpx.Response, - body: Dict[str, Any], + body: dict[str, Any], cost_usd: float, *, streaming: bool = True, @@ -2793,12 +2795,12 @@ async def _aiter_and_archive( assembled ``chat.completion`` response to ``~/.blockrun/data/`` and the cost row to ``~/.blockrun/cost_log.jsonl`` once the stream finishes — only for paid calls (cost_usd > 0).""" - assembled_id: Optional[str] = None - assembled_model: Optional[str] = None + assembled_id: str | None = None + assembled_model: str | None = None assembled_created: int = 0 - content_parts: List[str] = [] - finish_reason: Optional[str] = None - usage_dict: Optional[Dict[str, Any]] = None + content_parts: list[str] = [] + finish_reason: str | None = None + usage_dict: dict[str, Any] | None = None async for chunk in self._aiter_sse_chunks(response): if chunk.choices: @@ -2825,7 +2827,7 @@ async def _aiter_and_archive( if cost_usd > 0: from .cache import save_to_cache - response_data: Dict[str, Any] = { + response_data: dict[str, Any] = { "id": assembled_id or "stream", "object": "chat.completion", "created": assembled_created or int(__import__("time").time()), @@ -2879,7 +2881,7 @@ async def _aiter_sse_chunks(response: httpx.Response) -> AsyncIterator[ChatCompl _sign_payment_from_response = LLMClient._sign_payment_from_response _raise_stream_error = LLMClient._raise_stream_error - async def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> ChatResponse: + async def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> ChatResponse: """Make async request with automatic payment handling.""" url = f"{self.api_url}{endpoint}" req_headers = {"Content-Type": "application/json", "User-Agent": _get_user_agent()} @@ -2920,7 +2922,7 @@ async def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> Ch async def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> ChatResponse: """Handle 402 response asynchronously.""" @@ -3046,8 +3048,8 @@ async def _handle_payment_and_retry( return chat_response async def _request_with_payment_raw( - self, endpoint: str, body: Dict[str, Any] - ) -> Dict[str, Any]: + self, endpoint: str, body: dict[str, Any] + ) -> dict[str, Any]: """Make async request with automatic payment handling, returning raw JSON.""" from .cache import get_cached, save_to_cache @@ -3100,9 +3102,9 @@ async def _request_with_payment_raw( async def _handle_payment_and_retry_raw( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle 402 response asynchronously for raw endpoints.""" payment_header = response.headers.get("payment-required") if not payment_header: @@ -3178,8 +3180,8 @@ async def _handle_payment_and_retry_raw( return retry_response.json() async def _get_with_payment_raw( - self, endpoint: str, params: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, endpoint: str, params: dict[str, Any] | None = None + ) -> dict[str, Any]: """Async GET with x402 payment handling, returning raw JSON.""" from .cache import get_cached, save_to_cache @@ -3227,9 +3229,9 @@ async def _get_with_payment_raw( async def _handle_get_payment_and_retry( self, url: str, - params: Optional[Dict[str, Any]], + params: dict[str, Any] | None, response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle 402 response asynchronously for GET endpoints.""" payment_header = response.headers.get("payment-required") if not payment_header: @@ -3304,17 +3306,17 @@ async def _handle_get_payment_and_retry( async def image_edit( self, prompt: str, - image: Union[str, List[str]], + image: str | list[str], *, model: str = "openai/gpt-image-2", - mask: Optional[str] = None, + mask: str | None = None, size: str = "1024x1024", n: int = 1, ) -> ImageResponse: """Async image editing (img2img). ``image`` may be a single data URI or a list of 1-4 data URIs for multi-image fusion (openai/* up to 4, google/* up to 3).""" - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "prompt": prompt, "image": image, @@ -3331,13 +3333,13 @@ async def search( self, query: str, *, - sources: Optional[List[str]] = None, + sources: list[str] | None = None, max_results: int = 10, - from_date: Optional[str] = None, - to_date: Optional[str] = None, + from_date: str | None = None, + to_date: str | None = None, ) -> SearchResult: """Async standalone search.""" - body: Dict[str, Any] = { + body: dict[str, Any] = { "query": query, "max_results": max_results, } @@ -3353,107 +3355,107 @@ async def search( # ── Prediction Markets (Powered by Predexon) ──────────────────────────── - async def pm(self, path: str, **params: Any) -> Dict[str, Any]: + async def pm(self, path: str, **params: Any) -> dict[str, Any]: """Async query Predexon prediction market data (GET). Powered by Predexon.""" return await self._get_with_payment_raw(f"/v1/pm/{path}", params or None) - async def pm_query(self, path: str, query: Dict[str, Any]) -> Dict[str, Any]: + async def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: """Async structured query for Predexon data (POST). Powered by Predexon.""" return await self._request_with_payment_raw(f"/v1/pm/{path}", query) - async def pm_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_markets(self, **params: Any) -> dict[str, Any]: """List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("markets", **params) - async def pm_listings(self, **params: Any) -> Dict[str, Any]: + async def pm_listings(self, **params: Any) -> dict[str, Any]: """List venue-native executable listings (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("markets/listings", **params) - async def pm_outcome(self, predexon_id: str) -> Dict[str, Any]: + async def pm_outcome(self, predexon_id: str) -> dict[str, Any]: """Resolve a canonical Predexon outcome ID (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm(f"outcomes/{predexon_id}") - async def pm_polymarket_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("polymarket/markets", **params) - async def pm_polymarket_events(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_events(self, **params: Any) -> dict[str, Any]: """List Polymarket events (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("polymarket/events", **params) - async def pm_polymarket_markets_keyset(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_markets_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket markets with cursor-based keyset pagination. Tier 1 ($0.001/call).""" return await self.pm("polymarket/markets/keyset", **params) - async def pm_polymarket_events_keyset(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_events_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket events with cursor-based keyset pagination. Tier 1 ($0.001/call).""" return await self.pm("polymarket/events/keyset", **params) - async def pm_polymarket_positions(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_positions(self, **params: Any) -> dict[str, Any]: """Polymarket open positions (per-wallet, market-level PnL). Tier 1 ($0.001/call).""" return await self.pm("polymarket/positions", **params) - async def pm_polymarket_trades(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_trades(self, **params: Any) -> dict[str, Any]: """Recent Polymarket trades. Tier 1 ($0.001/call).""" return await self.pm("polymarket/trades", **params) - async def pm_polymarket_leaderboard(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_leaderboard(self, **params: Any) -> dict[str, Any]: """Polymarket trader leaderboard. Tier 1 ($0.001/call).""" return await self.pm("polymarket/leaderboard", **params) - async def pm_kalshi_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_kalshi_markets(self, **params: Any) -> dict[str, Any]: """List Kalshi markets. Tier 1 ($0.001/call).""" return await self.pm("kalshi/markets", **params) - async def pm_limitless_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: """List Limitless markets. Tier 1 ($0.001/call).""" return await self.pm("limitless/markets", **params) - async def pm_sports_categories(self) -> Dict[str, Any]: + async def pm_sports_categories(self) -> dict[str, Any]: """List available sports categories. Tier 1 ($0.001/call).""" return await self.pm("sports/categories") - async def pm_sports_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_sports_markets(self, **params: Any) -> dict[str, Any]: """List sports markets grouped by game. Tier 1 ($0.001/call).""" return await self.pm("sports/markets", **params) - async def pm_wallet_identity(self, wallet: str) -> Dict[str, Any]: + async def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: """Identity + profile for one wallet. Tier 2 ($0.005/call).""" return await self.pm(f"polymarket/wallet/identity/{wallet}") - async def pm_wallet_identities(self, addresses: List[str]) -> Dict[str, Any]: + async def pm_wallet_identities(self, addresses: list[str]) -> dict[str, Any]: """Bulk identity for up to 200 wallet addresses. Tier 2 ($0.005/call).""" return await self.pm_query("polymarket/wallet/identities", {"addresses": addresses}) - async def pm_wallet_cluster(self, address: str) -> Dict[str, Any]: + async def pm_wallet_cluster(self, address: str) -> dict[str, Any]: """Wallet-cluster discovery (on-chain transfers + identity proofs). Tier 2 ($0.005/call).""" return await self.pm(f"polymarket/wallet/{address}/cluster") # ── DefiLlama (DeFi protocols / TVL / yields / prices) ────────────────── - async def defi(self, path: str, **params: Any) -> Dict[str, Any]: + async def defi(self, path: str, **params: Any) -> dict[str, Any]: """Async query DefiLlama DeFi data (GET). $0.005/call ($0.001 for prices).""" return await self._get_with_payment_raw(f"/v1/defillama/{path}", params or None) - async def defi_protocols(self) -> Dict[str, Any]: + async def defi_protocols(self) -> dict[str, Any]: """Async: all DeFi protocols with TVL ($0.005/call).""" return await self.defi("protocols") - async def defi_protocol(self, slug: str) -> Dict[str, Any]: + async def defi_protocol(self, slug: str) -> dict[str, Any]: """Async: single protocol details + historical TVL ($0.005/call).""" return await self.defi(f"protocol/{slug}") - async def defi_chains(self) -> Dict[str, Any]: + async def defi_chains(self) -> dict[str, Any]: """Async: current TVL of every chain ($0.005/call).""" return await self.defi("chains") - async def defi_yields(self, **params: Any) -> Dict[str, Any]: + async def defi_yields(self, **params: Any) -> dict[str, Any]: """Async: yield pools with APY/TVL ($0.005/call).""" return await self.defi("yields", **params) - async def defi_prices(self, coins: Union[List[str], str]) -> Dict[str, Any]: + async def defi_prices(self, coins: list[str] | str) -> dict[str, Any]: """Async: token price lookup ($0.001/call).""" joined = ",".join(coins) if isinstance(coins, list) else coins return await self.defi(f"prices/{joined}") @@ -3465,74 +3467,74 @@ async def dex( path: str, *, method: str = "GET", - body: Optional[Dict[str, Any]] = None, + body: dict[str, Any] | None = None, **params: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Async query the 0x Swap / Gasless APIs (free passthrough).""" endpoint = f"/v1/zerox/{path}" if method.upper() == "POST": return await self._request_with_payment_raw(endpoint, body or {}) return await self._get_with_payment_raw(endpoint, params or None) - async def dex_price(self, **params: Any) -> Dict[str, Any]: + async def dex_price(self, **params: Any) -> dict[str, Any]: """Async: indicative Permit2 swap price (free).""" return await self.dex("price", **params) - async def dex_quote(self, **params: Any) -> Dict[str, Any]: + async def dex_quote(self, **params: Any) -> dict[str, Any]: """Async: firm Permit2 swap quote (free).""" return await self.dex("quote", **params) - async def dex_gasless_price(self, **params: Any) -> Dict[str, Any]: + async def dex_gasless_price(self, **params: Any) -> dict[str, Any]: """Async: gasless indicative price quote (free).""" return await self.dex("gasless/price", **params) - async def dex_gasless_quote(self, **params: Any) -> Dict[str, Any]: + async def dex_gasless_quote(self, **params: Any) -> dict[str, Any]: """Async: gasless firm quote — returns trade.eip712 to sign (free).""" return await self.dex("gasless/quote", **params) - async def dex_gasless_submit(self, body: Dict[str, Any]) -> Dict[str, Any]: + async def dex_gasless_submit(self, body: dict[str, Any]) -> dict[str, Any]: """Async: submit a signed gasless trade (free).""" return await self.dex("gasless/submit", method="POST", body=body) - async def dex_gasless_status(self, trade_hash: str) -> Dict[str, Any]: + async def dex_gasless_status(self, trade_hash: str) -> dict[str, Any]: """Async: poll a gasless trade's status (free).""" return await self.dex(f"gasless/status/{trade_hash}") - async def dex_chains(self) -> Dict[str, Any]: + async def dex_chains(self) -> dict[str, Any]: """Async: chains where the Swap API is supported (free).""" return await self.dex("swap/chains") - async def dex_gasless_chains(self) -> Dict[str, Any]: + async def dex_gasless_chains(self) -> dict[str, Any]: """Async: chains where the Gasless API is supported (free).""" return await self.dex("gasless/chains") # ── Modal Sandbox (pay-per-call cloud compute) ─────────────────────────── - async def modal(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def modal(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]: """Async call the Modal sandbox compute API (POST passthrough).""" return await self._request_with_payment_raw(f"/v1/modal/{path}", body or {}) - async def modal_sandbox_create(self, **body: Any) -> Dict[str, Any]: + async def modal_sandbox_create(self, **body: Any) -> dict[str, Any]: """Async: create a sandbox ($0.01 CPU / $0.05 GPU).""" return await self.modal("sandbox/create", body) async def modal_sandbox_exec( - self, sandbox_id: str, command: List[str], **body: Any - ) -> Dict[str, Any]: + self, sandbox_id: str, command: list[str], **body: Any + ) -> dict[str, Any]: """Async: execute a command in a sandbox ($0.001).""" return await self.modal( "sandbox/exec", {"sandbox_id": sandbox_id, "command": command, **body} ) - async def modal_sandbox_status(self, sandbox_id: str) -> Dict[str, Any]: + async def modal_sandbox_status(self, sandbox_id: str) -> dict[str, Any]: """Async: check a sandbox's status ($0.001).""" return await self.modal("sandbox/status", {"sandbox_id": sandbox_id}) - async def modal_sandbox_terminate(self, sandbox_id: str) -> Dict[str, Any]: + async def modal_sandbox_terminate(self, sandbox_id: str) -> dict[str, Any]: """Async: terminate a sandbox ($0.001).""" return await self.modal("sandbox/terminate", {"sandbox_id": sandbox_id}) - async def list_models(self) -> List[Dict[str, Any]]: + async def list_models(self) -> list[dict[str, Any]]: """List available LLM models asynchronously.""" response = await self._client.get(f"{self.api_url}/v1/models") @@ -3549,7 +3551,7 @@ async def list_models(self) -> List[Dict[str, Any]]: return response.json().get("data", []) - async def list_image_models(self) -> List[Dict[str, Any]]: + async def list_image_models(self) -> list[dict[str, Any]]: """List available image generation models asynchronously. ``/v1/images/models`` was deprecated server-side; this filters the @@ -3559,7 +3561,7 @@ async def list_image_models(self) -> List[Dict[str, Any]]: models = await self.list_models() return [m for m in models if "image" in (m.get("categories") or [])] - async def list_all_models(self) -> List[Dict[str, Any]]: + async def list_all_models(self) -> list[dict[str, Any]]: """ List all available models (chat, image, music, etc.) asynchronously. @@ -3587,7 +3589,7 @@ def is_testnet(self) -> bool: """Check if client is configured for testnet.""" return "testnet.blockrun.ai" in self.api_url - def _billing_meta(self) -> Dict[str, Optional[str]]: + def _billing_meta(self) -> dict[str, str | None]: """Billing metadata for cost-log entries.""" return { "wallet": self.account.address, @@ -3598,7 +3600,7 @@ def _billing_meta(self) -> Dict[str, Optional[str]]: def _log_transaction( self, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], response: Any, cost_usd: float, ) -> None: @@ -3700,7 +3702,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): # ============================================================================= -def testnet_client(private_key: Optional[str] = None, **kwargs) -> LLMClient: +def testnet_client(private_key: str | None = None, **kwargs) -> LLMClient: """ Create a testnet LLM client for development and testing. @@ -3736,7 +3738,7 @@ def testnet_client(private_key: Optional[str] = None, **kwargs) -> LLMClient: ) -async def async_testnet_client(private_key: Optional[str] = None, **kwargs) -> AsyncLLMClient: +async def async_testnet_client(private_key: str | None = None, **kwargs) -> AsyncLLMClient: """ Create an async testnet LLM client for development and testing. diff --git a/blockrun_llm/image.py b/blockrun_llm/image.py index 2084f56..e098d7a 100644 --- a/blockrun_llm/image.py +++ b/blockrun_llm/image.py @@ -26,14 +26,17 @@ result = client.generate("prompt", model="google/nano-banana-pro") """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any, List, Union +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account -from .types import ImageResponse, APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .tx_log import paid_request_error_prefix +from .types import APIError, ImageResponse, PaymentError from .validation import ( build_payment_rejected_error, sanitize_error_response, @@ -41,8 +44,7 @@ validate_private_key, validate_resource_url, ) -from .tx_log import paid_request_error_prefix - +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required # Load environment variables load_dotenv() @@ -72,8 +74,8 @@ class ImageClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 200.0, # gpt-image-2 at >=1536px can take ~180s server-side; 200s gives buffer ): """ @@ -125,8 +127,8 @@ def generate( self, prompt: str, *, - model: Optional[str] = None, - size: Optional[str] = None, + model: str | None = None, + size: str | None = None, n: int = 1, **kwargs: Any, ) -> ImageResponse: @@ -168,7 +170,7 @@ def generate( ) # Build request body - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.DEFAULT_MODEL, "prompt": prompt, "size": size or self.DEFAULT_SIZE, @@ -181,11 +183,11 @@ def generate( def edit( self, prompt: str, - image: Union[str, List[str]], + image: str | list[str], *, - model: Optional[str] = None, - mask: Optional[str] = None, - size: Optional[str] = None, + model: str | None = None, + mask: str | None = None, + size: str | None = None, n: int = 1, **kwargs: Any, ) -> ImageResponse: @@ -241,7 +243,7 @@ def edit( f"Valid parameters are: prompt, image, model, mask, size, n.{hint}" ) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or "openai/gpt-image-2", "prompt": prompt, "image": image, @@ -253,7 +255,7 @@ def edit( return self._request_with_payment("/v1/images/image2image", body) - def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> ImageResponse: + def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> ImageResponse: """ Make a request with automatic x402 payment handling. @@ -293,7 +295,7 @@ def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> ImageRes def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> ImageResponse: """Handle 402 response: parse requirements, sign payment, retry.""" @@ -378,9 +380,9 @@ def _absolute_url(self, url: str) -> str: our ``self.api_url`` already ends with ``/api`` so we strip it once to avoid double-prefixing. """ - if url.startswith("http://") or url.startswith("https://"): + if url.startswith(("http://", "https://")): return url - base = self.api_url[: -len("/api")] if self.api_url.endswith("/api") else self.api_url + base = self.api_url.removesuffix("/api") return f"{base}{url}" def _poll_until_completed( diff --git a/blockrun_llm/music.py b/blockrun_llm/music.py index 16058db..fc5750b 100644 --- a/blockrun_llm/music.py +++ b/blockrun_llm/music.py @@ -29,20 +29,23 @@ Note: Generated URLs expire in ~24h — download immediately if needed. """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account -from .types import MusicResponse, APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .tx_log import paid_request_error_prefix +from .types import APIError, MusicResponse, PaymentError from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, ) -from .tx_log import paid_request_error_prefix +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -63,8 +66,8 @@ class MusicClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 210.0, ): """ @@ -106,9 +109,9 @@ def generate( self, prompt: str, *, - model: Optional[str] = None, + model: str | None = None, instrumental: bool = True, - lyrics: Optional[str] = None, + lyrics: str | None = None, ) -> MusicResponse: """ Generate a music track from a text prompt. @@ -145,7 +148,7 @@ def generate( if instrumental and lyrics and lyrics.strip(): raise ValueError("Cannot specify lyrics when instrumental is True") - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.DEFAULT_MODEL, "prompt": prompt, "instrumental": instrumental, @@ -155,7 +158,7 @@ def generate( return self._request_with_payment("/v1/audio/generations", body) - def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> MusicResponse: + def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> MusicResponse: """Make a request with automatic x402 payment handling.""" url = f"{self.api_url}{endpoint}" @@ -184,7 +187,7 @@ def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> MusicRes def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> MusicResponse: """Handle 402 response: parse requirements, sign payment, retry.""" diff --git a/blockrun_llm/phone.py b/blockrun_llm/phone.py index f3ee5e1..65166f1 100644 --- a/blockrun_llm/phone.py +++ b/blockrun_llm/phone.py @@ -39,12 +39,14 @@ from __future__ import annotations import os -from typing import Any, Dict, Optional +from typing import Any import httpx from dotenv import load_dotenv from eth_account import Account +from typing_extensions import Self +from .tx_log import paid_request_error_prefix from .types import APIError, PaymentError from .validation import ( sanitize_error_response, @@ -56,13 +58,12 @@ extract_payment_details, parse_payment_required, ) -from .tx_log import paid_request_error_prefix load_dotenv() # Mirrors src/lib/twilio.ts PHONE_PRICES on the backend (settled USDC amount). -PHONE_PRICES: Dict[str, float] = { +PHONE_PRICES: dict[str, float] = { "lookup": 0.01, "lookup/fraud": 0.05, "numbers/buy": 5.00, @@ -86,8 +87,8 @@ class PhoneClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, ): from .wallet import load_wallet @@ -118,7 +119,7 @@ def __init__( # ------------------------------------------------------------------ Lookup - def lookup(self, phone_number: str) -> Dict[str, Any]: + def lookup(self, phone_number: str) -> dict[str, Any]: """ Carrier + line-type lookup. ~$0.01. @@ -131,7 +132,7 @@ def lookup(self, phone_number: str) -> Dict[str, Any]: self._require_e164(phone_number) return self._request("lookup", {"phoneNumber": phone_number.strip()}) - def lookup_fraud(self, phone_number: str) -> Dict[str, Any]: + def lookup_fraud(self, phone_number: str) -> dict[str, Any]: """ Lookup + fraud signals (SIM swap, call forwarding). ~$0.05. @@ -149,8 +150,8 @@ def lookup_fraud(self, phone_number: str) -> Dict[str, Any]: def buy_number( self, country: str = "US", - area_code: Optional[str] = None, - ) -> Dict[str, Any]: + area_code: str | None = None, + ) -> dict[str, Any]: """ Provision a dedicated phone number for 30 days. $5.00. @@ -173,14 +174,14 @@ def buy_number( """ if country not in ("US", "CA"): raise ValueError("country must be 'US' or 'CA'") - body: Dict[str, Any] = {"country": country} + body: dict[str, Any] = {"country": country} if area_code is not None: if not (isinstance(area_code, str) and area_code.isdigit() and len(area_code) == 3): raise ValueError("area_code must be a 3-digit string, e.g. '415'") body["areaCode"] = area_code return self._request("numbers/buy", body) - def renew_number(self, phone_number: str) -> Dict[str, Any]: + def renew_number(self, phone_number: str) -> dict[str, Any]: """ Extend an existing provisioned number by 30 days. $5.00. @@ -196,7 +197,7 @@ def renew_number(self, phone_number: str) -> Dict[str, Any]: self._require_e164(phone_number) return self._request("numbers/renew", {"phoneNumber": phone_number.strip()}) - def list_numbers(self) -> Dict[str, Any]: + def list_numbers(self) -> dict[str, Any]: """ List the wallet's active phone numbers. ~$0.001. @@ -208,7 +209,7 @@ def list_numbers(self) -> Dict[str, Any]: """ return self._request("numbers/list", {}) - def release_number(self, phone_number: str) -> Dict[str, Any]: + def release_number(self, phone_number: str) -> dict[str, Any]: """ Release a provisioned number back to the Twilio pool. Free, but the request still flows through x402 so the backend can verify ownership. @@ -232,7 +233,7 @@ def _require_e164(value: str) -> None: if not v.startswith("+") or not v[1:].isdigit() or not (8 <= len(v) <= 16): raise ValueError(f"phone_number must be E.164 (e.g. '+14155552671'), got {value!r}") - def _request(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: + def _request(self, path: str, body: dict[str, Any]) -> dict[str, Any]: url = f"{self.api_url}/v1/phone/{path}" response = self._client.post(url, json=body, headers={"Content-Type": "application/json"}) if response.status_code == 402: @@ -242,9 +243,9 @@ def _request(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: payment_header: Any = response.headers.get("payment-required") if not payment_header: try: @@ -294,7 +295,7 @@ def _handle_payment_and_retry( return data @staticmethod - def _unwrap(response: httpx.Response, *, after_payment: bool = False) -> Dict[str, Any]: + def _unwrap(response: httpx.Response, *, after_payment: bool = False) -> dict[str, Any]: if response.status_code == 200: return response.json() try: @@ -317,7 +318,7 @@ def get_wallet_address(self) -> str: def close(self) -> None: self._client.close() - def __enter__(self) -> "PhoneClient": + def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: diff --git a/blockrun_llm/portrait.py b/blockrun_llm/portrait.py index 395bb43..b388d67 100644 --- a/blockrun_llm/portrait.py +++ b/blockrun_llm/portrait.py @@ -36,30 +36,33 @@ print(p.assetId, p.name) """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from .tx_log import paid_request_error_prefix from .types import ( - PortraitEnrollment, - PortraitList, APIError, PaymentError, -) -from .x402 import ( - create_payment_payload, - parse_payment_required, - extract_payment_details, + PortraitEnrollment, + PortraitList, ) from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, validate_resource_url, ) -from .tx_log import paid_request_error_prefix +from .x402 import ( + create_payment_payload, + extract_payment_details, + parse_payment_required, +) load_dotenv() @@ -85,8 +88,8 @@ class PortraitClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 60.0, ): """ @@ -153,7 +156,7 @@ def enroll(self, name: str, image_url: str) -> PortraitEnrollment: if not image_url or not image_url.lower().startswith(("https://", "http://")): raise ValueError("image_url must be an http(s) URL") - body: Dict[str, Any] = { + body: dict[str, Any] = { "name": name, "image_url": image_url, } @@ -163,7 +166,7 @@ def enroll(self, name: str, image_url: str) -> PortraitEnrollment: # Listing (free, rate-limited) # ------------------------------------------------------------------ - def list_portraits(self, wallet_address: Optional[str] = None) -> PortraitList: + def list_portraits(self, wallet_address: str | None = None) -> PortraitList: """ List portraits enrolled by a wallet. Free, but rate-limited to ~20 requests / hour / IP (shared with the wallet-reconciliation @@ -198,7 +201,7 @@ def list_portraits(self, wallet_address: Optional[str] = None) -> PortraitList: # Internal: x402 paid POST # ------------------------------------------------------------------ - def _post_with_payment(self, endpoint: str, body: Dict[str, Any]) -> PortraitEnrollment: + def _post_with_payment(self, endpoint: str, body: dict[str, Any]) -> PortraitEnrollment: url = f"{self.api_url}{endpoint}" resp = self._client.post( @@ -218,7 +221,7 @@ def _post_with_payment(self, endpoint: str, body: Dict[str, Any]) -> PortraitEnr def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> PortraitEnrollment: payment_header = response.headers.get("payment-required") diff --git a/blockrun_llm/price.py b/blockrun_llm/price.py index 91b3e89..c1bac2e 100644 --- a/blockrun_llm/price.py +++ b/blockrun_llm/price.py @@ -34,20 +34,21 @@ from __future__ import annotations import os -from typing import Optional, Dict, Any, Literal +from typing import Any, Literal + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from typing_extensions import Self -from .types import APIError, PaymentError, PricePoint, PriceHistoryResponse, SymbolListResponse -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .tx_log import paid_request_error_prefix +from .types import APIError, PaymentError, PriceHistoryResponse, PricePoint, SymbolListResponse from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, ) -from .tx_log import paid_request_error_prefix - +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -72,8 +73,8 @@ class PriceClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, require_wallet: bool = True, ): @@ -113,8 +114,8 @@ def price( category: Category, symbol: str, *, - market: Optional[Market] = None, - session: Optional[Session] = None, + market: Market | None = None, + session: Session | None = None, ) -> PricePoint: """ Fetch a realtime price quote. @@ -122,7 +123,7 @@ def price( For ``stocks`` category the ``market`` param is required. """ endpoint = self._category_path(category, market, "price", symbol) - params: Dict[str, Any] = {} + params: dict[str, Any] = {} if session is not None: params["session"] = session data = self._get_with_payment(endpoint, params=params) @@ -147,14 +148,14 @@ def history( resolution: Resolution = "D", from_ts: int, to_ts: int, - market: Optional[Market] = None, - session: Optional[Session] = None, + market: Market | None = None, + session: Session | None = None, ) -> PriceHistoryResponse: """ Fetch OHLC bars between two Unix timestamps (seconds). """ endpoint = self._category_path(category, market, "history", symbol) - params: Dict[str, Any] = { + params: dict[str, Any] = { "resolution": resolution, "from": from_ts, "to": to_ts, @@ -173,15 +174,15 @@ def list_symbols( self, category: Category, *, - q: Optional[str] = None, + q: str | None = None, limit: int = 100, - market: Optional[Market] = None, + market: Market | None = None, ) -> SymbolListResponse: """ List available symbols in a category (free discovery endpoint). """ endpoint = self._category_path(category, market, "list", None) - params: Dict[str, Any] = {"limit": limit} + params: dict[str, Any] = {"limit": limit} if q: params["q"] = q data = self._get_with_payment(endpoint, params=params) @@ -199,9 +200,9 @@ def list_symbols( def _category_path( self, category: Category, - market: Optional[str], + market: str | None, kind: str, - symbol: Optional[str], + symbol: str | None, ) -> str: if category == "stocks": if not market: @@ -215,7 +216,7 @@ def _category_path( return f"{base}/{kind}" return f"{base}/{kind}/{symbol.upper()}" - def _get_with_payment(self, endpoint: str, *, params: Optional[Dict[str, Any]] = None) -> Any: + def _get_with_payment(self, endpoint: str, *, params: dict[str, Any] | None = None) -> Any: url = f"{self.api_url}{endpoint}" response = self._client.get(url, params=params) if response.status_code == 402: @@ -239,7 +240,7 @@ def _get_with_payment(self, endpoint: str, *, params: Optional[Dict[str, Any]] = def _pay_and_retry( self, url: str, - params: Optional[Dict[str, Any]], + params: dict[str, Any] | None, response: httpx.Response, ) -> Any: payment_header: Any = response.headers.get("payment-required") @@ -292,13 +293,13 @@ def _pay_and_retry( ) return retry.json() - def get_wallet_address(self) -> Optional[str]: + def get_wallet_address(self) -> str | None: return self.account.address if self.account else None def close(self) -> None: self._client.close() - def __enter__(self) -> "PriceClient": + def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: diff --git a/blockrun_llm/realface.py b/blockrun_llm/realface.py index d51f3ec..d930364 100644 --- a/blockrun_llm/realface.py +++ b/blockrun_llm/realface.py @@ -55,34 +55,37 @@ print(r.assetId, r.name) """ +from __future__ import annotations + import os import re import time -from typing import Optional, Dict, Any +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from .tx_log import paid_request_error_prefix from .types import ( - RealFaceInit, - RealFaceStatus, - RealFaceEnrollment, - RealFaceList, APIError, PaymentError, -) -from .x402 import ( - create_payment_payload, - parse_payment_required, - extract_payment_details, + RealFaceEnrollment, + RealFaceInit, + RealFaceList, + RealFaceStatus, ) from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, validate_resource_url, ) -from .tx_log import paid_request_error_prefix +from .x402 import ( + create_payment_payload, + extract_payment_details, + parse_payment_required, +) load_dotenv() @@ -115,8 +118,8 @@ class RealFaceClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 60.0, ): """ @@ -156,7 +159,7 @@ def __init__( # Step 1: init (free, rate-limited) # ------------------------------------------------------------------ - def init(self, name: str, group_id: Optional[str] = None) -> RealFaceInit: + def init(self, name: str, group_id: str | None = None) -> RealFaceInit: """ Start (or refresh) a RealFace enrollment. Free, but rate-limited to ~10 calls / hour / IP (each call creates an upstream session). @@ -182,7 +185,7 @@ def init(self, name: str, group_id: Optional[str] = None) -> RealFaceInit: if group_id is not None and not _GROUP_ID_RE.match(group_id): raise ValueError("group_id must look like 'legacy_rf_'") - body: Dict[str, Any] = {"name": name} + body: dict[str, Any] = {"name": name} if group_id: body["groupId"] = group_id @@ -304,7 +307,7 @@ def enroll(self, name: str, image_url: str, group_id: str) -> RealFaceEnrollment if not group_id or not _GROUP_ID_RE.match(group_id): raise ValueError("group_id must look like 'legacy_rf_'") - body: Dict[str, Any] = { + body: dict[str, Any] = { "name": name, "image_url": image_url, "group_id": group_id, @@ -315,7 +318,7 @@ def enroll(self, name: str, image_url: str, group_id: str) -> RealFaceEnrollment # Listing (free, rate-limited) # ------------------------------------------------------------------ - def list_realfaces(self, wallet_address: Optional[str] = None) -> RealFaceList: + def list_realfaces(self, wallet_address: str | None = None) -> RealFaceList: """ List RealFaces enrolled by a wallet. Free, but rate-limited to ~20 requests / hour / IP (shared with the wallet-reconciliation @@ -350,7 +353,7 @@ def list_realfaces(self, wallet_address: Optional[str] = None) -> RealFaceList: # Internal: x402 paid POST # ------------------------------------------------------------------ - def _post_with_payment(self, endpoint: str, body: Dict[str, Any]) -> RealFaceEnrollment: + def _post_with_payment(self, endpoint: str, body: dict[str, Any]) -> RealFaceEnrollment: url = f"{self.api_url}{endpoint}" resp = self._client.post( @@ -370,7 +373,7 @@ def _post_with_payment(self, endpoint: str, body: Dict[str, Any]) -> RealFaceEnr def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> RealFaceEnrollment: payment_header = response.headers.get("payment-required") diff --git a/blockrun_llm/router.py b/blockrun_llm/router.py index e7e4a90..0fd8367 100644 --- a/blockrun_llm/router.py +++ b/blockrun_llm/router.py @@ -14,10 +14,11 @@ print(f"Saved {result['routing']['savings'] * 100:.0f}%") """ -import re -import math -from typing import Dict, List, Optional, Literal, TypedDict +from __future__ import annotations +import math +import re +from typing import Literal, TypedDict # Type definitions Tier = Literal["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"] @@ -33,19 +34,19 @@ class RoutingDecision(TypedDict): cost_estimate: float baseline_cost: float savings: float # 0-1 percentage - fallbacks: List[str] # remaining models in tier order, for runtime fallback + fallbacks: list[str] # remaining models in tier order, for runtime fallback class TierConfig(TypedDict): primary: str - fallback: List[str] + fallback: list[str] class ScoringResult(TypedDict): score: float - tier: Optional[Tier] + tier: Tier | None confidence: float - signals: List[str] + signals: list[str] agentic_score: float @@ -224,7 +225,7 @@ class ScoringResult(TypedDict): # ─── Tier Configs by Profile ─── -AUTO_TIERS: Dict[Tier, TierConfig] = { +AUTO_TIERS: dict[Tier, TierConfig] = { "SIMPLE": { # moonshot/kimi-k2.7 is Moonshot's current flagship (256K context, # image+video input, reasoning_content). It is the only k2 visible in @@ -269,7 +270,7 @@ class ScoringResult(TypedDict): }, } -ECO_TIERS: Dict[Tier, TierConfig] = { +ECO_TIERS: dict[Tier, TierConfig] = { "SIMPLE": { # See AUTO_TIERS note: kimi-k2.7 is the catalog flagship. k2.6 and k2.5 # are hidden so the SDK no longer sees their pricing; primary must stay @@ -303,7 +304,7 @@ class ScoringResult(TypedDict): }, } -PREMIUM_TIERS: Dict[Tier, TierConfig] = { +PREMIUM_TIERS: dict[Tier, TierConfig] = { "SIMPLE": { "primary": "google/gemini-2.5-flash", "fallback": ["openai/gpt-5.4-nano", "anthropic/claude-haiku-4.5"], @@ -331,7 +332,7 @@ class ScoringResult(TypedDict): }, } -FREE_TIERS: Dict[Tier, TierConfig] = { +FREE_TIERS: dict[Tier, TierConfig] = { # NVIDIA free tier refresh 2026-04-28: retired nvidia/gpt-oss-120b and # nvidia/gpt-oss-20b (NVIDIA's free build.nvidia.com tier reserves the # right to use prompts/outputs for service improvement, conflicting with @@ -375,7 +376,7 @@ class ScoringResult(TypedDict): def _score_keyword_match( text: str, - keywords: List[str], + keywords: list[str], thresholds: tuple = (1, 2), scores: tuple = (0, 0.5, 1.0), ) -> tuple: @@ -395,7 +396,7 @@ def _calibrate_confidence(distance: float, steepness: float = 12) -> float: def classify_by_rules( prompt: str, - system_prompt: Optional[str], + system_prompt: str | None, estimated_tokens: int, ) -> ScoringResult: """ @@ -404,10 +405,10 @@ def classify_by_rules( """ text = f"{system_prompt or ''} {prompt}".lower() user_text = prompt.lower() - signals: List[str] = [] + signals: list[str] = [] # Dimension scores - scores: Dict[str, float] = {} + scores: dict[str, float] = {} # 1. Token count if estimated_tokens < 50: @@ -540,9 +541,9 @@ def classify_by_rules( def route( prompt: str, - system_prompt: Optional[str], + system_prompt: str | None, max_output_tokens: int, - model_pricing: Dict[str, Dict[str, float]], + model_pricing: dict[str, dict[str, float]], routing_profile: RoutingProfile = "auto", ) -> RoutingDecision: """ diff --git a/blockrun_llm/rpc.py b/blockrun_llm/rpc.py index 20369e4..0f9e5de 100644 --- a/blockrun_llm/rpc.py +++ b/blockrun_llm/rpc.py @@ -46,20 +46,23 @@ attempt, so new Tatum chains work without an SDK update. """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any, List, Union +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account -from .types import RpcResponse, APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .tx_log import paid_request_error_prefix +from .types import APIError, PaymentError, RpcResponse from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, ) -from .tx_log import paid_request_error_prefix +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -163,8 +166,8 @@ class RpcClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, ): """ @@ -206,9 +209,9 @@ def call( self, network: str, method: str, - params: Optional[List[Any]] = None, + params: list[Any] | None = None, *, - id: Union[str, int] = 1, + id: str | int = 1, ) -> RpcResponse: """ Make a single JSON-RPC 2.0 call. Flat $0.002. @@ -235,7 +238,7 @@ def call( block = client.call("ethereum", "eth_blockNumber") print(int(block.result, 16)) """ - body: Dict[str, Any] = {"jsonrpc": "2.0", "id": id, "method": method} + body: dict[str, Any] = {"jsonrpc": "2.0", "id": id, "method": method} if params is not None: body["params"] = params @@ -245,8 +248,8 @@ def call( def batch( self, network: str, - requests: List[Dict[str, Any]], - ) -> List[RpcResponse]: + requests: list[dict[str, Any]], + ) -> list[RpcResponse]: """ Make a JSON-RPC 2.0 batch call. Priced per element ($0.002 x N). @@ -292,7 +295,7 @@ def _to_response(data: Any, headers: httpx.Headers) -> RpcResponse: ) def _request_with_payment( - self, network: str, body: Union[Dict[str, Any], List[Dict[str, Any]]] + self, network: str, body: dict[str, Any] | list[dict[str, Any]] ) -> tuple: """POST the JSON-RPC body with automatic x402 payment handling.""" endpoint = f"/v1/rpc/{network}" @@ -324,7 +327,7 @@ def _handle_payment_and_retry( self, url: str, endpoint: str, - body: Union[Dict[str, Any], List[Dict[str, Any]]], + body: dict[str, Any] | list[dict[str, Any]], response: httpx.Response, ) -> tuple: """Handle 402 response: parse requirements, sign payment, retry.""" diff --git a/blockrun_llm/search.py b/blockrun_llm/search.py index 0e4882d..d9a8055 100644 --- a/blockrun_llm/search.py +++ b/blockrun_llm/search.py @@ -19,21 +19,24 @@ in the PAYMENT-SIGNATURE header. """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any, List, Literal +from typing import Any, Literal + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from typing_extensions import Self -from .types import SearchResult, APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .tx_log import paid_request_error_prefix +from .types import APIError, PaymentError, SearchResult from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, ) -from .tx_log import paid_request_error_prefix - +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -55,8 +58,8 @@ class SearchClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, ): from .wallet import load_wallet @@ -89,10 +92,10 @@ def search( self, query: str, *, - sources: Optional[List[SearchSourceLiteral]] = None, + sources: list[SearchSourceLiteral] | None = None, max_results: int = DEFAULT_MAX_RESULTS, - from_date: Optional[str] = None, - to_date: Optional[str] = None, + from_date: str | None = None, + to_date: str | None = None, ) -> SearchResult: """ Run a live search query. @@ -111,7 +114,7 @@ def search( if not 1 <= max_results <= 50: raise ValueError("max_results must be between 1 and 50") - body: Dict[str, Any] = { + body: dict[str, Any] = { "query": query, "max_results": max_results, } @@ -125,7 +128,7 @@ def search( data = self._request_with_payment("/v1/search", body) return SearchResult(**data) - def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> Dict[str, Any]: + def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]: url = f"{self.api_url}{endpoint}" response = self._client.post(url, json=body, headers={"Content-Type": "application/json"}) if response.status_code == 402: @@ -145,9 +148,9 @@ def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> Dict[str def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: payment_header: Any = response.headers.get("payment-required") if not payment_header: try: @@ -208,7 +211,7 @@ def get_wallet_address(self) -> str: def close(self) -> None: self._client.close() - def __enter__(self) -> "SearchClient": + def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: diff --git a/blockrun_llm/solana_client.py b/blockrun_llm/solana_client.py index 5dbaabd..9242313 100644 --- a/blockrun_llm/solana_client.py +++ b/blockrun_llm/solana_client.py @@ -23,45 +23,52 @@ import re import sys import threading -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union +from collections.abc import Iterator +from typing import Any import httpx +from typing_extensions import Self +# Shared with the Base client: signing is settlement on either chain, so the +# "already paid, do not retry on another model" tag has to mean the same thing +# in both fallback chains. client.py does not import this module, so there is +# no cycle. +from .client import _SETTLED_ATTR, _enforce_spend_limits, _mark_settled +from .price import Category, Market, Resolution, Session +from .realface import _GROUP_ID_RE +from .solana_wallet import get_solana_public_key +from .tx_log import ( + TransactionLogger, + _resolve_log_dir, + decode_settlement_header, + paid_request_error_prefix, + read_settlement_header, +) from .types import ( + APIError, ChatCompletionChunk, ChatResponse, ImageResponse, - VideoResponse, MusicResponse, - SpeechResponse, + PaymentError, PortraitEnrollment, PortraitList, - RealFaceInit, - RealFaceStatus, + PriceHistoryResponse, + PricePoint, RealFaceEnrollment, + RealFaceInit, RealFaceList, - PricePoint, - PriceHistoryResponse, - SymbolListResponse, + RealFaceStatus, RpcResponse, - APIError, - PaymentError, SearchResult, - stream_choice_content, - stream_choice_finish_reason, + SpeechResponse, + SymbolListResponse, + VideoResponse, chunk_meta, chunk_usage_dict, + stream_choice_content, + stream_choice_finish_reason, ) -from .solana_wallet import get_solana_public_key -from .tx_log import ( - TransactionLogger, - decode_settlement_header, - paid_request_error_prefix, - read_settlement_header, - _resolve_log_dir, -) -from .price import Category, Market, Resolution, Session -from .realface import _GROUP_ID_RE from .validation import ( build_payment_rejected_error, resolve_spend_limit, @@ -72,17 +79,11 @@ validate_video_input_type, ) -# Shared with the Base client: signing is settlement on either chain, so the -# "already paid, do not retry on another model" tag has to mean the same thing -# in both fallback chains. client.py does not import this module, so there is -# no cycle. -from .client import _SETTLED_ATTR, _enforce_spend_limits, _mark_settled - try: from x402 import x402ClientSync + from x402.http.utils import decode_payment_required_header, encode_payment_signature_header from x402.mechanisms.svm import KeypairSigner from x402.mechanisms.svm.exact.register import register_exact_svm_client - from x402.http.utils import decode_payment_required_header, encode_payment_signature_header _HAS_X402 = True except ImportError: @@ -237,9 +238,9 @@ def _get_user_agent() -> str: def _resolve_rpc_config( - rpc_url: Optional[str], - rpc_headers: Optional[Dict[str, str]], -) -> Tuple[str, Optional[Dict[str, str]]]: + rpc_url: str | None, + rpc_headers: dict[str, str] | None, +) -> tuple[str, dict[str, str] | None]: """Resolve the effective RPC URL + headers from explicit args, env vars, or defaults — in that priority order. @@ -273,7 +274,7 @@ def _resolve_rpc_config( resolved_url = rpc_url or os.environ.get("SOLANA_RPC_URL") or DEFAULT_SOLANA_RPC_URL - resolved_headers: Optional[Dict[str, str]] = None + resolved_headers: dict[str, str] | None = None if rpc_headers is not None: resolved_headers = dict(rpc_headers) else: @@ -296,7 +297,7 @@ def _register_svm_with_headers( x402_client: Any, signer: Any, rpc_url: str, - rpc_headers: Optional[Dict[str, str]], + rpc_headers: dict[str, str] | None, ) -> None: """Register the SVM exact scheme on an x402 client, with optional extra HTTP headers for the underlying Solana RPC. @@ -318,8 +319,8 @@ def _register_svm_with_headers( # header-less default. from solana.rpc.api import Client as SolanaClient from x402.mechanisms.svm.exact.client import ExactSvmScheme - from x402.mechanisms.svm.exact.v1.client import ExactSvmSchemeV1 from x402.mechanisms.svm.exact.register import V1_NETWORKS + from x402.mechanisms.svm.exact.v1.client import ExactSvmSchemeV1 pre_client = SolanaClient(rpc_url, extra_headers=rpc_headers) @@ -369,9 +370,7 @@ def _should_fallback_solana(exc: Exception) -> bool: return True if isinstance(exc, httpx.NetworkError): return True - if isinstance(exc, APIError) and exc.status_code in (502, 503, 504, 522, 524): - return True - return False + return bool(isinstance(exc, APIError) and exc.status_code in (502, 503, 504, 522, 524)) # Characters safe to interpolate into a single URL path segment. network / @@ -391,7 +390,7 @@ def _safe_path_segment(value: str, field: str) -> str: return value -def _receipt_from_headers(headers: Any) -> Optional[str]: +def _receipt_from_headers(headers: Any) -> str | None: """Pull the x402 settlement tx hash from a paid response's headers.""" if headers is None: return None @@ -459,16 +458,16 @@ class SolanaLLMClient: def __init__( self, - private_key: Optional[str] = None, + private_key: str | None = None, api_url: str = SOLANA_API_URL, - rpc_url: Optional[str] = None, + rpc_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, image_timeout: float = DEFAULT_IMAGE_TIMEOUT, search_timeout: float = DEFAULT_SEARCH_TIMEOUT, - rpc_headers: Optional[Dict[str, str]] = None, - transaction_log: Union[bool, str, "os.PathLike[str]", None] = None, - max_cost_per_call: Optional[float] = None, - max_session_cost: Optional[float] = None, + rpc_headers: dict[str, str] | None = None, + transaction_log: bool | str | os.PathLike[str] | None = None, + max_cost_per_call: float | None = None, + max_session_cost: float | None = None, ) -> None: """Initialise the Solana client. @@ -539,18 +538,18 @@ def __init__( self._max_session_cost = resolve_spend_limit(max_session_cost, "BLOCKRUN_MAX_SESSION_COST") self._session_calls = 0 self._last_call_cost: float = 0.0 - self._address: Optional[str] = None + self._address: str | None = None log_dir = _resolve_log_dir(transaction_log) - self._tx_logger: Optional[TransactionLogger] = ( + self._tx_logger: TransactionLogger | None = ( TransactionLogger(log_dir) if log_dir is not None else None ) - self._last_settlement: Optional[Dict[str, Any]] = None + self._last_settlement: dict[str, Any] | None = None # Response headers from the most recent raw paid POST — consumed by # rpc()/music()/speech() to surface the settlement receipt + gateway # metadata the shared JSON-only helper would otherwise drop. Read it # immediately after the helper returns (no intervening await). - self._last_raw_headers: Optional[httpx.Headers] = None + self._last_raw_headers: httpx.Headers | None = None # Initialize x402 SDK client for Solana payment signing. self._x402_client = x402ClientSync() @@ -584,7 +583,7 @@ def _sign_payment(self, payment_required: Any) -> Any: with self._payment_lock: return self._x402_client.create_payment_payload(payment_required) - def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, Any]]: + def _capture_settlement(self, response: httpx.Response) -> dict[str, Any] | None: """Decode the x402 settlement header on a Solana paid response. Solana facilitators put the on-chain transaction signature in the @@ -623,10 +622,10 @@ def get_balance(self) -> float: return get_solana_usdc_balance(self.get_wallet_address(), rpc_url=self._rpc_url) - def get_spending(self) -> Dict[str, Any]: + def get_spending(self) -> dict[str, Any]: return {"total_usd": self._session_total_usd, "calls": self._session_calls} - def _billing_meta(self) -> Dict[str, Optional[str]]: + def _billing_meta(self) -> dict[str, str | None]: """Billing metadata for cost-log entries.""" return { "wallet": self.get_wallet_address(), @@ -637,7 +636,7 @@ def _billing_meta(self) -> Dict[str, Optional[str]]: def _log_transaction( self, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], response: Any, cost_usd: float, ) -> None: @@ -670,16 +669,16 @@ def chat( self, model: str, prompt: str, - system: Optional[str] = None, + system: str | None = None, max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: Optional[float] = None, + temperature: float | None = None, search: bool = False, - timeout: Optional[float] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, + timeout: float | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, ) -> str: """Simple 1-line chat.""" - messages: List[Dict[str, str]] = [] + messages: list[dict[str, str]] = [] if system: messages.append({"role": "system", "content": system}) messages.append({"role": "user", "content": prompt}) @@ -698,17 +697,17 @@ def chat( def chat_completion( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: Optional[float] = None, - top_p: Optional[float] = None, + temperature: float | None = None, + top_p: float | None = None, search: bool = False, - search_parameters: Optional[Dict[str, Any]] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - timeout: Optional[float] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, + search_parameters: dict[str, Any] | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + timeout: float | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, ) -> ChatResponse: """Full chat completion (OpenAI-compatible). @@ -722,7 +721,7 @@ def chat_completion( large ``max_tokens`` runs against slow models. """ validate_max_tokens(max_tokens) - body: Dict[str, Any] = {"model": model, "messages": messages, "max_tokens": max_tokens} + body: dict[str, Any] = {"model": model, "messages": messages, "max_tokens": max_tokens} if temperature is not None: body["temperature"] = temperature if top_p is not None: @@ -745,13 +744,13 @@ def close(self) -> None: """Close the HTTP client.""" self._client.close() - def list_models(self) -> List[Dict[str, Any]]: + def list_models(self) -> list[dict[str, Any]]: resp = self._client.get(f"{self._api_url}/v1/models") resp.raise_for_status() return resp.json().get("data", []) @staticmethod - def _extract_payment_header(response: httpx.Response) -> Optional[str]: + def _extract_payment_header(response: httpx.Response) -> str | None: """Extract x402 payment header from a 402 response (header or body).""" payment_header = response.headers.get("payment-required") if not payment_header: @@ -787,19 +786,19 @@ def _extract_payment_header(response: httpx.Response) -> Optional[str]: def chat_completion_stream( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], *, max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: Optional[float] = None, - top_p: Optional[float] = None, + temperature: float | None = None, + top_p: float | None = None, search: bool = False, - search_parameters: Optional[Dict[str, Any]] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, - timeout: Optional[float] = None, + search_parameters: dict[str, Any] | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, + timeout: float | None = None, ) -> Iterator[ChatCompletionChunk]: """ Stream a chat completion via Server-Sent Events, paid in Solana USDC @@ -823,7 +822,7 @@ def chat_completion_stream( stream mode (HTTP 400). Codex / GPT-5.4-Pro also can't stream. """ validate_max_tokens(max_tokens) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "messages": messages, "stream": True, @@ -847,7 +846,7 @@ def chat_completion_stream( body["stop"] = stop attempts = [model, *(fallback_models or [])] - last_exc: Optional[Exception] = None + last_exc: Exception | None = None for i, attempt_model in enumerate(attempts): body["model"] = attempt_model @@ -876,8 +875,8 @@ def chat_completion_stream( def _stream_with_payment( self, endpoint: str, - body: Dict[str, Any], - timeout: Optional[float] = None, + body: dict[str, Any], + timeout: float | None = None, ) -> Iterator[ChatCompletionChunk]: """Whole-request payment-retry wrapper around :meth:`_stream_once`. @@ -911,8 +910,8 @@ def _stream_with_payment( def _stream_once( self, endpoint: str, - body: Dict[str, Any], - timeout: Optional[float] = None, + body: dict[str, Any], + timeout: float | None = None, ) -> Iterator[ChatCompletionChunk]: """402 → sign (SVM) → retry → SSE iter. Same shape as the Base :meth:`LLMClient._stream_with_payment`; differs only in the @@ -924,7 +923,7 @@ def _stream_once( backoffs = self._STREAM_5XX_BACKOFFS # ----- Phase 1: probe (no payment header) ----- - payment_headers: Optional[Dict[str, str]] = None + payment_headers: dict[str, str] | None = None cost_usd = 0.0 for attempt in range(len(backoffs) + 1): @@ -983,7 +982,7 @@ def _stream_once( def _iter_and_archive( self, response: httpx.Response, - body: Dict[str, Any], + body: dict[str, Any], cost_usd: float, ) -> Iterator[ChatCompletionChunk]: """Yield SSE chunks; on stream completion, archive the assembled @@ -992,12 +991,12 @@ def _iter_and_archive( in the same audit trail as non-stream paid calls. ``cost_usd == 0`` skips the archive (free models / unauth probe).""" - assembled_id: Optional[str] = None - assembled_model: Optional[str] = None + assembled_id: str | None = None + assembled_model: str | None = None assembled_created: int = 0 - content_parts: List[str] = [] - finish_reason: Optional[str] = None - usage_dict: Optional[Dict[str, Any]] = None + content_parts: list[str] = [] + finish_reason: str | None = None + usage_dict: dict[str, Any] | None = None for chunk in self._iter_sse_chunks(response): if chunk.choices: @@ -1024,7 +1023,7 @@ def _iter_and_archive( if cost_usd > 0: from .cache import save_to_cache - response_data: Dict[str, Any] = { + response_data: dict[str, Any] = { "id": assembled_id or "stream", "object": "chat.completion", "created": assembled_created or int(__import__("time").time()), @@ -1077,7 +1076,7 @@ def _iter_sse_chunks(response: httpx.Response) -> Iterator[ChatCompletionChunk]: def _sign_payment_from_response( self, response: httpx.Response, - ) -> Tuple[Dict[str, str], float]: + ) -> tuple[dict[str, str], float]: """Extract a 402 response's payment requirements, sign locally with the SVM x402 client, return ``(headers_with_PAYMENT_SIGNATURE, cost_usd)``. Mirrors the inline logic in @@ -1121,7 +1120,7 @@ def _raise_stream_error(response: httpx.Response, *, after_payment: bool) -> Non ) def _request_with_payment( - self, endpoint: str, body: Dict[str, Any], timeout: Optional[float] = None + self, endpoint: str, body: dict[str, Any], timeout: float | None = None ) -> ChatResponse: """Whole-request payment-retry wrapper around :meth:`_request_once`. @@ -1148,7 +1147,7 @@ def _request_with_payment( ) def _request_once( - self, endpoint: str, body: Dict[str, Any], timeout: Optional[float] = None + self, endpoint: str, body: dict[str, Any], timeout: float | None = None ) -> ChatResponse: url = f"{self._api_url}{endpoint}" headers = {"Content-Type": "application/json", "User-Agent": _get_user_agent()} @@ -1188,9 +1187,9 @@ def _request_once( def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> ChatResponse: eff_timeout = timeout if timeout is not None else self._timeout payment_header = self._extract_payment_header(response) @@ -1260,8 +1259,8 @@ def _handle_payment_and_retry( return ChatResponse(**response_data) def _request_with_payment_raw( - self, endpoint: str, body: Dict[str, Any], timeout: Optional[float] = None - ) -> Dict[str, Any]: + self, endpoint: str, body: dict[str, Any], timeout: float | None = None + ) -> dict[str, Any]: """Make a request with Solana x402 payment, returning raw JSON.""" from .cache import get_cached, save_to_cache @@ -1323,10 +1322,10 @@ def _request_with_payment_raw( def _handle_payment_and_retry_raw( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - timeout: Optional[float] = None, - ) -> Dict[str, Any]: + timeout: float | None = None, + ) -> dict[str, Any]: """Handle 402 for raw endpoints with Solana payment.""" eff_timeout = timeout if timeout is not None else self._timeout payment_header = self._extract_payment_header(response) @@ -1386,9 +1385,9 @@ def _handle_payment_and_retry_raw( def _get_with_payment_raw( self, endpoint: str, - params: Optional[Dict[str, Any]] = None, - timeout: Optional[float] = None, - ) -> Dict[str, Any]: + params: dict[str, Any] | None = None, + timeout: float | None = None, + ) -> dict[str, Any]: """GET with Solana x402 payment, returning raw JSON.""" from .cache import get_cached, save_to_cache @@ -1437,10 +1436,10 @@ def _get_with_payment_raw( def _handle_get_payment_and_retry( self, url: str, - params: Optional[Dict[str, Any]], + params: dict[str, Any] | None, response: httpx.Response, - timeout: Optional[float] = None, - ) -> Dict[str, Any]: + timeout: float | None = None, + ) -> dict[str, Any]: """Handle 402 for GET endpoints with Solana payment.""" eff_timeout = timeout if timeout is not None else self._timeout payment_header = self._extract_payment_header(response) @@ -1501,8 +1500,8 @@ def _absolute_url(self, url: str) -> str: configured ``api_url`` already includes the trailing ``/api`` so we strip it once to avoid ``/api/api/...``. """ - base = self._api_url[: -len("/api")] if self._api_url.endswith("/api") else self._api_url - if url.startswith("http://") or url.startswith("https://"): + base = self._api_url.removesuffix("/api") + if url.startswith(("http://", "https://")): # The poll loop sends (and re-signs) the wallet's PAYMENT-SIGNATURE # against this URL, so an absolute poll_url is pinned to the API # host+scheme — a gateway response pointing it elsewhere would leak @@ -1522,14 +1521,14 @@ def _absolute_url(self, url: str) -> str: def _request_image_with_payment( self, endpoint: str, - body: Dict[str, Any], - timeout: Optional[float] = None, + body: dict[str, Any], + timeout: float | None = None, *, - poll_budget_seconds: Optional[float] = None, - poll_interval_seconds: Optional[float] = None, + poll_budget_seconds: float | None = None, + poll_interval_seconds: float | None = None, max_resigns: int = 0, label: str = "Image", - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Sign + submit + poll wrapper for async media generation. Shared by :meth:`image` (5-min budget, no mid-poll re-signing needed) @@ -1815,8 +1814,8 @@ def image( model: str = "google/nano-banana", size: str = "1024x1024", n: int = 1, - quality: Optional[str] = None, - timeout: Optional[float] = None, + quality: str | None = None, + timeout: float | None = None, ) -> ImageResponse: """Generate an image from a text prompt (Solana payment). @@ -1842,7 +1841,7 @@ def image( Raises: ValueError: If ``quality`` is not one of the four accepted values. """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "prompt": prompt, "size": size, @@ -1857,14 +1856,14 @@ def image( def image_edit( self, prompt: str, - image: Union[str, List[str]], + image: str | list[str], *, model: str = "openai/gpt-image-2", - mask: Optional[str] = None, + mask: str | None = None, size: str = "1024x1024", n: int = 1, - quality: Optional[str] = None, - timeout: Optional[float] = None, + quality: str | None = None, + timeout: float | None = None, ) -> ImageResponse: """Edit an image using img2img (Solana payment). ``image`` may be a single data URI or a list of 1-4 data URIs for multi-image fusion @@ -1880,7 +1879,7 @@ def image_edit( Raises: ValueError: If ``quality`` is not one of the four accepted values. """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "prompt": prompt, "image": image, @@ -1904,21 +1903,21 @@ def video( self, prompt: str, *, - model: Optional[str] = None, - image_url: Optional[str] = None, - last_frame_url: Optional[str] = None, - reference_image_urls: Optional[List[str]] = None, - real_face_asset_id: Optional[str] = None, - duration_seconds: Optional[int] = None, - aspect_ratio: Optional[str] = None, - resolution: Optional[str] = None, - generate_audio: Optional[bool] = None, - seed: Optional[int] = None, - watermark: Optional[bool] = None, - return_last_frame: Optional[bool] = None, - input_type: Optional[str] = None, - budget_seconds: Optional[float] = None, - timeout: Optional[float] = None, + model: str | None = None, + image_url: str | None = None, + last_frame_url: str | None = None, + reference_image_urls: list[str] | None = None, + real_face_asset_id: str | None = None, + duration_seconds: int | None = None, + aspect_ratio: str | None = None, + resolution: str | None = None, + generate_audio: bool | None = None, + seed: int | None = None, + watermark: bool | None = None, + return_last_frame: bool | None = None, + input_type: str | None = None, + budget_seconds: float | None = None, + timeout: float | None = None, ) -> VideoResponse: """Generate a video clip from a text prompt (Solana payment). @@ -1967,11 +1966,11 @@ def video( def video_from_content( self, - content: List[Dict[str, Any]], + content: list[dict[str, Any]], *, - model: Optional[str] = None, - budget_seconds: Optional[float] = None, - timeout: Optional[float] = None, + model: str | None = None, + budget_seconds: float | None = None, + timeout: float | None = None, **options: Any, ) -> VideoResponse: """Generate a video from a Seedance ``content[]`` body (Solana payment). @@ -1982,7 +1981,7 @@ def video_from_content( """ if not content: raise ValueError("content must be a non-empty list of Seedance content items.") - body: Dict[str, Any] = {"content": content, **options} + body: dict[str, Any] = {"content": content, **options} if model is not None: body["model"] = model data = self._request_image_with_payment( @@ -2006,10 +2005,10 @@ def music( self, prompt: str, *, - model: Optional[str] = None, + model: str | None = None, instrumental: bool = True, - lyrics: Optional[str] = None, - timeout: Optional[float] = None, + lyrics: str | None = None, + timeout: float | None = None, ) -> MusicResponse: """Generate a music track from a text prompt (Solana payment). @@ -2018,7 +2017,7 @@ def music( """ if instrumental and lyrics and lyrics.strip(): raise ValueError("Cannot specify lyrics when instrumental is True") - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.MUSIC_DEFAULT_MODEL, "prompt": prompt, "instrumental": instrumental, @@ -2037,11 +2036,11 @@ def speech( self, input: str, *, - model: Optional[str] = None, - voice: Optional[str] = None, - response_format: Optional[str] = None, - speed: Optional[float] = None, - timeout: Optional[float] = None, + model: str | None = None, + voice: str | None = None, + response_format: str | None = None, + speed: float | None = None, + timeout: float | None = None, ) -> SpeechResponse: """Synthesize speech from text (Solana payment). @@ -2049,7 +2048,7 @@ def speech( with character count. Default model ``elevenlabs/flash-v2.5``, default voice ``sarah``. """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.SPEECH_DEFAULT_MODEL, "input": input, } @@ -2067,15 +2066,15 @@ def sound_effect( self, text: str, *, - model: Optional[str] = None, - duration_seconds: Optional[float] = None, - prompt_influence: Optional[float] = None, - response_format: Optional[str] = None, - timeout: Optional[float] = None, + model: str | None = None, + duration_seconds: float | None = None, + prompt_influence: float | None = None, + response_format: str | None = None, + timeout: float | None = None, ) -> SpeechResponse: """Generate a cinematic sound effect from a text prompt (Solana payment). Mirrors ``SpeechClient.sound_effect``. Flat $0.05, <=22s.""" - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.SOUNDFX_DEFAULT_MODEL, "text": text, } @@ -2089,7 +2088,7 @@ def sound_effect( self._attach_receipt(data) return SpeechResponse(**data) - def list_voices(self) -> List[Dict[str, Any]]: + def list_voices(self) -> list[dict[str, Any]]: """List available speech voices (free).""" url = f"{self._api_url}/v1/audio/voices" resp = self._client.get( @@ -2123,11 +2122,11 @@ def portrait_enroll(self, name: str, image_url: str) -> PortraitEnrollment: raise ValueError(f"name must be 64 chars or fewer (got {len(name)})") if not image_url or not image_url.lower().startswith(("https://", "http://")): raise ValueError("image_url must be an http(s) URL") - body: Dict[str, Any] = {"name": name, "image_url": image_url} + body: dict[str, Any] = {"name": name, "image_url": image_url} data = self._request_with_payment_raw("/v1/portrait/enroll", body) return PortraitEnrollment(**data) - def list_portraits(self, wallet_address: Optional[str] = None) -> PortraitList: + def list_portraits(self, wallet_address: str | None = None) -> PortraitList: """List Virtual Portraits enrolled by a wallet (free, rate-limited).""" addr = _safe_path_segment(wallet_address or self.get_wallet_address(), "wallet_address") url = f"{self._api_url}/v1/wallet/{addr}/portraits" @@ -2150,7 +2149,7 @@ def list_portraits(self, wallet_address: Optional[str] = None) -> PortraitList: # RealFace enrollment (Solana payment) # ------------------------------------------------------------------ - def realface_init(self, name: str, group_id: Optional[str] = None) -> RealFaceInit: + def realface_init(self, name: str, group_id: str | None = None) -> RealFaceInit: """Start/refresh a RealFace enrollment (free, rate-limited). Returns the ``group_id`` and an ``h5_link`` (render as a QR for the real person's phone liveness check).""" @@ -2160,7 +2159,7 @@ def realface_init(self, name: str, group_id: Optional[str] = None) -> RealFaceIn raise ValueError(f"name must be 64 chars or fewer (got {len(name)})") if group_id is not None and not _GROUP_ID_RE.match(group_id): raise ValueError("group_id must look like 'legacy_rf_'") - body: Dict[str, Any] = {"name": name} + body: dict[str, Any] = {"name": name} if group_id: body["groupId"] = group_id url = f"{self._api_url}/v1/realface/init" @@ -2238,11 +2237,11 @@ def realface_enroll(self, name: str, image_url: str, group_id: str) -> RealFaceE raise ValueError("image_url must be an http(s) URL") if not group_id or not _GROUP_ID_RE.match(group_id): raise ValueError("group_id must look like 'legacy_rf_'") - body: Dict[str, Any] = {"name": name, "image_url": image_url, "group_id": group_id} + body: dict[str, Any] = {"name": name, "image_url": image_url, "group_id": group_id} data = self._request_with_payment_raw("/v1/realface/enroll", body) return RealFaceEnrollment(**data) - def list_realfaces(self, wallet_address: Optional[str] = None) -> RealFaceList: + def list_realfaces(self, wallet_address: str | None = None) -> RealFaceList: """List RealFace assets enrolled by a wallet (free, rate-limited).""" addr = _safe_path_segment(wallet_address or self.get_wallet_address(), "wallet_address") url = f"{self._api_url}/v1/wallet/{addr}/realfaces" @@ -2269,20 +2268,20 @@ def list_realfaces(self, wallet_address: Optional[str] = None) -> RealFaceList: def _build_video_body( prompt: str, *, - model: Optional[str], - image_url: Optional[str], - last_frame_url: Optional[str], - reference_image_urls: Optional[List[str]], - real_face_asset_id: Optional[str], - duration_seconds: Optional[int], - aspect_ratio: Optional[str], - resolution: Optional[str], - generate_audio: Optional[bool], - seed: Optional[int], - watermark: Optional[bool], - return_last_frame: Optional[bool], - input_type: Optional[str], - ) -> Dict[str, Any]: + model: str | None, + image_url: str | None, + last_frame_url: str | None, + reference_image_urls: list[str] | None, + real_face_asset_id: str | None, + duration_seconds: int | None, + aspect_ratio: str | None, + resolution: str | None, + generate_audio: bool | None, + seed: int | None, + watermark: bool | None, + return_last_frame: bool | None, + input_type: str | None, + ) -> dict[str, Any]: """Validate video kwargs and build the request body. Shared by the sync and async ``video()`` so their validation and payload never drift. @@ -2317,7 +2316,7 @@ def _build_video_body( ) validate_video_input_type(input_type) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or SolanaLLMClient.VIDEO_DEFAULT_MODEL, "prompt": prompt, } @@ -2349,7 +2348,7 @@ def _build_video_body( @staticmethod def _rpc_response( - data: Any, headers: Optional[httpx.Headers], fallback_network: str + data: Any, headers: httpx.Headers | None, fallback_network: str ) -> RpcResponse: """Build an RpcResponse, surfacing gateway metadata from the paid response headers (canonical network, cache hit, settlement tx) exactly @@ -2369,7 +2368,7 @@ def _rpc_response( @staticmethod def _price_category_path( - category: str, market: Optional[str], kind: str, symbol: Optional[str] + category: str, market: str | None, kind: str, symbol: str | None ) -> str: if category == "stocks": if not market: @@ -2388,13 +2387,13 @@ def price( category: Category, symbol: str, *, - market: Optional[Market] = None, - session: Optional[Session] = None, + market: Market | None = None, + session: Session | None = None, ) -> PricePoint: """Fetch a realtime Pyth price quote (Solana payment for paid categories). ``market`` is required for ``category='stocks'``.""" endpoint = self._price_category_path(category, market, "price", symbol) - params: Dict[str, Any] = {} + params: dict[str, Any] = {} if session is not None: params["session"] = session data = self._get_with_payment_raw( @@ -2421,12 +2420,12 @@ def price_history( resolution: Resolution = "D", from_ts: int, to_ts: int, - market: Optional[Market] = None, - session: Optional[Session] = None, + market: Market | None = None, + session: Session | None = None, ) -> PriceHistoryResponse: """Fetch OHLC bars between two Unix timestamps (seconds).""" endpoint = self._price_category_path(category, market, "history", symbol) - params: Dict[str, Any] = {"resolution": resolution, "from": from_ts, "to": to_ts} + params: dict[str, Any] = {"resolution": resolution, "from": from_ts, "to": to_ts} if session is not None: params["session"] = session data = self._get_with_payment_raw(endpoint, params=params, timeout=DEFAULT_FAST_TIMEOUT) @@ -2441,13 +2440,13 @@ def list_symbols( self, category: Category, *, - q: Optional[str] = None, + q: str | None = None, limit: int = 100, - market: Optional[Market] = None, + market: Market | None = None, ) -> SymbolListResponse: """List available symbols in a Pyth category (free discovery).""" endpoint = self._price_category_path(category, market, "list", None) - params: Dict[str, Any] = {"limit": limit} + params: dict[str, Any] = {"limit": limit} if q: params["q"] = q data = self._get_with_payment_raw(endpoint, params=params, timeout=DEFAULT_FAST_TIMEOUT) @@ -2467,9 +2466,9 @@ def rpc( self, network: str, method: str, - params: Optional[List[Any]] = None, + params: list[Any] | None = None, *, - id: Union[str, int] = 1, + id: str | int = 1, ) -> RpcResponse: """Make a single JSON-RPC 2.0 call (Solana payment, flat $0.002). @@ -2477,18 +2476,18 @@ def rpc( (``eth``, ``sol``, ``base`` …); the gateway resolves it. """ _safe_path_segment(network, "network") - body: Dict[str, Any] = {"jsonrpc": "2.0", "id": id, "method": method} + body: dict[str, Any] = {"jsonrpc": "2.0", "id": id, "method": method} if params is not None: body["params"] = params data = self._request_with_payment_raw(f"/v1/rpc/{network}", body) return self._rpc_response(data, self._last_raw_headers, network) - def rpc_batch(self, network: str, requests: List[Dict[str, Any]]) -> List[RpcResponse]: + def rpc_batch(self, network: str, requests: list[dict[str, Any]]) -> list[RpcResponse]: """Make a JSON-RPC 2.0 batch call (Solana payment, $0.002 x N).""" if not requests: raise ValueError("batch requires at least one request") _safe_path_segment(network, "network") - body: List[Dict[str, Any]] = [] + body: list[dict[str, Any]] = [] for i, req in enumerate(requests): if "method" not in req: raise ValueError(f"batch request {i} is missing 'method'") @@ -2503,18 +2502,18 @@ def search( self, query: str, *, - sources: Optional[List[str]] = None, + sources: list[str] | None = None, max_results: int = 10, - from_date: Optional[str] = None, - to_date: Optional[str] = None, - timeout: Optional[float] = None, + from_date: str | None = None, + to_date: str | None = None, + timeout: float | None = None, ) -> SearchResult: """Standalone search (Solana payment). ``timeout`` overrides the per-call HTTP timeout (defaults to ``DEFAULT_SEARCH_TIMEOUT`` — deep web/X tool-use can run minutes). """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "query": query, "max_results": max_results, } @@ -2531,87 +2530,87 @@ def search( # ── Prediction Markets (Powered by Predexon) ──────────────────────────── - def pm(self, path: str, **params: Any) -> Dict[str, Any]: + def pm(self, path: str, **params: Any) -> dict[str, Any]: """Query Predexon prediction market data (GET, Solana payment). Powered by Predexon.""" return self._get_with_payment_raw(f"/v1/pm/{path}", params or None) - def pm_query(self, path: str, query: Dict[str, Any]) -> Dict[str, Any]: + def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: """Structured query for Predexon data (POST, Solana payment). Powered by Predexon.""" return self._request_with_payment_raw(f"/v1/pm/{path}", query) - def pm_markets(self, **params: Any) -> Dict[str, Any]: + def pm_markets(self, **params: Any) -> dict[str, Any]: """List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call).""" return self.pm("markets", **params) - def pm_listings(self, **params: Any) -> Dict[str, Any]: + def pm_listings(self, **params: Any) -> dict[str, Any]: """List venue-native executable listings (Predexon v2). Tier 1 ($0.001/call).""" return self.pm("markets/listings", **params) - def pm_outcome(self, predexon_id: str) -> Dict[str, Any]: + def pm_outcome(self, predexon_id: str) -> dict[str, Any]: """Resolve a canonical Predexon outcome ID (Predexon v2). Tier 1 ($0.001/call).""" return self.pm(f"outcomes/{predexon_id}") - def pm_polymarket_markets(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call).""" return self.pm("polymarket/markets", **params) - def pm_polymarket_events(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_events(self, **params: Any) -> dict[str, Any]: """List Polymarket events (Predexon v2). Tier 1 ($0.001/call).""" return self.pm("polymarket/events", **params) - def pm_polymarket_markets_keyset(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_markets_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket markets with cursor-based keyset pagination. Tier 1 ($0.001/call).""" return self.pm("polymarket/markets/keyset", **params) - def pm_polymarket_events_keyset(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_events_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket events with cursor-based keyset pagination. Tier 1 ($0.001/call).""" return self.pm("polymarket/events/keyset", **params) - def pm_polymarket_positions(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_positions(self, **params: Any) -> dict[str, Any]: """Polymarket open positions (per-wallet, market-level PnL). Tier 1 ($0.001/call).""" return self.pm("polymarket/positions", **params) - def pm_polymarket_trades(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_trades(self, **params: Any) -> dict[str, Any]: """Recent Polymarket trades. Tier 1 ($0.001/call).""" return self.pm("polymarket/trades", **params) - def pm_polymarket_leaderboard(self, **params: Any) -> Dict[str, Any]: + def pm_polymarket_leaderboard(self, **params: Any) -> dict[str, Any]: """Polymarket trader leaderboard. Tier 1 ($0.001/call).""" return self.pm("polymarket/leaderboard", **params) - def pm_kalshi_markets(self, **params: Any) -> Dict[str, Any]: + def pm_kalshi_markets(self, **params: Any) -> dict[str, Any]: """List Kalshi markets. Tier 1 ($0.001/call).""" return self.pm("kalshi/markets", **params) - def pm_limitless_markets(self, **params: Any) -> Dict[str, Any]: + def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: """List Limitless markets. Tier 1 ($0.001/call).""" return self.pm("limitless/markets", **params) - def pm_sports_categories(self) -> Dict[str, Any]: + def pm_sports_categories(self) -> dict[str, Any]: """List available sports categories. Tier 1 ($0.001/call).""" return self.pm("sports/categories") - def pm_sports_markets(self, **params: Any) -> Dict[str, Any]: + def pm_sports_markets(self, **params: Any) -> dict[str, Any]: """List sports markets grouped by game. Tier 1 ($0.001/call).""" return self.pm("sports/markets", **params) - def pm_wallet_identity(self, wallet: str) -> Dict[str, Any]: + def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: """Identity + profile for one wallet. Tier 2 ($0.005/call).""" return self.pm(f"polymarket/wallet/identity/{wallet}") - def pm_wallet_identities(self, addresses: List[str]) -> Dict[str, Any]: + def pm_wallet_identities(self, addresses: list[str]) -> dict[str, Any]: """Bulk identity for up to 200 wallet addresses. Tier 2 ($0.005/call).""" return self.pm_query("polymarket/wallet/identities", {"addresses": addresses}) - def pm_wallet_cluster(self, address: str) -> Dict[str, Any]: + def pm_wallet_cluster(self, address: str) -> dict[str, Any]: """Wallet-cluster discovery (on-chain transfers + identity proofs). Tier 2 ($0.005/call).""" return self.pm(f"polymarket/wallet/{address}/cluster") # ── Exa Web Search (Powered by Exa) ───────────────────────────────────── - def exa(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: + def exa(self, path: str, body: dict[str, Any]) -> dict[str, Any]: """Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa. Args: @@ -2624,7 +2623,7 @@ def exa(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: """ return self._request_with_payment_raw(f"/v1/exa/{path}", body, timeout=self._search_timeout) - def exa_search(self, query: str, **kwargs: Any) -> Dict[str, Any]: + def exa_search(self, query: str, **kwargs: Any) -> dict[str, Any]: """Neural and keyword web search via Exa (Solana payment, $0.01/request). Args: @@ -2639,7 +2638,7 @@ def exa_search(self, query: str, **kwargs: Any) -> Dict[str, Any]: "/v1/exa/search", {"query": query, **kwargs}, timeout=self._search_timeout ) - def exa_find_similar(self, url: str, **kwargs: Any) -> Dict[str, Any]: + def exa_find_similar(self, url: str, **kwargs: Any) -> dict[str, Any]: """Find pages semantically similar to a given URL via Exa (Solana payment, $0.01/request). Args: @@ -2654,7 +2653,7 @@ def exa_find_similar(self, url: str, **kwargs: Any) -> Dict[str, Any]: "/v1/exa/find-similar", {"url": url, **kwargs}, timeout=self._search_timeout ) - def exa_contents(self, urls: List[str], **kwargs: Any) -> Dict[str, Any]: + def exa_contents(self, urls: list[str], **kwargs: Any) -> dict[str, Any]: """Extract full text content from URLs via Exa (Solana payment, $0.002/URL). Args: @@ -2669,7 +2668,7 @@ def exa_contents(self, urls: List[str], **kwargs: Any) -> Dict[str, Any]: "/v1/exa/contents", {"urls": urls, **kwargs}, timeout=self._search_timeout ) - def exa_answer(self, query: str, **kwargs: Any) -> Dict[str, Any]: + def exa_answer(self, query: str, **kwargs: Any) -> dict[str, Any]: """AI-generated answer grounded in live web search via Exa (Solana payment, $0.01/request). Args: @@ -2686,28 +2685,28 @@ def exa_answer(self, query: str, **kwargs: Any) -> Dict[str, Any]: # ── DefiLlama (DeFi protocols / TVL / yields / prices) ────────────────── - def defi(self, path: str, **params: Any) -> Dict[str, Any]: + def defi(self, path: str, **params: Any) -> dict[str, Any]: """Query DefiLlama DeFi data (GET, Solana payment). $0.005/call ($0.001 for prices/{coins}).""" return self._get_with_payment_raw(f"/v1/defillama/{path}", params or None) - def defi_protocols(self) -> Dict[str, Any]: + def defi_protocols(self) -> dict[str, Any]: """All DeFi protocols with TVL ($0.005/call).""" return self.defi("protocols") - def defi_protocol(self, slug: str) -> Dict[str, Any]: + def defi_protocol(self, slug: str) -> dict[str, Any]: """Single protocol details + historical TVL ($0.005/call).""" return self.defi(f"protocol/{slug}") - def defi_chains(self) -> Dict[str, Any]: + def defi_chains(self) -> dict[str, Any]: """Current TVL of every chain ($0.005/call).""" return self.defi("chains") - def defi_yields(self, **params: Any) -> Dict[str, Any]: + def defi_yields(self, **params: Any) -> dict[str, Any]: """Yield pools with APY/TVL ($0.005/call).""" return self.defi("yields", **params) - def defi_prices(self, coins: Union[List[str], str]) -> Dict[str, Any]: + def defi_prices(self, coins: list[str] | str) -> dict[str, Any]: """Token price lookup ($0.001/call).""" joined = ",".join(coins) if isinstance(coins, list) else coins return self.defi(f"prices/{joined}") @@ -2719,68 +2718,68 @@ def dex( path: str, *, method: str = "GET", - body: Optional[Dict[str, Any]] = None, + body: dict[str, Any] | None = None, **params: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Query the 0x Swap / Gasless APIs (free — no x402 payment).""" endpoint = f"/v1/zerox/{path}" if method.upper() == "POST": return self._request_with_payment_raw(endpoint, body or {}) return self._get_with_payment_raw(endpoint, params or None) - def dex_price(self, **params: Any) -> Dict[str, Any]: + def dex_price(self, **params: Any) -> dict[str, Any]: """Indicative Permit2 swap price — no commitment (free).""" return self.dex("price", **params) - def dex_quote(self, **params: Any) -> Dict[str, Any]: + def dex_quote(self, **params: Any) -> dict[str, Any]: """Firm Permit2 swap quote with permit2.eip712 + tx data (free).""" return self.dex("quote", **params) - def dex_gasless_price(self, **params: Any) -> Dict[str, Any]: + def dex_gasless_price(self, **params: Any) -> dict[str, Any]: """Gasless indicative price quote (free).""" return self.dex("gasless/price", **params) - def dex_gasless_quote(self, **params: Any) -> Dict[str, Any]: + def dex_gasless_quote(self, **params: Any) -> dict[str, Any]: """Gasless firm quote — returns trade.eip712 to sign (free).""" return self.dex("gasless/quote", **params) - def dex_gasless_submit(self, body: Dict[str, Any]) -> Dict[str, Any]: + def dex_gasless_submit(self, body: dict[str, Any]) -> dict[str, Any]: """Submit a signed gasless trade; the 0x relayer pays gas (free).""" return self.dex("gasless/submit", method="POST", body=body) - def dex_gasless_status(self, trade_hash: str) -> Dict[str, Any]: + def dex_gasless_status(self, trade_hash: str) -> dict[str, Any]: """Poll a gasless trade's status by tradeHash (free).""" return self.dex(f"gasless/status/{trade_hash}") - def dex_chains(self) -> Dict[str, Any]: + def dex_chains(self) -> dict[str, Any]: """Chains where the Swap API is supported (free).""" return self.dex("swap/chains") - def dex_gasless_chains(self) -> Dict[str, Any]: + def dex_gasless_chains(self) -> dict[str, Any]: """Chains where the Gasless API is supported (free).""" return self.dex("gasless/chains") # ── Modal Sandbox (pay-per-call cloud compute) ─────────────────────────── - def modal(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def modal(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]: """Call the Modal sandbox compute API (POST, Solana payment).""" return self._request_with_payment_raw(f"/v1/modal/{path}", body or {}) - def modal_sandbox_create(self, **body: Any) -> Dict[str, Any]: + def modal_sandbox_create(self, **body: Any) -> dict[str, Any]: """Create a sandboxed compute environment ($0.01 CPU / $0.05 GPU).""" return self.modal("sandbox/create", body) def modal_sandbox_exec( - self, sandbox_id: str, command: List[str], **body: Any - ) -> Dict[str, Any]: + self, sandbox_id: str, command: list[str], **body: Any + ) -> dict[str, Any]: """Execute a command in a sandbox; returns stdout/stderr ($0.001).""" return self.modal("sandbox/exec", {"sandbox_id": sandbox_id, "command": command, **body}) - def modal_sandbox_status(self, sandbox_id: str) -> Dict[str, Any]: + def modal_sandbox_status(self, sandbox_id: str) -> dict[str, Any]: """Check a sandbox's status ($0.001).""" return self.modal("sandbox/status", {"sandbox_id": sandbox_id}) - def modal_sandbox_terminate(self, sandbox_id: str) -> Dict[str, Any]: + def modal_sandbox_terminate(self, sandbox_id: str) -> dict[str, Any]: """Terminate a sandbox ($0.001).""" return self.modal("sandbox/terminate", {"sandbox_id": sandbox_id}) @@ -2820,16 +2819,16 @@ class AsyncSolanaLLMClient: def __init__( self, - private_key: Optional[str] = None, + private_key: str | None = None, api_url: str = SOLANA_API_URL, - rpc_url: Optional[str] = None, + rpc_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, image_timeout: float = DEFAULT_IMAGE_TIMEOUT, search_timeout: float = DEFAULT_SEARCH_TIMEOUT, - rpc_headers: Optional[Dict[str, str]] = None, - transaction_log: Union[bool, str, "os.PathLike[str]", None] = None, - max_cost_per_call: Optional[float] = None, - max_session_cost: Optional[float] = None, + rpc_headers: dict[str, str] | None = None, + transaction_log: bool | str | os.PathLike[str] | None = None, + max_cost_per_call: float | None = None, + max_session_cost: float | None = None, ) -> None: """Async mirror of :class:`SolanaLLMClient.__init__`. Same env-var fallback for ``rpc_url`` / ``rpc_headers`` — see @@ -2875,18 +2874,18 @@ def __init__( self._max_session_cost = resolve_spend_limit(max_session_cost, "BLOCKRUN_MAX_SESSION_COST") self._session_calls = 0 self._last_call_cost: float = 0.0 - self._address: Optional[str] = None + self._address: str | None = None log_dir = _resolve_log_dir(transaction_log) - self._tx_logger: Optional[TransactionLogger] = ( + self._tx_logger: TransactionLogger | None = ( TransactionLogger(log_dir) if log_dir is not None else None ) - self._last_settlement: Optional[Dict[str, Any]] = None + self._last_settlement: dict[str, Any] | None = None # Response headers from the most recent raw paid POST — consumed by # rpc()/music()/speech() to surface the settlement receipt + gateway # metadata the shared JSON-only helper would otherwise drop. Read it # immediately after the helper returns (no intervening await). - self._last_raw_headers: Optional[httpx.Headers] = None + self._last_raw_headers: httpx.Headers | None = None # Async x402 client + same SVM signer the sync class uses. from x402 import x402Client # local import to keep optional dep clean @@ -2905,7 +2904,7 @@ def __init__( # Lazily created on first sign (avoids binding asyncio.Lock to a loop at # construction time). Serializes the async signing critical section so a # shared client is safe across concurrent coroutines — see _sign_payment. - self._payment_lock: Optional[asyncio.Lock] = None + self._payment_lock: asyncio.Lock | None = None async def _sign_payment(self, payment_required: Any) -> Any: """Task-safe async wrapper around ``x402_client.create_payment_payload``. @@ -2919,7 +2918,7 @@ async def _sign_payment(self, payment_required: Any) -> Any: async with self._payment_lock: return await self._x402_client.create_payment_payload(payment_required) - def _capture_settlement(self, response: httpx.Response) -> Optional[Dict[str, Any]]: + def _capture_settlement(self, response: httpx.Response) -> dict[str, Any] | None: """Async-Solana twin of :meth:`SolanaLLMClient._capture_settlement`.""" header = read_settlement_header(response.headers) settlement = decode_settlement_header(header) @@ -2937,7 +2936,7 @@ def _attach_receipt(self, data: Any) -> None: def _log_transaction( self, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], response: Any, cost_usd: float, ) -> None: @@ -2969,10 +2968,10 @@ def _log_transaction( async def close(self) -> None: await self._client.aclose() - async def __aenter__(self) -> "AsyncSolanaLLMClient": + async def __aenter__(self) -> Self: return self - async def __aexit__(self, *_exc: Any) -> None: + async def __aexit__(self, *_exc: object) -> None: await self.close() # ------------------------------------------------------------------ @@ -2987,10 +2986,10 @@ def get_wallet_address(self) -> str: def is_solana(self) -> bool: return "sol.blockrun.ai" in self._api_url - def get_spending(self) -> Dict[str, Any]: + def get_spending(self) -> dict[str, Any]: return {"total_usd": self._session_total_usd, "calls": self._session_calls} - def _billing_meta(self) -> Dict[str, Optional[str]]: + def _billing_meta(self) -> dict[str, str | None]: return { "wallet": self.get_wallet_address(), "network": "solana-mainnet" if self.is_solana() else "solana-other", @@ -3005,15 +3004,15 @@ async def chat( self, model: str, prompt: str, - system: Optional[str] = None, + system: str | None = None, max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: Optional[float] = None, + temperature: float | None = None, search: bool = False, - timeout: Optional[float] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, + timeout: float | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, ) -> str: - messages: List[Dict[str, str]] = [] + messages: list[dict[str, str]] = [] if system: messages.append({"role": "system", "content": system}) messages.append({"role": "user", "content": prompt}) @@ -3032,20 +3031,20 @@ async def chat( async def chat_completion( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: Optional[float] = None, - top_p: Optional[float] = None, + temperature: float | None = None, + top_p: float | None = None, search: bool = False, - search_parameters: Optional[Dict[str, Any]] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - timeout: Optional[float] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, + search_parameters: dict[str, Any] | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + timeout: float | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, ) -> ChatResponse: validate_max_tokens(max_tokens) - body: Dict[str, Any] = {"model": model, "messages": messages, "max_tokens": max_tokens} + body: dict[str, Any] = {"model": model, "messages": messages, "max_tokens": max_tokens} if temperature is not None: body["temperature"] = temperature if top_p is not None: @@ -3064,7 +3063,7 @@ async def chat_completion( body["stop"] = stop return await self._request_with_payment("/v1/chat/completions", body, timeout=timeout) - async def list_models(self) -> List[Dict[str, Any]]: + async def list_models(self) -> list[dict[str, Any]]: resp = await self._client.get(f"{self._api_url}/v1/models") resp.raise_for_status() return resp.json().get("data", []) @@ -3076,25 +3075,25 @@ async def list_models(self) -> List[Dict[str, Any]]: async def chat_completion_stream( self, model: str, - messages: List[Dict[str, Any]], + messages: list[dict[str, Any]], *, max_tokens: int = DEFAULT_MAX_TOKENS, - temperature: Optional[float] = None, - top_p: Optional[float] = None, + temperature: float | None = None, + top_p: float | None = None, search: bool = False, - search_parameters: Optional[Dict[str, Any]] = None, - tools: Optional[List[Dict[str, Any]]] = None, - tool_choice: Optional[Any] = None, - response_format: Optional[Dict[str, Any]] = None, - stop: Optional[Union[str, List[str]]] = None, - fallback_models: Optional[List[str]] = None, - timeout: Optional[float] = None, - ) -> "AsyncSolanaIterator": + search_parameters: dict[str, Any] | None = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: Any | None = None, + response_format: dict[str, Any] | None = None, + stop: str | list[str] | None = None, + fallback_models: list[str] | None = None, + timeout: float | None = None, + ) -> AsyncSolanaIterator: """Async streaming. Same protocol semantics as the sync :meth:`SolanaLLMClient.chat_completion_stream`; only the iteration protocol differs (``async for``).""" validate_max_tokens(max_tokens) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "messages": messages, "stream": True, @@ -3118,7 +3117,7 @@ async def chat_completion_stream( body["stop"] = stop attempts = [model, *(fallback_models or [])] - last_exc: Optional[Exception] = None + last_exc: Exception | None = None for i, attempt_model in enumerate(attempts): body["model"] = attempt_model @@ -3147,8 +3146,8 @@ async def chat_completion_stream( async def _stream_with_payment( self, endpoint: str, - body: Dict[str, Any], - timeout: Optional[float] = None, + body: dict[str, Any], + timeout: float | None = None, ): """Whole-request payment-retry wrapper around :meth:`_stream_once` (async). Re-runs the paid request on a recoverable payment rejection, @@ -3176,8 +3175,8 @@ async def _stream_with_payment( async def _stream_once( self, endpoint: str, - body: Dict[str, Any], - timeout: Optional[float] = None, + body: dict[str, Any], + timeout: float | None = None, ): """Async version of :meth:`SolanaLLMClient._stream_once`.""" url = f"{self._api_url}{endpoint}" @@ -3186,7 +3185,7 @@ async def _stream_once( backoffs = self._STREAM_5XX_BACKOFFS # ----- Phase 1: probe (no payment header) ----- - payment_headers: Optional[Dict[str, str]] = None + payment_headers: dict[str, str] | None = None cost_usd = 0.0 for attempt in range(len(backoffs) + 1): @@ -3263,16 +3262,16 @@ async def _aiter_sse_chunks(response: httpx.Response): async def _aiter_and_archive( self, response: httpx.Response, - body: Dict[str, Any], + body: dict[str, Any], cost_usd: float, ): """Async version of :meth:`SolanaLLMClient._iter_and_archive`.""" - assembled_id: Optional[str] = None - assembled_model: Optional[str] = None + assembled_id: str | None = None + assembled_model: str | None = None assembled_created: int = 0 - content_parts: List[str] = [] - finish_reason: Optional[str] = None - usage_dict: Optional[Dict[str, Any]] = None + content_parts: list[str] = [] + finish_reason: str | None = None + usage_dict: dict[str, Any] | None = None async for chunk in self._aiter_sse_chunks(response): if chunk.choices: @@ -3299,7 +3298,7 @@ async def _aiter_and_archive( if cost_usd > 0: from .cache import save_to_cache - response_data: Dict[str, Any] = { + response_data: dict[str, Any] = { "id": assembled_id or "stream", "object": "chat.completion", "created": assembled_created or int(__import__("time").time()), @@ -3337,7 +3336,7 @@ async def _aiter_and_archive( async def _sign_payment_from_response( self, response: httpx.Response, - ) -> Tuple[Dict[str, str], float]: + ) -> tuple[dict[str, str], float]: payment_header = SolanaLLMClient._extract_payment_header(response) if not payment_header: raise PaymentError("402 response but no payment requirements found") @@ -3360,7 +3359,7 @@ async def _sign_payment_from_response( _raise_stream_error = SolanaLLMClient._raise_stream_error async def _request_with_payment( - self, endpoint: str, body: Dict[str, Any], timeout: Optional[float] = None + self, endpoint: str, body: dict[str, Any], timeout: float | None = None ) -> ChatResponse: """Whole-request payment-retry wrapper around :meth:`_request_once` (async). Same policy as the sync path — recoverable payment rejections @@ -3381,7 +3380,7 @@ async def _request_with_payment( ) async def _request_once( - self, endpoint: str, body: Dict[str, Any], timeout: Optional[float] = None + self, endpoint: str, body: dict[str, Any], timeout: float | None = None ) -> ChatResponse: url = f"{self._api_url}{endpoint}" headers = {"Content-Type": "application/json", "User-Agent": _get_user_agent()} @@ -3420,9 +3419,9 @@ async def _request_once( async def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> ChatResponse: eff_timeout = timeout if timeout is not None else self._timeout payment_headers, cost_usd = await self._sign_payment_from_response(response) @@ -3472,8 +3471,8 @@ async def _handle_payment_and_retry( # ── Raw passthrough request helpers (async, Solana payment) ───────────── async def _request_with_payment_raw( - self, endpoint: str, body: Dict[str, Any], timeout: Optional[float] = None - ) -> Dict[str, Any]: + self, endpoint: str, body: dict[str, Any], timeout: float | None = None + ) -> dict[str, Any]: """POST with Solana x402 payment, returning raw JSON (async mirror of the sync :class:`SolanaLLMClient` helper).""" from .cache import get_cached, save_to_cache @@ -3542,9 +3541,9 @@ async def _request_with_payment_raw( async def _get_with_payment_raw( self, endpoint: str, - params: Optional[Dict[str, Any]] = None, - timeout: Optional[float] = None, - ) -> Dict[str, Any]: + params: dict[str, Any] | None = None, + timeout: float | None = None, + ) -> dict[str, Any]: """GET with Solana x402 payment, returning raw JSON (async).""" from .cache import get_cached, save_to_cache @@ -3615,18 +3614,18 @@ async def search( self, query: str, *, - sources: Optional[List[str]] = None, + sources: list[str] | None = None, max_results: int = 10, - from_date: Optional[str] = None, - to_date: Optional[str] = None, - timeout: Optional[float] = None, + from_date: str | None = None, + to_date: str | None = None, + timeout: float | None = None, ) -> SearchResult: """Standalone search (Solana payment). ``timeout`` overrides the per-call HTTP timeout (defaults to ``DEFAULT_SEARCH_TIMEOUT`` — deep web/X tool-use can run minutes). """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "query": query, "max_results": max_results, } @@ -3664,8 +3663,8 @@ async def image( model: str = "google/nano-banana", size: str = "1024x1024", n: int = 1, - quality: Optional[str] = None, - timeout: Optional[float] = None, + quality: str | None = None, + timeout: float | None = None, ) -> ImageResponse: """Generate an image from a text prompt (Solana payment). @@ -3682,7 +3681,7 @@ async def image( Raises: ValueError: If ``quality`` is not one of the four accepted values. """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "prompt": prompt, "size": size, @@ -3699,14 +3698,14 @@ async def image( async def image_edit( self, prompt: str, - image: Union[str, List[str]], + image: str | list[str], *, model: str = "openai/gpt-image-2", - mask: Optional[str] = None, + mask: str | None = None, size: str = "1024x1024", n: int = 1, - quality: Optional[str] = None, - timeout: Optional[float] = None, + quality: str | None = None, + timeout: float | None = None, ) -> ImageResponse: """Edit an image using img2img (Solana payment). ``image`` may be a single data URI or a list of 1-4 data URIs for multi-image fusion @@ -3720,7 +3719,7 @@ async def image_edit( Raises: ValueError: If ``quality`` is not one of the four accepted values. """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model, "prompt": prompt, "image": image, @@ -3741,8 +3740,8 @@ async def image_edit( def _absolute_url(self, url: str) -> str: """Resolve a server-supplied relative ``poll_url`` against the API host (``api_url`` already includes the trailing ``/api`` — strip it once).""" - base = self._api_url[: -len("/api")] if self._api_url.endswith("/api") else self._api_url - if url.startswith("http://") or url.startswith("https://"): + base = self._api_url.removesuffix("/api") + if url.startswith(("http://", "https://")): # The poll loop sends (and re-signs) the wallet's PAYMENT-SIGNATURE # against this URL, so an absolute poll_url is pinned to the API # host+scheme — a gateway response pointing it elsewhere would leak @@ -3765,21 +3764,21 @@ async def video( self, prompt: str, *, - model: Optional[str] = None, - image_url: Optional[str] = None, - last_frame_url: Optional[str] = None, - reference_image_urls: Optional[List[str]] = None, - real_face_asset_id: Optional[str] = None, - duration_seconds: Optional[int] = None, - aspect_ratio: Optional[str] = None, - resolution: Optional[str] = None, - generate_audio: Optional[bool] = None, - seed: Optional[int] = None, - watermark: Optional[bool] = None, - return_last_frame: Optional[bool] = None, - input_type: Optional[str] = None, - budget_seconds: Optional[float] = None, - timeout: Optional[float] = None, + model: str | None = None, + image_url: str | None = None, + last_frame_url: str | None = None, + reference_image_urls: list[str] | None = None, + real_face_asset_id: str | None = None, + duration_seconds: int | None = None, + aspect_ratio: str | None = None, + resolution: str | None = None, + generate_audio: bool | None = None, + seed: int | None = None, + watermark: bool | None = None, + return_last_frame: bool | None = None, + input_type: str | None = None, + budget_seconds: float | None = None, + timeout: float | None = None, ) -> VideoResponse: """Generate a video clip (Solana payment). Async mirror of :meth:`SolanaLLMClient.video`.""" @@ -3817,17 +3816,17 @@ async def video( async def video_from_content( self, - content: List[Dict[str, Any]], + content: list[dict[str, Any]], *, - model: Optional[str] = None, - budget_seconds: Optional[float] = None, - timeout: Optional[float] = None, + model: str | None = None, + budget_seconds: float | None = None, + timeout: float | None = None, **options: Any, ) -> VideoResponse: """Generate a video from a Seedance ``content[]`` body (Solana payment).""" if not content: raise ValueError("content must be a non-empty list of Seedance content items.") - body: Dict[str, Any] = {"content": content, **options} + body: dict[str, Any] = {"content": content, **options} if model is not None: body["model"] = model data = await self._request_image_with_payment( @@ -3849,15 +3848,15 @@ async def music( self, prompt: str, *, - model: Optional[str] = None, + model: str | None = None, instrumental: bool = True, - lyrics: Optional[str] = None, - timeout: Optional[float] = None, + lyrics: str | None = None, + timeout: float | None = None, ) -> MusicResponse: """Generate a music track (Solana payment).""" if instrumental and lyrics and lyrics.strip(): raise ValueError("Cannot specify lyrics when instrumental is True") - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or SolanaLLMClient.MUSIC_DEFAULT_MODEL, "prompt": prompt, "instrumental": instrumental, @@ -3872,14 +3871,14 @@ async def speech( self, input: str, *, - model: Optional[str] = None, - voice: Optional[str] = None, - response_format: Optional[str] = None, - speed: Optional[float] = None, - timeout: Optional[float] = None, + model: str | None = None, + voice: str | None = None, + response_format: str | None = None, + speed: float | None = None, + timeout: float | None = None, ) -> SpeechResponse: """Synthesize speech from text (Solana payment).""" - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or SolanaLLMClient.SPEECH_DEFAULT_MODEL, "input": input, } @@ -3897,14 +3896,14 @@ async def sound_effect( self, text: str, *, - model: Optional[str] = None, - duration_seconds: Optional[float] = None, - prompt_influence: Optional[float] = None, - response_format: Optional[str] = None, - timeout: Optional[float] = None, + model: str | None = None, + duration_seconds: float | None = None, + prompt_influence: float | None = None, + response_format: str | None = None, + timeout: float | None = None, ) -> SpeechResponse: """Generate a cinematic sound effect (Solana payment).""" - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or SolanaLLMClient.SOUNDFX_DEFAULT_MODEL, "text": text, } @@ -3920,7 +3919,7 @@ async def sound_effect( self._attach_receipt(data) return SpeechResponse(**data) - async def list_voices(self) -> List[Dict[str, Any]]: + async def list_voices(self) -> list[dict[str, Any]]: """List available speech voices (free).""" url = f"{self._api_url}/v1/audio/voices" resp = await self._client.get( @@ -3953,7 +3952,7 @@ async def portrait_enroll(self, name: str, image_url: str) -> PortraitEnrollment ) return PortraitEnrollment(**data) - async def list_portraits(self, wallet_address: Optional[str] = None) -> PortraitList: + async def list_portraits(self, wallet_address: str | None = None) -> PortraitList: """List Virtual Portraits enrolled by a wallet (free, rate-limited).""" addr = _safe_path_segment(wallet_address or self.get_wallet_address(), "wallet_address") url = f"{self._api_url}/v1/wallet/{addr}/portraits" @@ -3970,7 +3969,7 @@ async def list_portraits(self, wallet_address: Optional[str] = None) -> Portrait ) return PortraitList(**resp.json()) - async def realface_init(self, name: str, group_id: Optional[str] = None) -> RealFaceInit: + async def realface_init(self, name: str, group_id: str | None = None) -> RealFaceInit: """Start/refresh a RealFace enrollment (free, rate-limited).""" if not name or not name.strip(): raise ValueError("name is required (1-64 chars)") @@ -3978,7 +3977,7 @@ async def realface_init(self, name: str, group_id: Optional[str] = None) -> Real raise ValueError(f"name must be 64 chars or fewer (got {len(name)})") if group_id is not None and not _GROUP_ID_RE.match(group_id): raise ValueError("group_id must look like 'legacy_rf_'") - body: Dict[str, Any] = {"name": name} + body: dict[str, Any] = {"name": name} if group_id: body["groupId"] = group_id url = f"{self._api_url}/v1/realface/init" @@ -4060,7 +4059,7 @@ async def realface_enroll(self, name: str, image_url: str, group_id: str) -> Rea ) return RealFaceEnrollment(**data) - async def list_realfaces(self, wallet_address: Optional[str] = None) -> RealFaceList: + async def list_realfaces(self, wallet_address: str | None = None) -> RealFaceList: """List RealFace assets enrolled by a wallet (free, rate-limited).""" addr = _safe_path_segment(wallet_address or self.get_wallet_address(), "wallet_address") url = f"{self._api_url}/v1/wallet/{addr}/realfaces" @@ -4082,12 +4081,12 @@ async def price( category: Category, symbol: str, *, - market: Optional[Market] = None, - session: Optional[Session] = None, + market: Market | None = None, + session: Session | None = None, ) -> PricePoint: """Fetch a realtime Pyth price quote (Solana payment for paid categories).""" endpoint = SolanaLLMClient._price_category_path(category, market, "price", symbol) - params: Dict[str, Any] = {} + params: dict[str, Any] = {} if session is not None: params["session"] = session data = await self._get_with_payment_raw( @@ -4114,12 +4113,12 @@ async def price_history( resolution: Resolution = "D", from_ts: int, to_ts: int, - market: Optional[Market] = None, - session: Optional[Session] = None, + market: Market | None = None, + session: Session | None = None, ) -> PriceHistoryResponse: """Fetch OHLC bars between two Unix timestamps (seconds).""" endpoint = SolanaLLMClient._price_category_path(category, market, "history", symbol) - params: Dict[str, Any] = {"resolution": resolution, "from": from_ts, "to": to_ts} + params: dict[str, Any] = {"resolution": resolution, "from": from_ts, "to": to_ts} if session is not None: params["session"] = session data = await self._get_with_payment_raw( @@ -4136,13 +4135,13 @@ async def list_symbols( self, category: Category, *, - q: Optional[str] = None, + q: str | None = None, limit: int = 100, - market: Optional[Market] = None, + market: Market | None = None, ) -> SymbolListResponse: """List available symbols in a Pyth category (free discovery).""" endpoint = SolanaLLMClient._price_category_path(category, market, "list", None) - params: Dict[str, Any] = {"limit": limit} + params: dict[str, Any] = {"limit": limit} if q: params["q"] = q data = await self._get_with_payment_raw( @@ -4160,24 +4159,24 @@ async def rpc( self, network: str, method: str, - params: Optional[List[Any]] = None, + params: list[Any] | None = None, *, - id: Union[str, int] = 1, + id: str | int = 1, ) -> RpcResponse: """Make a single JSON-RPC 2.0 call (Solana payment, flat $0.002).""" _safe_path_segment(network, "network") - body: Dict[str, Any] = {"jsonrpc": "2.0", "id": id, "method": method} + body: dict[str, Any] = {"jsonrpc": "2.0", "id": id, "method": method} if params is not None: body["params"] = params data = await self._request_with_payment_raw(f"/v1/rpc/{network}", body) return SolanaLLMClient._rpc_response(data, self._last_raw_headers, network) - async def rpc_batch(self, network: str, requests: List[Dict[str, Any]]) -> List[RpcResponse]: + async def rpc_batch(self, network: str, requests: list[dict[str, Any]]) -> list[RpcResponse]: """Make a JSON-RPC 2.0 batch call (Solana payment, $0.002 x N).""" if not requests: raise ValueError("batch requires at least one request") _safe_path_segment(network, "network") - body: List[Dict[str, Any]] = [] + body: list[dict[str, Any]] = [] for i, req in enumerate(requests): if "method" not in req: raise ValueError(f"batch request {i} is missing 'method'") @@ -4191,14 +4190,14 @@ async def rpc_batch(self, network: str, requests: List[Dict[str, Any]]) -> List[ async def _request_image_with_payment( self, endpoint: str, - body: Dict[str, Any], - timeout: Optional[float] = None, + body: dict[str, Any], + timeout: float | None = None, *, - poll_budget_seconds: Optional[float] = None, - poll_interval_seconds: Optional[float] = None, + poll_budget_seconds: float | None = None, + poll_interval_seconds: float | None = None, max_resigns: int = 0, label: str = "Image", - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Async sign + submit + poll wrapper for async media generation — the async mirror of the sync :class:`SolanaLLMClient` helper. Shared by :meth:`image` and :meth:`video` (``max_resigns`` re-signs to survive @@ -4441,85 +4440,85 @@ async def _request_image_with_payment( # ── Prediction Markets (Powered by Predexon) ──────────────────────────── - async def pm(self, path: str, **params: Any) -> Dict[str, Any]: + async def pm(self, path: str, **params: Any) -> dict[str, Any]: """Query Predexon prediction market data (GET, Solana payment). Powered by Predexon.""" return await self._get_with_payment_raw(f"/v1/pm/{path}", params or None) - async def pm_query(self, path: str, query: Dict[str, Any]) -> Dict[str, Any]: + async def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: """Structured query for Predexon data (POST, Solana payment). Powered by Predexon.""" return await self._request_with_payment_raw(f"/v1/pm/{path}", query) - async def pm_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_markets(self, **params: Any) -> dict[str, Any]: """List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("markets", **params) - async def pm_listings(self, **params: Any) -> Dict[str, Any]: + async def pm_listings(self, **params: Any) -> dict[str, Any]: """List venue-native executable listings (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("markets/listings", **params) - async def pm_outcome(self, predexon_id: str) -> Dict[str, Any]: + async def pm_outcome(self, predexon_id: str) -> dict[str, Any]: """Resolve a canonical Predexon outcome ID (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm(f"outcomes/{predexon_id}") - async def pm_polymarket_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("polymarket/markets", **params) - async def pm_polymarket_events(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_events(self, **params: Any) -> dict[str, Any]: """List Polymarket events (Predexon v2). Tier 1 ($0.001/call).""" return await self.pm("polymarket/events", **params) - async def pm_polymarket_markets_keyset(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_markets_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket markets with cursor-based keyset pagination. Tier 1 ($0.001/call).""" return await self.pm("polymarket/markets/keyset", **params) - async def pm_polymarket_events_keyset(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_events_keyset(self, **params: Any) -> dict[str, Any]: """Polymarket events with cursor-based keyset pagination. Tier 1 ($0.001/call).""" return await self.pm("polymarket/events/keyset", **params) - async def pm_polymarket_positions(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_positions(self, **params: Any) -> dict[str, Any]: """Polymarket open positions (per-wallet, market-level PnL). Tier 1 ($0.001/call).""" return await self.pm("polymarket/positions", **params) - async def pm_polymarket_trades(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_trades(self, **params: Any) -> dict[str, Any]: """Recent Polymarket trades. Tier 1 ($0.001/call).""" return await self.pm("polymarket/trades", **params) - async def pm_polymarket_leaderboard(self, **params: Any) -> Dict[str, Any]: + async def pm_polymarket_leaderboard(self, **params: Any) -> dict[str, Any]: """Polymarket trader leaderboard. Tier 1 ($0.001/call).""" return await self.pm("polymarket/leaderboard", **params) - async def pm_kalshi_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_kalshi_markets(self, **params: Any) -> dict[str, Any]: """List Kalshi markets. Tier 1 ($0.001/call).""" return await self.pm("kalshi/markets", **params) - async def pm_limitless_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: """List Limitless markets. Tier 1 ($0.001/call).""" return await self.pm("limitless/markets", **params) - async def pm_sports_categories(self) -> Dict[str, Any]: + async def pm_sports_categories(self) -> dict[str, Any]: """List available sports categories. Tier 1 ($0.001/call).""" return await self.pm("sports/categories") - async def pm_sports_markets(self, **params: Any) -> Dict[str, Any]: + async def pm_sports_markets(self, **params: Any) -> dict[str, Any]: """List sports markets grouped by game. Tier 1 ($0.001/call).""" return await self.pm("sports/markets", **params) - async def pm_wallet_identity(self, wallet: str) -> Dict[str, Any]: + async def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: """Identity + profile for one wallet. Tier 2 ($0.005/call).""" return await self.pm(f"polymarket/wallet/identity/{wallet}") - async def pm_wallet_identities(self, addresses: List[str]) -> Dict[str, Any]: + async def pm_wallet_identities(self, addresses: list[str]) -> dict[str, Any]: """Bulk identity for up to 200 wallet addresses. Tier 2 ($0.005/call).""" return await self.pm_query("polymarket/wallet/identities", {"addresses": addresses}) - async def pm_wallet_cluster(self, address: str) -> Dict[str, Any]: + async def pm_wallet_cluster(self, address: str) -> dict[str, Any]: """Wallet-cluster discovery (on-chain transfers + identity proofs). Tier 2 ($0.005/call).""" return await self.pm(f"polymarket/wallet/{address}/cluster") # ── Exa Web Search (Powered by Exa) ───────────────────────────────────── - async def exa(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: + async def exa(self, path: str, body: dict[str, Any]) -> dict[str, Any]: """Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa. Args: @@ -4530,25 +4529,25 @@ async def exa(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: f"/v1/exa/{path}", body, timeout=self._search_timeout ) - async def exa_search(self, query: str, **kwargs: Any) -> Dict[str, Any]: + async def exa_search(self, query: str, **kwargs: Any) -> dict[str, Any]: """Neural and keyword web search via Exa (Solana payment, $0.01/request).""" return await self._request_with_payment_raw( "/v1/exa/search", {"query": query, **kwargs}, timeout=self._search_timeout ) - async def exa_find_similar(self, url: str, **kwargs: Any) -> Dict[str, Any]: + async def exa_find_similar(self, url: str, **kwargs: Any) -> dict[str, Any]: """Find pages semantically similar to a given URL via Exa (Solana payment, $0.01/request).""" return await self._request_with_payment_raw( "/v1/exa/find-similar", {"url": url, **kwargs}, timeout=self._search_timeout ) - async def exa_contents(self, urls: List[str], **kwargs: Any) -> Dict[str, Any]: + async def exa_contents(self, urls: list[str], **kwargs: Any) -> dict[str, Any]: """Extract full text content from URLs via Exa (Solana payment, $0.002/URL).""" return await self._request_with_payment_raw( "/v1/exa/contents", {"urls": urls, **kwargs}, timeout=self._search_timeout ) - async def exa_answer(self, query: str, **kwargs: Any) -> Dict[str, Any]: + async def exa_answer(self, query: str, **kwargs: Any) -> dict[str, Any]: """AI-generated answer grounded in live web search via Exa (Solana payment, $0.01/request).""" return await self._request_with_payment_raw( "/v1/exa/answer", {"query": query, **kwargs}, timeout=self._search_timeout @@ -4556,28 +4555,28 @@ async def exa_answer(self, query: str, **kwargs: Any) -> Dict[str, Any]: # ── DefiLlama (DeFi protocols / TVL / yields / prices) ────────────────── - async def defi(self, path: str, **params: Any) -> Dict[str, Any]: + async def defi(self, path: str, **params: Any) -> dict[str, Any]: """Query DefiLlama DeFi data (GET, Solana payment). $0.005/call ($0.001 for prices/{coins}).""" return await self._get_with_payment_raw(f"/v1/defillama/{path}", params or None) - async def defi_protocols(self) -> Dict[str, Any]: + async def defi_protocols(self) -> dict[str, Any]: """All DeFi protocols with TVL ($0.005/call).""" return await self.defi("protocols") - async def defi_protocol(self, slug: str) -> Dict[str, Any]: + async def defi_protocol(self, slug: str) -> dict[str, Any]: """Single protocol details + historical TVL ($0.005/call).""" return await self.defi(f"protocol/{slug}") - async def defi_chains(self) -> Dict[str, Any]: + async def defi_chains(self) -> dict[str, Any]: """Current TVL of every chain ($0.005/call).""" return await self.defi("chains") - async def defi_yields(self, **params: Any) -> Dict[str, Any]: + async def defi_yields(self, **params: Any) -> dict[str, Any]: """Yield pools with APY/TVL ($0.005/call).""" return await self.defi("yields", **params) - async def defi_prices(self, coins: Union[List[str], str]) -> Dict[str, Any]: + async def defi_prices(self, coins: list[str] | str) -> dict[str, Any]: """Token price lookup ($0.001/call).""" joined = ",".join(coins) if isinstance(coins, list) else coins return await self.defi(f"prices/{joined}") @@ -4589,70 +4588,70 @@ async def dex( path: str, *, method: str = "GET", - body: Optional[Dict[str, Any]] = None, + body: dict[str, Any] | None = None, **params: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Query the 0x Swap / Gasless APIs (free — no x402 payment).""" endpoint = f"/v1/zerox/{path}" if method.upper() == "POST": return await self._request_with_payment_raw(endpoint, body or {}) return await self._get_with_payment_raw(endpoint, params or None) - async def dex_price(self, **params: Any) -> Dict[str, Any]: + async def dex_price(self, **params: Any) -> dict[str, Any]: """Indicative Permit2 swap price — no commitment (free).""" return await self.dex("price", **params) - async def dex_quote(self, **params: Any) -> Dict[str, Any]: + async def dex_quote(self, **params: Any) -> dict[str, Any]: """Firm Permit2 swap quote with permit2.eip712 + tx data (free).""" return await self.dex("quote", **params) - async def dex_gasless_price(self, **params: Any) -> Dict[str, Any]: + async def dex_gasless_price(self, **params: Any) -> dict[str, Any]: """Gasless indicative price quote (free).""" return await self.dex("gasless/price", **params) - async def dex_gasless_quote(self, **params: Any) -> Dict[str, Any]: + async def dex_gasless_quote(self, **params: Any) -> dict[str, Any]: """Gasless firm quote — returns trade.eip712 to sign (free).""" return await self.dex("gasless/quote", **params) - async def dex_gasless_submit(self, body: Dict[str, Any]) -> Dict[str, Any]: + async def dex_gasless_submit(self, body: dict[str, Any]) -> dict[str, Any]: """Submit a signed gasless trade; the 0x relayer pays gas (free).""" return await self.dex("gasless/submit", method="POST", body=body) - async def dex_gasless_status(self, trade_hash: str) -> Dict[str, Any]: + async def dex_gasless_status(self, trade_hash: str) -> dict[str, Any]: """Poll a gasless trade's status by tradeHash (free).""" return await self.dex(f"gasless/status/{trade_hash}") - async def dex_chains(self) -> Dict[str, Any]: + async def dex_chains(self) -> dict[str, Any]: """Chains where the Swap API is supported (free).""" return await self.dex("swap/chains") - async def dex_gasless_chains(self) -> Dict[str, Any]: + async def dex_gasless_chains(self) -> dict[str, Any]: """Chains where the Gasless API is supported (free).""" return await self.dex("gasless/chains") # ── Modal Sandbox (pay-per-call cloud compute) ─────────────────────────── - async def modal(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def modal(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]: """Call the Modal sandbox compute API (POST, Solana payment).""" return await self._request_with_payment_raw(f"/v1/modal/{path}", body or {}) - async def modal_sandbox_create(self, **body: Any) -> Dict[str, Any]: + async def modal_sandbox_create(self, **body: Any) -> dict[str, Any]: """Create a sandboxed compute environment ($0.01 CPU / $0.05 GPU).""" return await self.modal("sandbox/create", body) async def modal_sandbox_exec( - self, sandbox_id: str, command: List[str], **body: Any - ) -> Dict[str, Any]: + self, sandbox_id: str, command: list[str], **body: Any + ) -> dict[str, Any]: """Execute a command in a sandbox; returns stdout/stderr ($0.001).""" return await self.modal( "sandbox/exec", {"sandbox_id": sandbox_id, "command": command, **body} ) - async def modal_sandbox_status(self, sandbox_id: str) -> Dict[str, Any]: + async def modal_sandbox_status(self, sandbox_id: str) -> dict[str, Any]: """Check a sandbox's status ($0.001).""" return await self.modal("sandbox/status", {"sandbox_id": sandbox_id}) - async def modal_sandbox_terminate(self, sandbox_id: str) -> Dict[str, Any]: + async def modal_sandbox_terminate(self, sandbox_id: str) -> dict[str, Any]: """Terminate a sandbox ($0.001).""" return await self.modal("sandbox/terminate", {"sandbox_id": sandbox_id}) diff --git a/blockrun_llm/solana_wallet.py b/blockrun_llm/solana_wallet.py index 17d5cff..44fedbf 100644 --- a/blockrun_llm/solana_wallet.py +++ b/blockrun_llm/solana_wallet.py @@ -11,7 +11,7 @@ import os import time from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: from .solana_client import SolanaLLMClient @@ -30,7 +30,7 @@ def _require_solders() -> None: ) -def create_solana_wallet() -> Dict[str, str]: +def create_solana_wallet() -> dict[str, str]: """ Create a new Solana wallet. @@ -144,7 +144,7 @@ def _expand_solana_seed(private_key: str) -> str: return private_key -def scan_solana_wallets() -> List[Dict[str, str]]: +def scan_solana_wallets() -> list[dict[str, str]]: """ Discover ~/./solana-wallet.json files from other providers. @@ -159,7 +159,7 @@ def scan_solana_wallets() -> List[Dict[str, str]]: list_discovered_solana_wallets() for an address derived from the key. """ home = Path.home() - results: List[tuple] = [] # (mtime, private_key, address, source) + results: list[tuple] = [] # (mtime, private_key, address, source) try: for entry in home.iterdir(): @@ -190,7 +190,7 @@ def scan_solana_wallets() -> List[Dict[str, str]]: return [{"private_key": pk, "address": addr, "source": src} for _, pk, addr, src in results] -def list_discovered_solana_wallets() -> List[Dict[str, str]]: +def list_discovered_solana_wallets() -> list[dict[str, str]]: """ List Solana wallets from other applications, safe to show to a user. @@ -257,7 +257,7 @@ def import_solana_wallet(address: str) -> str: ) -def load_solana_wallet() -> Optional[str]: +def load_solana_wallet() -> str | None: """ Load Solana wallet private key. @@ -275,7 +275,7 @@ def load_solana_wallet() -> Optional[str]: return None -def get_or_create_solana_wallet() -> Dict[str, object]: +def get_or_create_solana_wallet() -> dict[str, object]: """ Get existing Solana wallet or create new one. @@ -309,7 +309,7 @@ def get_or_create_solana_wallet() -> Dict[str, object]: return {**wallet, "is_new": True} -def format_solana_wallet_migration_notice(new_address: str) -> Optional[str]: +def format_solana_wallet_migration_notice(new_address: str) -> str | None: """ Warn when a new Solana wallet was created while provider wallets exist. @@ -363,7 +363,7 @@ def format_solana_wallet_migration_notice(new_address: str) -> Optional[str]: """ -def setup_agent_solana_wallet(silent: bool = False) -> "SolanaLLMClient": +def setup_agent_solana_wallet(silent: bool = False) -> SolanaLLMClient: """ Set up Solana wallet for agent use and return a SolanaLLMClient. @@ -402,7 +402,7 @@ def setup_agent_solana_wallet(silent: bool = False) -> "SolanaLLMClient": return SolanaLLMClient(private_key=result["private_key"]) -def get_solana_usdc_balance(address: str, rpc_url: Optional[str] = None) -> float: +def get_solana_usdc_balance(address: str, rpc_url: str | None = None) -> float: """ Get USDC-SPL balance for a Solana address. @@ -484,9 +484,10 @@ def generate_solana_qr_ascii(address: str) -> str: # Generate new QR try: - import qrcode from io import StringIO + import qrcode + qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, @@ -513,7 +514,7 @@ def generate_solana_qr_ascii(address: str) -> str: return f"[QR code requires 'qrcode' package: pip install qrcode[pil]]\nAddress: {address}" -def save_solana_wallet_qr(address: str, path: Optional[str] = None) -> str: +def save_solana_wallet_qr(address: str, path: str | None = None) -> str: """ Save Solana QR code as PNG image. @@ -560,8 +561,8 @@ def open_solana_wallet_qr(address: str) -> str: Returns: Path to saved QR image """ - import subprocess import platform + import subprocess qr_path = save_solana_wallet_qr(address) if qr_path: diff --git a/blockrun_llm/speech.py b/blockrun_llm/speech.py index 97f7fdd..c7869ab 100644 --- a/blockrun_llm/speech.py +++ b/blockrun_llm/speech.py @@ -35,20 +35,23 @@ Price = (characters / 1000) x model rate, minimum $0.001/request. """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any, List +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account -from .types import SpeechResponse, APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .tx_log import paid_request_error_prefix +from .types import APIError, PaymentError, SpeechResponse from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, ) -from .tx_log import paid_request_error_prefix +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -85,8 +88,8 @@ class SpeechClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 120.0, ): """ @@ -128,10 +131,10 @@ def generate( self, input: str, *, - model: Optional[str] = None, - voice: Optional[str] = None, - response_format: Optional[str] = None, - speed: Optional[float] = None, + model: str | None = None, + voice: str | None = None, + response_format: str | None = None, + speed: float | None = None, ) -> SpeechResponse: """ Synthesize speech from text (OpenAI-compatible TTS). @@ -162,7 +165,7 @@ def generate( result = client.generate("Welcome to BlockRun.", voice="george") print(result.data[0].url) """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.DEFAULT_MODEL, "input": input, } @@ -182,10 +185,10 @@ def sound_effect( self, text: str, *, - model: Optional[str] = None, - duration_seconds: Optional[float] = None, - prompt_influence: Optional[float] = None, - response_format: Optional[str] = None, + model: str | None = None, + duration_seconds: float | None = None, + prompt_influence: float | None = None, + response_format: str | None = None, ) -> SpeechResponse: """ Generate a cinematic sound effect from a text prompt. @@ -207,7 +210,7 @@ def sound_effect( result = client.sound_effect("crackling campfire at night") print(result.data[0].url) """ - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.DEFAULT_SOUNDFX_MODEL, "text": text, } @@ -220,7 +223,7 @@ def sound_effect( return self._request_with_payment("/v1/audio/sound-effects", body) - def list_voices(self) -> List[Dict[str, Any]]: + def list_voices(self) -> list[dict[str, Any]]: """ List available voices for TTS (free, rate-limited 60 req/min/IP). @@ -243,7 +246,7 @@ def list_voices(self) -> List[Dict[str, Any]]: return response.json().get("data", []) - def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> SpeechResponse: + def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> SpeechResponse: """Make a request with automatic x402 payment handling.""" url = f"{self.api_url}{endpoint}" @@ -273,7 +276,7 @@ def _handle_payment_and_retry( self, url: str, endpoint: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, ) -> SpeechResponse: """Handle 402 response: parse requirements, sign payment, retry.""" diff --git a/blockrun_llm/surf.py b/blockrun_llm/surf.py index 2711b48..824e95c 100644 --- a/blockrun_llm/surf.py +++ b/blockrun_llm/surf.py @@ -36,12 +36,14 @@ from __future__ import annotations import os -from typing import Any, Dict, List, Optional, Tuple +from typing import Any import httpx from dotenv import load_dotenv from eth_account import Account +from typing_extensions import Self +from .tx_log import paid_request_error_prefix from .types import APIError, PaymentError from .validation import ( sanitize_error_response, @@ -53,13 +55,12 @@ extract_payment_details, parse_payment_required, ) -from .tx_log import paid_request_error_prefix load_dotenv() # Mirrors src/lib/surf.ts SURF_TIER_*_PRICE on the backend. -SURF_TIER_PRICES: Dict[int, float] = { +SURF_TIER_PRICES: dict[int, float] = { 1: 0.001, 2: 0.005, 3: 0.020, @@ -69,7 +70,7 @@ # Mirrors src/lib/surf.ts SURF_ENDPOINTS. Each tuple is (path, method, tier, required_params). # Keep this list in sync when backend endpoints change — used for discovery, parameter # validation, and auto GET/POST routing in SurfClient.call(). -_SURF_CATALOG: List[Tuple[str, str, int, Tuple[str, ...]]] = [ +_SURF_CATALOG: list[tuple[str, str, int, tuple[str, ...]]] = [ # exchange ("exchange/markets", "GET", 1, ()), ("exchange/price", "GET", 1, ("pair",)), @@ -167,7 +168,7 @@ ("web/fetch", "GET", 2, ("url",)), ] -_CATALOG_BY_PATH: Dict[str, Tuple[str, int, Tuple[str, ...]]] = { +_CATALOG_BY_PATH: dict[str, tuple[str, int, tuple[str, ...]]] = { path: (method, tier, required) for path, method, tier, required in _SURF_CATALOG } @@ -186,8 +187,8 @@ class SurfClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = DEFAULT_TIMEOUT, ): from .wallet import load_wallet @@ -219,7 +220,7 @@ def __init__( # ------------------------------------------------------------ Discovery API @staticmethod - def endpoints() -> List[Dict[str, Any]]: + def endpoints() -> list[dict[str, Any]]: """Return the full Surf endpoint catalog with method, tier, and price.""" return [ { @@ -233,7 +234,7 @@ def endpoints() -> List[Dict[str, Any]]: ] @staticmethod - def endpoint_info(path: str) -> Optional[Dict[str, Any]]: + def endpoint_info(path: str) -> dict[str, Any] | None: """Return catalog info for one path, or None if unknown.""" entry = _CATALOG_BY_PATH.get(path) if not entry: @@ -257,12 +258,12 @@ def price(path: str) -> float: # ---------------------------------------------------------------- Core HTTP - def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]: """GET an `/v1/surf/{path}` endpoint with optional query params.""" self._validate_path(path, "GET", params or {}) return self._request("GET", path, params=params, json_body=None) - def post(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def post(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]: """POST an `/v1/surf/{path}` endpoint with an optional JSON body.""" self._validate_path(path, "POST", body or {}) return self._request("POST", path, params=None, json_body=body) @@ -271,9 +272,9 @@ def call( self, path: str, *, - params: Optional[Dict[str, Any]] = None, - body: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + params: dict[str, Any] | None = None, + body: dict[str, Any] | None = None, + ) -> dict[str, Any]: """ Generic helper that auto-routes to GET or POST based on the catalog. @@ -295,7 +296,7 @@ def call( # ---------------------------------------------------------------- Internals - def _validate_path(self, path: str, method: str, supplied: Dict[str, Any]) -> None: + def _validate_path(self, path: str, method: str, supplied: dict[str, Any]) -> None: info = self.endpoint_info(path) if info is None: return # allow forward-compat with newer backend endpoints @@ -312,9 +313,9 @@ def _request( method: str, path: str, *, - params: Optional[Dict[str, Any]], - json_body: Optional[Dict[str, Any]], - ) -> Dict[str, Any]: + params: dict[str, Any] | None, + json_body: dict[str, Any] | None, + ) -> dict[str, Any]: url = f"{self.api_url}/v1/surf/{path}" headers = {"Content-Type": "application/json"} if json_body is not None else {} response = self._client.request( @@ -332,10 +333,10 @@ def _handle_payment_and_retry( self, method: str, url: str, - params: Optional[Dict[str, Any]], - json_body: Optional[Dict[str, Any]], + params: dict[str, Any] | None, + json_body: dict[str, Any] | None, response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: payment_header: Any = response.headers.get("payment-required") if not payment_header: try: @@ -368,7 +369,7 @@ def _handle_payment_and_retry( extensions=extensions, ) - retry_headers: Dict[str, str] = {"PAYMENT-SIGNATURE": payment_payload} + retry_headers: dict[str, str] = {"PAYMENT-SIGNATURE": payment_payload} if json_body is not None: retry_headers["Content-Type"] = "application/json" @@ -388,7 +389,7 @@ def _handle_payment_and_retry( return data @staticmethod - def _unwrap(response: httpx.Response, *, after_payment: bool = False) -> Dict[str, Any]: + def _unwrap(response: httpx.Response, *, after_payment: bool = False) -> dict[str, Any]: if response.status_code == 200: return response.json() try: @@ -411,7 +412,7 @@ def get_wallet_address(self) -> str: def close(self) -> None: self._client.close() - def __enter__(self) -> "SurfClient": + def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: diff --git a/blockrun_llm/tx_log.py b/blockrun_llm/tx_log.py index 1fe2ece..44ead88 100644 --- a/blockrun_llm/tx_log.py +++ b/blockrun_llm/tx_log.py @@ -36,8 +36,7 @@ import time from datetime import datetime from pathlib import Path -from typing import Any, Dict, Optional, Union - +from typing import Any DEFAULT_LOG_DIR = Path("./log") LOG_NAME = "transactions.log" @@ -60,7 +59,7 @@ _SETTLEMENT_HEADER_NAMES = ("PAYMENT-RESPONSE", "X-PAYMENT-RESPONSE") -def read_settlement_header(headers: Any) -> Optional[str]: +def read_settlement_header(headers: Any) -> str | None: """Pull the raw settlement header out of a response, under either name. Single source of truth for the header name — call sites must not hand-roll @@ -78,7 +77,7 @@ def read_settlement_header(headers: Any) -> Optional[str]: return None -def decode_settlement_header(header_value: Optional[str]) -> Optional[Dict[str, Any]]: +def decode_settlement_header(header_value: str | None) -> dict[str, Any] | None: """Decode a ``PAYMENT-RESPONSE`` header into a settlement dict. The x402 facilitator returns a base64-encoded JSON describing what @@ -170,7 +169,7 @@ def paid_request_error_prefix(headers: Any) -> str: # --------------------------------------------------------------------------- -def _resolve_log_dir(option: Union[bool, str, "os.PathLike[str]", Path, None]) -> Optional[Path]: +def _resolve_log_dir(option: bool | str | os.PathLike[str] | Path | None) -> Path | None: """Translate the ``transaction_log=...`` constructor argument into a Path. ``True`` → default ``./log`` @@ -268,13 +267,13 @@ def _extract_tokens(response: Any) -> tuple[int, int]: def format_row( *, - ts: Optional[float] = None, + ts: float | None = None, endpoint: str, - model: Optional[str], + model: str | None, in_tokens: int, out_tokens: int, cost_usd: float, - tx_hash: Optional[str], + tx_hash: str | None, ) -> str: """Format one log row exactly like the example in the module docstring.""" if ts is None: @@ -313,7 +312,7 @@ class TransactionLogger: automatically via the ``transaction_log=`` constructor argument. """ - def __init__(self, directory: Union[str, "os.PathLike[str]", Path] = DEFAULT_LOG_DIR): + def __init__(self, directory: str | os.PathLike[str] | Path = DEFAULT_LOG_DIR): self.directory = Path(directory).expanduser() self.path = self.directory / LOG_NAME @@ -321,15 +320,15 @@ def log( self, *, endpoint: str, - request: Dict[str, Any], + request: dict[str, Any], response: Any, cost_usd: float, - model: Optional[str] = None, - wallet: Optional[str] = None, - network: Optional[str] = None, - client_kind: Optional[str] = None, - settlement: Optional[Dict[str, Any]] = None, - ) -> Optional[Path]: + model: str | None = None, + wallet: str | None = None, + network: str | None = None, + client_kind: str | None = None, + settlement: dict[str, Any] | None = None, + ) -> Path | None: """Append one formatted row to ``./log/transactions.log``. Returns the log path on success, or ``None`` if the file could not @@ -382,8 +381,8 @@ def entries(self) -> list[str]: __all__ = [ + "DEFAULT_LOG_DIR", "TransactionLogger", "decode_settlement_header", "format_row", - "DEFAULT_LOG_DIR", ] diff --git a/blockrun_llm/types.py b/blockrun_llm/types.py index c9fdf8c..9b15850 100644 --- a/blockrun_llm/types.py +++ b/blockrun_llm/types.py @@ -1,6 +1,7 @@ """Type definitions for BlockRun LLM SDK.""" -from typing import List, Optional, Literal, Dict, Any, Union +from typing import Any, Dict, List, Literal, Optional, Union + from pydantic import BaseModel @@ -308,8 +309,6 @@ class PaymentRequired(BaseModel): class BlockrunError(Exception): """Base exception for BlockRun SDK.""" - pass - class PaymentError(BlockrunError): """Payment-related error. @@ -717,7 +716,7 @@ class PriceBar(BaseModel): t: Optional[int] = None # Bar open time (unix seconds) o: Optional[float] = None h: Optional[float] = None - l: Optional[float] = None # noqa: E741 — Pyth bar field name + l: Optional[float] = None c: Optional[float] = None v: Optional[float] = None diff --git a/blockrun_llm/validation.py b/blockrun_llm/validation.py index 859c5a6..e043867 100644 --- a/blockrun_llm/validation.py +++ b/blockrun_llm/validation.py @@ -9,8 +9,10 @@ - Resource URLs match expected domains """ +from __future__ import annotations + import re -from typing import Optional, Dict, Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse if TYPE_CHECKING: @@ -62,7 +64,7 @@ def _looks_like_solana_key(key: str) -> bool: EVM 64-char range — so a malformed 64-char hex key still routes to the regular hex error rather than the Solana hint. """ - candidate = key[2:] if key.startswith("0x") else key + candidate = key.removeprefix("0x") if len(candidate) == 64 or not (40 <= len(candidate) <= 90): return False return any(c in _BASE58_ONLY_CHARS for c in candidate) @@ -167,7 +169,7 @@ def validate_model(model: str) -> None: pass -def validate_video_input_type(input_type: Optional[str]) -> None: +def validate_video_input_type(input_type: str | None) -> None: """ Validate the optional `input_type` seed-mode assertion on video generation. @@ -193,7 +195,7 @@ def validate_video_input_type(input_type: Optional[str]) -> None: ) -def validate_image_quality(quality: Optional[str]) -> None: +def validate_image_quality(quality: str | None) -> None: """ Validate the optional `quality` knob on Solana image generation/editing. @@ -239,7 +241,7 @@ def validate_image_quality(quality: Optional[str]) -> None: MAX_TOKENS_SANITY_LIMIT = 1_000_000 -def validate_max_tokens(max_tokens: Optional[int]) -> None: +def validate_max_tokens(max_tokens: int | None) -> None: """ Validate max_tokens parameter. @@ -283,7 +285,7 @@ def validate_max_tokens(max_tokens: Optional[int]) -> None: ) -def validate_temperature(temperature: Optional[float]) -> None: +def validate_temperature(temperature: float | None) -> None: """ Validate temperature parameter. @@ -309,7 +311,7 @@ def validate_temperature(temperature: Optional[float]) -> None: raise ValueError("temperature must be between 0 and 2") -def validate_top_p(top_p: Optional[float]) -> None: +def validate_top_p(top_p: float | None) -> None: """ Validate top_p parameter (nucleus sampling). @@ -370,7 +372,7 @@ def validate_api_url(url: str) -> None: ) -def build_payment_rejected_error(response: Any) -> "PaymentError": +def build_payment_rejected_error(response: Any) -> PaymentError: """Translate a 402 retry response into a :class:`PaymentError` that preserves the gateway's original failure reason. @@ -427,7 +429,7 @@ def build_payment_rejected_error(response: Any) -> "PaymentError": return PaymentError(msg, status_code=402, response=sanitized) -def sanitize_error_response(error_body: Any) -> Dict[str, Any]: +def sanitize_error_response(error_body: Any) -> dict[str, Any]: """ Sanitize API error responses to prevent information leakage. @@ -465,7 +467,7 @@ def sanitize_error_response(error_body: Any) -> Dict[str, Any]: if isinstance(nested, dict): message = nested.get("message") code = nested.get("code") or error_body.get("code") - result: Dict[str, Any] = { + result: dict[str, Any] = { "message": message if isinstance(message, str) else "API request failed", "code": code if isinstance(code, str) else None, } @@ -537,7 +539,7 @@ def validate_resource_url(url: str, base_url: str) -> str: return f"{base_url}/v1/chat/completions" -def resolve_spend_limit(explicit: Optional[float], env_var: str) -> Optional[float]: +def resolve_spend_limit(explicit: float | None, env_var: str) -> float | None: """Resolve a spend limit from the constructor argument or its env var. ``None`` means unlimited, which is the default and the pre-1.9.0 behavior. @@ -566,10 +568,10 @@ def resolve_spend_limit(explicit: Optional[float], env_var: str) -> Optional[flo def check_spend_limits( cost_usd: float, *, - max_cost_per_call: Optional[float], - max_session_cost: Optional[float], + max_cost_per_call: float | None, + max_session_cost: float | None, session_spent_usd: float, - model: Optional[str] = None, + model: str | None = None, ) -> None: """Refuse a quote that would breach a caller-configured spend limit. diff --git a/blockrun_llm/video.py b/blockrun_llm/video.py index 5f9252b..f79802c 100644 --- a/blockrun_llm/video.py +++ b/blockrun_llm/video.py @@ -28,21 +28,24 @@ print(result.data[0].duration_seconds) """ +from __future__ import annotations + import os import time -from typing import Optional, Dict, Any, List +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account -from .types import VideoResponse, APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details +from .types import APIError, PaymentError, VideoResponse from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, validate_video_input_type, ) +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() @@ -93,8 +96,8 @@ class VideoClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 360.0, ): """ @@ -136,20 +139,20 @@ def generate( self, prompt: str, *, - model: Optional[str] = None, - image_url: Optional[str] = None, - last_frame_url: Optional[str] = None, - reference_image_urls: Optional[List[str]] = None, - real_face_asset_id: Optional[str] = None, - duration_seconds: Optional[int] = None, - aspect_ratio: Optional[str] = None, - resolution: Optional[str] = None, - generate_audio: Optional[bool] = None, - seed: Optional[int] = None, - watermark: Optional[bool] = None, - return_last_frame: Optional[bool] = None, - input_type: Optional[str] = None, - budget_seconds: Optional[float] = None, + model: str | None = None, + image_url: str | None = None, + last_frame_url: str | None = None, + reference_image_urls: list[str] | None = None, + real_face_asset_id: str | None = None, + duration_seconds: int | None = None, + aspect_ratio: str | None = None, + resolution: str | None = None, + generate_audio: bool | None = None, + seed: int | None = None, + watermark: bool | None = None, + return_last_frame: bool | None = None, + input_type: str | None = None, + budget_seconds: float | None = None, ) -> VideoResponse: """ Generate a video clip from a text prompt (or text + image / face asset). @@ -247,7 +250,7 @@ def generate( ) validate_video_input_type(input_type) - body: Dict[str, Any] = { + body: dict[str, Any] = { "model": model or self.DEFAULT_MODEL, "prompt": prompt, } @@ -284,10 +287,10 @@ def generate( def generate_from_content( self, - content: List[Dict[str, Any]], + content: list[dict[str, Any]], *, - model: Optional[str] = None, - budget_seconds: Optional[float] = None, + model: str | None = None, + budget_seconds: float | None = None, **options: Any, ) -> VideoResponse: """ @@ -321,7 +324,7 @@ def generate_from_content( if not content: raise ValueError("content must be a non-empty list of Seedance content items.") - body: Dict[str, Any] = {"content": content, **options} + body: dict[str, Any] = {"content": content, **options} if model is not None: body["model"] = model @@ -336,7 +339,7 @@ def generate_from_content( def _submit_and_poll( self, - body: Dict[str, Any], + body: dict[str, Any], budget_seconds: float, submit_path: str = "/v1/videos/generations", ) -> VideoResponse: @@ -486,13 +489,13 @@ def _sign_from_challenge(self, resp402: httpx.Response, fallback_url: str) -> st ) def _absolute(self, url: str) -> str: - if url.startswith("http://") or url.startswith("https://"): + if url.startswith(("http://", "https://")): return url # self.api_url already ends without '/'; poll_url starts with '/api/...' - base = self.api_url[: -len("/api")] if self.api_url.endswith("/api") else self.api_url + base = self.api_url.removesuffix("/api") return f"{base}{url}" - def _extract_payment_required(self, resp: httpx.Response) -> Dict[str, Any]: + def _extract_payment_required(self, resp: httpx.Response) -> dict[str, Any]: header = resp.headers.get("payment-required") if header: return parse_payment_required(header) diff --git a/blockrun_llm/voice.py b/blockrun_llm/voice.py index db39288..8a9ded0 100644 --- a/blockrun_llm/voice.py +++ b/blockrun_llm/voice.py @@ -33,29 +33,32 @@ Pricing: $0.54 per outbound call (regardless of duration up to max_duration). """ +from __future__ import annotations + import os -from typing import Optional, Dict, Any, List +from typing import Any + import httpx -from eth_account import Account from dotenv import load_dotenv +from eth_account import Account +from .tx_log import paid_request_error_prefix from .types import APIError, PaymentError -from .x402 import create_payment_payload, parse_payment_required, extract_payment_details from .validation import ( - validate_private_key, - validate_api_url, sanitize_error_response, + validate_api_url, + validate_private_key, ) -from .tx_log import paid_request_error_prefix +from .x402 import create_payment_payload, extract_payment_details, parse_payment_required load_dotenv() # Built-in Bland.ai voice presets — any string accepted by Bland is also valid. -VOICE_PRESETS: List[str] = ["nat", "josh", "maya", "june", "paige", "derek", "florian"] +VOICE_PRESETS: list[str] = ["nat", "josh", "maya", "june", "paige", "derek", "florian"] # Bland.ai conversation models -CALL_MODELS: List[str] = ["base", "enhanced", "turbo"] +CALL_MODELS: list[str] = ["base", "enhanced", "turbo"] # Settled price per call (USD) CALL_PRICE_USD: float = 0.54 @@ -80,8 +83,8 @@ class VoiceClient: def __init__( self, - private_key: Optional[str] = None, - api_url: Optional[str] = None, + private_key: str | None = None, + api_url: str | None = None, timeout: float = 60.0, ): """ @@ -124,15 +127,15 @@ def call( to: str, task: str, *, - from_: Optional[str] = None, - voice: Optional[str] = None, + from_: str | None = None, + voice: str | None = None, max_duration: int = 5, language: str = "en-US", - first_sentence: Optional[str] = None, - wait_for_greeting: Optional[bool] = None, - interruption_threshold: Optional[int] = None, - model: Optional[str] = None, - ) -> Dict[str, Any]: + first_sentence: str | None = None, + wait_for_greeting: bool | None = None, + interruption_threshold: int | None = None, + model: str | None = None, + ) -> dict[str, Any]: """ Initiate an AI-powered outbound phone call. @@ -196,7 +199,7 @@ def call( if interruption_threshold is not None and not (50 <= interruption_threshold <= 500): raise ValueError("interruption_threshold must be between 50 and 500") - body: Dict[str, Any] = { + body: dict[str, Any] = { "to": to.strip(), "task": task.strip(), "max_duration": max_duration, @@ -217,7 +220,7 @@ def call( return self._request_with_payment("/v1/voice/call", body) - def get_status(self, call_id: str) -> Dict[str, Any]: + def get_status(self, call_id: str) -> dict[str, Any]: """ Poll the status of an in-progress or completed call. Free — no payment. @@ -256,7 +259,7 @@ def get_status(self, call_id: str) -> Dict[str, Any]: return response.json() - def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> Dict[str, Any]: + def _request_with_payment(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]: """Make a POST with automatic x402 payment handling.""" url = f"{self.api_url}{endpoint}" @@ -285,9 +288,9 @@ def _request_with_payment(self, endpoint: str, body: Dict[str, Any]) -> Dict[str def _handle_payment_and_retry( self, url: str, - body: Dict[str, Any], + body: dict[str, Any], response: httpx.Response, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle 402: parse requirements, sign payment, retry.""" payment_header = response.headers.get("payment-required") if not payment_header: diff --git a/blockrun_llm/wallet.py b/blockrun_llm/wallet.py index b6f5d23..0ca1f5d 100644 --- a/blockrun_llm/wallet.py +++ b/blockrun_llm/wallet.py @@ -13,7 +13,7 @@ import os import time from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING from eth_account import Account @@ -31,7 +31,7 @@ BASE_CHAIN_ID = "8453" -def create_wallet() -> Tuple[str, str]: +def create_wallet() -> tuple[str, str]: """ Create a new Ethereum wallet. @@ -59,7 +59,7 @@ def save_wallet(private_key: str) -> Path: return WALLET_FILE -def scan_wallets() -> List[Dict[str, str]]: +def scan_wallets() -> list[dict[str, str]]: """ Discover ~/./wallet.json files from other providers. @@ -73,7 +73,7 @@ def scan_wallets() -> List[Dict[str, str]]: for an address derived from the key. """ home = Path.home() - results: List[tuple] = [] # (mtime, private_key, address, source) + results: list[tuple] = [] # (mtime, private_key, address, source) try: for entry in home.iterdir(): @@ -99,7 +99,7 @@ def scan_wallets() -> List[Dict[str, str]]: return [{"private_key": pk, "address": addr, "source": src} for _, pk, addr, src in results] -def list_discovered_wallets() -> List[Dict[str, str]]: +def list_discovered_wallets() -> list[dict[str, str]]: """ List wallets from other applications, safe to show to a user. @@ -172,7 +172,7 @@ def import_wallet(address: str) -> str: ) -def load_wallet() -> Optional[str]: +def load_wallet() -> str | None: """ Load wallet private key from file. @@ -200,7 +200,7 @@ def load_wallet() -> Optional[str]: return None -def get_or_create_wallet() -> Tuple[str, str, bool]: +def get_or_create_wallet() -> tuple[str, str, bool]: """ Get existing wallet or create new one. @@ -235,7 +235,7 @@ def get_or_create_wallet() -> Tuple[str, str, bool]: return address, key, True -def get_wallet_address() -> Optional[str]: +def get_wallet_address() -> str | None: """ Get wallet address without exposing private key. @@ -299,9 +299,10 @@ def generate_wallet_qr_ascii(address: str) -> str: # Generate new QR try: - import qrcode from io import StringIO + import qrcode + qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, @@ -328,7 +329,7 @@ def generate_wallet_qr_ascii(address: str) -> str: return f"[QR code requires 'qrcode' package: pip install qrcode[pil]]\nAddress: {address}" -def save_wallet_qr(address: str, path: Optional[str] = None, with_logo: bool = True) -> str: +def save_wallet_qr(address: str, path: str | None = None, with_logo: bool = True) -> str: """ Save QR code as PNG image (EIP-681 format with optional Base logo). @@ -341,10 +342,11 @@ def save_wallet_qr(address: str, path: Optional[str] = None, with_logo: bool = T Path to saved QR image """ try: + import io + import urllib.request + import qrcode from PIL import Image - import urllib.request - import io # Use EIP-681 format for MetaMask compatibility eip681_uri = get_eip681_uri(address) @@ -404,8 +406,8 @@ def open_wallet_qr(address: str) -> str: Returns: Path to saved QR image """ - import subprocess import platform + import subprocess qr_path = save_wallet_qr(address) if qr_path: @@ -443,7 +445,7 @@ def get_payment_links(address: str) -> dict: } -def format_wallet_migration_notice(new_address: str) -> Optional[str]: +def format_wallet_migration_notice(new_address: str) -> str | None: """ Warn when a new wallet was created while other provider wallets exist. @@ -606,7 +608,7 @@ def format_funding_message_compact(address: str) -> str: Check my balance: {links['basescan']}""" -def setup_agent_wallet(silent: bool = False) -> "LLMClient": +def setup_agent_wallet(silent: bool = False) -> LLMClient: """ Set up wallet for agent use and return an LLMClient. diff --git a/blockrun_llm/x402.py b/blockrun_llm/x402.py index 97656a4..491f75a 100644 --- a/blockrun_llm/x402.py +++ b/blockrun_llm/x402.py @@ -5,15 +5,17 @@ The private key is used ONLY for local signing and NEVER leaves the client. """ -import json -import time +from __future__ import annotations + import base64 +import json import secrets -from typing import Dict, Any, Optional +import time +from typing import Any + from eth_account import Account from eth_account.messages import encode_typed_data - # Chain and token constants for mainnet BASE_CHAIN_ID = 8453 USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" @@ -30,15 +32,15 @@ def with_builder_code_service_code( - extensions: Optional[Dict[str, Any]], -) -> Dict[str, Any]: + extensions: dict[str, Any] | None, +) -> dict[str, Any]: """Merge BlockRun's service code (``s``) into the payload's ``builder-code`` extension, preserving any app code (``a``) the server echoed back in its 402. The CDP facilitator reads ``builder-code.info.s`` and encodes it into the settlement calldata suffix — no CBOR/encoding happens client-side. """ - merged: Dict[str, Any] = dict(extensions or {}) + merged: dict[str, Any] = dict(extensions or {}) existing = dict(merged.get("builder-code") or {}) info = dict(existing.get("info") or {}) info["s"] = [BLOCKRUN_SERVICE_CODE] @@ -93,9 +95,9 @@ def create_payment_payload( resource_url: str = "https://blockrun.ai/api/v1/chat/completions", resource_description: str = "BlockRun AI API call", max_timeout_seconds: int = 300, - extra: Optional[Dict[str, str]] = None, - extensions: Optional[Dict[str, Any]] = None, - asset: Optional[str] = None, + extra: dict[str, str] | None = None, + extensions: dict[str, Any] | None = None, + asset: str | None = None, ) -> str: """ Create a signed x402 v2 payment payload. @@ -205,7 +207,7 @@ def create_payment_payload( return base64.b64encode(json.dumps(payment_data).encode()).decode() -def parse_payment_required(header_value: str) -> Dict[str, Any]: +def parse_payment_required(header_value: str) -> dict[str, Any]: """ Parse the X-Payment-Required header from a 402 response. @@ -223,7 +225,7 @@ def parse_payment_required(header_value: str) -> Dict[str, Any]: raise ValueError("Failed to parse payment required header: invalid format") -def extract_payment_details(payment_required: Dict[str, Any]) -> Dict[str, Any]: +def extract_payment_details(payment_required: dict[str, Any]) -> dict[str, Any]: """ Extract payment details from parsed payment required response. diff --git a/examples/arbitrage_analyzer.py b/examples/arbitrage_analyzer.py index 888add6..477fec8 100644 --- a/examples/arbitrage_analyzer.py +++ b/examples/arbitrage_analyzer.py @@ -13,7 +13,8 @@ """ from dataclasses import dataclass -from blockrun_llm import LLMClient, AsyncLLMClient, PaymentError, APIError + +from blockrun_llm import APIError, AsyncLLMClient, LLMClient, PaymentError @dataclass diff --git a/examples/benchmark_claude.py b/examples/benchmark_claude.py old mode 100644 new mode 100755 index 5b0bcba..cfd9b24 --- a/examples/benchmark_claude.py +++ b/examples/benchmark_claude.py @@ -43,7 +43,7 @@ import time from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any SOLANA_API_URL = "https://sol.blockrun.ai/api" BASE_API_URL = "https://blockrun.ai/api" @@ -55,7 +55,7 @@ ) -def _percentile(values: List[float], pct: float) -> float: +def _percentile(values: list[float], pct: float) -> float: """Nearest-rank percentile (pct in [0,100]). Empty → nan.""" if not values: return float("nan") @@ -85,8 +85,8 @@ def _count_tokens(text: str, model_hint: str = "") -> int: @dataclass class ReqResult: ok: bool - ttft: Optional[float] = None # seconds to first content token - latency: Optional[float] = None # seconds request → last token + ttft: float | None = None # seconds to first content token + latency: float | None = None # seconds request → last token out_tokens: int = 0 error: str = "" @@ -100,8 +100,8 @@ class Bench: concurrency: int prompt: str max_tokens: int - private_key: Optional[str] = None - results: List[ReqResult] = field(default_factory=list) + private_key: str | None = None + results: list[ReqResult] = field(default_factory=list) def _client(self): if self.chain == "solana": @@ -122,8 +122,8 @@ def _client(self): def _one_streaming(self, client) -> ReqResult: messages = [{"role": "user", "content": self.prompt}] start = time.perf_counter() - ttft: Optional[float] = None - text_parts: List[str] = [] + ttft: float | None = None + text_parts: list[str] = [] try: for chunk in client.chat_completion_stream( model=self.model, messages=messages, max_tokens=self.max_tokens @@ -139,7 +139,7 @@ def _one_streaming(self, client) -> ReqResult: latency = time.perf_counter() - start out = _count_tokens("".join(text_parts), self.model) return ReqResult(ok=True, ttft=ttft, latency=latency, out_tokens=out) - except Exception as exc: # noqa: BLE001 - benchmark records, never crashes + except Exception as exc: return ReqResult(ok=False, error=f"{type(exc).__name__}: {exc}") def run_throughput_phase(self) -> float: @@ -180,7 +180,7 @@ def cache_probe(self) -> float: usage = getattr(resp, "usage", None) if usage is None: return 0.0 - u: Dict[str, Any] = ( + u: dict[str, Any] = ( usage.model_dump(exclude_none=True) if hasattr(usage, "model_dump") else dict(usage) ) prompt_tokens = u.get("prompt_tokens") or 0 @@ -198,7 +198,7 @@ def cache_probe(self) -> float: return 100.0 * cached / prompt_tokens return 0.0 - def report(self, wall: float, cache_hit: Optional[float]) -> None: + def report(self, wall: float, cache_hit: float | None) -> None: ok = [r for r in self.results if r.ok] ttfts = [r.ttft for r in ok if r.ttft is not None] lats = [r.latency for r in ok if r.latency is not None] @@ -209,7 +209,7 @@ def report(self, wall: float, cache_hit: Optional[float]) -> None: succ = 100.0 * len(ok) / self.requests if self.requests else 0.0 def fmt(x: float) -> str: - return "nan" if x != x else f"{x:.3f}" # x!=x → NaN + return "nan" if x != x else f"{x:.3f}" # noqa: PLR0124 — x!=x is the NaN test print("\n" + "=" * 56) print(f" Claude E2E benchmark — {self.model} ({self.chain})") @@ -280,7 +280,7 @@ def main() -> None: print("[benchmark] cache probe (2 non-streaming calls) …") try: cache_hit = bench.cache_probe() - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f"[benchmark] cache probe failed (→ 0): {type(exc).__name__}: {exc}") cache_hit = 0.0 bench.report(wall, cache_hit) diff --git a/examples/sweep_all_chat_models.py b/examples/sweep_all_chat_models.py index ea20c66..a0914a0 100644 --- a/examples/sweep_all_chat_models.py +++ b/examples/sweep_all_chat_models.py @@ -25,21 +25,20 @@ import sys import time from dataclasses import asdict, dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any import httpx from blockrun_llm import AsyncLLMClient, LLMClient from blockrun_llm.types import APIError, PaymentError - # --------------------------------------------------------------------------- # Sweep targets — hardcoded so we also probe hidden / retired model ids that # the /v1/models endpoint deliberately omits. Mutually-exclusive groups, in # the order the report displays them. # --------------------------------------------------------------------------- -SWEEP_TARGETS: List[str] = [ +SWEEP_TARGETS: list[str] = [ # OpenAI "openai/gpt-5.5", "openai/gpt-5.4", @@ -157,16 +156,16 @@ class ProbeResult: provider: str status: str latency_ms: int - tokens_in: Optional[int] = None - tokens_out: Optional[int] = None - tokens_total: Optional[int] = None + tokens_in: int | None = None + tokens_out: int | None = None + tokens_total: int | None = None cost_delta_usd: float = 0.0 - expected_cost_usd: Optional[float] = None - cost_drift_pct: Optional[float] = None - redirected_to: Optional[str] = None + expected_cost_usd: float | None = None + cost_drift_pct: float | None = None + redirected_to: str | None = None response_preview: str = "" contains_4: bool = False - error_message: Optional[str] = None + error_message: str | None = None timestamp: float = field(default_factory=time.time) @@ -253,7 +252,7 @@ def preflight() -> LLMClient: # --------------------------------------------------------------------------- -def forward_compat_check(client: LLMClient) -> Dict[str, Dict[str, Any]]: +def forward_compat_check(client: LLMClient) -> dict[str, dict[str, Any]]: print(">>> Forward-compat check vs /v1/models") try: listed_raw = client.list_models() @@ -262,7 +261,7 @@ def forward_compat_check(client: LLMClient) -> Dict[str, Dict[str, Any]]: print() return {} - listed_chat: Dict[str, Dict[str, Any]] = {} + listed_chat: dict[str, dict[str, Any]] = {} for m in listed_raw: cats = m.get("categories") if cats is None or "chat" in cats: @@ -300,7 +299,7 @@ def forward_compat_check(client: LLMClient) -> Dict[str, Dict[str, Any]]: def probe_one( client: LLMClient, model_id: str, - pricing: Dict[str, Dict[str, Any]], + pricing: dict[str, dict[str, Any]], ) -> ProbeResult: provider = provider_of(model_id) max_toks = PROBE_MAX_TOKENS_REASONING if model_id in REASONING_MODELS else PROBE_MAX_TOKENS @@ -440,11 +439,11 @@ def probe_one( def run_sweep( client: LLMClient, - targets: List[str], + targets: list[str], args: argparse.Namespace, - pricing: Dict[str, Dict[str, Any]], -) -> List[ProbeResult]: - results: List[ProbeResult] = [] + pricing: dict[str, dict[str, Any]], +) -> list[ProbeResult]: + results: list[ProbeResult] = [] n = len(targets) warned = False @@ -496,7 +495,7 @@ def run_sweep( # --------------------------------------------------------------------------- -async def _async_probe(client: AsyncLLMClient, model_id: str) -> Dict[str, Any]: +async def _async_probe(client: AsyncLLMClient, model_id: str) -> dict[str, Any]: t0 = time.monotonic() try: response = await client.chat_completion( @@ -528,13 +527,13 @@ async def _async_probe(client: AsyncLLMClient, model_id: str) -> Dict[str, Any]: } -async def _async_smoke() -> List[Dict[str, Any]]: +async def _async_smoke() -> list[dict[str, Any]]: async with AsyncLLMClient() as client: coros = [_async_probe(client, m) for m in ASYNC_SMOKE_MODELS] return await asyncio.gather(*coros) -def run_async_smoke() -> List[Dict[str, Any]]: +def run_async_smoke() -> list[dict[str, Any]]: print(">>> Async smoke (asyncio.gather over 3 models)") t0 = time.monotonic() results = asyncio.run(_async_smoke()) @@ -559,8 +558,8 @@ def run_async_smoke() -> List[Dict[str, Any]]: def report( client: LLMClient, - results: List[ProbeResult], - async_results: Optional[List[Dict[str, Any]]], + results: list[ProbeResult], + async_results: list[dict[str, Any]] | None, started_at: float, args: argparse.Namespace, ) -> bool: @@ -586,7 +585,7 @@ def report( print() print(">>> Provider summary") - by_provider: Dict[str, List[ProbeResult]] = {} + by_provider: dict[str, list[ProbeResult]] = {} for r in results: by_provider.setdefault(r.provider, []).append(r) for provider in sorted(by_provider): @@ -680,7 +679,7 @@ def main() -> int: results = run_sweep(client, targets, args, listed) - async_results: Optional[List[Dict[str, Any]]] = None + async_results: list[dict[str, Any]] | None = None if not args.skip_async: async_results = run_async_smoke() diff --git a/examples/sweep_all_media_models.py b/examples/sweep_all_media_models.py index e8d6ceb..82d7fb7 100644 --- a/examples/sweep_all_media_models.py +++ b/examples/sweep_all_media_models.py @@ -23,15 +23,14 @@ import sys import time from dataclasses import asdict, dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any import httpx from blockrun_llm import ImageClient, LLMClient, MusicClient from blockrun_llm.types import APIError, PaymentError - -IMAGE_TARGETS: List[Dict[str, Any]] = [ +IMAGE_TARGETS: list[dict[str, Any]] = [ # Each entry: model_id + size override if model has a constrained set. {"model": "google/nano-banana", "size": "1024x1024"}, {"model": "google/nano-banana-pro", "size": "1024x1024"}, @@ -43,7 +42,7 @@ {"model": "xai/grok-imagine-image-pro", "size": "1024x1024"}, ] -MUSIC_TARGETS: List[str] = [ +MUSIC_TARGETS: list[str] = [ "minimax/music-2.5+", "minimax/music-2.5", ] @@ -59,8 +58,8 @@ class ProbeResult: status: str # ok / http_error / timeout / payment_error / unexpected latency_ms: int cost_delta_usd: float = 0.0 - artifact_url: Optional[str] = None # first asset URL/data preview - error_message: Optional[str] = None + artifact_url: str | None = None # first asset URL/data preview + error_message: str | None = None timestamp: float = field(default_factory=time.time) @@ -95,8 +94,8 @@ def preview_url(url: str, max_len: int = 60) -> str: def probe_image( client: ImageClient, - target: Dict[str, Any], - pricing: Dict[str, float], + target: dict[str, Any], + pricing: dict[str, float], ) -> ProbeResult: model_id = target["model"] t0 = time.monotonic() @@ -153,7 +152,7 @@ def probe_image( def probe_music( client: MusicClient, model_id: str, - pricing: Dict[str, float], + pricing: dict[str, float], ) -> ProbeResult: t0 = time.monotonic() try: @@ -236,8 +235,8 @@ def preflight() -> tuple: # Image + music pricing both come from /v1/models filtered by category; # the legacy /v1/images/models endpoint currently returns 404 server-side # (2026-05-09), so don't rely on it. - image_pricing: Dict[str, float] = {} - music_pricing: Dict[str, float] = {} + image_pricing: dict[str, float] = {} + music_pricing: dict[str, float] = {} try: for m in llm.list_models(): mid = m.get("id", "") @@ -262,13 +261,13 @@ def preflight() -> tuple: def run_image_sweep( client: ImageClient, - pricing: Dict[str, float], + pricing: dict[str, float], args: argparse.Namespace, spent_so_far: float = 0.0, -) -> List[ProbeResult]: +) -> list[ProbeResult]: print(f">>> Image sweep ({len(IMAGE_TARGETS)} models)") print() - results: List[ProbeResult] = [] + results: list[ProbeResult] = [] n = len(IMAGE_TARGETS) warned = False spent = spent_so_far @@ -306,14 +305,14 @@ def run_image_sweep( def run_music_sweep( - pricing: Dict[str, float], + pricing: dict[str, float], args: argparse.Namespace, spent_so_far: float = 0.0, -) -> List[ProbeResult]: +) -> list[ProbeResult]: print(f">>> Music sweep ({len(MUSIC_TARGETS)} models)") print() client = MusicClient() - results: List[ProbeResult] = [] + results: list[ProbeResult] = [] n = len(MUSIC_TARGETS) spent = spent_so_far for i, model_id in enumerate(MUSIC_TARGETS, start=1): @@ -347,7 +346,7 @@ def run_music_sweep( def report( - results: List[ProbeResult], + results: list[ProbeResult], started_at: float, args: argparse.Namespace, initial_balance: float, @@ -365,7 +364,7 @@ def report( print() print(">>> Modality summary") - by_mod: Dict[str, List[ProbeResult]] = {} + by_mod: dict[str, list[ProbeResult]] = {} for r in results: by_mod.setdefault(r.modality, []).append(r) for modality in sorted(by_mod): @@ -421,7 +420,7 @@ def main() -> int: started_at = time.monotonic() image_client, image_pricing, music_pricing, initial_balance = preflight() - results: List[ProbeResult] = [] + results: list[ProbeResult] = [] spent = 0.0 if not args.skip_image: image_results = run_image_sweep(image_client, image_pricing, args, spent) diff --git a/pyproject.toml b/pyproject.toml index 0cd7b9e..db82f26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dev = [ "pytest-asyncio>=0.21.0", "black==24.10.0", # Pin version for consistent formatting "mypy>=1.0.0", - "ruff==0.14.11", # Pin version: an unpinned linter breaks CI with no code change + "ruff==0.16.0", # Pin version: an unpinned linter breaks CI with no code change ] anthropic = [ "anthropic>=0.40.0", @@ -76,6 +76,34 @@ target-version = ["py39"] line-length = 100 target-version = "py39" +[tool.ruff.lint] +# Deferred, NOT blessed. Each of these is a behaviour change in a payments SDK +# and belongs in its own reviewable PR, not folded into a typing sweep: +# +# BLE001 157 blind `except Exception` — several are deliberate best-effort +# paths (telemetry, cleanup) where raising would be worse than +# swallowing. Which ones are deliberate has to be read case by case. +# S110/ 54 try/except/pass and try/except/continue — same question. +# S112 +# TRY004 8 raising something other than TypeError on a type check. +# DTZ005/ 4 naive datetime.now()/fromtimestamp() in cache.py, tx_log.py and +# DTZ006 wallet.py. Worth fixing — a transaction log without timezone is +# genuinely ambiguous — but it changes recorded values, so it needs +# its own change and its own migration thought. +# RUF012 2 mutable class defaults, both in examples/ and tests/. +ignore = ["BLE001", "S110", "S112", "TRY004", "DTZ005", "DTZ006", "RUF012"] + [tool.mypy] python_version = "3.9" strict = true + +[tool.ruff.lint.per-file-ignores] +# pydantic EVALUATES annotations at runtime to build each model. Under +# `from __future__ import annotations` they are strings, and on Python 3.9 +# evaluating "str | None" is a TypeError — PEP 604 does not exist there. +# +# Parsing is not the constraint; evaluation is. compileall passes on 3.9 and +# the import still fails, which is exactly how this got shipped to CI once. +# +# So this file keeps typing.Optional/List and no future import. +"blockrun_llm/types.py" = ["FA100", "UP006", "UP007", "UP035", "UP045"] diff --git a/tests/helpers.py b/tests/helpers.py index c394c78..990a209 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,9 +2,12 @@ Test utilities and mock builders for BlockRun LLM SDK tests. """ -import json +from __future__ import annotations + import base64 -from typing import Dict, Any, Optional +import json +from typing import Any + from eth_account import Account # Test private key (DO NOT use in production) @@ -23,7 +26,7 @@ def build_payment_required_response( amount: str = "1000000", recipient: str = TEST_RECIPIENT, network: str = "eip155:8453", - resource: Optional[Dict[str, str]] = None, + resource: dict[str, str] | None = None, ) -> str: """Build a mock 402 Payment Required response.""" payment_required = { @@ -54,7 +57,7 @@ def build_chat_response( model: str = "gpt-5.2", prompt_tokens: int = 10, completion_tokens: int = 20, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Build a mock successful chat response.""" return { "id": "chatcmpl-test123", @@ -80,7 +83,7 @@ def build_error_response( error: str = "Test error message", code: str = "test_error", include_sensitive: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Build a mock error response.""" response = {"error": error, "code": code} @@ -97,7 +100,7 @@ def build_error_response( return response -def build_models_response() -> Dict[str, Any]: +def build_models_response() -> dict[str, Any]: """Build a mock models list response.""" return { "data": [ @@ -132,16 +135,16 @@ class MockResponse: def __init__( self, status_code: int, - json_data: Optional[Dict[str, Any]] = None, - text_data: Optional[str] = None, - headers: Optional[Dict[str, str]] = None, + json_data: dict[str, Any] | None = None, + text_data: str | None = None, + headers: dict[str, str] | None = None, ): self.status_code = status_code self._json_data = json_data self._text_data = text_data or (json.dumps(json_data) if json_data else "") self.headers = headers or {} - def json(self) -> Dict[str, Any]: + def json(self) -> dict[str, Any]: if self._json_data is None: raise ValueError("No JSON data available") return self._json_data diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index bf40f6f..2e2aee5 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,6 +1,7 @@ """Pytest configuration for integration tests.""" import os + import pytest diff --git a/tests/integration/test_production_api.py b/tests/integration/test_production_api.py index 88feba2..1ca60c5 100644 --- a/tests/integration/test_production_api.py +++ b/tests/integration/test_production_api.py @@ -14,7 +14,8 @@ import time import pytest -from blockrun_llm import LLMClient, AsyncLLMClient + +from blockrun_llm import AsyncLLMClient, LLMClient WALLET_KEY = os.environ.get("BASE_CHAIN_WALLET_KEY") PRODUCTION_API = "https://blockrun.ai/api" diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 883399c..65b549c 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,13 +1,16 @@ """Unit tests for LLMClient.""" -import pytest from unittest.mock import Mock, patch -from blockrun_llm import LLMClient, APIError + +import pytest + +from blockrun_llm import APIError, LLMClient + from ..helpers import ( TEST_PRIVATE_KEY, + MockResponse, build_error_response, build_models_response, - MockResponse, ) diff --git a/tests/unit/test_cost_log.py b/tests/unit/test_cost_log.py index 4360acd..40a41bd 100644 --- a/tests/unit/test_cost_log.py +++ b/tests/unit/test_cost_log.py @@ -16,8 +16,7 @@ def _write_log(path, rows): with open(path, "w") as f: - for row in rows: - f.write(json.dumps(row) + "\n") + f.writelines(json.dumps(row) + "\n" for row in rows) @pytest.fixture diff --git a/tests/unit/test_image_edit.py b/tests/unit/test_image_edit.py index 9ac8fe4..2e65122 100644 --- a/tests/unit/test_image_edit.py +++ b/tests/unit/test_image_edit.py @@ -11,7 +11,6 @@ from __future__ import annotations import json -from typing import List import httpx @@ -22,7 +21,7 @@ DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC" -def _image_edit_transport(calls: List[httpx.Request]) -> httpx.MockTransport: +def _image_edit_transport(calls: list[httpx.Request]) -> httpx.MockTransport: """First POST → 402 with payment requirements; retry with signature → 200.""" def handler(request: httpx.Request) -> httpx.Response: @@ -48,14 +47,14 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.MockTransport(handler) -def _make_client(calls: List[httpx.Request]) -> ImageClient: +def _make_client(calls: list[httpx.Request]) -> ImageClient: client = ImageClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client(transport=_image_edit_transport(calls)) return client def test_edit_single_image_passes_string_through(): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(calls) result = client.edit("Make the sky purple", image=DATA_URI) @@ -72,7 +71,7 @@ def test_edit_single_image_passes_string_through(): def test_edit_defaults_to_gpt_image_2(): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(calls) client.edit("Make the sky purple", image=DATA_URI) @@ -83,7 +82,7 @@ def test_edit_defaults_to_gpt_image_2(): def test_edit_multi_image_passes_list_through(): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(calls) images = [DATA_URI, DATA_URI] diff --git a/tests/unit/test_image_poll.py b/tests/unit/test_image_poll.py index 5d2c74f..acc897d 100644 --- a/tests/unit/test_image_poll.py +++ b/tests/unit/test_image_poll.py @@ -13,8 +13,6 @@ from __future__ import annotations -from typing import List - import httpx import pytest @@ -47,7 +45,7 @@ def test_image_generate_polls_to_completion_on_202(monkeypatch: pytest.MonkeyPat status=completed, then return the image.""" monkeypatch.setattr(ImageClient, "IMAGE_POLL_INTERVAL_SECONDS", 0.0) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] poll_state = {"count": 0} def handler(request: httpx.Request) -> httpx.Response: @@ -210,7 +208,7 @@ def test_image_generate_fast_path_unchanged(monkeypatch: pytest.MonkeyPatch) -> identically — the poll path is only entered on 202.""" monkeypatch.setattr(ImageClient, "IMAGE_POLL_INTERVAL_SECONDS", 0.0) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) diff --git a/tests/unit/test_invalid_message_fail_fast.py b/tests/unit/test_invalid_message_fail_fast.py index 9aa29fb..d53f17f 100644 --- a/tests/unit/test_invalid_message_fail_fast.py +++ b/tests/unit/test_invalid_message_fail_fast.py @@ -15,8 +15,8 @@ from typing import Any from blockrun_llm.solana_client import ( - _is_unrecoverable_payment_error, _is_permanent_payment_error, + _is_unrecoverable_payment_error, ) from blockrun_llm.validation import build_payment_rejected_error diff --git a/tests/unit/test_passthrough_defi_dex_modal.py b/tests/unit/test_passthrough_defi_dex_modal.py index 17ba20a..a7ab409 100644 --- a/tests/unit/test_passthrough_defi_dex_modal.py +++ b/tests/unit/test_passthrough_defi_dex_modal.py @@ -1,6 +1,7 @@ """Unit tests for the DefiLlama / 0x DEX / Modal passthrough methods.""" import os + import pytest from blockrun_llm import LLMClient diff --git a/tests/unit/test_payment_error_helper.py b/tests/unit/test_payment_error_helper.py index db1f75d..945f5ea 100644 --- a/tests/unit/test_payment_error_helper.py +++ b/tests/unit/test_payment_error_helper.py @@ -8,8 +8,7 @@ from __future__ import annotations -from typing import Any, Dict - +from typing import Any from blockrun_llm.types import PaymentError from blockrun_llm.validation import build_payment_rejected_error @@ -55,7 +54,7 @@ class TestBuildPaymentRejectedError: def test_preserves_gateway_details(self) -> None: """The whole reason this helper exists: ``details`` must survive from the gateway's body to ``exc.response`` and into ``str(exc)``.""" - gateway_body: Dict[str, Any] = { + gateway_body: dict[str, Any] = { "error": "Payment settlement failed", "details": "transaction_simulation_failed", } diff --git a/tests/unit/test_portrait.py b/tests/unit/test_portrait.py index 6b3b7e6..3d2b8bc 100644 --- a/tests/unit/test_portrait.py +++ b/tests/unit/test_portrait.py @@ -1,6 +1,7 @@ """Unit tests for PortraitClient input validation.""" import os + import pytest from blockrun_llm import PortraitClient diff --git a/tests/unit/test_realface.py b/tests/unit/test_realface.py index 3e60a4f..5719ff1 100644 --- a/tests/unit/test_realface.py +++ b/tests/unit/test_realface.py @@ -1,6 +1,7 @@ """Unit tests for RealFaceClient input validation.""" import os + import pytest from blockrun_llm import RealFaceClient diff --git a/tests/unit/test_rpc.py b/tests/unit/test_rpc.py index c4f6f1d..ba56729 100644 --- a/tests/unit/test_rpc.py +++ b/tests/unit/test_rpc.py @@ -1,10 +1,11 @@ """Unit tests for RpcClient request construction and response parsing.""" import os + import httpx import pytest -from blockrun_llm import RpcClient, RpcResponse, SUPPORTED_NETWORKS, NETWORK_ALIASES +from blockrun_llm import NETWORK_ALIASES, SUPPORTED_NETWORKS, RpcClient, RpcResponse @pytest.fixture diff --git a/tests/unit/test_solana_client.py b/tests/unit/test_solana_client.py index c427c63..9778aec 100644 --- a/tests/unit/test_solana_client.py +++ b/tests/unit/test_solana_client.py @@ -1,8 +1,10 @@ """Unit tests for SolanaLLMClient.""" -import pytest import os -from blockrun_llm.solana_client import SolanaLLMClient, AsyncSolanaLLMClient + +import pytest + +from blockrun_llm.solana_client import AsyncSolanaLLMClient, SolanaLLMClient TEST_BS58_KEY = ( "433C7KFcM4y1ZEVdZYSH7wheSNAM384UcbgXEyD5FV7Q2HsQ1BwjEDx4GbBZUqPkZTVhFPyLyuZnzK8wCeAkU7wG" diff --git a/tests/unit/test_solana_max_tokens.py b/tests/unit/test_solana_max_tokens.py index d8d0464..614a87d 100644 --- a/tests/unit/test_solana_max_tokens.py +++ b/tests/unit/test_solana_max_tokens.py @@ -12,7 +12,7 @@ from __future__ import annotations -import unittest.mock as mock +from unittest import mock import httpx import pytest @@ -20,8 +20,8 @@ pytest.importorskip("x402") pytest.importorskip("solders") -from blockrun_llm import SolanaLLMClient # noqa: E402 -from blockrun_llm.validation import MAX_TOKENS_SANITY_LIMIT # noqa: E402 +from blockrun_llm import SolanaLLMClient +from blockrun_llm.validation import MAX_TOKENS_SANITY_LIMIT MESSAGES = [{"role": "user", "content": "hi"}] diff --git a/tests/unit/test_solana_media.py b/tests/unit/test_solana_media.py index 92e06f3..4d618a5 100644 --- a/tests/unit/test_solana_media.py +++ b/tests/unit/test_solana_media.py @@ -10,7 +10,7 @@ from __future__ import annotations from types import SimpleNamespace -from typing import Any, Dict, List +from typing import Any from unittest import mock import httpx @@ -22,19 +22,18 @@ pytest.importorskip("x402") pytest.importorskip("solders") -from blockrun_llm.solana_client import ( # noqa: E402 +from blockrun_llm.solana_client import ( AsyncSolanaLLMClient, SolanaLLMClient, _assert_same_payment_terms, ) -from blockrun_llm.types import ( # noqa: E402 +from blockrun_llm.types import ( APIError, MusicResponse, PaymentError, SpeechResponse, ) - # --------------------------------------------------------------------------- # _assert_same_payment_terms — the mid-poll re-sign guard # --------------------------------------------------------------------------- @@ -109,7 +108,7 @@ class accepted: return client -def _paid_flow(calls: List[httpx.Request], ok_body: Dict[str, Any]): +def _paid_flow(calls: list[httpx.Request], ok_body: dict[str, Any]): """402 on the unsigned probe, then ``ok_body`` once signed. Captures the signed request so tests can assert the forwarded JSON body + path.""" @@ -138,7 +137,7 @@ class TestMediaDispatch: def test_music_body_and_response(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _MUSIC_OK)) resp = client.music("lo-fi beats") assert isinstance(resp, MusicResponse) @@ -151,7 +150,7 @@ def test_music_body_and_response(self) -> None: def test_speech_body_and_response(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _SPEECH_OK)) resp = client.speech("hello world", voice="sarah") assert isinstance(resp, SpeechResponse) @@ -162,7 +161,7 @@ def test_speech_body_and_response(self) -> None: assert sent["voice"] == "sarah" def test_sound_effect_endpoint(self) -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _SPEECH_OK)) client.sound_effect("thunder clap") assert calls[-1].url.path == "/api/v1/audio/sound-effects" @@ -305,7 +304,7 @@ class accepted: return client -def _resign_handler(signed_poll_codes: List[int]): +def _resign_handler(signed_poll_codes: list[int]): """Drive a video job through the mid-poll re-sign path. probe → 402; signed POST → 202 + poll_url; each *signed* GET poll returns @@ -346,7 +345,7 @@ def handler(request: httpx.Request) -> httpx.Response: return handler -_HELPER_KW: Dict[str, Any] = { +_HELPER_KW: dict[str, Any] = { "poll_budget_seconds": 5.0, "poll_interval_seconds": 0.001, "max_resigns": 2, @@ -413,7 +412,7 @@ async def test_async_resign_reprice_propagates(self, monkeypatch: pytest.MonkeyP # --------------------------------------------------------------------------- -def _fresh_sig_handler(n_in_progress: int, poll_sigs: List[str]): +def _fresh_sig_handler(n_in_progress: int, poll_sigs: list[str]): """Video job that NEVER 402s on a poll: n_in_progress in-progress polls, then completed. Records the PAYMENT-SIGNATURE seen on every signed poll so a test can assert the proactive re-sign refreshed it each time.""" @@ -464,7 +463,7 @@ def test_sync_refreshes_signature_every_poll(self, monkeypatch: pytest.MonkeyPat # Fire the proactive re-sign on every poll (0s freshness window). monkeypatch.setattr(SolanaLLMClient, "MEDIA_RESIGN_FRESH_SECONDS", 0.0) - poll_sigs: List[str] = [] + poll_sigs: list[str] = [] client = _make_client(_fresh_sig_handler(3, poll_sigs)) data = client._request_image_with_payment( "/v1/videos/generations", dict(_VIDEO_BODY), **_HELPER_KW @@ -489,7 +488,7 @@ async def test_async_refreshes_signature_every_poll( ) monkeypatch.setattr(SolanaLLMClient, "MEDIA_RESIGN_FRESH_SECONDS", 0.0) - poll_sigs: List[str] = [] + poll_sigs: list[str] = [] client = _make_async_client(_fresh_sig_handler(3, poll_sigs)) try: data = await client._request_image_with_payment( @@ -504,7 +503,7 @@ async def test_async_refreshes_signature_every_poll( # max_resigns == 0 (the image path) must NOT proactively re-sign, even with a # 0s freshness window: every poll reuses the single submit-time signature so # the image flow is provably untouched by the video-only fix. - _IMAGE_KW: Dict[str, Any] = { + _IMAGE_KW: dict[str, Any] = { "poll_budget_seconds": 5.0, "poll_interval_seconds": 0.001, "max_resigns": 0, @@ -521,7 +520,7 @@ def test_sync_image_path_never_resigns(self, monkeypatch: pytest.MonkeyPatch) -> ) monkeypatch.setattr(SolanaLLMClient, "MEDIA_RESIGN_FRESH_SECONDS", 0.0) - poll_sigs: List[str] = [] + poll_sigs: list[str] = [] client = _make_client(_fresh_sig_handler(3, poll_sigs)) data = client._request_image_with_payment( "/v1/images/generations", dict(_VIDEO_BODY), **self._IMAGE_KW @@ -543,7 +542,7 @@ async def test_async_image_path_never_resigns(self, monkeypatch: pytest.MonkeyPa ) monkeypatch.setattr(SolanaLLMClient, "MEDIA_RESIGN_FRESH_SECONDS", 0.0) - poll_sigs: List[str] = [] + poll_sigs: list[str] = [] client = _make_async_client(_fresh_sig_handler(3, poll_sigs)) try: data = await client._request_image_with_payment( @@ -575,7 +574,7 @@ class TestSolanaImageQuality: def test_image_forwards_quality(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _IMAGE_OK)) client.image("a cat", model="openai/gpt-image-2", quality="low") sent = json.loads(calls[-1].content) @@ -584,7 +583,7 @@ def test_image_forwards_quality(self) -> None: def test_image_omits_quality_when_unset(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _IMAGE_OK)) client.image("a cat") assert "quality" not in json.loads(calls[-1].content) @@ -592,7 +591,7 @@ def test_image_omits_quality_when_unset(self) -> None: def test_image_edit_forwards_quality(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _IMAGE_OK)) client.image_edit("make it green", _DATA_URI, quality="high") sent = json.loads(calls[-1].content) @@ -603,20 +602,20 @@ def test_image_edit_forwards_quality(self) -> None: def test_image_accepts_every_gateway_quality(self, value: str) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _IMAGE_OK)) client.image("a cat", model="openai/gpt-image-2", quality=value) assert json.loads(calls[-1].content)["quality"] == value def test_image_rejects_unknown_quality_before_paying(self) -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _IMAGE_OK)) with pytest.raises(ValueError, match="quality must be one of"): client.image("a cat", model="openai/gpt-image-2", quality="hd") assert calls == [] # rejected locally — no request, no payment def test_image_edit_rejects_unknown_quality_before_paying(self) -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _IMAGE_OK)) with pytest.raises(ValueError, match="quality must be one of"): client.image_edit("make it green", _DATA_URI, quality="ultra") @@ -627,7 +626,7 @@ class TestSolanaVideoInputType: def test_video_forwards_input_type(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _VIDEO_OK)) client.video( "the flower blooms", @@ -640,13 +639,13 @@ def test_video_forwards_input_type(self) -> None: def test_video_omits_input_type_when_unset(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _VIDEO_OK)) client.video("a calm lake") assert "input_type" not in json.loads(calls[-1].content) def test_video_rejects_unknown_input_type_before_paying(self) -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_paid_flow(calls, _VIDEO_OK)) with pytest.raises(ValueError, match="input_type must be one of"): client.video("x", input_type="img") @@ -686,14 +685,14 @@ class TestAsyncMediaParamParity: async def test_async_video_forwards_input_type(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_paid_flow(calls, _VIDEO_OK)) await client.video("a calm lake", input_type="text") assert json.loads(calls[-1].content)["input_type"] == "text" @pytest.mark.asyncio async def test_async_video_rejects_unknown_input_type_before_paying(self) -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_paid_flow(calls, _VIDEO_OK)) with pytest.raises(ValueError, match="input_type must be one of"): await client.video("x", input_type="img") @@ -703,7 +702,7 @@ async def test_async_video_rejects_unknown_input_type_before_paying(self) -> Non async def test_async_image_forwards_quality(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_paid_flow(calls, _IMAGE_OK)) await client.image("a cat", model="openai/gpt-image-2", quality="low") assert json.loads(calls[-1].content)["quality"] == "low" @@ -712,7 +711,7 @@ async def test_async_image_forwards_quality(self) -> None: async def test_async_image_edit_forwards_quality(self) -> None: import json - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_paid_flow(calls, _IMAGE_OK)) await client.image_edit("make it green", _DATA_URI, quality="high") assert json.loads(calls[-1].content)["quality"] == "high" @@ -720,7 +719,7 @@ async def test_async_image_edit_forwards_quality(self) -> None: @pytest.mark.asyncio async def test_async_image_rejects_unknown_quality_before_paying(self) -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_paid_flow(calls, _IMAGE_OK)) with pytest.raises(ValueError, match="quality must be one of"): await client.image("a cat", quality="hd") diff --git a/tests/unit/test_solana_settled_payment.py b/tests/unit/test_solana_settled_payment.py index 1d4d000..51a8049 100644 --- a/tests/unit/test_solana_settled_payment.py +++ b/tests/unit/test_solana_settled_payment.py @@ -16,9 +16,9 @@ pytest.importorskip("x402") pytest.importorskip("solders") -from blockrun_llm.client import _mark_settled # noqa: E402 -from blockrun_llm.solana_client import _should_fallback_solana # noqa: E402 -from blockrun_llm.types import APIError, PaymentError # noqa: E402 +from blockrun_llm.client import _mark_settled +from blockrun_llm.solana_client import _should_fallback_solana +from blockrun_llm.types import APIError, PaymentError class TestSolanaSettledTag: diff --git a/tests/unit/test_solana_timeout_routing.py b/tests/unit/test_solana_timeout_routing.py index 1909181..eba6d78 100644 --- a/tests/unit/test_solana_timeout_routing.py +++ b/tests/unit/test_solana_timeout_routing.py @@ -17,8 +17,7 @@ from __future__ import annotations -import unittest.mock as mock -from typing import List +from unittest import mock import httpx import pytest @@ -26,9 +25,8 @@ pytest.importorskip("x402") pytest.importorskip("solders") -from blockrun_llm import SolanaLLMClient # noqa: E402 -from blockrun_llm import solana_client as sol # noqa: E402 - +from blockrun_llm import SolanaLLMClient +from blockrun_llm import solana_client as sol # --------------------------------------------------------------------------- # Fixtures / helpers @@ -105,7 +103,7 @@ def _payment_required(request: httpx.Request) -> httpx.Response: _SEARCH_OK = {"query": "q", "summary": "s"} -def _json_flow(calls: List[httpx.Request], ok_body: dict) -> httpx.MockTransport: +def _json_flow(calls: list[httpx.Request], ok_body: dict) -> httpx.MockTransport: """402 on the unsigned probe, then the success body once signed.""" def handler(request: httpx.Request) -> httpx.Response: @@ -123,14 +121,14 @@ def handler(request: httpx.Request) -> httpx.Response: def test_chat_uses_chat_timeout_default() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _CHAT_OK)) client.chat_completion("openai/gpt-5.2", [{"role": "user", "content": "hi"}]) assert _read_timeout(calls[-1]) == sol.DEFAULT_CHAT_TIMEOUT def test_image_uses_image_timeout_default() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _IMAGE_OK)) client.image("a cat", model="openai/gpt-image-2") # Probe + signed submit both carry the image budget, not the chat one. @@ -140,14 +138,14 @@ def test_image_uses_image_timeout_default() -> None: def test_search_uses_search_timeout_default() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _SEARCH_OK)) client.search("deep query") assert _read_timeout(calls[-1]) == sol.DEFAULT_SEARCH_TIMEOUT def test_exa_uses_search_timeout_default() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, {"results": []})) client.exa_search("latest AI papers") assert _read_timeout(calls[-1]) == sol.DEFAULT_SEARCH_TIMEOUT @@ -159,7 +157,7 @@ def test_exa_uses_search_timeout_default() -> None: def test_chat_per_call_override() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _CHAT_OK)) client.chat_completion("openai/gpt-5.2", [{"role": "user", "content": "hi"}], timeout=7.0) assert _read_timeout(calls[0]) == 7.0 @@ -167,14 +165,14 @@ def test_chat_per_call_override() -> None: def test_image_per_call_override() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _IMAGE_OK)) client.image("a cat", model="openai/gpt-image-2", timeout=9.0) assert _read_timeout(calls[-1]) == 9.0 def test_search_per_call_override() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _SEARCH_OK)) client.search("q", timeout=11.0) assert _read_timeout(calls[-1]) == 11.0 @@ -186,12 +184,12 @@ def test_search_per_call_override() -> None: def test_constructor_image_and_search_timeout_respected() -> None: - img_calls: List[httpx.Request] = [] + img_calls: list[httpx.Request] = [] img_client = _make_client(_json_flow(img_calls, _IMAGE_OK), image_timeout=42.0) img_client.image("a cat", model="openai/gpt-image-2") assert _read_timeout(img_calls[-1]) == 42.0 - s_calls: List[httpx.Request] = [] + s_calls: list[httpx.Request] = [] s_client = _make_client(_json_flow(s_calls, _SEARCH_OK), search_timeout=99.0) s_client.search("q") assert _read_timeout(s_calls[-1]) == 99.0 @@ -200,7 +198,7 @@ def test_constructor_image_and_search_timeout_respected() -> None: def test_legacy_flat_timeout_still_governs_chat() -> None: """Backwards-compat: old ``SolanaLLMClient(timeout=...)`` callers keep controlling the chat budget through the single keyword.""" - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_client(_json_flow(calls, _CHAT_OK), timeout=33.0) client.chat_completion("openai/gpt-5.2", [{"role": "user", "content": "hi"}]) assert _read_timeout(calls[-1]) == 33.0 @@ -239,7 +237,7 @@ class accepted: async def test_async_chat_uses_chat_timeout_default() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_json_flow(calls, _CHAT_OK)) await client.chat_completion("openai/gpt-5.2", [{"role": "user", "content": "hi"}]) assert _read_timeout(calls[-1]) == sol.DEFAULT_CHAT_TIMEOUT @@ -247,7 +245,7 @@ async def test_async_chat_uses_chat_timeout_default() -> None: async def test_async_chat_per_call_override() -> None: - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = _make_async_client(_json_flow(calls, _CHAT_OK)) await client.chat_completion( "openai/gpt-5.2", [{"role": "user", "content": "hi"}], timeout=13.0 diff --git a/tests/unit/test_solana_wallet.py b/tests/unit/test_solana_wallet.py index ab47e2b..8f6ea6e 100644 --- a/tests/unit/test_solana_wallet.py +++ b/tests/unit/test_solana_wallet.py @@ -1,10 +1,11 @@ """Unit tests for Solana wallet utilities.""" import pytest + from blockrun_llm.solana_wallet import ( create_solana_wallet, - solana_key_to_bytes, get_solana_public_key, + solana_key_to_bytes, ) # A valid test bs58 secret key (64 bytes, valid keypair from deterministic seed) diff --git a/tests/unit/test_speech.py b/tests/unit/test_speech.py index e918efe..3042b47 100644 --- a/tests/unit/test_speech.py +++ b/tests/unit/test_speech.py @@ -1,6 +1,7 @@ """Unit tests for SpeechClient request construction and response parsing.""" import os + import pytest from blockrun_llm import SpeechClient, SpeechResponse diff --git a/tests/unit/test_streaming.py b/tests/unit/test_streaming.py index 2ba29c9..572f7f6 100644 --- a/tests/unit/test_streaming.py +++ b/tests/unit/test_streaming.py @@ -18,7 +18,6 @@ from __future__ import annotations import json -from typing import List import httpx import pytest @@ -28,15 +27,14 @@ from ..helpers import TEST_PRIVATE_KEY, build_payment_required_response - # --------------------------------------------------------------------------- # Synthetic SSE bodies # --------------------------------------------------------------------------- -def _sse_events(deltas: List[str], finish: str = "stop", model: str = "test/model") -> bytes: +def _sse_events(deltas: list[str], finish: str = "stop", model: str = "test/model") -> bytes: """Render a list of content deltas as raw SSE bytes ending with [DONE].""" - lines: List[str] = [] + lines: list[str] = [] # First chunk — role only. lines.append( "data: " @@ -82,7 +80,7 @@ def _sse_events(deltas: List[str], finish: str = "stop", model: str = "test/mode return body.encode("utf-8") -def _sse_with_garbage(deltas: List[str]) -> bytes: +def _sse_with_garbage(deltas: list[str]) -> bytes: """Same as ``_sse_events`` but with a couple of malformed lines mixed in to verify the parser is tolerant.""" base = _sse_events(deltas).decode("utf-8") @@ -98,7 +96,7 @@ def _sse_with_garbage(deltas: List[str]) -> bytes: # --------------------------------------------------------------------------- -def _make_free_model_transport(sse_body: bytes, calls: List[httpx.Request]) -> httpx.MockTransport: +def _make_free_model_transport(sse_body: bytes, calls: list[httpx.Request]) -> httpx.MockTransport: def handler(request: httpx.Request) -> httpx.Response: calls.append(request) return httpx.Response( @@ -110,7 +108,7 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.MockTransport(handler) -def _make_paid_model_transport(sse_body: bytes, calls: List[httpx.Request]) -> httpx.MockTransport: +def _make_paid_model_transport(sse_body: bytes, calls: list[httpx.Request]) -> httpx.MockTransport: """First call → 402 with valid payment-required header; second → 200 SSE.""" def handler(request: httpx.Request) -> httpx.Response: @@ -140,13 +138,13 @@ def handler(request: httpx.Request) -> httpx.Response: class TestSyncStreaming: def test_free_model_streams_without_payment(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_free_model_transport(_sse_events(["Hello", " world"]), calls) ) - chunks: List[ChatCompletionChunk] = list( + chunks: list[ChatCompletionChunk] = list( client.chat_completion_stream( "nvidia/deepseek-v4-flash", [{"role": "user", "content": "hi"}], @@ -168,7 +166,7 @@ def test_free_model_streams_without_payment(self): assert finishes == ["stop"] def test_paid_model_signs_and_retries(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_paid_model_transport(_sse_events(["Paid"]), calls) @@ -199,7 +197,7 @@ def test_paid_stream_chunks_carry_real_cost(self): """Every paid-path chunk carries the real per-call x402 charge as ``chunk.cost_usd`` (race-free, vs the shared ``_last_call_cost``), so a streaming consumer can report the actual wallet deduction.""" - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_paid_model_transport(_sse_events(["Paid"]), calls) @@ -221,7 +219,7 @@ def test_paid_stream_chunks_carry_real_cost(self): def test_free_stream_chunks_have_no_cost(self): """Free models skip the 402/sign path (and the archive), so chunks carry no ``cost_usd`` — consumers treat that as 'no real charge'.""" - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_free_model_transport(_sse_events(["hi"]), calls) @@ -236,7 +234,7 @@ def test_free_stream_chunks_have_no_cost(self): assert all(getattr(c, "cost_usd", None) is None for c in chunks) def test_malformed_chunks_dont_abort_stream(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_free_model_transport(_sse_with_garbage(["A", "B"]), calls) @@ -254,7 +252,7 @@ def test_malformed_chunks_dont_abort_stream(self): def test_paid_path_propagates_payment_rejected(self): """If the retry also returns 402, surface PaymentError.""" - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -289,7 +287,7 @@ def handler(request: httpx.Request) -> httpx.Response: class TestAsyncStreaming: @pytest.mark.asyncio async def test_async_free_model(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = AsyncLLMClient(private_key=TEST_PRIVATE_KEY) # Swap in mock transport (same pattern as sync). await client._client.aclose() @@ -297,7 +295,7 @@ async def test_async_free_model(self): transport=_make_free_model_transport(_sse_events(["Hi", "!"]), calls) ) - chunks: List[ChatCompletionChunk] = [] + chunks: list[ChatCompletionChunk] = [] async for chunk in client.chat_completion_stream( "nvidia/deepseek-v4-flash", [{"role": "user", "content": "hi"}], @@ -313,14 +311,14 @@ async def test_async_free_model(self): @pytest.mark.asyncio async def test_async_paid_model_signs_and_retries(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = AsyncLLMClient(private_key=TEST_PRIVATE_KEY) await client._client.aclose() client._client = httpx.AsyncClient( transport=_make_paid_model_transport(_sse_events(["X"]), calls) ) - chunks: List[ChatCompletionChunk] = [] + chunks: list[ChatCompletionChunk] = [] async for chunk in client.chat_completion_stream( "openai/gpt-5.5", [{"role": "user", "content": "hi"}], @@ -341,7 +339,7 @@ async def test_async_paid_model_signs_and_retries(self): def _make_flaky_free_transport( sse_body: bytes, fail_count: int, - calls: List[httpx.Request], + calls: list[httpx.Request], status: int = 503, ) -> httpx.MockTransport: """Returns ``status`` (default 503) for the first ``fail_count`` requests, @@ -366,7 +364,7 @@ def test_recovers_after_two_503s(self, monkeypatch): # Zero out sleeps to keep tests fast. monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_flaky_free_transport(_sse_events(["OK"]), fail_count=2, calls=calls) @@ -384,7 +382,7 @@ def test_recovers_after_two_503s(self, monkeypatch): def test_raises_after_exhausting_retries(self, monkeypatch): monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -414,7 +412,7 @@ def test_5xx_retry_also_works_after_payment(self, monkeypatch): also trigger in-band retries before raising.""" monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] body = _sse_events(["paid-OK"]) def handler(request: httpx.Request) -> httpx.Response: @@ -462,7 +460,7 @@ class TestStreamingFallback: def test_falls_back_to_next_model_on_503(self, monkeypatch): monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -502,15 +500,15 @@ def test_no_fallback_after_first_chunk(self, monkeypatch): # Build SSE that's truncated (no [DONE]) so iter_lines simulates a # mid-stream connection drop via httpx parsing exception. truncated = ( - 'data: {"id":"x","object":"chat.completion.chunk","created":1,' - '"model":"primary/bad","choices":[{"index":0,"delta":{"role":"assistant"},' - '"finish_reason":null}]}\n\n' - 'data: {"id":"x","object":"chat.completion.chunk","created":1,' - '"model":"primary/bad","choices":[{"index":0,"delta":{"content":"par"},' - '"finish_reason":null}]}\n\n' - ).encode() + b'data: {"id":"x","object":"chat.completion.chunk","created":1,' + b'"model":"primary/bad","choices":[{"index":0,"delta":{"role":"assistant"},' + b'"finish_reason":null}]}\n\n' + b'data: {"id":"x","object":"chat.completion.chunk","created":1,' + b'"model":"primary/bad","choices":[{"index":0,"delta":{"content":"par"},' + b'"finish_reason":null}]}\n\n' + ) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -545,7 +543,7 @@ def test_non_retriable_error_does_not_fall_back(self, monkeypatch): permanent client errors, not transient upstream issues.""" monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -634,8 +632,8 @@ def _sse_with_tool_call(model: str = "anthropic/claude-haiku-4-5") -> bytes: return ("\n\n".join(lines) + "\n\n").encode("utf-8") -def _collect_tool_args(chunks: List[ChatCompletionChunk]) -> str: - out: List[str] = [] +def _collect_tool_args(chunks: list[ChatCompletionChunk]) -> str: + out: list[str] = [] for c in chunks: if not c.choices: continue @@ -655,7 +653,7 @@ class TestStreamedToolCalls: """ def test_sync_streamed_tool_call(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client( transport=_make_paid_model_transport(_sse_with_tool_call(), calls) @@ -679,13 +677,13 @@ def test_sync_streamed_tool_call(self): @pytest.mark.asyncio async def test_async_streamed_tool_call(self): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = AsyncLLMClient(private_key=TEST_PRIVATE_KEY) await client._client.aclose() client._client = httpx.AsyncClient( transport=_make_paid_model_transport(_sse_with_tool_call(), calls) ) - chunks: List[ChatCompletionChunk] = [] + chunks: list[ChatCompletionChunk] = [] async for chunk in client.chat_completion_stream( "openai/gpt-5.5", [{"role": "user", "content": "weather?"}], @@ -736,7 +734,7 @@ def test_sync_streamed_tool_call_non_function_type(self): sse = ( "\n\n".join("data: " + json.dumps(f) for f in frames) + "\n\ndata: [DONE]\n\n" ).encode() - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client(transport=_make_paid_model_transport(sse, calls)) chunks = list( @@ -777,7 +775,7 @@ def test_sync_stream_archive_survives_model_construct_chunk_missing_id(self): sse = ( "\n\n".join("data: " + json.dumps(f) for f in frames) + "\n\ndata: [DONE]\n\n" ).encode() - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] client = LLMClient(private_key=TEST_PRIVATE_KEY) client._client = httpx.Client(transport=_make_paid_model_transport(sse, calls)) # Must not raise: the archive loop reads chunk.id via the dict/attr-tolerant diff --git a/tests/unit/test_streaming_solana.py b/tests/unit/test_streaming_solana.py index dafc22f..386db30 100644 --- a/tests/unit/test_streaming_solana.py +++ b/tests/unit/test_streaming_solana.py @@ -11,7 +11,6 @@ from __future__ import annotations import json -from typing import List import httpx import pytest @@ -23,14 +22,13 @@ from blockrun_llm import SolanaLLMClient from blockrun_llm.types import APIError, PaymentError - # --------------------------------------------------------------------------- # Helpers — synthetic SSE bodies (same shape Base tests use) # --------------------------------------------------------------------------- -def _sse_events(deltas: List[str], finish: str = "stop", model: str = "test/model") -> bytes: - lines: List[str] = [] +def _sse_events(deltas: list[str], finish: str = "stop", model: str = "test/model") -> bytes: + lines: list[str] = [] lines.append( "data: " + json.dumps( @@ -82,7 +80,7 @@ def solana_client(): """Build a SolanaLLMClient without going through the x402 SDK signer init (which needs real keys + an RPC). We monkey-patch the signer after construction by replacing the x402_client with a fake.""" - import unittest.mock as mock + from unittest import mock with ( mock.patch("blockrun_llm.solana_client.register_exact_svm_client"), @@ -123,7 +121,7 @@ def _patch_sse_helpers(monkeypatch): # --------------------------------------------------------------------------- -def _free_transport(sse_body: bytes, calls: List[httpx.Request]) -> httpx.MockTransport: +def _free_transport(sse_body: bytes, calls: list[httpx.Request]) -> httpx.MockTransport: def handler(request: httpx.Request) -> httpx.Response: calls.append(request) return httpx.Response(200, headers={"content-type": "text/event-stream"}, content=sse_body) @@ -131,7 +129,7 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.MockTransport(handler) -def _paid_transport(sse_body: bytes, calls: List[httpx.Request]) -> httpx.MockTransport: +def _paid_transport(sse_body: bytes, calls: list[httpx.Request]) -> httpx.MockTransport: """First call → 402; second call (with PAYMENT-SIGNATURE) → 200 SSE.""" def handler(request: httpx.Request) -> httpx.Response: @@ -151,7 +149,7 @@ def handler(request: httpx.Request) -> httpx.Response: def _flaky_transport( - sse_body: bytes, fail_count: int, calls: List[httpx.Request], status: int = 503 + sse_body: bytes, fail_count: int, calls: list[httpx.Request], status: int = 503 ) -> httpx.MockTransport: def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -169,7 +167,7 @@ def handler(request: httpx.Request) -> httpx.Response: class TestSolanaStreaming: def test_free_model_streams_directly(self, solana_client, monkeypatch): - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] solana_client._client = httpx.Client( transport=_free_transport(_sse_events(["Hello", " world"]), calls) ) @@ -188,7 +186,7 @@ def test_free_model_streams_directly(self, solana_client, monkeypatch): def test_paid_model_signs_and_retries(self, solana_client, monkeypatch): _patch_sse_helpers(monkeypatch) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] solana_client._client = httpx.Client( transport=_paid_transport(_sse_events(["Paid"]), calls) ) @@ -210,7 +208,7 @@ def test_paid_model_signs_and_retries(self, solana_client, monkeypatch): def test_retries_5xx_with_backoff(self, solana_client, monkeypatch): monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] solana_client._client = httpx.Client( transport=_flaky_transport(_sse_events(["OK"]), fail_count=2, calls=calls) ) @@ -227,7 +225,7 @@ def test_retries_5xx_with_backoff(self, solana_client, monkeypatch): def test_raises_after_exhausting_retries(self, solana_client, monkeypatch): monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -248,7 +246,7 @@ def handler(request: httpx.Request) -> httpx.Response: def test_fallback_models_walks_chain(self, solana_client, monkeypatch): _patch_sse_helpers(monkeypatch) monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) @@ -277,7 +275,7 @@ def handler(request: httpx.Request) -> httpx.Response: def test_payment_rejected_raises_payment_error(self, solana_client, monkeypatch): _patch_sse_helpers(monkeypatch) monkeypatch.setattr("time.sleep", lambda _s: None) - calls: List[httpx.Request] = [] + calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: calls.append(request) diff --git a/tests/unit/test_tx_log.py b/tests/unit/test_tx_log.py index 9be1ff5..e39a243 100644 --- a/tests/unit/test_tx_log.py +++ b/tests/unit/test_tx_log.py @@ -16,7 +16,6 @@ import json from pathlib import Path - from blockrun_llm.tx_log import ( DEFAULT_LOG_DIR, TransactionLogger, @@ -25,7 +24,6 @@ format_row, ) - # --------------------------------------------------------------------------- # Logger writes # --------------------------------------------------------------------------- diff --git a/tests/unit/test_validation.py b/tests/unit/test_validation.py index 89ea00b..3ae142e 100644 --- a/tests/unit/test_validation.py +++ b/tests/unit/test_validation.py @@ -1,16 +1,17 @@ """Unit tests for validation module.""" import pytest + from blockrun_llm.validation import ( MAX_TOKENS_SANITY_LIMIT, - validate_private_key, + sanitize_error_response, validate_api_url, - validate_model, validate_max_tokens, + validate_model, + validate_private_key, + validate_resource_url, validate_temperature, validate_top_p, - sanitize_error_response, - validate_resource_url, ) diff --git a/tests/unit/test_video_params.py b/tests/unit/test_video_params.py index b35dcbe..4cd3d6e 100644 --- a/tests/unit/test_video_params.py +++ b/tests/unit/test_video_params.py @@ -1,6 +1,7 @@ """Unit tests for VideoClient.generate() parameter validation and body construction.""" import os + import pytest from blockrun_llm import VideoClient diff --git a/tests/unit/test_x402.py b/tests/unit/test_x402.py index a93ac25..0ccb26d 100644 --- a/tests/unit/test_x402.py +++ b/tests/unit/test_x402.py @@ -1,14 +1,17 @@ """Unit tests for x402 payment protocol.""" -import pytest import base64 import json + +import pytest + from blockrun_llm.x402 import ( create_nonce, create_payment_payload, - parse_payment_required, extract_payment_details, + parse_payment_required, ) + from ..helpers import TEST_ACCOUNT, TEST_RECIPIENT @@ -286,8 +289,8 @@ def test_decode_solana_payment_required(self): def test_keypair_signer_address(self): """KeypairSigner should derive correct public key from bs58 secret.""" - from x402.mechanisms.svm import KeypairSigner from solders.keypair import Keypair + from x402.mechanisms.svm import KeypairSigner # Generate a valid keypair and get its base58 representation kp = Keypair()