Skip to content

v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608

Description

@Hmbown

Goal

Separate provider facts, model facts, provider model offerings, route resolution, and UI projections so CodeWhale stops mixing provider identity, model identity, and provider-specific wire ids.

The key invariant:

A model string alone is never enough to select a route.

Execution requires a ReadyRouteCandidate.
A ReadyRouteCandidate can only be produced by RouteResolver.
RouteResolver resolves provider-scoped model references against catalog facts, user config,
auth config, endpoint config, capability requirements, and validation.

Core Problem

Current provider/model logic mixes several different meanings into one string:

  • canonical model identity, such as a provider-agnostic logical model;
  • provider identity, such as DeepSeek, OpenRouter, Together, Ollama, or a custom endpoint;
  • provider wire model id, such as deepseek-ai/DeepSeek-V4-Pro on Together or deepseek/deepseek-v4-pro on OpenRouter;
  • aggregator namespace hints, such as anthropic/..., openai/..., deepseek-ai/..., or qwen/...;
  • user custom model ids for local or OpenAI-compatible endpoints.

Those are not interchangeable. A prefixed wire id can carry provider/catalog namespace information, but it is not enough to decide canonical ownership or route validity outside the provider offering that supplied it.

Examples:

  • deepseek-ai/DeepSeek-V4-Pro can be a Together wire id.
  • deepseek/deepseek-v4-pro can be an OpenRouter wire id.
  • A custom OpenAI-compatible endpoint might legitimately use a string that resembles an aggregator namespace.

So CodeWhale must stop treating deepseek-ai/ or deepseek/ as proof that the active provider must be DeepSeek, or that a non-DeepSeek provider is invalid.

Architecture Contract

Use three catalog concepts plus one runtime concept:

  1. ProviderDescriptor: provider identity and transport facts only.
  2. ModelProfile: canonical, provider-agnostic model facts.
  3. ProviderModelOffering: provider + canonical model + provider-owned wire id + endpoint + provider-specific overrides.
  4. ReadyRouteCandidate: runtime-resolved executable route produced by the route resolver.

Provider facts describe how to talk to a provider.
Model facts describe what a model can do.
Provider model offerings describe which provider serves which model under which wire id.
Route resolution is the only layer that combines provider facts, model facts, config, auth, base URL, aliases, and validation into an executable route.
UI consumes projections of candidates, never raw string matches.

Data Ownership

Provider-owned:

  • provider id/display/config key/aliases;
  • env vars/auth schemes;
  • default base URL/default endpoint;
  • wire format/request protocol;
  • live model support;
  • model-id policy.

Model-owned:

  • canonical model id;
  • display name/family/aliases;
  • intrinsic context window/max output;
  • modalities;
  • reasoning/tool/json/streaming traits;
  • supported params and tool budget hints;
  • provenance.

Offering-owned:

  • provider + canonical model relation;
  • provider wire model id;
  • provider-scoped aliases;
  • endpoint key;
  • provider-specific capability overrides;
  • route/default hints;
  • provider/offering-scoped pricing SKU and/or usage meter metadata.

Runtime-derived:

  • resolved base URL;
  • auth source/key readiness;
  • pass-through/custom mode;
  • normalized reasoning effort;
  • merged resolved-route capability profile;
  • resolved-route pricing/usage display;
  • health/live-cache freshness;
  • final config snapshot.

Wire Id And Namespace Rule

Provider-prefixed or organization-prefixed model strings are wire ids or namespace hints unless a provider-scoped offering says otherwise.

Do:

  • Resolve provider + model selector inside that provider's catalog/offering scope.
  • Keep wire_model_id separate from canonical_model_id.
  • Preserve custom/pass-through model strings exactly for local/custom endpoints.
  • Let aggregators and broad catalogs be permissive for syntactically valid unknown ids.
  • Reject direct-provider contamination only when CodeWhale is confident the active provider cannot serve that route.

Do not:

  • Infer provider switching from deepseek-ai/, deepseek/, anthropic/, openai/, or any similar prefix.
  • Treat a provider namespace prefix as canonical model ownership by itself.
  • Reject hosted aggregator offerings because the wire id appears to belong to another organization.
  • Add more model.contains(...) validation paths.

Usage And Cost Rule

Do not assume every route has token SKU pricing.

Resolved-route usage/cost metadata should support:

  • token pricing with provenance, such as DeepSeek native or many hosted API routes;
  • subscription/quota usage percentage and reset metadata, such as ChatGPT/Codex OAuth-style routes when available;
  • account credits when a provider exposes balance/credit data;
  • local/resource/not-applicable states for local runtimes;
  • explicit unknown/stale states for custom or unsupported providers.

UI should show the route-appropriate meter. Do not show fake token pricing for OAuth/subscription routes, and do not imply local or unknown routes are free.

Storage Split

Static bundled data should contain provider descriptors, built-in model seeds, known route mappings/offerings, conservative model profiles, alias/deprecation rules, sourced pricing seeds, and known usage-meter capabilities.

Live cached provider data should contain secret-free /models results, provider model ids, provider-returned context/pricing/parameter hints when available, usage/quota hints when available, reachability/health, fetched_at, ttl, and provenance.

User config should contain only user intent and overrides: selected provider/model, custom base URL, credentials/auth mode, fallback chain, custom model ids, explicit profile/pricing/usage overrides, routing preferences, Fleet model class preferences, and cost-saving mode.

Module Direction

Suggested eventual boundary:

catalog/
  provider_descriptor.rs
  model_profile.rs
  provider_model_offering.rs
  capabilities.rs
  pricing.rs
  usage_meter.rs
  provenance.rs
  catalog_snapshot.rs
  catalog_indices.rs
  catalog_compiler.rs
  live_cache.rs

route/
  route_request.rs
  route_candidate.rs
  route_resolver.rs
  route_validation.rs
  semantic_roles.rs
  ranking.rs
  errors.rs

adapters/
  provider_adapter.rs
  wire_protocol_adapter.rs
  openai_chat.rs
  openai_responses.rs
  anthropic_messages.rs
  ollama.rs
  local.rs

ui_projection/
  provider_dashboard.rs
  model_picker.rs
  route_explain.rs

Keep dependency direction clear:

catalog + config -> routing -> adapters / ui_projection -> engine

Rules:

  • catalog cannot read env vars;
  • catalog cannot read user config;
  • config cannot own provider behavior;
  • UI cannot import adapters;
  • adapters cannot choose models;
  • engine cannot accept raw provider/model strings;
  • semantic routing cannot branch on model family strings.

Relationship To Child Issues

Acceptance Criteria

  • Provider facts, model facts, provider model offerings, and runtime route candidates are represented separately.
  • Provider-scoped aliases and wire ids are resolved only inside provider scope unless the user explicitly asks for global search.
  • A provider-prefixed wire id is not used as global provider-switching evidence.
  • User config stores intent and overrides, not provider behavior tables.
  • Pricing is provider/offering/route-owned, not model-owned.
  • Usage/quota meters are first-class alongside pricing and can represent subscription/OAuth/local/unknown states.
  • CapabilityProfile is resolved-route derived data.
  • ModelProfile is intrinsic model behavior and can be consumed by prompts/tools without knowing serving provider.
  • UI picker/dashboard rows are projections from route/catalog data, not string heuristics.

Out Of Scope

  • Rewriting unrelated provider catalogs.
  • Adding new hosted/cloud behavior by default.
  • Model-specific hacks outside the route/profile layer.
  • A separate Fleet-only provider/model selector.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestv0.8.65Targeting v0.8.65

    Projects

    Status
    Done

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions