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)
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).
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
base_url="https://api.llm4agents.com", # optional
mcp_url="https://mcp.llm4agents.com/mcp", # optional
timeout=30.0, # optional, seconds, default 30
)
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
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.3.2.tar.gz.
File metadata
- Download URL: llm4agents_sdk-2.3.2.tar.gz
- Upload date:
- Size: 36.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 |
1c9d6c2e26a9bd2e9103a15d7b94f8f39c161bfc564b0e86e5ef5eced3220c1f
|
|
| MD5 |
4f4850b9df52122bb5f959ba944c7156
|
|
| BLAKE2b-256 |
2e4df48e5b1bff7c0c474dbe33b8ae80ce78bb3483aa9c1230443b6c7ddd9271
|
File details
Details for the file llm4agents_sdk-2.3.2-py3-none-any.whl.
File metadata
- Download URL: llm4agents_sdk-2.3.2-py3-none-any.whl
- Upload date:
- Size: 29.4 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 |
b459a2297012643f6f4ad4214d5f871a747d90b61728d0abe93f22e62ecd839d
|
|
| MD5 |
5daa25cbd4af203cb93a34896dd648f6
|
|
| BLAKE2b-256 |
76cb33ba5b7aabc8c5d4380de8ff7add2a3092505de4813a80e7ea5f6087b41e
|