Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ jobs:
pytest tests/unit --ignore=tests/unit/test_solana_client.py --ignore=tests/unit/test_solana_wallet.py -k "not SolanaX402"
fi

# llama-index-llms-blockrun, published from integrations/ as its own PyPI
# package. Installed against THIS checkout of the SDK, so an SDK change that
# breaks the adapter fails here rather than after both are released. The
# lowest leg pins the floor of every declared range: a floor that is never
# installed is a floor nobody has checked (0.6.5 of the OpenAI serializer
# raises on a ThinkingBlock that 0.8 skips).
llama-index:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- python-version: '3.10'
pins: 'llama-index-core==0.14.5 llama-index-llms-openai==0.6.5'
- python-version: '3.12'
pins: ''
defaults:
run:
working-directory: integrations/llama-index-llms-blockrun
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: pip install -e "../..[solana]" -e ".[dev]" ${{ matrix.pins }}
- name: Run unit tests
run: pytest

# Fails when a number in the docs disagrees with brand-numbers.json.
# Offline by design: it reads the committed snapshot and never fetches, so a
# blockrun.ai deploy in progress cannot fail this repo's CI. Its own job
Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/publish-llama-index.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Publish llama-index-llms-blockrun to PyPI

# Released separately from the SDK: cut a GitHub release tagged
# llama-index-llms-blockrun-v<version> (the version in
# integrations/llama-index-llms-blockrun/pyproject.toml). publish.yml skips
# these tags.
on:
release:
types: [published]

jobs:
build:
if: ${{ startsWith(github.event.release.tag_name, 'llama-index-llms-blockrun-v') }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/llama-index-llms-blockrun
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install package and test deps
run: pip install -e "../..[solana]" -e ".[dev]" build

# PyPI never lets a published file be replaced, so a tag that names the
# wrong version is refused here, before anything is uploaded.
- name: Verify the release tag matches the package version
run: |
tag="${{ github.event.release.tag_name }}"
declared="llama-index-llms-blockrun-v$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
if [ "$tag" != "$declared" ]; then
echo "Release tag $tag does not match pyproject version ($declared)." >&2
exit 1
fi
echo "Release tag $tag matches pyproject version."

- name: Verify the release is publishable
run: pytest -q

- name: Build package
run: python -m build

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-llama-index
path: integrations/llama-index-llms-blockrun/dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist-llama-index
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:

jobs:
build:
# llama-index-llms-blockrun-v* releases publish a different package, from
# publish-llama-index.yml; this job would only fail their VERSION check.
if: ${{ !startsWith(github.event.release.tag_name, 'llama-index-llms-blockrun-v') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,24 @@ The 402 from that host names `eip155:5042`, and the SDK signs the EIP-3009 autho
2. Fund it with USDC on Arc (Arc's native token, shown as the ERC-20 at `0x3600…0000`)
3. `api_url="https://arc.blockrun.ai/api"` — payments are automatic via x402

## LlamaIndex

[`llama-index-llms-blockrun`](integrations/llama-index-llms-blockrun) makes any
BlockRun chat model a LlamaIndex LLM — chat, streaming, async, tool calling and
agents — paying through this SDK on the chain you pick:

```bash
pip install llama-index-llms-blockrun # pay on Base
pip install "llama-index-llms-blockrun[solana]" # pay on Solana
```

```python
from llama_index.llms.blockrun import BlockRun

llm = BlockRun(model="openai/gpt-5.5") # USDC on Base
llm = BlockRun(model="openai/gpt-5.5", chain="solana") # USDC on Solana
```

## Smart Routing (Router Core)

Let the SDK automatically pick the cheapest capable model for each request:
Expand Down
21 changes: 21 additions & 0 deletions integrations/llama-index-llms-blockrun/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 BlockRun

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
128 changes: 128 additions & 0 deletions integrations/llama-index-llms-blockrun/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# LlamaIndex LLM Integration: BlockRun

[BlockRun](https://blockrun.ai) is one OpenAI-compatible gateway to models from
OpenAI, Anthropic, Google, DeepSeek, xAI, NVIDIA and others, where every call
pays for itself in USDC over [x402](https://x402.org). There is no API key and
no account: you pick the chain the wallet pays from, Base or Solana.

## Why not `OpenAILike`?

`OpenAILike(api_base="https://blockrun.ai/api/v1", api_key="fake")` reaches the
gateway and stops at the first paid call. The gateway answers an unpaid request
with HTTP 402 and a price, and nothing in the OpenAI client knows how to sign a
payment for it. This package hands that step to the
[`blockrun-llm`](https://pypi.org/project/blockrun-llm/) SDK, which signs the
payment locally and retries the request with the signature attached.

## Install

```bash
pip install llama-index-llms-blockrun # pay on Base
pip install "llama-index-llms-blockrun[solana]" # pay on Solana
```

## Pick a chain

| `chain=` | Pays with | Wallet, in order of precedence |
| ------------------ | -------------- | ---------------------------------------------------------------------------------------- |
| `"base"` (default) | USDC on Base | `private_key=` (EVM hex key), `BLOCKRUN_WALLET_KEY`, `~/.blockrun/.session` |
| `"solana"` | USDC on Solana | `private_key=` (base58 secret key), `SOLANA_WALLET_KEY`, a Solana wallet file on disk |

The key never leaves your machine; it only signs the payment for each call.
`llm.wallet_address` prints the address to fund.

```python
from llama_index.llms.blockrun import BlockRun

llm = BlockRun(model="openai/gpt-5.5") # Base
llm = BlockRun(model="openai/gpt-5.5", chain="solana") # Solana

print(llm.wallet_address)
```

## Usage

```python
from llama_index.core.llms import ChatMessage
from llama_index.llms.blockrun import BlockRun

llm = BlockRun(model="anthropic/claude-sonnet-4.6")

print(llm.complete("Paris is the capital of"))

response = llm.chat(
[
ChatMessage(role="system", content="Answer in one sentence."),
ChatMessage(role="user", content="What is x402?"),
]
)
print(response.message.content)
print(response.additional_kwargs.get("cost_usd")) # USD charged, where the SDK reports it (Base)

for chunk in llm.stream_complete("Write a haiku about USDC."):
print(chunk.delta, end="", flush=True)
```

Async works the same way: `achat`, `acomplete`, `astream_chat`, `astream_complete`.

### As the default LLM

```python
from llama_index.core import Settings

Settings.llm = BlockRun(model="openai/gpt-5.5")
```

### Tools and agents

`BlockRun` is a `FunctionCallingLLM`, so tool calling, `predict_and_call`,
structured outputs and `FunctionAgent` work as they do with OpenAI:

```python
from llama_index.core.agent.workflow import FunctionAgent


def multiply(a: float, b: float) -> float:
"""Multiply two numbers."""
return a * b


agent = FunctionAgent(tools=[multiply], llm=BlockRun(model="openai/gpt-5.5"))
print(await agent.run("What is 1234 * 4567?"))
```

## Spending

Each call is quoted by the gateway before anything is signed. Cap what a single
call may cost, and a quote above it is refused before it is paid:

```python
llm = BlockRun(model="openai/gpt-5.5", max_cost_per_call=0.05)
```

A refused quote raises `blockrun_llm.SpendLimitError`; an empty wallet raises
`blockrun_llm.PaymentError`. Prices per model are listed at
https://blockrun.ai/api/v1/models.

## Options

| Parameter | Default | Notes |
| --------------------------- | ---------------- | ---------------------------------------------------------------------------------------- |
| `model` | required | Any chat model id from https://blockrun.ai/api/v1/models |
| `chain` | `"base"` | `"base"` or `"solana"` |
| `private_key` | from environment | Never serialized with the LLM |
| `temperature` | unset | Unset lets each model use its own default |
| `max_tokens` | unset | The SDK sends 1024 when unset |
| `context_window` | from catalog | Read once from `/v1/models`; set it to skip that request |
| `max_cost_per_call` | unset | USD ceiling per call |
| `api_url` | chain's mainnet | `https://testnet.blockrun.ai/api` for Base Sepolia |
| `timeout` | 600 s | Reasoning models can think for minutes |
| `additional_kwargs` | `{}` | Sent with every request, e.g. `top_p`, `stop`, `response_format` |
| `is_function_calling_model` | `True` | Set `False` for a model without tool support |

## Development

```bash
pip install -e "../..[dev]" -e ".[dev]"
pytest
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from llama_index.llms.blockrun.base import BlockRun

__all__ = ["BlockRun"]
Loading
Loading