somm explained · The Life of a Call

The Life of a Call

One SommLLM.generate() call, followed end to end: how a named workload becomes a policy check, a capability-filtered route, a provider dispatch with fallback — and finally an immutable row in a local SQLite ledger.

Entry point: somm.llm(project=...) and the workload contract

Everything starts with a client. somm.llm(project=...) constructs a SommLLM, and that constructor does real work: it initializes a project repository, the configured provider adapters, a health tracker, the router, pricing data, and an asynchronous telemetry writer (packages/somm/src/somm/client.py:666). There is no service to stand up first — the primary call path is self-hosted and locally recorded, designed to work with minimal configuration (packages/somm/README.md:3).

Every call is tagged with a workload, and the workload is a contract, not a label. When generate() runs, it first registers or validates the workload for this call (packages/somm/src/somm/client.py:910). In the default observe mode, an unknown workload is auto-registered so adoption stays frictionless; in strict mode, a missing registration is an error — the same mechanism doubles as enforceable governance (packages/somm/src/somm/client.py:658).

Policy first: privacy class, daily budget, and capability requirements

Workloads declared with SommLLM.register_workload(...) carry privacy, budget, and capability requirements (docs/errors/SOMM_WORKLOAD_UNREGISTERED.md:19, docs/BLUEPRINT.md:118). Before any provider is contacted, generate() merges the workload's declared capabilities with capabilities inferred from the request itself, then runs ordered, mutable pre_call hooks registered through hooks.register_hook(...) (packages/somm/src/somm/client.py:910, packages/somm/src/somm/hooks.py:160).

Hooks are deliberately weak in two ways. Hook events omit prompt and response bodies, and a failing hook cannot break the call path — hooks fail open, because auxiliary intelligence must never take down live inference (docs/plugins.md:79, packages/somm/src/somm/hooks.py:246). Budgets are the opposite: they fail closed. After the hooks run, generate() checks the workload's daily budget, and a refusal is fatal (packages/somm/src/somm/client.py:571).

Budget refusal happens before dispatch — by design The check runs before any provider is selected, so fallback can never route around a spent cap. A rejected call creates no spend and stops with a fatal SOMM_BUDGET_EXCEEDED error (docs/errors/SOMM_BUDGET_EXCEEDED.md:23).

Routing: filter incapable models, pace quotas, cool transient failures

With policy satisfied, the call dispatches either to a pinned provider or to the preference-ordered router (packages/somm/src/somm/client.py:910). Pins are "try first" by default — if the pinned route fails, the router can still fall back — while no_fallback=True supplies pinned-or-bust semantics for experiments where rerouting would invalidate results (packages/somm/src/somm/client.py:1133).

flowchart TD
    A["SommLLM.generate()"] --> B["Resolve workload policy
(auto-register in observe mode)"] B --> C["Merge declared + inferred capabilities"] C --> D["Run mutable pre_call hooks
(fail open)"] D --> E{"Daily budget?"} E -- "exceeded" --> F["SOMM_BUDGET_EXCEEDED
fatal, no dispatch"] E -- "ok" --> G{"Provider pin?"} G -- "pinned" --> H["Try pin first"] G -- "unpinned" --> I["Preference-ordered router"] I --> J["Filter explicitly incapable
provider/model pairs"] J --> K["Apply plan governor:
defer over-paced, drop blocked"] K --> L["Skip cooled providers"] L --> M["Dispatch"] H --> M M -- "transient failure" --> N["Cool provider,
fall through to next"] N --> M M -- "success" --> O["Normalize result +
immutable Call record"] O --> P["Batched telemetry write"] P --> Q["post_call hooks (inline)
post_process (background)"]

The router applies three filters before any network access (packages/somm/src/somm/routing.py:158):

Recording the call: tokens, cost, latency, outcome, provenance

Whatever happens at dispatch, the call leaves a normalized Call record: provider/model attribution, tokens, cost, latency, hashes, outcome, errors, correlation data, cache usage, and citations (packages/somm/src/somm/client.py:1265). In somm-core's data model, calls are immutable telemetry events; late outcome changes go into a separate call_updates table, preserving a deterministic audit history rather than rewriting the original row (packages/somm-core/src/somm_core/models.py:225, packages/somm-core/src/somm_core/repository.py:1403, docs/BLUEPRINT.md:56).

Cost is computed locally: cost_for_call() converts token counts into per-call USD using pricing seeded from an offline bundled snapshot and cached for ten minutes (packages/somm-core/src/somm_core/pricing.py:233, packages/somm-core/src/somm_core/pricing.py:108). Missing pricing fails open to zero cost with a once-per-provider/model warning for paid providers — visible, but never blocking a call (packages/somm-core/src/somm_core/pricing.py:240, tests/test_pricing_safety.py:87).

What the record does not contain matters just as much. Prompt and response bodies are stored only through opt-in sampling (packages/somm-core/src/somm_core/repository.py:642); shadow body capture is opt-in, deterministically sampled, size-capped, and forbidden for private workloads (packages/somm/src/somm/client.py:850). The full privacy story is on The Data Substrate.

When things break: transient fallthrough vs. fatal SOMM_* errors

somm splits failure into two regimes. Transient failures — timeouts, rate limits, 5xx responses, insufficient credit — cool the offending provider and fall through to the next candidate. Fatal policy or configuration failures stop immediately, and each has a canonical SOMM_* page documenting the problem, cause, behavior, and fix (docs/errors/index.html:28). Budget exhaustion and privacy violations are on the fatal side precisely so that fallback can never launder a policy refusal into a successful call.

Failures are deliberately visible and bounded in telemetry rather than silently swallowed. — somm design decisions, README.md:162

Failed calls are still calls: they land in the same ledger with their error details, and somm calls --status error surfaces them from the CLI — it's the route bug reports are asked to include (.github/ISSUE_TEMPLATE/bug_report.md:28). Integrations follow the same philosophy: the LangChain adapter raises failures by default so retry and circuit-breaker middleware can react, with an opt-in mode that returns an empty message carrying failure metadata instead (packages/somm-langchain/src/somm_langchain/chat_model.py:63).

The write path: batched telemetry with JSONL spill for resilience

Telemetry never blocks the hot path. A per-process writer queue batches short writes into the project-local SQLite database — .somm/calls.sqlite (README.md:67) — keeping the no-service-required call path fast (packages/somm/src/somm/telemetry.py:38, notes/PLAN.md:1179). If database writes fail, records spill to permission-restricted JSONL files for later atomic replay, so telemetry degrades to disk rather than being lost (packages/somm/src/somm/telemetry.py:270).

The database underneath is built for this pattern: constructing a Repository creates a permission-restricted SQLite file, applies packaged migrations automatically, and configures WAL, foreign keys, and per-thread, fork-aware connections (packages/somm-core/src/somm_core/repository.py:177). Migrations commit DDL and version stamps together, so a database is never left partially upgraded (packages/somm-core/src/somm_core/schema.py:91).

The whole round trip is cheap enough to gate in CI:

500warmed generate() calls in the CI perf gate
p50the only latency that fails the build (p95 is reported)
v19current SQLite schema version
10 minpricing cache TTL for cost computation

The performance check runs the warmed hot path against a fake provider and a temporary repository, isolating somm's own overhead from network and provider latency; only median latency gates CI, reducing sensitivity to noisy outliers (scripts/check_perf_budget.py:69, scripts/check_perf_budget.py:100). Schema v19 is where the original workload/prompt/call ledger has evolved to (packages/somm-core/src/somm_core/version.py:7).

Once the row is written, this call's life is over — but its afterlife is the point. Every downstream service consumes calls without rewriting them: evaluation, model intelligence, and recommendations all read the same ledger (docs/BLUEPRINT.md:171).

The Data Substrate

The SQLite schema the Call record lands in: immutability, owner-only permissions, opt-in body capture, and call_updates.

The Intelligence Loop

How accumulated call telemetry becomes shadow evaluations, model intelligence, and evidence-backed recommendations.

Agents and Integrations

The same call path behind LangChain, an OpenAI-shaped shim, MCP tools, and an Anthropic-compatible proxy.