AI

Hardcoded Model Names Are Product Debt

July 4, 2026

The configuration drift problem appeared again in a smaller object: a model name.

The name was openrouter/owl-alpha. It appeared in Sivella after the live plan configuration had already been changed. The production database was correct; four code paths could still bypass it.

backend/clara/settings.py gave OPENROUTER_MODEL the stale default. get_llm_provider() consumed it. RoutedGateway used that legacy provider whenever a workspace route was empty. Planner curation, decision summaries, and memory extraction also constructed bare LLMGateway() instances without plan routing.

That string had become product debt.

Model selection is behavior

For an AI assistant, the model is not an implementation detail in the same way a database driver is. Users experience its latency, tone, reliability, structure, and failure modes. It also changes what a request costs.

A stale model name is therefore a production behavior bug, not a tidy-up task.

Configuration needs one authority

Each request needs one obvious authority for its provider and model. In Sivella that authority was already the database-backed PlanModel fallback chain for the workspace plan. The legacy environment variable was a second authority hiding underneath it.

The smallest version of the bug looked like this:

if not route:
    return LLMGateway().complete_with_tools(request)

The no-argument gateway eventually read OPENROUTER_MODEL. The corrected contract made an empty route an error and moved every production completion through the routed gateway:

if not route:
    raise LLMConfigurationError("No model route is available")

The three background features moved to complete_routed(). The setting and its production references were deleted rather than renamed to another model.

Test and evaluation code can still have explicit fake or evaluation models. It must not be an authority over production selection.

The logs should answer “why this model?”

A product that calls an AI provider should be able to explain model selection without logging private prompts.

For each call, the useful fields are the selected provider, served model, service tier, fallback position, latency, and token usage. Sivella's route records last_model and last_service_tier, so the model that actually answered also supplies the billing multiplier.

This matters after a fallback. If the preferred model is charged at 1.0× and a 2.0× model serves the answer, billing should debit the latter. A hundred raw tokens become two hundred effective tokens. Recording only the first choice would make both debugging and usage reporting wrong.

Fallbacks should not hide drift

The existing plan chain already crossed providers, but its behavior was uneven. A provider error advanced the chain. A missing key or unknown provider aborted it. Empty content from OpenRouter triggered retry, while empty content from another provider could look like success.

The production RoutedGateway was changed to walk the whole configured chain. Provider errors, unusable empty completions, and misconfigured links advance to the next model; only exhausting the chain raises. Tool calls without text remain valid.

That is useful resilience because it follows the product's configured order. A silent escape to an unrelated environment default is not resilience. It is hidden drift.

Building with AI: Several AI coding sessions added reasonable model calls in isolation. Together they created four paths around the plan-backed routing design because constructing a bare gateway was locally convenient. I reviewed the authority boundary across the repository, removed the second source of truth, and added a guard that fails if the deleted model setting returns.

One routing authority

A repository-wide search found every remaining reference to the old environment authority and model name. I removed them from production code, then exercised cross-provider configuration failures, empty completions, valid tool-only responses, and a fully exhausted fallback chain.

Planner, decisions, memory extraction, and chat can no longer fall through to an unplanned model. Selection has one database-backed route, empty routes fail, and a small guard rejects any attempt to restore the deleted setting.