API

Three install patterns — the easier the install, the more secure-by-default.

The headline install is the drop-in proxy. The decorator and the direct API cover corner cases without changing the architectural guarantee. Framework adapters for LiteLLM, LangChain, and the OpenAI Agents SDK live on the Integrations page. The surface below is v0.1.

Three install patterns

Drop-in proxy first. One line of code.

The headline install is an OpenAI- and Anthropic-compatible HTTP proxy. Swap the SDK’s base_url and every outbound call now flows through Runelm. The other two patterns serve corner cases without changing the guarantee.

Drop-in

OpenAI-compatible proxy

recommended

Run runelm in front of your application. Change one line in the SDK constructor. Every call now flows through classification, routing, and bounded rehydration.

# server side
$ runelm serve --config runelm.yaml

# client side — the only change
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8080/v1",  # <-- swap
    api_key="sk-runelm-...",
)

resp = client.chat.completions.create(
    model="gpt-5",
    messages=[{"role": "user", "content": prompt}],
)
Planned · Phase E

Python decorator

Not yet shipped (Phase E). Wrap the function that calls the LLM. The decorator handles classification, pseudonymization, routing, and rehydration around the call site.

from runelm import runelm_protect, Level

@runelm_protect(session_id="conv-42", min_level=Level.MEDIUM)
def ask_llm(prompt: str) -> str:
    from openai import OpenAI
    client = OpenAI()  # ordinary client, no proxy
    resp = client.chat.completions.create(
        model="gpt-5",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content
Planned · Phase E

Direct API

Not yet shipped (Phase E). For custom pipelines. Call classify, pseudonymize, route, and rehydrate yourself.

from runelm import Runelm

rl = Runelm.from_config("runelm.yaml")

result = rl.classify(text=prompt, session_id="conv-42")
sanitized = rl.pseudonymize(result, session_id="conv-42")
provider = rl.route(level=result.level)

# call provider.base_url with sanitized.text
response_text = your_llm_call(sanitized.text, provider)

clean = rl.rehydrate(response_text, session_id="conv-42")

Proxy headers

The proxy speaks both the OpenAI (/v1/chat/completions) and Anthropic (/v1/messages) wire shapes. Runelm-specific behavior is opt-in via request headers.

Request headerRequiredPurpose
Authorization: Bearer <jwt>requiredJWT verified against a configured issuer (JWKS). The caller identity — never self-reported.
X-Runelm-Session-IDoptionalSession identity for substitution-map scoping. Auto-inferred if absent.
X-Runelm-Engagement-IDoptionalGroups sessions under an engagement for lifecycle management.
X-Runelm-Caller-IDdev onlyDevelopment-mode caller identity. Production refuses it and demands JWT or mTLS.

Every response carries the classification and routing decision back out — so callers can observe what happened without the proxy ever echoing the prompt.

Response headerMeaning
X-Runelm-ClassificationThe level assigned this call: LOW / MEDIUM / HIGH / BLOCKED.
X-Runelm-RouteProvider tier the request reached — or BLOCKED if it never left.
X-Runelm-Entities-RedactedHow many entities were pseudonymized before dispatch.
X-Runelm-Latency-MSEnd-to-end proxy overhead added to the call.
X-Runelm-Audit-IDAudit-row id — cross-reference the call without ever storing its text.

Dry-run classification is its own endpoint — POST /v1/dryrun (and the runelm dryrun CLI): submit up to 100 prompts, get per-prompt levels and entity counts with zero upstream calls and no stored prompt text.

Configuration — runelm.yaml

Shared across all three install patterns. Provider tier registry, secrets backend, audit destination, classifier configuration.

runelm:
  mode: production            # production enables the fail-closed gates

  secrets:
    backend: file             # file-backed keys (KMS/Vault on the roadmap)
    aes_key_path: ./aes.key   # AES-256-GCM key for the substitution map
    hmac_secret_path: ./hmac.key

  providers:
    - id: ollama-local
      tier: 1                 # L1 local — the only tier HIGH can reach
      adapter: ollama
      base_url: http://localhost:11434
    - id: openai-prod
      tier: 2                 # L2 contracted — requires DPA + zero-retention
      adapter: openai
      base_url: https://api.openai.com
      dpa_on_file: 2026-01-15
      zero_retention: true
    - id: openrouter
      tier: 3                 # L3 public — LOW only
      adapter: openai
      base_url: https://openrouter.ai/api

  pseudonym:
    counter_start_min: 100
    counter_start_max: 999
    salt_byte_length: 2       # bytes of per-session salt baked into each placeholder
    map_ttl_hours: 24

  # Routing is structural, not a config knob: BLOCKED -> none, HIGH -> L1,
  # MEDIUM -> L1+L2, LOW -> any. There is no flag to relax it.

CLI surface

Everything in the dashboard is also a command. Operators who prefer the terminal never need the UI.

  • $ runelm serve [--config runelm.yaml]
  • $ runelm config validate [--config runelm.yaml]
  • $ runelm engagement create|list|close <id> [--reason=...]
  • $ runelm override set|list|revoke --principal=... --min-level=... --expires=...
  • $ runelm audit tail [--follow]
  • $ runelm audit query --since=... --until=... [--caller=...] [--level=...]
  • $ runelm audit verify # walk the tamper-evident hash chain
  • $ runelm audit rotate-hmac-secret # re-key the audit chain across epochs
  • $ runelm dryrun --corpus=traffic.jsonl
  • $ runelm vacuum
  • $ runelm version