Python SDK for llm4agents.com — gasless AI agent infrastructure
Project description
llm4agents-sdk
Python SDK for llm4agents.com — gasless AI agent infrastructure. Chat completions, wallet management, gasless stablecoin transfers, and MCP-powered tools through a single async client.
Install
pip install llm4agents-sdk
eth-account is bundled as a dependency for gasless transfers (no extra install needed).
Get an API Key
- Go to api.llm4agents.com/docs
- Register your agent to receive a key in the format
sk-proxy-... - Pass it to the client constructor
Quick Start
import asyncio
from llm4agents import LLM4AgentsClient
async def main():
client = LLM4AgentsClient(api_key="sk-proxy-...")
# Chat completion
response = await client.chat.completions.create({
"model": "anthropic/claude-sonnet-4",
"messages": [{"role": "user", "content": "Hello"}],
})
print(response["choices"][0]["message"]["content"])
# Conversation with MCP tools
conv = client.chat.conversation({
"model": "anthropic/claude-sonnet-4",
"system": "You are a research assistant",
"tools": client.tools,
})
answer = await conv.say("Search for Bitcoin news and summarize the top 3")
print(answer.content)
# Gasless stablecoin transfer
result = await client.transfer.send({
"chain": "polygon", "token": "USDC",
"to": "0xRecipient...", "amount": "10.50",
"private_key": "0x...",
})
print(result["tx_hash"], result["explorer_url"])
asyncio.run(main())
Chat
Completions
# Non-streaming
response = await client.chat.completions.create({
"model": "anthropic/claude-sonnet-4",
"messages": [{"role": "user", "content": "Hello"}],
})
print(response["choices"][0]["message"]["content"])
# Streaming
from llm4agents import FinalUsage
def on_final_usage(usage: FinalUsage) -> None:
print(f"\n[usage] prompt={usage.prompt_tokens} completion={usage.completion_tokens} total={usage.total_tokens}")
stream = await client.chat.completions.create(
{
"model": "anthropic/claude-sonnet-4",
"messages": [{"role": "user", "content": "Count to 10"}],
"stream": True,
},
on_final_usage=on_final_usage, # fires once after the stream ends
)
async for chunk in stream:
print(chunk.choices[0]["delta"].get("content", ""), end="", flush=True)
# With extended thinking
response = await client.chat.completions.create({
"model": "anthropic/claude-sonnet-4",
"messages": [{"role": "user", "content": "Solve step by step: 47 * 83"}],
"reasoning": True,
"include_reasoning": True,
})
# Vision (multimodal) input
analysis = await client.chat.completions.create({
"model": "openai/gpt-4o",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "https://example.com/image.png"}},
],
}],
})
# Model fallbacks — try primary, fall back to secondary if it fails
response = await client.chat.completions.create({
"models": ["anthropic/claude-sonnet-4", "openai/gpt-4o"],
"messages": [{"role": "user", "content": "Hello"}],
})
# X-Model-Used response header indicates which model actually responded
# Force/restrict tool use
response = await client.chat.completions.create({
"model": "openai/gpt-4o",
"messages": [...],
"tools": await client.tools.fetch_definitions(),
"tool_choice": "required", # "none" | "auto" | "required" | {"type": "function", "function": {"name": "..."}}
})
Conversation with Tools
from llm4agents import LLM4AgentsError, ResponseMeta
def on_round_meta(meta: ResponseMeta) -> None:
cents = meta.cost_usd_cents or 0
bal = meta.balance_remaining_cents or 0
print(f"Round cost: ${cents / 100:.4f}, balance: ${bal / 100:.2f}")
conv = client.chat.conversation({
"model": "anthropic/claude-sonnet-4",
"system": "You are a research assistant",
"tools": client.tools,
"history": [], # optional: rehydrate from persisted messages
"on_tool_call": lambda name, args: print(f"Calling {name}...") or True,
"on_tool_result": lambda name, result: print(f"{name}: {result.text[:80]}"),
"on_round_meta": on_round_meta,
"on_tools_ignored": lambda model: print(f"WARN: {model} ignored tools"),
"enable_prompt_tool_fallback": True, # retry round 0 with tools in prompt
"max_tool_rounds": 5, # default 10
})
# Single turn
answer = await conv.say("Search for Bitcoin news and summarize the top 3")
print(answer.content)
for call in answer.tool_calls:
print(f" {call['name']} → {call['result'].text[:60]}")
# Streaming conversation
async for event in conv.stream("Now find the current price"):
if event["type"] == "text":
print(event["content"], end="", flush=True)
elif event["type"] == "reasoning":
print(f"<think>{event['content']}</think>", end="")
elif event["type"] == "meta":
print(f"\n[meta] request_id={event['meta'].request_id}")
elif event["type"] == "tool_call":
print(f"\n[tool] {event['name']}({event['args']})")
elif event["type"] == "tool_result":
print(f"[done] {event['result'].text[:60]}")
elif event["type"] == "done":
print(f"\nUsage: {event['response']['usage']}")
# History management
messages = conv.messages # list[ChatMessage] — JSON-serializable
conv.clear() # reset to empty, keeps system prompt and options
branch = conv.fork() # copy history + all callbacks into a new Conversation
on_tool_call receives (name: str, args: dict) and should return True to proceed or False to cancel the tool call.
on_round_meta fires after each round with a ResponseMeta containing cost_usd_cents, balance_remaining_cents, tokens_input, tokens_output, tokens_reasoning, model_used, and request_id parsed from response headers.
on_tools_ignored(model) fires once if you pass tools but the model returns no tool calls on the first round — useful for detecting models without native function calling.
enable_prompt_tool_fallback=True adds an automatic recovery path for those models. When round 0 ignores tools, the SDK retries the round once with the tool definitions injected into the system prompt and asks the model to emit <tool_call>{"name":"...","arguments":{...}}</tool_call> blocks. Parsed blocks are executed exactly like native tool calls, so the rest of the loop is unchanged. In stream() you'll see a {"type": "fallback", "reason": "tools_ignored", "model": ...} event before the fallback round runs (the fallback round itself is non-streamed). If the fallback also returns no blocks, its plain-text reply is returned as the answer.
ConversationResponse.usage includes prompt_tokens, completion_tokens, total_tokens, and (when the upstream model exposes them) reasoning_tokens accumulated across every round, including any fallback round.
To restore a conversation from a previous session:
conv = client.chat.conversation({
"model": "anthropic/claude-sonnet-4",
"history": saved_messages, # rehydrate from your store
})
McpToolResult
All tool calls return an McpToolResult instead of a plain string:
result = await client.tools.scraper.fetch_html("https://example.com")
result.text # str — joined text from all text parts (convenience)
str(result) # same as result.text — backward-compatible
result.content # tuple[McpContent, ...] — typed content parts
for part in result.content:
if part.type == "text":
print(part.text)
elif part.type == "image":
print(part.mimeType, len(part.data)) # base64-encoded image
elif part.type == "resource":
print(part.uri, part.text)
The transport auto-normalizes raw MCP responses: snake_case mime_type is aliased to mimeType, imageBase64/pngBase64 keys are mapped to data, MIME types are sniffed from base64 magic bytes when missing, and JSON-wrapped image/PDF payloads embedded inside text blocks (e.g. {"imageBase64": "...", "mimeType": "image/png"}) are promoted to typed McpImageContent / McpResourceContent automatically.
Agents
# Register a new agent — call before you have an API key
client = LLM4AgentsClient(api_key="") # empty key is fine for registration
reg = await client.agents.register("My Agent")
# The returned api_key is shown only once — save it immediately
print(reg.api_key) # sk-proxy-...
print(reg.uuid)
Wallets
# Generate a deposit wallet
wallet = await client.wallets.generate({"chain": "polygon", "token": "USDC"})
print(wallet["address"])
# Check balance
balance = await client.wallets.balance()
print(balance["available_usd"])
for w in balance["wallets"]:
print(f'{w["chain"]}/{w["token"]}: ${w["available_usd"]}')
# Transaction history
txs = await client.wallets.transactions({"limit": 20, "type": "deposit"})
for tx in txs["transactions"]:
print(f'{tx["type"]}: ${tx["amount_usd_cents"] / 100} — {tx["description"]}')
type filter accepts "deposit", "usage", "refund", or "gas_sponsored".
Gasless Transfers
# One-call convenience
result = await client.transfer.send({
"chain": "polygon", "token": "USDC",
"to": "0xRecipient...", "amount": "10.50",
"private_key": "0x...",
})
print(result["tx_hash"], result["explorer_url"])
# Two-step — inspect the fee before committing
quote = await client.transfer.quote({
"chain": "polygon", "token": "USDC",
"from_": "0xSender...", "to": "0xRecipient...", "amount": "10.50",
})
print(f"Fee: {quote.fee_formatted}")
print(f"Forwarder: {quote.forwarder_address}")
result = await client.transfer.submit(quote, "0xPrivateKey...")
print(result.tx_hash)
x402 Walk-up Payment
The proxy supports the x402 protocol for per-request stablecoin
payments on POST /v1/chat/completions. Instead of pre-funding an agent account,
the client signs an EIP-3009 TransferWithAuthorization for USDC on Base /
Base-Sepolia, attaches it as an X-PAYMENT header, and the proxy settles
on-chain after the response is delivered.
Scope: this SDK signs x402 payments going OUT. The SDK builds and signs
X-PAYMENTheaders so your code can pay any x402-compatible server (the llm4agents API, an x402engine endpoint, or any third party). It does not include server-side helpers: there is noverify_payment, nosettle_payment, norequire_paymentmiddleware. If your agent needs to receive x402 payments (run its own paywall), use a server library directly —x402-fastapifor FastAPI,x402-flaskfor Flask,coinbase-x402for any framework, or the reference servers for other stacks. See Roadmap below for the server-side direction.
Two modes are mutually exclusive — pick one at construction time:
| Mode | Set via | Required | Use when |
|---|---|---|---|
| Bearer (default) | omit payment or PaymentConfig(mode="bearer") |
api_key |
You have an agent and a pre-funded balance |
| x402 walk-up | payment=PaymentConfig(mode="x402", signer=...) |
signer (eth_account or custom) |
You want one-shot calls billed per-request from a wallet, no agent registration |
Bearer vs x402 — at a glance
from llm4agents import LLM4AgentsClient, PaymentConfig, eth_account_to_signer
from eth_account import Account
# Bearer (existing) — pre-funded agent
bearer = LLM4AgentsClient(api_key="sk-proxy-...")
# x402 walk-up — pay per call from a wallet
account = Account.from_key("0xYOUR_PRIVATE_KEY")
x402 = LLM4AgentsClient(
api_key="", # ignored in x402 mode
payment=PaymentConfig(
mode="x402",
signer=eth_account_to_signer(account),
network="base-sepolia", # or "base" for mainnet
),
)
# Same API surface — the SDK probes the proxy for a 402, signs an
# EIP-3009 authorization, and retries with X-PAYMENT automatically.
res = await x402.chat.completions.create({
"model": "openai/gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}],
})
Custom signers (no eth_account dependency)
eth_account is already a hard dependency of the SDK (used by gasless transfers),
so eth_account_to_signer is the default path. If you want to plug in a hardware
wallet, KMS-backed key, or WalletConnect, implement the Signer Protocol
directly — the SDK depends on the Protocol, not on eth_account:
from llm4agents import Signer
class HsmSigner:
address = "0xYourAddress..."
async def sign_typed_data(self, *, domain, types, primary_type, message):
# Defer to your HSM / KMS / hardware wallet. Return a 0x-prefixed
# 65-byte signature.
return "0x..."
client = LLM4AgentsClient(
api_key="",
payment=PaymentConfig(mode="x402", signer=HsmSigner(), network="base"),
)
sign_typed_data may be sync or async — the SDK awaits the result if it's
a coroutine.
Streaming receipts
x402-mode streaming responses end with a trailing SSE event after [DONE]
containing the on-chain settlement receipt. The Conversation.stream() helper
surfaces this as a typed event yielded BEFORE the matching done event:
conv = x402.chat.conversation({"model": "openai/gpt-4o-mini"})
async for ev in conv.stream("Tell me a joke"):
if ev["type"] == "text":
print(ev["content"], end="", flush=True)
elif ev["type"] == "x402_receipt":
print(f"\nsettled: {ev['transaction']} on {ev['network']}")
elif ev["type"] == "done":
print(f"\n{ev['response']['usage']}")
For the lower-level chat.completions.create() API, pass on_x402_receipt:
def handle_receipt(receipt):
print(f"settled {receipt.amount} on {receipt.network}: {receipt.transaction}")
stream = await x402.chat.completions.create(
{"model": "openai/gpt-4o-mini", "messages": [...], "stream": True},
on_x402_receipt=handle_receipt,
)
async for chunk in stream:
...
Lower-level helpers — client.x402
For advanced use cases (signing without sending, inspecting the 402 response
shape, batch signing), the client.x402 namespace exposes the building blocks:
# Probe the proxy and get the typed PaymentRequirements
requirements = await x402.x402.probe()
print(requirements.max_amount_required, requirements.network)
# Probe + sign in one call — returns SignedPayment(payment_payload, encoded_header, requirements)
signed = await x402.x402.sign()
# signed.encoded_header → base64-encoded X-PAYMENT value
# signed.payment_payload → the parsed PaymentPayload (typed)
# signed.requirements → the proxy-advertised requirements the signature is bound to
# Sign against caller-supplied requirements (no HTTP) — useful for testing
signed2 = await x402.x402.sign_from_requirements(requirements)
Error handling
When the proxy rejects payment (signature invalid, nonce reused, etc.) the SDK
raises X402PaymentRequiredError carrying the typed requirements so the caller
can re-sign with a different amount or network:
from llm4agents import X402PaymentRequiredError
try:
await x402.chat.completions.create({...})
except X402PaymentRequiredError as err:
print("Payment rejected. accepted offers:", err.payment_requirements)
print("x402 version:", err.x402_version)
Networks:
"base"(mainnet, USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) and"base-sepolia"(testnet, USDC0x036CbD53842c5426634e7929541eC2318f3dCF7e) are currently supported. The USDC EIP-712 domain name differs between them (USD CoinvsUSDC);eth_account_to_signerhandles this automatically.
Endpoints accepting x402 (signed per-call USDC):
POST /v1/chat/completions— chat with any model (per-token signed upper bound)POST /v1/scrape/{markdown,fetch_html,links,screenshot,pdf,extract}— one-shot scrapingPOST /v1/search/{google,news,maps,batch}— Google search (Serper)POST /v1/image/{generate,edit,analyze}— image generation / edit / visionPer-call x402 prices are seeded ~10% below x402engine.app reference rates (e.g. scrape markdown ~$0.0045, screenshot ~$0.009, image gen ~$0.0135-$0.045). Prices are admin-editable from the operator panel without redeploy.
Browser sessions (
session_*) and other endpoints (/v1/embeddings,/api/v1/wallets/*, etc.) stay Bearer-only — sessions are pre-deposit by design. Using x402 mode on a non-allowed path raisesLLM4AgentsErrorwith codex402_payment_requiredand a clear message.
REST scrape / search / image with x402
The same payment=PaymentConfig(mode="x402", signer=..., network=...)
client config that works for chat completions also works for the MCP
REST surface:
import httpx
from eth_account import Account
from llm4agents import LLM4AgentsClient, PaymentConfig, eth_account_to_signer
account = Account.from_key("0xYOUR_KEY")
client = LLM4AgentsClient(
api_key="",
payment=PaymentConfig(
mode="x402",
signer=eth_account_to_signer(account),
network="base-sepolia",
),
)
# Probe + sign once, then hit the REST endpoint directly with the X-PAYMENT
signed = await client.x402.sign()
async with httpx.AsyncClient() as http:
res = await http.post(
"https://api.llm4agents.com/v1/scrape/markdown",
headers={"x-payment": signed.encoded_header},
json={"url": "https://example.com"},
)
print(res.json())
The MCP tools accessor (client.tools.scraper.markdown(...)) currently
uses Bearer auth via the MCP transport; the REST surface above is the
path for walk-up.
Roadmap — server-side x402
Today the SDK is client-only: it signs X-PAYMENT headers so an agent
can pay for outbound services. The mirror direction (an agent serving its
own paywalled endpoint and receiving x402 payments from third parties) is
not implemented and not on the v2.x roadmap.
If you want to monetize your agent with x402, use a server library directly:
| Stack | Library |
|---|---|
| FastAPI | x402-fastapi |
| Flask | x402-flask |
| Any framework (low-level, JWT-auth'd CDP facilitator) | coinbase-x402 |
| Other stacks | see the x402 reference servers |
If a first-class llm4agents-sdk-server (verify/settle/middleware helpers
around coinbase-x402 with llm4agents-flavoured defaults) would help your
use case, open an issue at
llmforagents/sdk-python.
MCP Tools
All tool methods return McpToolResult. Access .text for the plain-text representation.
Scraper
html = await client.tools.scraper.fetch_html("https://example.com")
md = await client.tools.scraper.markdown("https://example.com")
links = await client.tools.scraper.links("https://example.com")
shot = await client.tools.scraper.screenshot("https://example.com", full_page=True)
pdf = await client.tools.scraper.pdf("https://example.com")
data = await client.tools.scraper.extract("https://example.com", schema={
"type": "object",
"properties": {"title": {"type": "string"}},
})
All scraper methods accept an optional proxy keyword: "none", "datacenter", or "residential".
Browser sessions
session = await client.tools.scraper.session_create(proxy="residential")
result = await client.tools.scraper.session_run(
session_id=session.text,
actions=[{"type": "navigate", "url": "https://example.com"}],
)
status = await client.tools.scraper.session_status(session_id=session.text)
await client.tools.scraper.session_close(session_id=session.text)
Search
results = await client.tools.search.google("TypeScript SDK design")
news = await client.tools.search.google_news("Bitcoin", tbs="qdr:d")
places = await client.tools.search.google_maps("coffee near me")
batch = await client.tools.search.google_batch(["python", "golang"])
Image
img = await client.tools.image.generate("A robot writing code")
edited = await client.tools.image.edit("Make it blue", image_url="https://...")
analysis = await client.tools.image.analyze("What is this?", image_url="https://...")
image.generate and image.edit may return McpImageContent parts (base64 PNG) alongside text.
Tool Definitions
client.tools.definitions returns the cached list[ToolDefinition] (populated after first tool call).
Use fetch_definitions() to eagerly fetch and cache the list:
defs = await client.tools.fetch_definitions() # list[ToolDefinition] in OpenAI function format
Pass these to any LLM that supports function calling, or let conversation() manage them automatically when "tools": client.tools is set.
Models
result = await client.models.list()
for m in result.models:
print(f"{m['slug']} — ${m['inputPricePer1M']}/1M in, ${m['outputPricePer1M']}/1M out")
if m.get("feePct") is not None:
print(f" platform fee: {m['feePct']}%")
# Filter by name
result = await client.models.list(search="claude")
models.list() returns a ModelListResult with .models (list of dicts), .fee_pct (int | None — the agent's platform fee percentage, applied to every billed call), and .request_id (str | None). Each model dict contains slug, displayName, provider, inputPricePer1M, outputPricePer1M, contextWindow, lastSyncedAt, and an optional feePct (platform fee percentage).
Embeddings
res = await client.embeddings.create(
model="openai/text-embedding-3-large",
input="How many vectors fit in a haystack?",
)
print(len(res.data[0].embedding)) # → e.g. 3072
print(res.usage.prompt_tokens, res.model)
# Batch input
batch = await client.embeddings.create(
model="openai/text-embedding-3-small",
input=["first", "second", "third"],
)
for item in batch.data:
print(item.index, item.embedding)
embeddings.create() accepts model (slug), input (string or list of strings, max 2048 entries), and the optional encoding_format, dimensions, and user keyword args. Returns an EmbeddingsResponse with .data: list[EmbeddingItem], .model, and .usage (prompt_tokens, total_tokens). Embeddings have no completion tokens, so billing is input-only.
Catalog: Embedding models do not appear in OpenRouter's public catalog endpoint, so the proxy maintains them by hand. New embedding models can be added through the admin panel — see
model_type='embedding'rows.
Error Handling
All errors are instances of LLM4AgentsError:
from llm4agents import LLM4AgentsClient
from llm4agents.errors import LLM4AgentsError
try:
await client.chat.completions.create(...)
except LLM4AgentsError as err:
print(err.code, err.status_code, err.request_id, err.message)
code |
HTTP status | Description |
|---|---|---|
auth_error |
401, 403 | Invalid or missing API key |
insufficient_balance |
402 | Not enough balance to cover the request |
rate_limited |
429 | Too many requests |
model_not_found |
404 | Requested model does not exist in the catalog |
model_disabled |
422 | Model exists but is currently disabled |
context_overflow |
— | Prompt + max_tokens exceeds the model's context window |
gas_spike |
409 | Network gas price spiked above safe threshold during transfer |
signature_mismatch |
422 | EIP-712 permit signature could not be verified |
invalid_token |
422 | Unsupported token or chain for gasless transfer |
operator_unavailable |
503 | Gasless relayer is temporarily unavailable |
deadline_expired |
400 | EIP-712 permit deadline passed before submission |
tool_not_found |
— | MCP tool name not found in the server's tool list |
tool_execution_error |
— | MCP tool returned an error result |
tool_loop_limit |
— | Conversation exceeded max_tool_rounds without a final answer |
network_error |
— | Connection failed (DNS failure, TCP reset, etc.) |
timeout |
— | Request exceeded the configured timeout |
api_error |
4xx, 5xx | Any other non-success response |
Constructor Options
client = LLM4AgentsClient(
api_key="sk-proxy-...", # required in Bearer mode; "" in x402 mode
base_url="https://api.llm4agents.com", # optional
mcp_url="https://mcp.llm4agents.com/mcp", # optional
timeout=30.0, # optional, seconds, default 30
payment=PaymentConfig(mode="bearer"), # optional, default; or PaymentConfig(mode="x402", signer=..., network=...)
)
What's New in v2.5
- x402 walk-up payment mode — pay per-request from a wallet on
/v1/chat/completionswithout registering an agent. Passpayment=PaymentConfig(mode="x402", signer=..., network=...)to the client constructor. Supports botheth_account.LocalAccount(viaeth_account_to_signer) and any customSignerProtocol implementation (HSM, KMS, hardware wallets, WalletConnect) thanks to the Ports & Adapters design —sign_typed_datamay be sync or async. - Streaming responses emit a typed
x402_receiptevent after[DONE], surfacing the on-chain settlement receipt (transaction,network,amount,payer) toconv.stream()consumers and theon_x402_receiptcallback onchat.completions.create(). - New
client.x402namespace —probe(),sign(recipient=...), andsign_from_requirements(req, recipient=...)helpers for low-level integrations. - New top-level exports:
PaymentConfig,PaymentPayload,PaymentRequirements,Signer,SignedPayment,X402Namespace,X402Network,X402PaymentRequiredError,X402Receipt,eth_account_to_signer,build_transfer_with_authorization_typed_data,generate_nonce,network_to_caip2,sign_from_requirements,encode_payment_header,decode_payment_required_header,pick_supported_requirements,USDC_ADDRESS_BY_NETWORK,USDC_DOMAIN_NAME_BY_NETWORK,X402_CAIP2_BY_NETWORK,CHAIN_ID_BY_NETWORK,TRANSFER_WITH_AUTHORIZATION_TYPES,DEFAULT_VALID_FOR_SECONDS. - x402 allowlist extended to the MCP REST surface — clients in x402
mode can now hit
/v1/scrape/*,/v1/search/*, and/v1/image/*in addition to chat. Prices are admin-editable in cents from the operator panel (parallelvaluefor balance /x402_valuefor walk-up per tool).
What's New in v2.4
client.embeddings.create(model=..., input=...)— OpenAI-compatible embeddings againstPOST /v1/embeddings. Pass a string or a list of up to 2048 strings; receive anEmbeddingsResponsewithdata: list[EmbeddingItem],model, andusage(prompt_tokens, total_tokens). Embeddings are billed input-only — there are no completion tokens.- New types exported from
llm4agents:Embeddings,EmbeddingItem,EmbeddingsResponse,EmbeddingsUsage. The embedding-model catalog is curated by hand on the server because OpenRouter omits embedding models from its public catalog endpoint; admins maintain the list via the proxy admin panel.
What's New in v2.3
This release brings the Python SDK to feature parity with the TypeScript SDK at v2.3.1. Seven fixes that an external playground audit flagged as real-world breakages:
- Prompt-mode tool fallback (
enable_prompt_tool_fallback=True) — when a model ignores nativetoolson round 0, the SDK retries the round with the tool definitions injected into the system prompt and parses<tool_call>{"name":"...","arguments":{...}}</tool_call>blocks from the response. Recovers tool use on models without native function calling.stream()emits a{"type": "fallback", "reason": "tools_ignored", "model": ...}event before the fallback round. - MCP
Acceptheader — MCPrpcrequests now sendAccept: application/json, text/event-stream. Streamable HTTP MCP servers were rejecting every tool call with HTTP 406 without it. - Models endpoint trailing slash —
client.models.list()now hits/api/v1/models(no trailing slash), matching the deployed API. reasoning_tokenspropagation —ConversationResponse.usage["reasoning_tokens"]now accumulates across all rounds, andResponseMeta.tokens_reasoningexposes the per-round value parsed from thex-tokens-reasoningheader.fee_pctinModelListResult— the platform fee percentage returned by the API is now surfaced asresult.fee_pct: int | None.on_final_usagecallback in streaming completions —chat.completions.create(params, on_final_usage=cb)firescb(FinalUsage(prompt_tokens, completion_tokens, total_tokens, reasoning_tokens))once after the SSE stream ends, using the lastusagechunk emitted by providers that sendstream_options: {include_usage: true}.- Assistant
content: nullnormalization — when a model returnscontent: nullfor a tool-only message (OpenAI / Gemini convention), the SDK now normalizes it to""before pushing to history. Strict backends were rejecting follow-up requests becausemessages[*].contentwas null.
What's New in v2.1
Conversation.stream()— async generator yielding typed events:text,reasoning,tool_call,tool_result,meta,done. Each round emits ametaevent withResponseMetaparsed from response headers.on_round_meta— callback fired per round withResponseMeta(cost, balance, tokens, model used, request id).on_tools_ignored(model)— callback fired when a model returns no tool calls on the first round despite being given tools — useful for detecting models without native function-calling support.agents.register(name)— register a new agent and receive itsapi_key(only shown once).forwarder_addressinQuoteResult— the EIP-2771 forwarder contract used for the gasless transfer.- Auto-normalization in MCP transport — JSON-wrapped image/PDF payloads embedded inside text blocks (
{"imageBase64": "..."},{"pngBase64": "..."},{"pdfBase64": "..."}) are auto-promoted to typedMcpImageContent/McpResourceContent. Snake_casemime_typeis aliased tomimeTypeand MIME types are sniffed from base64 magic bytes when missing. - Top-level exports —
from llm4agents import AgentRegistration, ResponseMeta, Conversation, McpToolResult, ...(all public types now re-exported from the package root). Transaction.typefilter accepts"gas_sponsored"in addition to"deposit","usage","refund".ModelInfo.feePct— platform fee percentage now exposed in the model catalog.
Migration from v1.x
| Before (v1) | After (v2) |
|---|---|
result = await tools.call(name, args) → str |
result.text or str(result) |
conv.on_tool_result callback receives str |
callback now receives McpToolResult |
models = await client.models.list() → list |
result.models (access via .models) |
StreamEvent["tool_end"]["result"] → str |
.result is now McpToolResult |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm4agents_sdk-2.5.1.tar.gz.
File metadata
- Download URL: llm4agents_sdk-2.5.1.tar.gz
- Upload date:
- Size: 65.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1ae45180edaf0029c42d10d8d85e18f2b3a3ebc48883cb7aa6deb4feb3fa717
|
|
| MD5 |
8b9bc284d40bd62bb79215b762359a8a
|
|
| BLAKE2b-256 |
143dd9dfde3518edaa61755b8f14325e4bf1a60883c288991c8d1b9011f80580
|
File details
Details for the file llm4agents_sdk-2.5.1-py3-none-any.whl.
File metadata
- Download URL: llm4agents_sdk-2.5.1-py3-none-any.whl
- Upload date:
- Size: 49.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a416e26b1527cbdb49bab6b604fc2546d12a1fdd20408ab12826f8bea601b944
|
|
| MD5 |
4b89aae1d49610b399965c4d04221d55
|
|
| BLAKE2b-256 |
b91fe2ca37f8f02e83e6a1614d29032faeda1f0e815d01356e0df55f100fd132
|