Skip to content

feat: add account API key support across Python clients - #58

Closed
KillerQueen-Z wants to merge 3 commits into
BlockRunAI:mainfrom
KillerQueen-Z:feat/api-key-auth
Closed

feat: add account API key support across Python clients#58
KillerQueen-Z wants to merge 3 commits into
BlockRunAI:mainfrom
KillerQueen-Z:feat/api-key-auth

Conversation

@KillerQueen-Z

@KillerQueen-Z KillerQueen-Z commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Users can use BlockRun account keys across synchronous/asynchronous chat and streaming, named media/data clients, and the native Anthropic adapter without loading or creating a wallet. Generic APIClient/AsyncAPIClient cover Responses and other account endpoints. Explicit wallet credentials preserve wallet mode; setup_agent_client prefers Solana for new wallets and preserves existing chain choices.

Shared account authentication binds credentials to the configured origin, rejects redirects, strips payment proofs, bypasses wallet caches and preserves status/Retry-After. Account 402 errors never invoke x402. The Anthropic account adapter defaults to max_retries=0 so an ambiguous 429/5xx does not replay a potentially billed POST. Accepted jobs recover from temporary polling errors by GETting the original full poll_url within the original timeout. Auth/credit/rate-limit errors remain visible.

Add synchronous Solana client context-manager cleanup, align Surf pricing estimates, and replace “timeout means no payment” text. README covers registration, keys, credits, Activity, signed polling URLs, independent wallet/account balances and wallet switching. It explicitly distinguishes this source checkout from currently published releases. The optional Anthropic extra remains below 1 because this adapter uses httpx transports.

Validation:

  • 825 local unit tests pass, including public service/401/402/429 contracts, sync/async polling recovery and native Anthropic no-replay regressions. Black, Ruff and offline brand checks pass.
  • Built a wheel and installed it in a clean Python 3.13 environment with core dependencies only. Sync/async Base/Solana-named account calls, 16 named constructors, explicit wallet mode and the optional Anthropic installation hint pass.
  • Five-product HTTP acceptance against merged Enterprise gateway source: 72 requests with simulated debit/meter reconciliation; Python loads the installed wheel.
  • Strict mypy still reports 185 errors on both the unchanged PR head and updated source with this environment; normalized error sets are identical. It is not claimed as passing. The local full suite ran on Python 3.14; Python 3.9/3.11/3.12 remain CI gates.

Local provider/storage responses are fixtures. This update does not claim every live provider/model or real on-chain wallet payment was tested, and does not publish a package or deploy the gateway.

@VickyXAI

VickyXAI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Superseded — main already has this feature, and it is released.

#59 landed feat(apikey): a BlockRun API key works everywhere a wallet key does at 16:19 UTC today (44c8e9a), and blockrun-llm 1.15.0 went to PyPI three minutes later. I unpacked the published wheel to confirm: blockrun_llm/apikey.py is in it and importable. That is why this branch shows CONFLICTING — not a stale rebase, but the same feature arriving twice.

The two designs are not equivalent, and #59 is the fuller one:

#58 (this) #59 (merged)
module api_key.py apikey.py
exported functions 1 (resolve_api_auth) 11 (is_api_key, resolve_api_key, api_key_base_url, auth_headers, configure_credential, payment_mode, resolve_poll_url, raise_for_api_key_402, …)
key tests 1 file tests/unit/test_apikey.py, 25 tests
setup_agent_wallet with a key touched setup.py handled in wallet.py:632
202-on-first-post claimed present in image.py / video.py

So I'd close this rather than rebase it. One thing here is worth keeping, though, and it is not in main: tests/unit/test_api_service_contracts.py. main's 25 tests cover resolution and configuration; that file covers the wire contract — 30 client×method combinations × 3 error statuses, asserting each reaches its documented host/path/verb with a Bearer header, sends no payment-signature/x-payment, preserves the status, makes exactly one request (no silent resign or resubmit), and — the one I'd most want kept — that the API key never appears in the error response it hands back. Plus test_accepted_media_failure_never_resubmits_or_signs, which is a double-charge regression test.

Porting it needs two adjustments to main's design: client._api_auth becomes headers=auth_headers(...), and blockrun_llm.api_key.time.sleep becomes the per-client POLL_INTERVAL_SECONDS sleeps in image.py/video.py.

It also asserts failure.value.retry_after == "17", which main cannot satisfyAPIError has no such field and nothing populates it, so account-rail 429s currently drop the header the enterprise service sets on purpose. Filed as #61 with the scale (90 raise sites across 15 files) and a suggested shape.

Whoever picks this up: closing #58 and opening a small PR with just that test file (plus #61's fix, so the retry_after assertion can stay) gets the value without the conflict.

@KillerQueen-Z

Copy link
Copy Markdown
Contributor Author

Closing this superseded API implementation: #59 is already merged, and rebasing this parallel design would duplicate and conflict with main. Follow-up client fixes are tracked in #60. Per the review, the remaining wire-contract, credential-redaction and accepted-job no-resubmission tests should be ported to the current design together with Retry-After issue #61; closing this PR does not claim that work is complete. Keeping the branch and test history available for that port.

VickyXAI pushed a commit that referenced this pull request Sep 5, 2026
Closes #61.

api.blockrun.ai answers a rate limit with Retry-After and the gateway sets it
deliberately: it is what turns a refused request into a caller that waits
instead of one that spins against the limit the header exists to prevent. It
never survived the SDK boundary — APIError carried message, status_code and
response, and `grep -rn retry_after blockrun_llm/` returned nothing. Every
consumer on the account rail had to guess or spin.

APIError gains `retry_after`, kept as the raw header string. The HTTP spec
allows both a delay in seconds and an HTTP-date, and inventing a number for the
date form would be worse than handing back what arrived; `retry_after_seconds`
parses the delay form and answers None for everything else, including a
negative value.

`retry_after_of()` is the single reader. It tolerates a response with no
headers attribute, because it runs inside an error path and raising there would
replace the real failure with an AttributeError about the failure.

Migrated all 75 raise sites that hold a response, across 14 files. The
remaining 15 have no response to read — poll-budget timeouts, a missing
poll_url, stream probes that exhausted retries — so they carry None because
there is nothing to carry, not because they were skipped. The issue warns that
a partial migration is worse than none; this one is complete.

portrait.py, realface.py and video.py each carried a byte-identical private
`_raise_api_error`. They now delegate to one `raise_api_error` in validation.py,
so the next change to how failures are reported lands in three places at once
rather than two out of three.

45 new cases, including the 30 client x method table from #58 that this issue
asked to salvage. Mutants that die: the reader always returning None, the
shared helper dropping the header, a negative delay passing through, the
seconds parser inventing a number for an unparseable value, and stripping every
retry_after kwarg from any one client file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMLNFbR6Jg2HBFpRUGBjyh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants