Vendor-neutral agent runtime: one protocol, pluggable adapters for Claude Code, GitHub Copilot, Moonshot Kimi, AWS Bedrock, the OpenCode HTTP agent server, and OpenAI-compatible gateways.
Project description
airframe
One protocol, every agent SDK. Vendor-neutral runtime for
Python AI agents — write once against a small AgentRuntime
protocol and run on AWS Bedrock, Claude Code, GitHub Copilot,
Moonshot Kimi, the OpenCode HTTP agent server, OpenCode Go,
OpenCode Zen, or OpenRouter by changing a single config value.
Quickstart
pip install airframe-agents[claude] # or [bedrock] / [copilot] / [kimi] / [openai-compat] / [all]
from airframe import runtime_for, ProviderModel
from pydantic import BaseModel
class Brief(BaseModel):
summary: str
risks: list[str]
# Provider ID comes from config — YAML, env, CLI flag, whatever.
provider_id = "claude" # or bedrock / github-copilot / kimi / opencode / opencode-go / opencode-zen / openrouter
cls = runtime_for(provider_id) # discovery lookup by ID
runtime = cls() # auth resolves from env / credential files
result = await runtime.execute(
"Brief me on the project structure.",
schema=Brief,
model=ProviderModel(provider_id, "claude-haiku-4-5"),
)
print(result.structured) # {"summary": "...", "risks": [...]}
print(result.cost.cost_usd) # 0.0042
await runtime.close()
The same agent code now serves any installed adapter — swap
provider_id (and model) in config, no import or instantiation
changes. Add a new vendor to your project's YAML and ship.
Direct imports still work when you only ever need one adapter:
from airframe import ClaudeCodeRuntime
runtime = ClaudeCodeRuntime()
Use list_providers() to enumerate installed adapters at startup
(handy for validating YAML config):
from airframe import list_providers
list_providers() # ["claude", "github-copilot"] — whichever extras are installed
The PyPI distribution name is airframe-agents. The import name
is airframe.
Supported providers
| Adapter | PROVIDER_ID |
Vendor SDK | Auth |
|---|---|---|---|
BedrockRuntime |
bedrock |
aioboto3 |
boto3 chain (env / AWS_PROFILE / IAM role) + AWS_REGION |
ClaudeCodeRuntime |
claude |
claude-agent-sdk |
Claude Max OAuth → ~/.claude/credentials.json → ANTHROPIC_API_KEY |
CopilotRuntime |
github-copilot |
github-copilot-sdk |
GITHUB_TOKEN → gh auth |
KimiRuntime |
kimi |
kimi-agent-sdk |
KIMI_API_KEY (Python 3.12+ only; mcp-version conflict with [claude]) |
OpenCodeGoRuntime |
opencode-go |
OpenAI compatible | OPENCODE_API_KEY → opencode auth.json::opencode-go.key |
OpenCodeServerRuntime |
opencode |
opencode-ai |
HTTP Basic (loopback unauthenticated; OPENCODE_SERVER_PASSWORD for remote) |
OpenCodeZenRuntime |
opencode-zen |
OpenAI compatible | OPENCODE_API_KEY → opencode auth.json::opencode.key |
OpenRouterRuntime |
openrouter |
OpenAI compatible | OPENROUTER_API_KEY |
The OpenAI-compatible family (OpenCodeZenRuntime per-token,
OpenCodeGoRuntime subscription, OpenRouterRuntime multi-vendor
gateway today; Together / Groq / Fireworks as future siblings) shares
the OpenAICompatibleRuntime base — subclasses are ~30 lines. See
docs/adapters/third-party.md.
BedrockRuntime is the enterprise / managed-cloud path —
AWS-billed access to a multi-vendor catalog (Anthropic, Meta,
Mistral, Cohere, Amazon Nova) behind IAM-rooted auth and region
pinning. Distinct from the OpenAI-compatible family because Bedrock
speaks Converse, not Chat Completions.
Each adapter has one canonical provider ID. "anthropic" is
reserved for a future direct-API AnthropicRuntime; "openai"
for a future OpenAIRuntime. Current adapters cover the
subscription paths (Claude Max, Copilot, ChatGPT Plus,
opencode-go), the per-token gateways (OpenCode Zen, OpenRouter),
the AWS-billed managed path (Bedrock), and the self-hosted
agent server path (OpenCode Server — wraps opencode serve and
fronts whichever upstream providers opencode auth login has
configured, including ChatGPT-OAuth subscriptions).
ClaudeCodeRuntime is the only adapter that accepts Claude
bindings. CopilotRuntime declines them — Claude served via
Copilot Chat Completions emits markdown-fenced JSON instead of
honouring tool calls, so it can't satisfy the structured-output
contract.
Capability matrix
Current snapshot (run
uv run python examples/probe_supports.py for the live version):
| Feature | Bedrock | Claude | Copilot | Kimi | OpenAI-compat | OpenCode |
|---|---|---|---|---|---|---|
STRUCTURED_OUTPUT_JSON_SCHEMA |
✓ | ✓ | ✓ | ◐ | ✓ | ✗ (SDK gap) |
STREAMING / CANCEL |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
SESSION_RESUME |
✗ | ✓ | ✓ | ✓ | ✗ | ✓ |
REASONING_EFFORT |
✓ (Anthropic) | ✓ | ✓ | ✓ (bool) | ✓ | ✓ (per-upstream) |
REASONING_BUDGET_TOKENS |
✓ (Anthropic) | ✓ | ✗ | ✗ | ✗ | ✓ (Anthropic upstream) |
VISION_INPUT |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
FILE_INPUT |
✓ (Anthropic) | ✓ | ✓ | ✗ | ✗ | ✓ |
TOOLS_FUNCTION |
✓ | ✓ | ✓ | ✗ | ✓ | ✗ (SDK gap) |
TOOLS_MCP_STDIO / _HTTP |
✗ | ✓ | ✓ | ✓ | ✗ | ✗ (SDK gap) |
TOOLS_MCP_SSE |
✗ | ✓ | ✗ | ✓ | ✗ | ✗ (SDK gap) |
PERMISSION_CALLBACK |
✓ | ✓ | ✓ | ✓ | ✗ | ✗ (SDK gap) |
LIFECYCLE_HOOKS |
✓ (6 kinds) | ✓ (8 kinds) | ✓ (7 kinds) | ✓ (7 kinds) | ✓ (6 kinds) | ✓ (6 kinds) |
BUDGET_USD_CAP |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
BUDGET_TURN_CAP |
✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
Capability flags are statically declared per adapter. Check
runtime.supports(Feature.X) before invoking a feature; declined
capabilities raise UnsupportedFeatureError with a feature=
attribute so the call fails fast.
Full per-feature semantics in docs/capabilities.md;
per-adapter quirks under docs/adapters/.
Why?
Each vendor ships a Python SDK that does something subtly different:
the Claude Agent SDK exposes a subprocess + JSON-RPC interface;
GitHub's Copilot SDK exposes a session + tool registration model;
Moonshot's Kimi Agent SDK spawns the kimi-cli subprocess and
streams typed WireMessage events; AWS Bedrock's aioboto3 client
fronts a multi-vendor model catalog behind the Converse envelope
and IAM auth; sst/opencode's opencode-ai SDK fronts a model-
agnostic HTTP agent server (opencode serve) routing through any
upstream opencode auth login has configured; the OpenAI-compatible
gateways (OpenCode Zen, OpenCode Go, OpenRouter) speak Chat
Completions HTTP. Each has its
own auth chain, error taxonomy, cost-reporting shape,
structured-output mechanism, and
models-endpoint shape. Airframe
collapses those differences behind one
execute / session / reset / close / validate_binding / list_models / supports / unwrap interface, classifies every vendor's failures
into a single hierarchy, and produces a single CostRecord /
ModelInfo shape regardless of the vendor.
The protocol is intentionally narrow. The eight methods are the contract; everything else (auth chains, session caching, tool-call forcing, JSON-schema mode, envelope unwrapping, per-model metadata joining) lives inside each adapter, where vendor-specific behaviour belongs.
Anything above the protocol — retry policy, fallback across vendors, conversation memory, multi-agent orchestration — is left to the consumer. Airframe is the adapter layer; the application composes its own behaviour on top.
The shape — one narrow protocol plus pluggable vendor adapters, discovered by ID — is borrowed from JDBC, with the same goal: let the application code stay vendor-agnostic while each adapter absorbs its vendor's quirks.
Install
pip install airframe-agents[bedrock] # BedrockRuntime
pip install airframe-agents[claude] # ClaudeCodeRuntime
pip install airframe-agents[copilot] # CopilotRuntime
pip install airframe-agents[kimi] # KimiRuntime (Python 3.12+, separate venv — see note below)
pip install airframe-agents[opencode] # OpenCodeServerRuntime (local opencode serve)
pip install airframe-agents[openai-compat] # OpenCodeGoRuntime + OpenCodeZenRuntime + OpenRouterRuntime (+ future siblings)
pip install airframe-agents[all] # Everything except [kimi] (mcp-version conflict)
pip install airframe-agents[testing] # Conformance contract suite (pytest)
[kimi] co-installation note. kimi-agent-sdk 0.0.5 →
kimi-cli 1.12 → fastmcp 2.12.5 → mcp<1.17, but
claude-agent-sdk 0.2 requires mcp>=1.23. The two SDKs cannot
co-install in one environment until upstream resolves; airframe
declares the conflict in [tool.uv.conflicts] and excludes
[kimi] from [all]. Users wanting both extras must split into
separate venvs.
list_providers() filters by which extras you installed:
airframe-agents[copilot] makes list_providers() return
["github-copilot"]. Pass installed_only=False to see every
built-in provider for documentation UIs.
Sessions, streaming, and the new kwargs
runtime.execute(...) is convenient single-turn sugar. The full
surface lives on runtime.session(...):
from airframe import (
ClaudeCodeRuntime, FunctionTool, McpServerRef,
PermissionCallback, PermissionDecision, PermissionRequest,
HookEvent, ClaudeOptions, TextDelta, TurnComplete,
)
from pydantic import BaseModel
class AddArgs(BaseModel):
a: float
b: float
async def add(args: AddArgs) -> float:
return args.a + args.b
class ApproveAll(PermissionCallback):
async def handle(self, req: PermissionRequest) -> PermissionDecision:
return "allow"
def log_event(e: HookEvent) -> None:
print(f"[{e.kind}] {e.payload}")
runtime = ClaudeCodeRuntime()
sess = runtime.session(
system="You are a careful math assistant.",
tools=[FunctionTool(name="add", description="Add two numbers.",
params=AddArgs, handler=add)],
mcp_servers=[McpServerRef(name="docs", transport="http",
url="https://mcp.example.com",
auth_token="...")],
on_permission=ApproveAll(),
on_event=log_event,
provider_options=ClaudeOptions(strict_mcp_config=True),
)
try:
async for event in sess.stream("What is 17 + 25?",
max_turns=10, max_budget_usd=0.05):
if isinstance(event, TextDelta):
print(event.text, end="", flush=True)
elif isinstance(event, TurnComplete):
print(f"\nfinal cost: ${event.result.cost.cost_usd}")
finally:
await sess.close()
session.stream() yields a discriminated union of five event
variants: TextDelta, ReasoningDelta, ToolCallStart,
ToolCallResult, TurnComplete. The variant set is shape-locked.
Per-kwarg semantics live in
docs/capabilities.md; per-adapter quirks
in each docs/adapters/ page.
Errors
Adapters classify vendor failures into a small hierarchy:
| Error | Meaning |
|---|---|
RuntimeAuthError |
Credentials bad / expired / missing |
RuntimeModelNotFoundError |
Server doesn't serve that model on this binding |
RuntimeTransientError |
5xx, rate limit, brief outage — recoverable |
RuntimeStructuredOutputError |
Transport OK; payload didn't match schema |
RuntimeBudgetExceededError |
max_turns= / max_budget_usd= cap tripped |
UnsupportedFeatureError |
Capability declined (carries feature= attr) |
Full list and the rest of the hierarchy in
docs/reference.md#errors.
Escape hatch: unwrap()
When the portable surface doesn't expose a vendor-specific knob,
reach the native SDK object via unwrap():
from claude_agent_sdk import ClaudeSDKClient
sess = runtime.session()
await sess.execute("hi")
client: ClaudeSDKClient = sess.unwrap(ClaudeSDKClient)
await client.interrupt()
Each adapter declares the native types it accepts; unsupported
types raise TypeError. Runtime-level types via
runtime.unwrap(...); session-level vendor objects via
session.unwrap(...).
Live probes
examples/probe_*.py exercise each adapter end-to-end against a
real CLI / HTTP endpoint. They're runnable demos, not part of
make test. Auth issues surface as classified Runtime*Error.
uv run python examples/probe_supports.py # capability matrix
uv run python examples/probe_streaming.py # stream() against any installed adapter
uv run python examples/probe_tools.py # FunctionTool round-trip
uv run python examples/probe_mcp.py # external MCP server
uv run python examples/probe_permission.py # PermissionCallback
uv run python examples/probe_hooks.py # HookEvent observation
uv run python examples/probe_budget.py # max_turns / max_budget_usd
Full list with one-line descriptions in
docs/cookbook.md.
Documentation
- Architecture & design — protocol shape, runtime-vs-session split, streaming event taxonomy.
- Capabilities — per-
Featuresemantics across adapters. - Authentication — per-adapter credential resolution chains and CI patterns.
- API reference — every public name with cross-links into the source.
- Cookbook — runnable recipes via the probe scripts.
- Per-adapter notes — Bedrock · Claude · Copilot · Kimi · OpenCode Go · OpenCode Server · OpenCode Zen · OpenRouter.
- Writing your own adapter —
the
airframe.adaptersentry-point group + conformance contracts. - Changelog · Contributing · Security.
Development
uv sync --all-extras --group dev
make test # full suite (incl. integration tests, which self-skip without creds)
make test-fast # exclude `integration` marker
make lint # ruff
make typecheck # mypy
make ci # lint + format + typecheck + test
Integration tests run automatically when credentials for an adapter are configured (see auth.md).
License
MIT — see LICENSE.
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 airframe_agents-0.8.0.tar.gz.
File metadata
- Download URL: airframe_agents-0.8.0.tar.gz
- Upload date:
- Size: 703.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29e4ec7ba9f3d1ece6b96771e42c9ba4fba0623b474b1cd7acc1779bbcdd353f
|
|
| MD5 |
d93f83a0414540e8b343df47f1bd208d
|
|
| BLAKE2b-256 |
99747144a18067c49e40133a3e0173b14361510f0cb284a13adeaff36969c510
|
Provenance
The following attestation bundles were made for airframe_agents-0.8.0.tar.gz:
Publisher:
release.yml on get2knowio/airframe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
airframe_agents-0.8.0.tar.gz -
Subject digest:
29e4ec7ba9f3d1ece6b96771e42c9ba4fba0623b474b1cd7acc1779bbcdd353f - Sigstore transparency entry: 1576041272
- Sigstore integration time:
-
Permalink:
get2knowio/airframe@2434a62b5c31aa90e7a98260c2274719995ef93f -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/get2knowio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2434a62b5c31aa90e7a98260c2274719995ef93f -
Trigger Event:
push
-
Statement type:
File details
Details for the file airframe_agents-0.8.0-py3-none-any.whl.
File metadata
- Download URL: airframe_agents-0.8.0-py3-none-any.whl
- Upload date:
- Size: 206.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ee0594028b405315a1210fe4dda6f0381bfc5b94c7c217e771a1b072e08975a
|
|
| MD5 |
9790387a0c64ac6f43c20c60423873ac
|
|
| BLAKE2b-256 |
e490a0e94a867bb1f7d4da781e2e4c16c939e8df7aae291b9f20a9fb8d32d97d
|
Provenance
The following attestation bundles were made for airframe_agents-0.8.0-py3-none-any.whl:
Publisher:
release.yml on get2knowio/airframe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
airframe_agents-0.8.0-py3-none-any.whl -
Subject digest:
4ee0594028b405315a1210fe4dda6f0381bfc5b94c7c217e771a1b072e08975a - Sigstore transparency entry: 1576041334
- Sigstore integration time:
-
Permalink:
get2knowio/airframe@2434a62b5c31aa90e7a98260c2274719995ef93f -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/get2knowio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2434a62b5c31aa90e7a98260c2274719995ef93f -
Trigger Event:
push
-
Statement type: