LLM facade Django app for the Stapel framework
Project description
stapel-agent
LLM facade — one JSON-completion/translation surface in front of swappable model providers, with a prompt cache and a token ledger
Part of the Stapel framework — composable Django apps for building production-grade platforms.
Python port of the iron-agent NestJS service. Same HTTP paths and contracts
(stapel-translate's AgentProvider keeps working unchanged), plus a comm
surface so monolith deployments call it in-process without HTTP.
Installation
pip install stapel-agent # core
pip install stapel-agent[anthropic] # + the Anthropic SDK for the default provider
Quick start
# settings.py
INSTALLED_APPS = [
...
'stapel_agent',
]
STAPEL_AGENT = {
"ANTHROPIC_API_KEY": "sk-ant-...",
}
# urls.py — paths stay 1:1 with iron-agent under the agent/ mount
urlpatterns = [
...
path("agent/", include("stapel_agent.urls")),
]
Two surfaces, same contracts:
# HTTP (service-to-service: X-API-KEY, or a staff session)
POST /agent/api/llm/complete {"prompt": "...", "model": "small|medium|large",
"provider"?: "...", "system_prompt"?: "..."}
POST /agent/api/llm/translate {"from": "auto", "to": "de", "entries": {"key": "text"}}
# comm (in-process in a monolith, transport chosen by STAPEL_COMM)
from stapel_core.comm import call
call("llm.complete", {"prompt": "...", "model": "small"})
call("llm.translate", {"from_lang": "auto", "to": "de", "entries": {...}})
Responses follow the iron-agent contract: LLM failures are HTTP 200 with
{"status": "failure", "reason": ...} — 4xx/5xx are reserved for request
validation and auth. Successful completions return the parsed JSON in
result, prose around it in comment, and snake_case usage
(input_tokens / output_tokens).
Every provider call writes a PromptLog row: model, size, source, status,
duration and the full token ledger (input / output / thinking / cache-read /
cache-write) — per-user and per-source cost accounting needs no other table.
Settings — STAPEL_AGENT
| Key | Default | Meaning |
|---|---|---|
MODELS |
{"small": "claude-haiku-4-5-20251001", "medium": "claude-sonnet-5", "large": "claude-opus-4-8"} |
Size → model-name map |
PROVIDERS |
{} |
Overlay merged over the built-in registry (anthropic / openai-compat / claude-code) — add/override entries, None removes one; resolved lazily per request |
DEFAULT_PROVIDER |
"anthropic" |
Provider used when a request names none |
ANTHROPIC_API_KEY |
"" |
Key for the Anthropic SDK provider (read lazily) |
OPENAI_COMPAT_BASE_URL |
"" |
Base URL of any OpenAI-compatible endpoint |
OPENAI_COMPAT_API_KEY |
"" |
Bearer token for that endpoint |
OPENAI_COMPAT_MODELS |
{} |
Optional size → model map for openai-compat (missing sizes fall back to MODELS) |
CLI_BINARY |
"claude" |
Claude Code CLI binary (opt-in provider only) |
CLI_TIMEOUT |
120 |
Provider timeout, seconds |
MAX_TOKENS |
4096 |
Completion token cap |
CACHE_LOOKUP |
{"llm_facade": False, "translate": True} |
Per-source cache-by-prompt toggle (used by the default cache policy) |
CACHE_TTL |
604800 |
Cache window in seconds (7 days); older rows are ignored (default policy) |
CACHE_POLICY |
"stapel_agent.cache.PromptLogCachePolicy" |
Dotted path to a CachePolicy subclass — swap the prompt cache (Redis, no-op, ...) without forking |
Provider matrix
| Name | Class | Backend | Needs |
|---|---|---|---|
anthropic (default) |
providers.anthropic.AnthropicProvider |
Anthropic SDK | anthropic extra + ANTHROPIC_API_KEY |
openai-compat |
providers.openai_compat.OpenAICompatProvider |
Any /chat/completions dialect: OpenAI, DeepSeek, MiMo, GLM, Kimi |
OPENAI_COMPAT_BASE_URL (+ key) |
claude-code |
providers.claude_cli.ClaudeCodeCLIProvider |
Spawns claude -p ... --output-format json |
The CLI in the host image |
No CLI in any default path. claude-code is strictly opt-in: it exists for
hosts that ship the Claude Code CLI in their image and want the CLI to handle
its own authentication (provider: "claude-code" per request, or
DEFAULT_PROVIDER override). Unlike iron-agent there is no OAuth credential
reading and no background token-refresh — that plumbing was deliberately
dropped.
Adding, overriding and removing providers (merge semantics)
STAPEL_AGENT["PROVIDERS"] is an overlay merged over the built-ins, not a
replacement dict — adding your provider never requires restating the three
shipped ones, and setting a name to None removes it:
# settings.py — one line per change, built-ins stay available
STAPEL_AGENT = {
"PROVIDERS": {
"acme": "myproject.llm.AcmeProvider", # add a custom backend
"claude-code": None, # remove a built-in
},
"DEFAULT_PROVIDER": "acme",
}
Or register at runtime from your app's AppConfig.ready() (highest
precedence):
from stapel_agent import register_provider
register_provider("acme", AcmeProvider) # class or dotted path
A custom backend is just a stapel_agent.LlmProvider subclass returning a
ProviderResult. Django system checks (stapel_agent.E001/W001/W002) flag a
DEFAULT_PROVIDER that is not in the effective registry, unimportable dotted
paths and non-LlmProvider entries at startup. See MODULE.md
for the full extension-point map.
License
MIT — see LICENSE
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 stapel_agent-0.1.0.tar.gz.
File metadata
- Download URL: stapel_agent-0.1.0.tar.gz
- Upload date:
- Size: 37.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ee579ec3ede7e6d48fae293331a19bee71236e2be72669abd75db69188ef224
|
|
| MD5 |
8caf415003581888bdd1d772b2ded28b
|
|
| BLAKE2b-256 |
6d7ef2e9e4723a8bb8cd4f1ce4f1191eb8e7a279b7db01d1ad8c53f5e19e1407
|
Provenance
The following attestation bundles were made for stapel_agent-0.1.0.tar.gz:
Publisher:
publish.yml on usestapel/stapel-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_agent-0.1.0.tar.gz -
Subject digest:
5ee579ec3ede7e6d48fae293331a19bee71236e2be72669abd75db69188ef224 - Sigstore transparency entry: 2069464733
- Sigstore integration time:
-
Permalink:
usestapel/stapel-agent@298c1e4a9e8f35fd773e3c97404b1453eab0a3fc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@298c1e4a9e8f35fd773e3c97404b1453eab0a3fc -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_agent-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stapel_agent-0.1.0-py3-none-any.whl
- Upload date:
- Size: 48.4 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 |
8e152b532e760d91e91e2aefd212dfa6094334f467edbb64fd77289f87c27531
|
|
| MD5 |
4ec1d7e3569c1e1dd2b0a27bd2ea2ee0
|
|
| BLAKE2b-256 |
97c1781686359a0582087b3055e821736400b548b0130e2a88cbcb1aeed258b9
|
Provenance
The following attestation bundles were made for stapel_agent-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_agent-0.1.0-py3-none-any.whl -
Subject digest:
8e152b532e760d91e91e2aefd212dfa6094334f467edbb64fd77289f87c27531 - Sigstore transparency entry: 2069465068
- Sigstore integration time:
-
Permalink:
usestapel/stapel-agent@298c1e4a9e8f35fd773e3c97404b1453eab0a3fc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@298c1e4a9e8f35fd773e3c97404b1453eab0a3fc -
Trigger Event:
push
-
Statement type: