Skip to main content

stapel-agent

CI coverage pypi downloads python license llms.txt

LLM facade: JSON completion, translation, transcription and summarization in front of swappable model / speech-to-text / image-generation providers (Anthropic, OpenAI-compatible, Claude Code CLI, whisper-http, ElevenLabs, AssemblyAI, openai-images), with a prompt cache and a per-call token ledger. Python port of a prior NestJS service — same HTTP paths/contracts, plus an in-process comm surface for monolith deployments.

Part of the Stapel framework — composable Django apps that deploy as a monolith or as microservices without changing module code.

Install

pip install stapel-agent

At a glance

Fact Value
Version 0.16.0
Python >=3.11 (3.11, 3.12, 3.13, 3.14)
HTTP operations 5
Config axes 4
Usage surface 5
Extension points 4
Fleet dependencies stapel-core

Documentation

capabilities.json · llms.txt (for agents)

What this is

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.

Quick start

pip install stapel-agent[anthropic]  # + the Anthropic SDK for the default provider
# 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|xlarge", "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|xlarge",
                                "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, the full token ledger (input / output / thinking / cache-read / cache-write) and what it cost — cost_usd with a cost_basis saying whether that is the provider's own figure, this package's rate card, or unknown. Attribution is the optional user_id / workspace_id pair every llm.* comm function accepts, so per-user and per-tenant cost accounting needs no other table. The cost is stored as computed at call time and never recomputed: re-pricing an old row against today's card would quietly restate history. Transcriptions land there too (source=transcribe, model = STT provider name, LLM token columns NULL but audio_duration_ms carrying the billable audio length and cost_usd the rate card applied to it, 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", "xlarge": "claude-fable-5"} 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
STT_DOWNLOAD_MAX_BYTES 134217728 Byte cap on an audio-URL download (128 MiB), enforced mid-stream
STT_DOWNLOAD_TIMEOUT 30.0 Per-socket connect/read timeout for one hop of that download
STT_DOWNLOAD_TOTAL_DEADLINE 300.0 Ceiling on the whole download incl. redirects; a per-call timeout= may only lower it
STT_DOWNLOAD_ALLOWED_HOSTS [] Exact-host allowlist for audio URLs, applied to every hop. Empty = the download is REFUSED (empty is not a wildcard) and W015 warns at boot. Settings-only (NO_ENV) — an env var cannot fill it
STT_DOWNLOAD_ALLOW_ANY_HOST False Opt-out: accept audio from any public host when the allowlist is empty. The other fetch guards still apply
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_ALLOW_UNSCOPED [] Sources whose content the host declares non-personal; a call without user_id otherwise skips the cache
PROMPT_LOG_RETENTION_DAYS 90 Age after which purge_prompt_logs scrubs a ledger row's text, keeping its token counters
PROMPT_LOG_RETENTION_SCHEDULED False Declares that the host's scheduler runs purge_prompt_logs; otherwise a system check (W014) says the window is unenforced
MODEL_SIZE_CEILING_ENTITLEMENT None Entitlement key (STAPEL_BILLING plan catalog) capping how far up the size ladder a caller's plan reaches; unset = seam closed, no billing lookup at all — see resolve_size_ceiling
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.

Privacy & retention

PromptLog is both an accounting ledger and a copy of the conversation, so it is treated as customer content:

  • The cache is per-tenant. A cached answer belongs to the user_id that paid for it; a call with no scope skips the cache entirely unless its source is listed in CACHE_ALLOW_UNSCOPED.
  • Subject requests reach it. AgentGDPRProvider (section agent) is registered into stapel_core.gdpr.gdpr_registry at app startup — export returns the user's rows, erasure scrubs their text and unlinks the subject while the token counters survive for accounting.
  • Text expires. Schedule manage.py purge_prompt_logs (--days N, --dry-run): it scrubs prompt, system prompt, response and error text on rows older than PROMPT_LOG_RETENTION_DAYS and keeps the counters.

License

MIT — see LICENSE.


This page is assembled by stapel-readme from docs/readme.md plus the contract artifacts in docs/. Edit the prose in docs/readme.md; the badges, facts and links above and below it are generated — do not hand-edit README.md.

Release files for stapel-agent 0.16.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stapel-agent 0.16.0
File Size Uploaded
stapel_agent-0.16.0.tar.gz 323.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stapel-agent 0.16.0
File Interpreter ABI Platform
stapel_agent-0.16.0-py3-none-any.whl Python 3 none any Details

Total release size: 582.3 kB

Release files / stapel_agent-0.16.0.tar.gz

Download URL stapel_agent-0.16.0.tar.gz
Size 323.3 kB
Tags Source
SHA-256 checksum
How to use checksums
c10faeff4a7191ed6ba5a936de51f46f1412348268d0eef979cfc0423a9c36a3
BLAKE2b-256 checksum
How to use checksums
54d63737546cfb07a24dfc284040433ffd834e9f1e4373a654a2be25599cb819
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

Release files / stapel_agent-0.16.0-py3-none-any.whl

Download URL stapel_agent-0.16.0-py3-none-any.whl
Size 259.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a1211ddc72d8d3ac0c4f54f58c7027c18beb62229a1ec4aab6a68d4d7f2c23b
BLAKE2b-256 checksum
How to use checksums
762b802a827f33efa850fb85534728d67889a4c782b06e41e763d51855076718
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 30, 2026.

Transparency log

Release history Release notifications | RSS feed

0.31.1

2 release files

0.31.0

2 release files

0.30.1

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.0

2 release files

0.23.0

2 release files

This release

0.16.0 This release

2 release files

0.15.0

2 release files

0.14.3

2 release files

0.14.2

2 release files

0.14.1

2 release files

0.13.3

2 release files

0.13.1

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.7

2 release files

0.6.6

2 release files

0.6.5

2 release files

0.6.4

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.11

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.4

2 release files

0.2.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page