Model Catalog & Routing
Scope cascade × workType × pool model routing.
The model catalog is the platform's registry of LLM models, their providers, pricing, and capabilities. Routing rules determine which model is dispatched when a workflow, agent, or session requests an LLM without specifying one explicitly.
"Routing" here means which LLM a request dispatches to - a different concept from placement (which sandbox pool a session runs on) or selection (which live agent receives delegated work). See Placement, Selection & Routing Vocabulary for how the terms on this site relate.
The Model Catalog
Every model the platform can dispatch must have a catalog entry. Entries are keyed by (provider, modelId) and are never hard-deleted while anything still points at them - retirement runs through deprecation and migration instead.
Catalog Entry Schema
interface ModelCatalogEntryDTO {
id: string
provider: ProviderId // 'claude', 'codex', 'gemini', etc.
modelId: string // 'claude-opus-4-1', 'gpt-5', etc.
displayName: string // Human label shown in dropdowns
description: string | null
deprecated: boolean // Soft deprecation flag
replacedByCatalogId: string | null // Link to successor entry
efforts: EffortOption[] // Reasoning effort tiers if supported
authModes: AuthMode[] // 'byok' | 'metered' | 'shared' | 'host-session' | 'local'
contextWindow: number | null // Max input tokens
capabilityTags: string[] // ['long-context', 'reasoning', 'vision', ...]
pricing: ModelPricing | null // Input/output cost per M tokens, or flat rate
createdAt: string
updatedAt: string
}Example Entry
{
"id": "cat_claude_opus_4_1",
"provider": "claude",
"modelId": "claude-opus-4-1-20250514",
"displayName": "Claude Opus 4.1",
"description": "Latest Anthropic flagship for complex reasoning.",
"deprecated": false,
"replacedByCatalogId": null,
"efforts": [
{ "value": "low", "label": "Fast" },
{ "value": "medium", "label": "Balanced", "providerParam": { "reasoningEffort": "medium" } },
{ "value": "high", "label": "Deep", "providerParam": { "reasoningEffort": "high" } }
],
"authModes": ["byok", "metered", "shared", "host-session"],
"contextWindow": 200000,
"capabilityTags": ["long-context", "reasoning", "tool-use"],
"pricing": {
"inputPerMTokenCents": 3,
"outputPerMTokenCents": 15
}
}Managing the Catalog
The catalog itself is an operator surface. Everything in this section happens in
the operator console at Admin → Model Catalog, or through the operator-only
/api/admin/model-catalog API behind it. As a tenant you consume the catalog
through profiles; you do not edit catalog entries.
Via UI: the catalog list at /admin/model-catalog offers New entry for
hand-authoring a single model, a per-provider Bulk import toolbar for pulling
a provider's published inventory (below), and a view toggle between a flat
per-provider list and a dedup-by-model view that groups the hosts serving the
same model.
Via API: /api/admin/model-catalog is operator-authenticated. Wider
operator-level catalog configuration is covered in the operator docs.
Importing models from a provider
Providers publish and rename models faster than anyone wants to hand-maintain a catalog, so the console can pull a provider's inventory directly. The point of the flow is that you choose which models land in the catalog - importing a large provider inventory does not have to mean flooding every profile dropdown in the platform with models nobody will use.
Import runs in two stages, and only the second one writes anything.
Preview (nothing is written). Pick a provider from the Bulk import toolbar. The platform fetches that provider's current model list and diffs it against the catalog entirely in memory. You get back a plan: how many models upstream returned, how many the catalog already holds for that provider, and every upstream model tagged new, changed, or existing. Nothing has touched the database at this point - the plan lives in your browser, and the provider's credentials and raw response never leave the server.
Select, then apply. Narrow the list, tick the models you want, and confirm with Apply N selected. Only the models you selected are written, in a single transaction. The result tells you how many entries were created, restored, and updated, and how many were already present.
Filtering and selecting
The preview gives you a search box over provider model IDs and a status filter (All statuses / New / Changed / Existing), plus a checkbox on every row. Two buttons act in bulk:
- Select visible (N) adds every model matching the current filter to your selection. It only ever adds.
- Clear selection empties the selection completely.
Filtering never changes hidden selections. This is the guarantee the counter
under the filter bar is stating, and it is worth trusting: your selection is a
set of models, not a property of the current view. Search for haiku, tick three
models, then switch the filter to opus - those three are still selected, still
counted, and still imported when you apply. The count shown is the size of your
whole selection, including rows the current filter is hiding, and Apply
submits that whole selection regardless of what is on screen.
The practical consequence: build a selection across several passes with different filters, and read the count rather than the visible checkboxes to know what you are about to import. The one action that reaches across the filter destructively is Clear selection, which drops hidden selections along with visible ones.
Because Select visible is scoped to the filter, importing a provider's entire inventory means resetting the search box and setting the status filter back to All statuses first, then selecting visible.
What import will not do
- It never deletes. Catalog entries that no longer appear in the provider's upstream list are reported to you as a notice, not removed. If one is genuinely retired, open it and mark it deprecated - which runs the migration gate described below.
- It will not apply a stale plan. Apply is bound to the exact inventory the preview showed, for the same provider and the same provider account. If the upstream list changes underneath you, or the preview sits unused for more than five minutes, apply is refused and you take a fresh preview. Two operators importing the same provider at once cannot interleave into a half-applied state.
Profiles: The Dispatch Configuration
A profile is a resolved model specification with auth mode, provider config, and scope. It's what you actually use when dispatching a workflow or session. Think of it as a "model choice" you can name and version.
Profile Schema
interface ProfileDTO {
id: string
scope: 'system' | 'org' | 'project' // Where it's defined
orgId: string | null
projectId: string | null
name: string // e.g. "default", "fast", "reasoning"
slug: string // e.g. "default", "fast", "reasoning"
description: string | null
provider: ProviderId // 'claude', 'codex', 'gemini', etc.
modelCatalogId: string | null // Link to catalog entry for pricing/capabilities
effort: string | null // 'low' | 'medium' | 'high' if catalog supports it
subAgent: SubAgentOverride | null // Optional: override for sub-agent routing
providerConfig: ProviderConfig // Provider-specific options (context window, etc.)
authMode: AuthMode // 'byok' | 'metered' | 'shared' | 'host-session' | 'local'
credentialId: string | null // For BYOK: link to the stored API key
archived: boolean
createdAt: string
updatedAt: string
}Creating a Profile
Via UI:
- Settings → Model Profiles → New Profile.
- Choose a name (e.g. "fast-claude", "reasoning-gpt").
- Select provider (Anthropic, OpenAI, etc.).
- Select auth mode.
- If BYOK: select the API key credential.
- If
local: enter the endpoint URL. - Select a model from the catalog (optional; you can also paste a custom model ID).
- Set reasoning effort if the model supports it.
- Advanced: add provider-specific config (context window overrides, etc.).
- Click Create.
Via API:
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "fast-claude",
"provider": "claude",
"modelId": "claude-opus-4-1-20250514",
"authMode": "metered",
"effort": "low",
"scope": "project",
"projectId": "my-project"
}' \
https://app.rensei.ai/api/org/projects/my-project/profilesRouting: Scope Cascade and WorkType Overrides
When a workflow or agent requests an LLM without specifying a profile, the dispatcher resolves the best match using a cascade:
Resolution Order
- Explicit profile selection - If the workflow/session specifies
profileId, use that. - Node-level override - If an LLM node in a workflow sets
modelIdoreffort, use it (overrides profile default). - Project-scoped workType override - If a project has a work-type routing rule (e.g. "research → 'reasoning-gpt'"), use it.
- Project default - If the project has a default profile, use it.
- Org-scoped workType override - If the org has a work-type routing rule, use it.
- Org default - If the org has a default profile, use it.
- System default - The platform's hardcoded system default (Anthropic Claude with metered auth).
Work-Type Routing
Work types represent the lifecycle stage of a request (research, development, QA, acceptance). Route different models to different work types to balance cost and quality.
Example routing policy:
# research → use cheaper fast model
research:
provider: claude
modelId: claude-opus-4-1
effort: low # Fast reasoning
# development → use balanced model
development:
provider: claude
modelId: claude-opus-4-1
effort: medium
# qa → use expensive deep-reasoning model
qa:
provider: claude
modelId: claude-opus-4-1
effort: high
providerConfig:
contextWindow: 200000
# acceptance → human review (no LLM dispatch)
acceptance: nullSet work-type routing (UI):
- Settings → Model Profiles → Work-Type Routing.
- Select scope (org or project).
- For each work type, choose a default profile or model.
- Click Save.
Cedar Policy Enforcement
The Cedar policy engine intercepts profile resolution to enforce compliance rules. Example:
// No metered auth for regulated orgs
permit (principal, action == "agent:dispatch", resource)
if principal.org in ["regulated-org-1", "regulated-org-2"]
&& resource.profile.authMode == "metered"
then deny;If a policy denies the profile, dispatch fails with a clear error message.
Provider-Specific Config
Each provider accepts its own configuration options via the providerConfig block:
Anthropic
{
"anthropic": {
"contextWindow": 200000,
"cacheControl": true,
"budget": { "maxInputTokens": 100000 }
}
}OpenAI (Codex)
{
"openai": {
"serviceTier": "auto",
"endpoint": "https://api.openai.com/v1"
}
}Gemini
{
"gemini": {
"safetySettings": [
{ "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" }
]
}
}Consult the provider's page (/docs/model-routing/providers/anthropic) for the full schema.
Deprecation & Migration
Retiring a model is migration-gated: you cannot mark a catalog entry deprecated while anything is still using it. Attempting to deprecate an entry that live profiles or assignments still reference is refused with 409 and the message "migrate every active profile before deprecating this catalog entry". Soft-deleting it is refused the same way. So the order is fixed - move the usage first, then retire the entry - and there is no window where a model is marked dead while configurations still point at it.
Moving usage off a model
The operator console does this with a replace-usages operation on the entry being retired, backed by a two-call API:
| Call | Does |
|---|---|
POST /api/admin/model-catalog/{sourceId}/usage-migration | Preview. Read-only. Given a targetCatalogId, returns every reference to the source model, each classified safe or blocked, counted by scope, plus a revision fingerprint of the whole plan. |
PUT /api/admin/model-catalog/{sourceId}/usage-migration | Apply. Takes targetCatalogId, the previewRevision you were given, an idempotencyKey, and the explicit selected references to move. |
Apply is gated on that previewRevision. If anything covered by the plan changed
between preview and apply, the revision no longer matches and the call returns
409 with a freshly computed preview attached, rather than migrating against a
picture that has gone out of date.
What actually gets rewritten. A migration rewrites the places that hold a model choice: a profile's primary model, a profile's sub-agent override, and the inline profile stored on an assignment. Profiles that inherit their model from an ancestor profile are reported in the preview as impacted, so you can see the blast radius, but they are never written directly - fixing the ancestor is what moves them. This is why the API makes you submit complete closures: references that must move together have to be selected together, and submitting a partial set is refused rather than half-applied.
Concurrency is a wait, not a race. Apply takes a writer lock on the profile tables as its very first act, so an ordinary profile edit landing at the same moment blocks until the migration finishes, and vice versa. If that concurrent edit changed something the plan covered, the migration then fails the revision check and asks you to preview again. What you cannot get is a lost update.
Retries are safe. The idempotencyKey you supply makes apply replayable: if a
call is interrupted somewhere ambiguous, resending it with the same key returns
the original result flagged as a replay and performs no second mutation. Reusing a
key for a different selection or target is refused outright rather than quietly
applying something you did not intend.
Every attempt leaves a signed receipt. Success and failure each append one
signed, hash-chained entry to the tenant audit chain (model_profile.bulk_migrated
and model_profile.bulk_migration_failed). The receipt records identifiers and
revisions only - never model display names, profile names, or provider
configuration. See Audit Trail for how those entries
are verified.
What deprecation does once it is set
- The model stops appearing in the model list that feeds profile pickers, so it is no longer offered for new profiles.
- It can no longer be chosen as the target of a usage migration - you cannot migrate a fleet onto a model that is itself on the way out.
- It is not a runtime kill switch. Deprecation is enforced as a precondition on the catalog entry (you could only set it once usage had already been migrated away), not as a dispatch-time rejection.
replacedByCatalogIdrecords the successor so the console can point at where usage went.
Cost Insights
View cost by model/provider:
# Cost rollup over a rolling window: total, by-provider, by-pool
rensei capacity cost
# Widen the window (default 24h)
rensei capacity cost --window=7d
rensei capacity cost --window=30d
# Machine-readable
rensei capacity cost --jsonFiner-grained slices (by model, by work type, by auth mode) live in the Factory Analytics cost breakdown on the platform UI and its metrics API.
Use this data to:
- Optimize work-type routing (e.g., cheaper models for research stages).
- Identify runaway models and deprecate them.
- Forecast budget for upcoming deployments.
The OSS two-axis provider model
The execution layer underneath all of this is the open-source donmai runner, and its provider architecture is documented canonically on donmai.dev - read those pages for how a run is actually assembled:
- Providers - the two-axis model - a run
pairs a harness (the loop driver: Claude Code, Codex, OpenCode,
Antigravity, Amp, or the in-box
rawloop) with a model endpoint (the company serving the model: Anthropic, OpenAI, Google, or a local server). - Capability matrix - which harness × endpoint cells are valid. The matrix is computed from each side's declared transports and auth modes, never hand-authored.
The platform's catalog/profile/routing layer documented on this page sits on
top of those cells: a catalog entry's provider + auth mode resolves to one
cell of the OSS matrix at dispatch.
Google is one provider, two cells. The platform collapsed the former
gemini-cli provider into gemini: key-based auth modes (byok / metered /
shared) run API-direct, while local / host-session rewrite at dispatch to
the Antigravity agy CLI harness under the user's own Google subscription. See
Gemini provider for the full mapping.
Quick Reference
| Concept | Definition | Where to set |
|---|---|---|
| Catalog entry | Registry of a model's capabilities and pricing | Operator console: Admin → Model Catalog |
| Profile | Your named choice of model + auth mode + config | Settings → Model Profiles |
| Default profile | The profile used if no other routing rule matches | Settings → Model Profiles → Defaults |
| Work-type routing | Model selection by lifecycle stage (research → fast, QA → deep) | Settings → Model Profiles → Work-Type Routing |
| Provider config | Model-specific tuning (context window, safety, effort) | Profile editor → Advanced |