Skip to content
This repository was archived by the owner on Jul 19, 2026. It is now read-only.
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
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@

Agent-native inference routing by Ainfera. Signed AgentCards, provider-neutral routing, hash-chained AuditChains — out of the box.


## Compatibility

| Feature | ainfera SDK | openai SDK | anthropic SDK |
|---------|-------------|------------|---------------|
| Routed inference | ✅ | ❌ | ❌ |
| Streaming | ✅ | ✅ | ✅ |
| Tool use | ✅ | ✅ | ✅ |
| Receipt verification | ✅ | ❌ | ❌ |
| Audit chain | ✅ | ❌ | ❌ |
| Agent cards | ✅ | ❌ | ❌ |
| Provider-neutral | ✅ | ❌ | ❌ |
| Async support | ✅ | ✅ | ✅ |

## API Versioning

The SDK targets Ainfera API v1. Breaking changes follow semantic versioning.

## Install

```bash
Expand Down Expand Up @@ -64,24 +82,24 @@ ok = agent.audit_chain.verify() # walks the chain, verifies hashes offline

## What is Ainfera?

**The Inference of AI Agents.** Ainfera Inference (the flagship product — the routing brain) picks the best model under your agent's budget and latency caps. Point at `ainfera-inference` and trust the researched decision. One Agent Card across 57 models. Every routing decision and inference call cryptographically audited. See [ainfera.ai](https://ainfera.ai) and the [`ainfera-routing`](https://github.com/ainfera-ai/routing) decision library.
**The Inference of AI Agents.** Ainfera Inference (the flagship product — the routing brain) picks the best model under your agent's budget and latency caps. Point at `ainfera-inference` and trust the researched decision. One Agent Card across all routed models. Every routing decision and inference call cryptographically audited. See [ainfera.ai](https://ainfera.ai) and the [`ainfera-routing`](https://github.com/ainfera-ai/routing) decision library.

## Features

- **Signed AgentCards** per Agent (JWS, RFC 7515)
- **Provider-neutral routing** across 57 models from many providers (Anthropic, OpenAI, Together, and more)
- **Provider-neutral routing** across all routed models from many providers (Anthropic, OpenAI, Together, and more)
- **Atomic per-call settlement** out of an Agent-scoped Wallet
- **Tamper-evident hash-chained AuditChain** for every Agent
- **Local verification** — auditors can verify a chain offline, no Ainfera trust required
- **Sync + async** clients sharing one resource surface

## Concepts

- [Agent](https://ainfera.ai/concepts/agent)
- [AgentCard](https://ainfera.ai/concepts/agent-card)
- [Inference](https://ainfera.ai/concepts/inference)
- [Wallet](https://ainfera.ai/concepts/wallet)
- [AuditChain](https://ainfera.ai/concepts/audit-chain)
- [Agent](https://ainfera.ai/docs#agent)
- [AgentCard](https://ainfera.ai/docs#agent-card)
- [Inference](https://ainfera.ai/docs#inference)
- [Wallet](https://ainfera.ai/docs#wallet)
- [AuditChain](https://ainfera.ai/docs#audit-chain)

## Compose, don't invent

Expand Down
31 changes: 31 additions & 0 deletions examples/async_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Async usage of the Ainfera SDK."""

import asyncio

from ainfera import AsyncAinferaClient


async def main() -> None:
client = AsyncAinferaClient(api_key="ak_...")
agent = await client.agents.retrieve("ag_...")

response = await agent.inference(
model="ainfera-inference",
messages=[{"role": "user", "content": "Hello"}],
)
print(response.text)

# Concurrent requests
tasks = [
agent.inference(
model="ainfera-inference",
messages=[{"role": "user", "content": f"Fact {i}"}],
)
for i in range(3)
]
results = await asyncio.gather(*tasks)
for r in results:
print(r.text)


asyncio.run(main())
41 changes: 41 additions & 0 deletions examples/error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Production error handling with Ainfera SDK."""

import logging

from ainfera import (
AinferaClient,
AuthenticationError,
RateLimitError,
ServerError,
)
from ainfera import (
TimeoutError as APITimeoutError,
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

client = AinferaClient(api_key="ak_...", timeout=30.0)
agent = client.agents.retrieve("ag_...")

try:
response = agent.inference(
model="ainfera-inference",
messages=[{"role": "user", "content": "Hello"}],
)
print(response.text)

except AuthenticationError:
logger.error("Invalid API key — check your credentials")

except RateLimitError as e:
logger.warning(f"Rate limited — retry after {e.retry_after}s")

except APITimeoutError:
logger.error("Request timed out — try a simpler prompt or increase timeout")

except ServerError as e:
logger.error(f"Server error {e.status_code}: {e.message}")

except Exception as e:
logger.exception(f"Unexpected error: {e}")
6 changes: 2 additions & 4 deletions examples/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def main() -> int:
print("\n→ Running first signed Inference…")
try:
inference = agent.inference(
model="claude-haiku-4-5",
model="ainfera-inference",
messages=[
{
"role": "user",
Expand All @@ -95,9 +95,7 @@ def main() -> int:

print(f" ✓ content = {inference.content!r}")
print(f" ✓ model_used = {inference.model_used}")
print(
f" ✓ tokens = {inference.input_tokens} in / {inference.output_tokens} out"
)
print(f" ✓ tokens = {inference.input_tokens} in / {inference.output_tokens} out")
print(f" ✓ cost_usd = ${inference.cost_usd}")
print(f" ✓ inference_id = {inference.inference_id}")
print(f" ✓ receipt_id = {inference.receipt_id}")
Expand Down
26 changes: 26 additions & 0 deletions examples/receipt_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Verify an inference receipt locally."""

from ainfera import AinferaClient
from ainfera.verify import verify_receipt

client = AinferaClient(api_key="ak_...")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether AinferaClient auto-reads AINFERA_API_KEY from env,
# and whether verify_receipt exists in ainfera.verify.

# Check AinferaClient constructor signature
ast-grep outline python/ainfera/client.py --match 'AinferaClient' --view expanded

# Check for env var reading in client constructor
rg -n -C3 'AINFERA_API_KEY|os.environ|getenv' python/ainfera/client.py

# Check verify_receipt exists in ainfera.verify
rg -n 'def verify_receipt' python/ainfera/verify.py

Repository: ainfera-ai/sdk-python

Length of output: 2207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the receipt verification example and related example files that may use the same pattern.
git ls-files 'examples/*.py'

printf '\n--- examples/receipt_verification.py ---\n'
cat -n examples/receipt_verification.py

printf '\n--- api_key literals in examples ---\n'
rg -n 'api_key\s*=\s*"' examples/*.py

printf '\n--- AINFERA_API_KEY usage in examples ---\n'
rg -n 'AINFERA_API_KEY|os\.environ|getenv' examples/*.py

Repository: ainfera-ai/sdk-python

Length of output: 2120


Use AINFERA_API_KEY in this example
api_key="ak_..." teaches the wrong credential pattern in sample code. Use os.environ["AINFERA_API_KEY"] (or omit api_key if the client already reads from env) so the example matches the SDK guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/receipt_verification.py` at line 6, Update the AinferaClient
initialization in the receipt verification example to use the AINFERA_API_KEY
environment variable via os.environ, or omit the api_key argument if the client
automatically reads it; add the required os import if needed.

Source: Coding guidelines

agent = client.agents.retrieve("ag_...")

response = agent.inference(
model="ainfera-inference",
messages=[{"role": "user", "content": "Hello"}],
)

# Verify the receipt locally — no API call needed
result = verify_receipt(response.receipt)

if result.valid:
print("Receipt is valid")
print(f" Sequence: {result.sequence}")
print(f" Previous hash: {result.previous_hash}")
print(f" Signature: {result.signature[:16]}...")
else:
print(f"Receipt INVALID: {result.error}")

# Local verification proves log integrity relative to the published signing key.
# It does not prevent tampering — it makes tamper-evidence verifiable.
20 changes: 20 additions & 0 deletions examples/retries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Retry strategies for Ainfera inference."""

from ainfera import AinferaClient

client = AinferaClient(
api_key="ak_...",
max_retries=3,
retry_delay=1.0,
retry_backoff=2.0,
retry_on_status=[429, 500, 502, 503],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retries example invalid client kwargs

Medium Severity

examples/retries.py constructs AinferaClient with max_retries, retry_delay, retry_backoff, and retry_on_status, but AinferaClient.__init__ only accepts api_key, base_url, and timeout, so the example fails immediately with unexpected keyword arguments.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1090b1a. Configure here.


agent = client.agents.retrieve("ag_...")

response = agent.inference(
model="ainfera-inference",
messages=[{"role": "user", "content": "Hello"}],
)

print(response.text)
15 changes: 15 additions & 0 deletions examples/streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Streaming inference through Ainfera."""

from ainfera import AinferaClient

client = AinferaClient(api_key="ak_...")
agent = client.agents.retrieve("ag_...")

stream = agent.inference_stream(
model="ainfera-inference",
messages=[{"role": "user", "content": "Explain quantum computing in 3 sentences."}],
)

for chunk in stream:
print(chunk.text, end="", flush=True)
print()
34 changes: 34 additions & 0 deletions examples/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Tool use through Ainfera inference."""

from ainfera import AinferaClient

client = AinferaClient(api_key="ak_...")
agent = client.agents.retrieve("ag_...")

tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
},
"required": ["location"],
},
},
}
]

response = agent.inference(
model="ainfera-inference",
messages=[{"role": "user", "content": "What is the weather in Jakarta?"}],
tools=tools,
)

print(response.text)
if response.tool_calls:
for call in response.tool_calls:
print(f"Tool: {call.function.name}({call.function.arguments})")
26 changes: 17 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# sdk
# ainfera — Python SDK for Ainfera

Agent-native inference routing by Ainfera. Python SDK — signed AgentCards, routed inference, audit c
# Agent-native inference routing by Ainfera. Python SDK — signed AgentCards, routed inference, audit chains.

[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "sdk"
version = "0.1.0"
description = "Agent-native inference routing by Ainfera. Python SDK — signed AgentCards, routed inference, audit c"
name = "ainfera"
version = "1.1.0"
description = "Agent-native inference routing by Ainfera. Python SDK — signed AgentCards, routed inference, audit chains."
readme = "README.md"
requires-python = ">=3.10"
license = {text = "Apache-2.0"}
authors = [{name = "Ainfera Inc"}]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand All @@ -24,18 +24,26 @@ classifiers = [

[project.optional-dependencies]
dev = [
"httpx>=0.27",
"pydantic>=2.0",
"joserfc>=0.9",
"pytest>=7.0",
"pytest-cov>=4.0",
"ruff>=0.4.0",
"pytest-asyncio>=0.23",
"pyright>=1.1",
"click>=8.0",
"pyyaml>=6.0",
"respx>=0.21",
]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
]
Comment on lines 25 to 42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Runtime dependencies belong in [project.dependencies], not dev extras.

httpx, pydantic, joserfc, click, and pyyaml are imported by core SDK modules (http.py, jws.py, cli.py, etc.) but are only available via pip install ainfera[dev]. A plain pip install ainfera or pip install -e . will fail at import time. These should be declared under [project.dependencies].

Additionally, the test extras group is incomplete — it includes only pytest and pytest-cov but omits pytest-asyncio and respx, both of which are required by the test suite (e.g., test_async_resources.py, test_http.py). Anyone running pip install ainfera[test] followed by pytest will hit ImportError.

📦 Proposed fix for dependency grouping
 [project]
 name = "ainfera"
 version = "1.1.0"
 description = "Agent-native inference routing by Ainfera. Python SDK — signed AgentCards, routed inference, audit chains."
 readme = "README.md"
 requires-python = ">=3.10"
 license = {text = "Apache-2.0"}
 authors = [{name = "Ainfera Inc"}]
 classifiers = [
     "Development Status :: 4 - Beta",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
 ]
+dependencies = [
+    "httpx>=0.27",
+    "pydantic>=2.0",
+    "joserfc>=0.9",
+    "click>=8.0",
+    "pyyaml>=6.0",
+]
 
 [project.optional-dependencies]
 dev = [
-    "httpx>=0.27",
-    "pydantic>=2.0",
-    "joserfc>=0.9",
     "pytest>=7.0",
     "pytest-cov>=4.0",
     "ruff>=0.4.0",
     "pytest-asyncio>=0.23",
     "pyright>=1.1",
-    "click>=8.0",
-    "pyyaml>=6.0",
     "respx>=0.21",
 ]
 test = [
     "pytest>=7.0",
     "pytest-cov>=4.0",
+    "pytest-asyncio>=0.23",
+    "respx>=0.21",
 ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 25 - 42, Move the runtime packages httpx,
pydantic, joserfc, click, and pyyaml from [project.optional-dependencies].dev
into [project.dependencies] so plain installations include SDK imports. Update
the test extra to include pytest-asyncio and respx alongside pytest and
pytest-cov, while keeping only development tooling and test-related packages in
the dev group.


[tool.setuptools.packages.find]
where = ["src"]
include = ["sdk*"]
where = ["python"]
include = ["ainfera*"]

[tool.ruff]
target-version = "py310"
Expand All @@ -45,5 +53,5 @@ line-length = 100
select = ["E", "F", "W", "I", "N", "B", "A", "C4", "SIM", "UP"]

[tool.pytest.ini_options]
testpaths = ["tests"]
testpaths = ["python/tests"]
addopts = "-v --tb=short"
4 changes: 1 addition & 3 deletions python/ainfera/_internal/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ def request(
timeout: float | None = None,
) -> dict[str, Any]:
timeout_arg = timeout if timeout is not None else httpx.USE_CLIENT_DEFAULT
response = self._client.request(
method, path, json=json, params=params, timeout=timeout_arg
)
response = self._client.request(method, path, json=json, params=params, timeout=timeout_arg)
if response.status_code >= 400:
raise _map_error(response)
return _parse_body(response)
Expand Down
4 changes: 1 addition & 3 deletions python/ainfera/_internal/jws.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ def verify_compact(token: str, public_key: bytes | str) -> dict[str, Any]:
AgentCardInvalid: Signature did not verify, the alg was rejected,
or the payload was not valid JSON.
"""
key_material = (
public_key.decode("utf-8") if isinstance(public_key, bytes) else public_key
)
key_material = public_key.decode("utf-8") if isinstance(public_key, bytes) else public_key

try:
sig_unverified = jws.extract_compact(token.encode("ascii"))
Expand Down
8 changes: 2 additions & 6 deletions python/ainfera/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,7 @@ def inference(

def _require_client(self) -> AinferaClient:
if self._client is None:
raise RuntimeError(
"Agent is not bound to a client; retrieve it via client.agents"
)
raise RuntimeError("Agent is not bound to a client; retrieve it via client.agents")
return self._client


Expand Down Expand Up @@ -291,9 +289,7 @@ class SignupResult(BaseModel):
owner_handle: str | None = None
canonical_uri: str | None = None
did_web: str | None = None
api_key: str = Field(
description="One-time API key — shown once; persist before discarding."
)
api_key: str = Field(description="One-time API key — shown once; persist before discarding.")
agent_card_jws: str | None = None

@property
Expand Down
8 changes: 2 additions & 6 deletions python/ainfera/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,12 @@ async def verify(self) -> bool:

async def verify_remote(self) -> AuditVerifyResult:
"""Ask the API to verify the chain server-side."""
body = await self._require_http().request(
"GET", endpoints.audit_verify(self.agent_id)
)
body = await self._require_http().request("GET", endpoints.audit_verify(self.agent_id))
return AuditVerifyResult.model_validate(body)

async def annex_iv_bundle(self) -> dict[str, Any]:
"""Fetch the Annex IV-style audit export bundle."""
return await self._require_http().request(
"GET", endpoints.audit_annex_iv(self.agent_id)
)
return await self._require_http().request("GET", endpoints.audit_annex_iv(self.agent_id))

def _require_http(self) -> AsyncHttpClient:
if self._http is None:
Expand Down
7 changes: 2 additions & 5 deletions python/ainfera/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ def _load_manifest(path: Path) -> dict[str, Any]:
import yaml
except ImportError as exc:
raise click.ClickException(
"PyYAML is required for `ainfera install`. "
"Install with `pip install 'ainfera[cli]'`."
"PyYAML is required for `ainfera install`. Install with `pip install 'ainfera[cli]'`."
) from exc

raw = yaml.safe_load(path.read_text(encoding="utf-8"))
Expand Down Expand Up @@ -281,9 +280,7 @@ def install(
if minted:
click.echo(f"\nKeys saved to {rel_keys} ({minted} key(s) written).")
else:
click.echo(
f"\nNo new api_keys returned (tenant key already on file in {rel_keys})."
)
click.echo(f"\nNo new api_keys returned (tenant key already on file in {rel_keys}).")

dashboard_url = result.get("dashboard_url") or f"https://app.ainfera.ai/{handle}"
click.echo(f"\nDashboard: {dashboard_url}")
Expand Down
Loading
Loading