LLM facade Django app for the Stapel framework
Project description
stapel-agent
LLM facade — JSON completion, translation, transcription and summarization in front of swappable model/STT 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 a prior 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 — mount the app under agent/
urlpatterns = [
...
path("agent/", include("stapel_agent.urls")),
]
Two surfaces, same contracts:
| Surface | HTTP | comm Function | Does |
|---|---|---|---|
| Complete | POST /agent/api/llm/complete |
llm.complete |
JSON completion: {"prompt", "model": "small|medium|large", "system_prompt"?, "provider"?, "images"?} → parsed JSON in result, prose in comment. images (vision) entries are {url} or {data_b64, mime?} — OCR, screenshots, photo moderation |
| Translate | POST /agent/api/llm/translate |
llm.translate |
{"from"/"from_lang", "to", "entries": {key: text}} → {key: translated} (cached by prompt) |
| Transcribe | POST /agent/api/llm/transcribe |
llm.transcribe |
{"audio_url", "language"?, "diarization"?, "provider"?, "timeout_seconds"?} → a normalized transcript (words, utterances, speakers, timings) via the STT router |
| Summarize | POST /agent/api/llm/summarize |
llm.summarize |
exactly one of text / transcript (+ language?, model?, provider?) → Markdown summary + aggregated usage; long inputs are map-reduced |
| Generate image | POST /agent/api/llm/generate-image |
llm.generate_image |
{"prompt", "size"?, "n"? (1-10), "provider"?} → raw provider results [{url? | data_b64?, mime}] — storing them in a CDN/asset library is the caller's job |
# HTTP (service-to-service: X-API-KEY, or a staff session)
POST /agent/api/llm/complete {"prompt": "...", "model": "small|medium|large",
"provider"?: "...", "system_prompt"?: "...",
"images"?: [{"url": "https://..."} | {"data_b64": "...", "mime"?: "image/webp"}]}
POST /agent/api/llm/translate {"from": "auto", "to": "de", "entries": {"key": "text"}}
POST /agent/api/llm/transcribe {"audio_url": "https://...presigned...", "language"?: "en",
"diarization"?: true, "provider"?: "elevenlabs"}
POST /agent/api/llm/summarize {"text": "..."} | {"transcript": {...llm.transcribe output...}}
POST /agent/api/llm/generate-image {"prompt": "a cat", "size"?: "1024x1024", "n"?: 2}
# 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.complete", {"prompt": "what is on this?", "model": "small",
"images": [{"url": "https://..."}]}) # url or data_b64 — never raw bytes
call("llm.translate", {"from_lang": "auto", "to": "de", "entries": {...}})
call("llm.transcribe", {"audio_url": "https://..."}) # URLs only — never raw bytes
call("llm.summarize", {"transcript": result["transcript"]})
call("llm.generate_image", {"prompt": "a cat", "n": 2})
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.
Transcriptions land there too (source=transcribe, model = STT provider
name, token columns NULL, the fallback walk in metadata.attempts); each
summarize pass is a normal LLM row (source=summarize); image generations
log as source=generate_image with {count, mimes, bytes_total} in
metadata — image bytes never touch the ledger, and multimodal completions
never collide with the text-keyed prompt cache.
Transcription routes through an STT provider chain — explicit provider in
the request beats STT_LANGUAGE_ROUTES[lang], which beats
DEFAULT_STT_PROVIDER + STT_FALLBACK_CHAIN — and falls back only on
transient failures (429/5xx/timeouts); bad audio or auth errors never
retry on another provider.
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 |
STT_PROVIDERS |
{} |
Overlay merged over the built-in STT registry (whisper-http / elevenlabs / assemblyai) — same semantics as PROVIDERS |
DEFAULT_STT_PROVIDER |
"whisper-http" |
STT provider used when a request pins none and no language route matches |
STT_FALLBACK_CHAIN |
[] |
STT providers tried in order after the default — on transient failure only |
STT_LANGUAGE_ROUTES |
{} |
{"ru": ["gigaam", "whisper-http"], ...} language matrix (beats the default chain) |
STT_TIMEOUT |
1800 |
Hard cap (seconds) on one STT provider's submit+poll cycle |
WHISPER_BASE_URL / WHISPER_API_KEY / WHISPER_MODEL |
"" / "" / "whisper-1" |
OpenAI-compatible Whisper endpoint — the OpenAI API or self-hosted faster-whisper (key optional) |
ELEVENLABS_API_KEY / ELEVENLABS_STT_URL / ELEVENLABS_STT_MODEL |
"" / Scribe URL / "scribe_v2" |
ElevenLabs Scribe |
ASSEMBLYAI_API_KEY / ASSEMBLYAI_BASE_URL / ASSEMBLYAI_MODEL |
"" / "https://api.assemblyai.com" / "universal" |
AssemblyAI (async submit+poll) |
IMAGE_PROVIDERS |
{} |
Overlay merged over the built-in image registry (openai-images) — same semantics as PROVIDERS |
DEFAULT_IMAGE_PROVIDER |
"openai-images" |
Image provider used when a request pins none |
IMAGES_BASE_URL / IMAGES_API_KEY |
"" / "" |
OpenAI-compatible /images/generations endpoint + key; both fall back to the OPENAI_COMPAT_* pair |
IMAGES_MODEL |
"" |
Optional model name (gpt-image-1, ...); empty = omitted from the request |
CACHE_LOOKUP |
{"llm_facade": False, "translate": True, "summarize": False} |
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). 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.
STT provider matrix
| Name | Class | Backend | Needs |
|---|---|---|---|
whisper-http (default) |
stt.providers.whisper_http.WhisperHttpProvider |
OpenAI Whisper API or any self-hosted server speaking the same dialect (faster-whisper, whisper.cpp); accepts url/path/bytes refs | WHISPER_BASE_URL (+ key for OpenAI) |
elevenlabs |
stt.providers.elevenlabs.ElevenLabsProvider |
ElevenLabs Scribe (synchronous multipart), diarization | ELEVENLABS_API_KEY; URL refs only |
assemblyai |
stt.providers.assemblyai.AssemblyAIProvider |
AssemblyAI async submit+poll, diarization, 99-language universal model |
ASSEMBLYAI_API_KEY; URL refs only |
The STT registry has the same merge semantics as the LLM one
(STAPEL_AGENT["STT_PROVIDERS"] overlay, None removes, or
register_stt_provider() at runtime), and its own startup checks
(stapel_agent.W003/W004). A custom engine is a stapel_agent.SttProvider
subclass returning a NormalizedTranscript — MODULE.md walks through a
self-hosted GigaAM adapter as the worked example.
Vision & image generation
The two vision-capable LLM providers map images into their dialects
automatically — anthropic to image content blocks (URL or base64 source),
openai-compat to image_url parts (URL or data URI). claude-code has no
vision: an image request through it fails fast with status: "failure".
Image generation ships one built-in backend, openai-images — anything
speaking the OpenAI POST {base}/images/generations dialect (OpenAI,
Together, self-hosted). It is the third instance of the merge-registry
pattern (STAPEL_AGENT["IMAGE_PROVIDERS"] overlay, None removes,
register_image_provider() at runtime; startup checks
stapel_agent.W005/W006). Vendors with their own protocols (Stability,
Ideogram, ...) are an app-layer stapel_agent.ImageGenProvider subclass —
recipe in MODULE.md. The agent returns raw results and writes
the ledger; storage/placement belongs to the calling tier.
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.2.11.tar.gz.
File metadata
- Download URL: stapel_agent-0.2.11.tar.gz
- Upload date:
- Size: 95.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca4957e90d7263b47c97a40daa7614d3d4ce79181f1c9c3cef0163e092ae61f0
|
|
| MD5 |
a8eed0b12e0aa1030a422a3c722466b4
|
|
| BLAKE2b-256 |
3fb233148105464f7af13b729696669ecddd431168e504121ce3bffadd3a6ff2
|
Provenance
The following attestation bundles were made for stapel_agent-0.2.11.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.2.11.tar.gz -
Subject digest:
ca4957e90d7263b47c97a40daa7614d3d4ce79181f1c9c3cef0163e092ae61f0 - Sigstore transparency entry: 2215160995
- Sigstore integration time:
-
Permalink:
usestapel/stapel-agent@bf5d7151460550e21a644445bfa7010085f9efc5 -
Branch / Tag:
refs/tags/v0.2.11 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bf5d7151460550e21a644445bfa7010085f9efc5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_agent-0.2.11-py3-none-any.whl.
File metadata
- Download URL: stapel_agent-0.2.11-py3-none-any.whl
- Upload date:
- Size: 75.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4596185358fa8cf4ed63dbb2cc02a52ff437c9ef95d4620ba39df6318841f07a
|
|
| MD5 |
6de0fa182e134bce23adf47eb7ef12b5
|
|
| BLAKE2b-256 |
12ccf0d2c8973522556916174ce4a8039822745049f09d92eac85351e65f7260
|
Provenance
The following attestation bundles were made for stapel_agent-0.2.11-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.2.11-py3-none-any.whl -
Subject digest:
4596185358fa8cf4ed63dbb2cc02a52ff437c9ef95d4620ba39df6318841f07a - Sigstore transparency entry: 2215161053
- Sigstore integration time:
-
Permalink:
usestapel/stapel-agent@bf5d7151460550e21a644445bfa7010085f9efc5 -
Branch / Tag:
refs/tags/v0.2.11 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bf5d7151460550e21a644445bfa7010085f9efc5 -
Trigger Event:
push
-
Statement type: