Safe, budget-aware multi-model agent experiments via OpenRouter
Project description
agent-preflight
Safe, budget-aware multi-model agent experiments via OpenRouter.
When you're running a field of LLMs on an autonomous task — each in its own Docker container, each with its own API key and spend cap — you need: preflight validation, per-model budget enforcement, eject policies, event logging, and a status dashboard. That's what this library provides.
Background
This pattern emerged from two independent experiments:
-
Loanville2 — an LLM lending benchmark where models autonomously underwrite loan applications through a full Loan Origination System REST API. Each model gets a provisioned sub-key and budget cap; a preflight check validates balance, model availability, and provisioning before the run starts.
-
pi-docker — a game strategy experiment where models compete to improve a Kurve AI in Docker containers. Budget awareness was injected as a file every 30 seconds; eject policies killed models that spent 25%+ of their budget without running a single test.
Both converged on the same infrastructure. This library extracts it.
Install
pip install ai-preflight
Requires Python 3.11+. Docker is used via the CLI (no Docker SDK dependency).
The package ships with py.typed for mypy/pyright users.
Quickstart
from agent_preflight import preflight, provision_key, BudgetTracker
# 1. Validate before spending anything
preflight(
admin_key="sk-or-admin-...",
or_key="sk-or-...",
models=["meta-llama/llama-3.3-70b-instruct"],
budget_per_model=0.25,
)
# 2. Provision a capped sub-key
key = provision_key(admin_key, label="my-run", limit_usd=0.25)
# Optional: prefer {} on model-list fetch errors
from agent_preflight import get_available_models_or_empty
models = get_available_models_or_empty("sk-or-...")
# 3. Track spend inside your agent loop
tracker = BudgetTracker(api_key=key, limit_usd=0.25, mode="detailed")
status = tracker.poll()
print(status) # Budget: $0.25 remaining of $0.25 (100% left)
Prompt caching
Prompt caching is built into the runner, cache report, and PromptBuilder.
- Anthropic defaults to explicit per-block
cache_controlbecause that is the most reliable OpenRouter behavior we have observed. - OpenAI-style providers benefit from a stable, append-only prompt prefix.
- The detailed guide, behavior notes, and practical best practices live in CACHE.md.
Full Docker harness
from agent_preflight import run_multi, BudgetFractionEject, IdleTimeoutEject, CompositeEject
results = run_multi(
models=[
("meta-llama/llama-3.3-70b-instruct", "llama"),
("google/gemini-flash-1.5", "gemini"),
],
image_name="my-agent:latest",
admin_key="sk-or-admin-...",
or_key="sk-or-...",
budget_usd=0.25,
mounts=[
("/path/to/TASK.md", "/workspace/TASK.md", "ro"),
"/path/to/cache:/workspace/cache:delegated",
],
eject_policy=CompositeEject([
BudgetFractionEject(check_after_pct=0.25,
progress_fn=lambda ctx: ctx["tool_calls"] > 0),
IdleTimeoutEject(timeout_s=180),
]),
budget_mode="detailed",
)
Each container:
- Gets a provisioned sub-key with a hard spend cap
- Receives
budget.txtin its workspace every 30s (so the agent can see its balance) - Is killed if an eject policy triggers
Status dashboard
# Watch live run progress
watch -n 10 python -m agent_preflight status
# Or a specific run
python -m agent_preflight status run-20260312-120000
Output:
Run: run-20260312-120000
============================================================
alias status events tools snaps cost time note
──────────────────────────────────────────────────────────────────
llama DONE 87 42 5 $0.18 4m22s
gemini running 34 18 2 $0.07 1m45s* $0.18 remaining...
Preflight checks
preflight() runs up to 7 checks before your experiment starts:
| Check | What it validates |
|---|---|
balance |
Account balance > $0.10 |
math |
Balance covers n_models × budget_per_model |
models |
Each model exists on OpenRouter (with pricing) |
tool_support |
Models in required_params / common_required_params support those request params |
key_smoke |
Provisions a $0.01 test key and makes a real API call |
cache_smoke |
Sends two Anthropic cacheable requests to verify prompt caching |
docker_image |
Docker image exists locally (if docker_image= set) |
preflight(
admin_key=..., or_key=...,
models=[...], budget_per_model=0.25,
checks=["balance", "math", "models", "tool_support"],
common_required_params=["tools"],
required_params={"anthropic/claude-sonnet-4": ["tools"]},
custom_checks=[my_health_check], # zero-arg or context-aware
)
from agent_preflight import run_single
result = run_single(
model_id="z-ai/glm-5",
extra_models=[
"google/gemini-2.5-flash",
"anthropic/claude-sonnet-4",
],
image_name="my-agent:latest",
admin_key="sk-or-admin-...",
or_key="sk-or-...",
budget_usd=0.25,
)
Context-aware custom checks receive a dict with already-fetched data:
def check_tool_support(ctx):
model = "anthropic/claude-sonnet-4"
supported = set((ctx["available_models"] or {}).get(model, {}).get("supported_parameters", []))
missing = {"tools"} - supported
return (
"Custom tool support",
not missing,
"ok" if not missing else f"missing: {', '.join(sorted(missing))}",
)
Eject policies
Built-in policies:
| Class | Triggers when |
|---|---|
BudgetExceededEject |
Measured spend ≥ limit |
IdleTimeoutEject(300) |
No events for N seconds |
BudgetFractionEject(0.20, progress_fn) |
progress_fn returns False at N% budget spent |
CompositeEject([...]) |
Any sub-policy triggers |
default_eject_policies() |
BudgetExceededEject + IdleTimeoutEject(300s) |
Custom policy:
from agent_preflight import EjectPolicy, EjectDecision
class MyEject(EjectPolicy):
def check(self, *, budget_status, events, context, elapsed_s, **_):
if context.get("output_written"):
return EjectDecision(should_eject=False)
if budget_status.pct_spent > 0.5:
return EjectDecision(should_eject=True, reason="no output after 50% budget")
return EjectDecision(should_eject=False)
Budget injection
Agents that read /workspace/output/budget.txt get real-time cost visibility:
Simple mode:
Budget: $0.086 remaining of $0.15 (57% left)
Detailed mode:
Budget: $0.086 remaining of $0.15 (57% left)
Recent costs:
- $0.0012 (bash)
- $0.0031 (read_file)
- $0.0008 (bash)
Total actions: 14 | Avg cost/action: $0.0010
Estimated actions remaining: ~86
Agent contract
Your Docker image needs to:
- Accept
OPENROUTER_API_KEYas an env var - Optionally read
/workspace/output/budget.txtfor cost awareness - Write output files to
/workspace/output/ - (For pi-style RPC mode) Accept a JSON prompt on stdin
Beyond that, the harness is framework-agnostic. Works with pi-coding-agent, Claude Code, custom agents, or any container that calls OpenRouter.
Environment variables for the CLI
OR_ADMIN_KEY=sk-or-admin-...
OPENROUTER_API_KEY=sk-or-...
PREFLIGHT_MODELS=meta-llama/llama-3.3-70b-instruct,google/gemini-flash-1.5
PREFLIGHT_BUDGET=0.25
python -m agent_preflight preflight
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 ai_preflight-0.3.0.tar.gz.
File metadata
- Download URL: ai_preflight-0.3.0.tar.gz
- Upload date:
- Size: 62.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9651dc26f81a4fcd561714156eb66fc2352bc4ef01d98a63a40dcd51d127ec49
|
|
| MD5 |
b1ea0f3ee20fd6aa03d932b4aef4e441
|
|
| BLAKE2b-256 |
bedfddbb316a95024f0a52d49c8207bc78432d94ac2c862ed368b95162c69366
|
Provenance
The following attestation bundles were made for ai_preflight-0.3.0.tar.gz:
Publisher:
ci.yml on seadotdev/agent-preflight
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_preflight-0.3.0.tar.gz -
Subject digest:
9651dc26f81a4fcd561714156eb66fc2352bc4ef01d98a63a40dcd51d127ec49 - Sigstore transparency entry: 1154470410
- Sigstore integration time:
-
Permalink:
seadotdev/agent-preflight@9ea4ece93c601e3f3042dd2358aab946b5475069 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/seadotdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@9ea4ece93c601e3f3042dd2358aab946b5475069 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ai_preflight-0.3.0-py3-none-any.whl.
File metadata
- Download URL: ai_preflight-0.3.0-py3-none-any.whl
- Upload date:
- Size: 46.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8d7f3ad123749fb3dbecb61eeb39f4096a5442873fd3a98eb236fe08f218f54
|
|
| MD5 |
033d19be64f5afaf5c1cd32ba8449521
|
|
| BLAKE2b-256 |
7fdab1031a491882ad7615da9fd7527cde54a0760ab34454e26fd6b21d6224e3
|
Provenance
The following attestation bundles were made for ai_preflight-0.3.0-py3-none-any.whl:
Publisher:
ci.yml on seadotdev/agent-preflight
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_preflight-0.3.0-py3-none-any.whl -
Subject digest:
b8d7f3ad123749fb3dbecb61eeb39f4096a5442873fd3a98eb236fe08f218f54 - Sigstore transparency entry: 1154470411
- Sigstore integration time:
-
Permalink:
seadotdev/agent-preflight@9ea4ece93c601e3f3042dd2358aab946b5475069 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/seadotdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@9ea4ece93c601e3f3042dd2358aab946b5475069 -
Trigger Event:
push
-
Statement type: