Mimir

Mimir is an embeddable routing oracle, pricing source, and decision vocabulary for LLM workloads. You consult it in-process: hand it a workload descriptor and an operational snapshot, and it hands back a placement (or a reasoned no-candidate answer) plus an auditable decision record. A gateway service built on top of Mimir is one possible embedder — not a requirement. A single application can link the library directly and route its own calls with no service in between.

The name is a nod to the consulted head: every carrier embeds the same oracle. The well it drinks from — metered access, minted keys, fleet state — is the embedder's business, not this library's.

Installation

Add mimir to your list of dependencies in mix.exs:

def deps do
[
{:mimir, "~> 0.4.0"}
]
end

Module inventory

ModuleWhat it does
Mimir.DescriptorValidated workload descriptor — the contract a workflow step presents to the oracle.
Mimir.OraclePure filter-then-rank placement decision over catalog entries.
Mimir.CatalogConfig-sourced routable entries, with an injectable model resolver seam.
Mimir.SnapshotExplicit-inputs operational snapshot the oracle ranks against (pricing, health, budget).
Mimir.HealthFailure-streak table for router lanes, driven by telemetry.
Mimir.DecisionRecordTyped routing-decision record; to_event/1 renders the binary-keyed audit map.
Mimir.RouteLogTyped route outcome plus a request-log meta builder.
Mimir.PricingToken usage to integer microdollar cost, config-first over a vendored LiteLLM pricing DB.
Mimir.EventDomain-typed event vocabulary (llm.* / agent.* / workflow.*) — the vocabulary root Mimir.TurnEvents buffers and Mimir.Ingest promotes raw events onto.
Mimir.Event.OTelCanonical OTel GenAI semantic-convention rendering for Mimir.Event at the export edge — the one place that vocabulary still lives, on purpose.
Mimir.TurnEventsPer-request ordered Mimir.Event buffer; the buffer, not the caller, owns seq/ts.
Mimir.RouterClientBehaviour for routing clients, with an HTTP (Req-based) implementation. Returns a parsed %Mimir.RouteResponse{}.
Mimir.RouteResponseParsed routing-call result; new/1 is the single boundary from wire map to struct.
Mimir.GrantMinted routing grant: key, budget, expiry.
Mimir.PlacementFlat chosen-model placement: lane, model, runtime.
Mimir.CandidateOne catalog entry's routing verdict: chosen, ranked, or excluded.
Mimir.RedactSecret masking and payload-capture gating helpers.
Mimir.GuardTurn-guard builders for a session's between-turn hook — grant-budget halts and mimir-less caps.
Mimir.IngestDecision-correlated ingestion of raw session events, promoted to Mimir.Event and appended to Mimir.TurnEvents.
Mimir.SessionsCanonical recipe: route response to session options (model_config, turn_guard, telemetry_metadata).

Design rules

Mimir has no dependency on any agent-runtime or LLM client library. It does not call models, does not manage conversation state, and does not mint or verify auth. Governance — budget enforcement, key issuance, multi-tenant isolation — composes in the embedder, on top of the plain data Mimir returns.

Supervision

Mimir.Health and Mimir.TurnEvents are GenServers that own ETS tables. Add the ones you use to your application's supervision tree:

children = [
Mimir.Health,
Mimir.TurnEvents
]

Everything else in the library (Descriptor, Oracle, Catalog, Snapshot, DecisionRecord, RouteLog, Pricing, RouterClient, Redact) is stateless — no process, no supervision needed.

Configuration reference

All configuration lives under the :mimir application:

KeyUsed byMeaning
:catalogMimir.CatalogList of routable entry configs (id, model, lane, runtime, ...).
:pricingMimir.Pricing, Mimir.SnapshotConfig-table token rates, "provider:model" => %{input:, output:}. Wins over the vendored DB.
:pricing_db_pathMimir.PricingOverride path to the vendored pricing DB (useful in tests).
:health_thresholdMimir.HealthFailure-streak count at which a lane is reported :degraded. Default 3.
:completion_eventMimir.HealthTelemetry event Health.attach/0 listens on. Default [:mimir, :completion].
:turn_events_tablesMimir.TurnEvents{seq_table, buf_table} ETS table names, for running more than one buffer instance.
:gateway_base_urlMimir.SessionsDefault :base_url for opts/2's model_config, when not passed explicitly.

Examples

Runnable, heavily-commented examples ship with the package:

Development

Gateway-less mode

A single-app deployment embeds the library directly — no routing service, no minted keys, no fleet state:

# config/config.exs
config :mimir, :catalog, [
%{id: "local-qwen", model: "ollama:qwen3", lane: "local", runtime: "local", priority: 10},
%{id: "claude", model: "anthropic:claude-sonnet-4-6", lane: "anthropic", runtime: "managed"}
]
config :mimir, :pricing, %{
"anthropic:claude-sonnet-4-6" => %{input: 3_000_000, output: 15_000_000}
}
# at the call site
{:ok, descriptor} =
Mimir.Descriptor.parse(%{
task_class: "extraction",
budget_ceiling_microdollars: 50_000,
latency_tolerance_ms: 30_000
})
snapshot = Mimir.Snapshot.assemble([]) # degenerate: all lanes healthy, config pricing
case Mimir.Oracle.decide(descriptor, Mimir.Catalog.entries(), %Mimir.Oracle.Policy{}, snapshot) do
{:decision, decision} -> run_step_on(decision.entry)
{:no_candidate, reasons, _candidates} -> handle_no_candidate(reasons)
end

Same descriptors, same decision records, no service required. Budget guards without minted keys are plain caps, no grant needed:

turn_guard = Mimir.Guard.caps(max_cost_microdollars: 50_000, model: "anthropic:claude-sonnet-4-6")

See Governance composition below for the grant-backed form and the rest of the composition layer.

Routing vocabulary

c:Mimir.RouterClient.route/2 returns {:ok, %Mimir.RouteResponse{}} — a parsed struct, never a raw map. RouteResponse.new/1 is the single boundary where a decoded wire response becomes mimir's struct vocabulary:

Everything downstream — Mimir.Sessions.opts/2, Mimir.Guard.for_grant/3, Mimir.Ingest.from_route/2 — consumes these structs directly; none of them touch a raw route map.

Governance composition

Mimir hands back plain data — a placement, a grant, a decision record. Turning that into enforcement is the embedder's job, and it splits into two planes:

Mimir.Sessions.opts/2 is the canonical recipe that wires both planes from a single route response:

{:ok, resp} = Mimir.RouterClient.route(descriptor, client_opts)
# resp is a %Mimir.RouteResponse{} — see Routing vocabulary, above
session_opts = Mimir.Sessions.opts(resp, base_url: gateway_url)
Session.run(provider, session_opts ++ [handler: MyTools, prompt: prompt])

opts/2 raises ArgumentError on a no-candidate or malformed route response — fail at composition time, not mid-session. Mimir.Guard handles the mid-run side and never raises.

To correlate raw session events back to the routing decision for metering, call Mimir.Ingest.handle_event/2 from your session handler's event hook; drain the buffered, correlated %Mimir.Event{} list with Mimir.TurnEvents.take/1 when you meter the run.

Event vocabulary

Mimir.Event is the domain-typed vocabulary root for everything that travels the llm.* / agent.* / workflow.* streams — LLM turns, agent-session lifecycle, and workflow steps each get their own closed type union under a domain, instead of one untyped junk-drawer payload. Build one with Event.llm/2, Event.agent/2, or Event.workflow/2; Event.to_wire/1 / Event.from_wire/1 are the struct-in-BEAM / JSON-at-the-boundary pair — to_wire/1 is exactly the shape a caller should persist.

Mimir.TurnEvents.append/2 stores a %Mimir.Event{} under a request id; the buffer — not the caller — owns seq/ts, stamping both at append time. Mimir.TurnEvents.take/1 returns the buffered events in that buffer-assigned order.

The OpenTelemetry GenAI semantic-convention vocabulary (gen_ai.*) is not a domain concept here — it is one export-edge rendering, owned by Mimir.Event.OTel.render/1. Only that module renders gen_ai.* attributes; everything upstream of it works in typed Mimir.Event domains.

Documentation

Full API docs are published on HexDocs once released, and can be generated locally with mix docs.

License

Apache-2.0. See LICENSE.