Releases: BlockRunAI/blockrun-llm
Release list
v1.9.0 — client-side spend limits
Added
-
Client-side spend limits.
max_cost_per_callandmax_session_coston
every client (Base and Solana, sync and async) refuse a quote that costs more
than you allowed:client = LLMClient(max_cost_per_call=0.25, max_session_cost=10.00)
Also settable per-deployment without code changes, via
BLOCKRUN_MAX_COST_PER_CALLandBLOCKRUN_MAX_SESSION_COST. An explicit
argument wins over the env var; a malformed env value is ignored rather than
raising, so a bad deploy variable cannot brick every client.The refusal happens before the paid request is sent, so nothing settles
and nothing is charged — signing alone moves no money, the gateway submitting
the signed authorization does. The newSpendLimitErrorcarriesquoted_usd,
limit_usdandscope("call"or"session"), and subclasses
PaymentErrorso existing handlers keep working and the model fallback chain
refuses it rather than shopping for a cheaper model.Both limits are opt-in and unset by default, so nothing changes for
existing callers. Until now the SDK had no ceiling anywhere: it computed
cost_usdand signed the quote in the next statement, with nothing compared
against anything — whilechat_completiondocumented a
PaymentError: If budget is set and would be exceededfor abudget
parameter that did not exist. That docstring is now true.
v1.8.2 — payment-safety fixes, and a version the package reports honestly
Supersedes 1.8.1, which was published from a tree where VERSION and
__init__.py still read 1.8.0. That wheel reports __version__ == "1.8.0" and
cannot be corrected in place, since PyPI does not allow overwriting a published
file. Install 1.8.2 to get a package whose self-reported version is truthful.
Security
- A failed paid request no longer triggers a second payment, on either
chain. Any error raised after thePAYMENT-SIGNATUREwent out is now
refused for model fallback, for both Base (_should_fallback) and Solana
(_should_fallback_solana). Previously a post-settlement failure was
indistinguishable from a transient one, so the chain advanced to the next
model and signed again —smart_chaton the premium complex tier could
settle six payments and return no tokens, the "CHARGED BUT REQUEST FAILED"
outcome this file already documents under 1.7.1. This covers every error
escaping the paid leg, not
only timeouts: the dominant post-settlement failure is a paid 5xx, which
arrives asAPIError(503)— exactly a status the fallback logic treats as
retriable. Callers catchinghttpx.TimeoutExceptionare unaffected; the
marker is an attribute, not a new exception type. - The clamp warning cannot break the request it warns about. Its parse ran
onresource.description, a server-controlled string, immediately before
signing. A non-string value raisedTypeErrorand aborted the call, and the
pattern backtracked super-linearly on a long digit run (measured on CPython
3.13: 0.49s at 8k digits, 1.95s at 16k, and it keeps squaring). The number is
now matched by a bounded pattern against a
length-capped slice, an ambiguous description (a per-unit rate alongside the
ceiling) stays silent instead of naming the wrong number, and the whole helper
swallows its own failures. - Removed a documented payment guard that does not exist.
chat_completion()advertisedPaymentError: If budget is set and would be exceeded. There is nobudgetparameter anywhere in the SDK and no
client-side spend cap — every 402 quote is signed automatically. The
docstring now says that plainly and points atget_spending().
Changed
max_tokensabove a model's ceiling is no longer silently absorbed. The
gateway does not reject an over-ceiling value; it clamps to the model's own
ceiling and quotes payment for the clamped value. Verified against the live
402 leg on 2026-07-21:claude-opus-4.8sent 262144 and 1000000 both quote
the 128000 price, andgpt-5.2sent 1e12 returns a quote rather than a 400.
The 402 disclosed the clamp inresource.descriptionand the SDK discarded
it while signing. Callers now get a warning naming what they asked for and
what they are being charged for, before the signature goes out.- The comment and
ValueErroraroundMAX_TOKENS_SANITY_LIMITclaimed the
gateway "enforces the real per-model ceiling and reports it". It does not.
Both now describe clamping.
Fixed
max_tokensis validated on Solana too.validate_max_tokenswas called
from the Base client and nowhere else; the Solana client put the caller's
value straight into paid request bodies at all four chat entry points, so
max_tokens=2_000_000raised on Base and was signed and sent on Solana. With
the gateway clamping rather than rejecting, nothing on either side caught it.boolno longer passes as a number.boolis anintsubclass, so
isinstance(True, int)isTrueandmax_tokens=True,temperature=True
andtop_p=Falseall sailed through and reached the wire as JSONtrue/
false. All three numeric validators now reject it, namingboolrather
than saying "must be a number" — which reads as wrong to anyone who knows
boolis one.- Line endings are normalized repo-wide via
.gitattributes(* text=auto).
17 tracked files were CRLF against an otherwise-LF tree, which turned a
51-line change into a 1045-line diff in #27.
1.8.1 — max_tokens no longer capped below real model ceilings
validate_max_tokens rejected anything over 100,000 client-side. That number was not a model limit and not the gateway's — a hardcoded sanity check with no comment that quietly became the binding constraint on every caller. Asking zai/glm-5.2 for the 262,144 it advertises raised a ValueError instead of sending a request.
Probed the live gateway 2026-07-21 with the guard bypassed: 19 models advertise a ceiling above 100,000 and all 19 accepted it — zai/glm-5.2 at 262,144, and the whole 128,000 class (claude-opus-4.8, claude-sonnet-5, claude-fable-5, gpt-5.6-sol/terra/luna, gpt-5.5, gpt-5.4, gpt-5.3-codex, glm-5/5.1/5-turbo). Zero rejections.
MAX_TOKENS_SANITY_LIMIT is now 1,000,000, documented as a typo guard. The bound stays so a stray 1e9 or a byte count fails locally rather than becoming a payment quote; it just must never be reachable by a real request.
The old message made this worse: max_tokens too large (maximum: 100000) reads like a provider response, and during an investigation it was taken for exactly that and recorded as an upstream model ceiling in a downstream token table. A test now pins that the message names the SDK's own number and disclaims being a model limit.
373 pass.
v1.7.0 — video input_type + Solana image quality
Adds the two media params the gateway already accepts but the SDK had no way to send.
Added
input_type on video generation — VideoClient.generate, SolanaLLMClient.video, AsyncSolanaLLMClient.video. Declares the intended seed mode (text / image / first_last_frame / reference). The gateway infers the mode from the seed fields and returns 400 before charging when the declared value disagrees.
Worth it when seed fields are built dynamically: if image_url arrives empty, the request otherwise degrades silently to text-to-video and you still pay for the clip. Declaring input_type="image" makes that a 400 instead. Accepted on both chains.
client.generate(
"the portrait turns to face the camera",
model="bytedance/seedance-2.0",
image_url=maybe_empty_url, # if this is falsy...
input_type="image", # ...you get a 400, not a text-to-video bill
)quality on Solana image generation + editing — SolanaLLMClient.image / image_edit, sync and async. low / medium / high / auto for openai/gpt-image-*; low meaningfully cuts generation time.
Solana only, by design. The Base gateway defines no quality field and strips unknown keys, so a value sent there would be silently dropped. ImageClient.generate/edit therefore keep rejecting it — now with a hint naming the Solana client.
Not included
Reference-to-video (reference_videos / reference_audios) is deliberately not exposed. Both gateways gate it behind R2V_ENABLED, currently off, so every call would return 503. It slots in once that flips.
Notes
Validation covers spelling only. Whether a declared mode matches the seed fields, and which models accept quality, stay the gateway's call — it answers both before billing, so a second copy in the SDK would only drift. Enums verified equal to the gateway zod enums on both chains.
336 unit tests; CI green on 3.9 / 3.11 / 3.12. Full notes in CHANGELOG.md, which also backfills 1.5.0 → 1.6.1.
v1.4.2 — Solana wallet auto-load (parity with Base)
Fixed
- Solana clients auto-load the on-disk wallet (parity with Base).
SolanaLLMClient/AsyncSolanaLLMClientnow resolve the key asprivate_key→SOLANA_WALLET_KEY→ on-disk wallet (newest~/.<provider>/solana-wallet.json, else~/.blockrun/.solana-session), soSOLANA_WALLET_KEYis no longer required when a wallet session exists. A malformed key from any source raises a cleanValueError, and an unreadable session file is treated as "no wallet" rather than crashing. (#10)
Full Changelog: https://github.com/BlockRunAI/blockrun-llm/blob/main/CHANGELOG.md
v1.4.1 — streamed tool-call fix
Fixed
- Streamed tool calls no longer crash the SDK (
'dict' object has no attribute 'delta'). OpenAI streams tool calls incrementally (first frameid+function.name, later frames onlyfunction.argumentsfragments), which the strict non-streamToolCallschema rejected — forcing amodel_constructfallback that leftchoicesas raw dicts and crashed the stream-archiving loop. Added lenientChatChunkToolCall/ChatChunkFunctionCalltypes for streamingdelta.tool_calls, plus dict-tolerant accessors in the four sync/async archive loops. AffectsLLMClientandSolanaLLMClient, sync and async. (#9)
Notes
- Supersedes the never-published 1.4.0 stamp, so this release also carries the Coinbase Onramp (
LLMClient.onramp()) addition from 1.4.0.
Full Changelog: https://github.com/BlockRunAI/blockrun-llm/blob/main/CHANGELOG.md