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
19 changes: 19 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Normalize line endings in the repository. Without this the working tree's
# native endings leak into commits: 3 files were converted CRLF->LF inside an
# unrelated behavioural fix, inflating that diff from 51 real lines to 1045 and
# burying the change under formatting noise. 14 other tracked files were still
# CRLF, so the next contributor would have regenerated it.
* text=auto

# Binary formats git must not touch.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.woff binary
*.woff2 binary
*.mp3 binary
*.mp4 binary
*.wav binary
90 changes: 45 additions & 45 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 10) else 1)"; then
pip install -e ".[dev,solana]"
else
pip install -e ".[dev]"
fi
- name: Check formatting
run: black --check .
- name: Lint
run: ruff check .
- name: Run unit tests
run: |
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 10) else 1)"; then
pytest tests/unit
else
pytest tests/unit --ignore=tests/unit/test_solana_client.py --ignore=tests/unit/test_solana_wallet.py -k "not SolanaX402"
fi
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.11', '3.12']

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 10) else 1)"; then
pip install -e ".[dev,solana]"
else
pip install -e ".[dev]"
fi

- name: Check formatting
run: black --check .

- name: Lint
run: ruff check .

- name: Run unit tests
run: |
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 10) else 1)"; then
pytest tests/unit
else
pytest tests/unit --ignore=tests/unit/test_solana_client.py --ignore=tests/unit/test_solana_wallet.py -k "not SolanaX402"
fi
36 changes: 34 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,40 @@ jobs:
with:
python-version: '3.11'

- name: Install build tools
run: pip install build
# 1.8.1 shipped with VERSION and __init__.py still reading 1.8.0. The
# guard test caught it, but only on the push-triggered CI run, after the
# release had been cut and published. PyPI does not allow overwriting a
# published file, so that artifact is permanently wrong.
#
# These steps run ci.yml's checks on the 3.11 leg only — black, ruff and
# the unit suite. They are NOT full CI: the 3.9 and 3.12 legs still run
# only on push, so version-incompatible syntax is caught there, not here.
# Extras match the 3.11 CI job so the gate covers the Solana paths; with
# the plain extra those tests importorskip and pass silently.
- name: Install package and test deps
run: pip install -e ".[dev,solana]" build

# A release tag that disagrees with the package version means the wrong
# tree is being published. PyPI would reject a duplicate version, but
# only after the release is cut; fail here instead.
- name: Verify the release tag matches VERSION
run: |
tag="${{ github.event.release.tag_name }}"
declared="v$(tr -d '[:space:]' < VERSION)"
if [ "$tag" != "$declared" ]; then
echo "Release tag $tag does not match VERSION ($declared)." >&2
exit 1
fi
echo "Release tag $tag matches VERSION."

- name: Check formatting
run: black --check blockrun_llm/ tests/

- name: Lint
run: ruff check blockrun_llm/ tests/

- name: Verify the release is publishable
run: pytest tests/unit -q

- name: Build package
run: python -m build
Expand Down
132 changes: 66 additions & 66 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,66 +1,66 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
env/
.venv/
.env/
# Environment files
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test / coverage
.coverage
.pytest_cache/
htmlcov/
.tox/
.nox/
# mypy
.mypy_cache/
# Jupyter
.ipynb_checkpoints/
# Local Claude Code config (machine-specific allowlists)
.claude/settings.local.json
# Sweep / test-run artifacts
sweep-*.json
sweep-*.log
# Ruff cache
.ruff_cache/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
env/
.venv/
.env/

# Environment files
.env
.env.local
.env.*.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Test / coverage
.coverage
.pytest_cache/
htmlcov/
.tox/
.nox/

# mypy
.mypy_cache/

# Jupyter
.ipynb_checkpoints/

# Local Claude Code config (machine-specific allowlists)
.claude/settings.local.json

# Sweep / test-run artifacts
sweep-*.json
sweep-*.log

# Ruff cache
.ruff_cache/
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,60 @@

All notable changes to blockrun-llm will be documented in this file.

## 1.8.2 — 2026-07-21

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 the `PAYMENT-SIGNATURE` went 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_chat` on 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 as `APIError(503)` — exactly a status the fallback logic treats as
retriable. Callers catching `httpx.TimeoutException` are unaffected; the
marker is an attribute, not a new exception type.
- **The clamp warning cannot break the request it warns about.** Its parse ran
on `resource.description`, a server-controlled string, immediately before
signing. A non-string value raised `TypeError` and 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()` advertised `PaymentError: If budget is set and would be
exceeded`. There is no `budget` parameter anywhere in the SDK and no
client-side spend cap — every 402 quote is signed automatically. The
docstring now says that plainly and points at `get_spending()`.

### Changed
- **`max_tokens` above 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.8` sent 262144 and 1000000 both quote
the 128000 price, and `gpt-5.2` sent 1e12 returns a quote rather than a 400.
The 402 disclosed the clamp in `resource.description` and 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 `ValueError` around `MAX_TOKENS_SANITY_LIMIT` claimed the
gateway "enforces the real per-model ceiling and reports it". It does not.
Both now describe clamping.

### Fixed
- 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 — 2026-07-21

### Fixed
Expand Down
42 changes: 21 additions & 21 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,21 +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.
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.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.8.1
1.8.2
Loading
Loading