DOCS / ARCHITECTURE / MODEL REGISTRY & ROUTING

Model Registry & Routing

The registry

Models are declared once, centrally, in apps/gateway/server/registry/models.config.ts:

models.config.ts
Authorization: Bearer lattice_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
idstring, required
The stable, client-facing model name (what goes in the request's model field).
provider'openai' | 'anthropic' | 'google' | 'ollama' | 'vllm'
Which adapter serves this model. Must have credentials configured — see Providers.
providerModelstring, required
The upstream model name actually sent to the provider (e.g. claude-sonnet-4-5).
fallbacksstring[], optional
An ordered list of other registry ids to try if this one's provider fails. Flat, not recursive — a fallback's own fallbacks are not chained in.
aliasesstring[], optional
Extra names a client may request that resolve to this exact entry (e.g. company/smart). An alias can never resolve to a different entry, and can't itself be used as a fallback target.
pricing{ inputPerMillionUsd, outputPerMillionUsd }, optional
Static USD-per-million-token pricing, used only for cost-aware ranking and cost estimation on request logs. A model with no pricing still works — it just never wins a cost or balanced sort and its request logs show no estimated_cost_usd.

Boot-time validation

The registry is validated once, at module load — before the server accepts any request. Any of the following crashes startup with a model_registry_invalid error instead of surfacing as a runtime failure on some unlucky request:

  • Duplicate id across entries.
  • An alias colliding with an existing id, or duplicated across entries.
  • A model listing its own id in its own fallbacks (self-reference).
  • A fallbacks entry that doesn't match any known id (aliases are not valid fallback targets).
  • The same fallback listed twice in one entry's fallbacks.
  • A provider with no configured credentials (see Providers for what "configured" means per provider).

Resolving a request

  1. Lookup: lookupModel(registry, request.model) matches against every entry's id first, then every entry's aliases.
  2. Candidate order: [primary, ...primary.fallbacks], flattened — the order declared in the config, unless overridden by strategy.
  3. Circuit filter: candidates whose provider is currently open are dropped. If that would leave zero candidates, the original, unfiltered order is used instead — a stuck circuit should never be worse than no circuit breaker at all. See Failover & Circuit Breaking.
  4. Strategy reorder: the remaining candidates are sorted by the application's routingStrategy.
  5. Attempt loop: candidates are tried in the resulting order until one succeeds or all are exhausted. See Request Lifecycle for retry semantics.

Routing strategies

An application can pin a routingStrategy (set through the Management API):

StrategyCandidate order
unset (default)The registry's declared [primary, ...fallbacks] order.
costAscending blended $/M input+output price.
latencyAscending rolling-average latency per model.
balanced Cost and latency, each min-max normalized to [0, 1], then averaged evenly (0.5 × cost + 0.5 × latency).

A candidate with no price or no latency sample yet is still tried — it just sorts after every candidate that has a real score, rather than being dropped. Ties and fully-unscored groups preserve their original relative order (a stable sort).

Latency tracking

Every successful provider call records its latency against providerLatencyState, keyed by the registry model id, as a rolling average. That average is what latency and balanced strategies sort on. It's periodically synced across gateway instances the same way circuit breaker state is — see the next page.

Cost estimation

estimateCost(entry, usage) returns undefined unless the entry has pricingand the response reported both promptTokens and completionTokens:

text
type ProviderAdapter = {
  id: ProviderId;
  createChatCompletion(
    request: ChatCompletionRequest,
    ctx: ProviderRequestContext,
  ): Promise<ChatCompletionResponse>;
  streamChatCompletion(
    request: ChatCompletionRequest,
    ctx: ProviderRequestContext,
  ): AsyncGenerator<ChatCompletionChunk, void, void>;
};

type ProviderRequestContext = {
  apiKey?: string;
  providerModel: string;
  signal: AbortSignal;
};

This value is what populates estimated_cost_usd on request logs and the usage summary endpoints.

LATTICE — INTERNAL AI INFRASTRUCTURE
GITHUB © 2026