Is your feature request related to a problem? Please describe.
Virtual models (redirects) can only target concrete provider models. A redirect whose target is another virtual model's source is rejected at validation time with target "<name>" refers to another virtual model (internal/virtualmodels/service.go, validateRedirectStructure; covered by TestService_RejectsRedirectTargetingAnotherRedirect). Self-targeting is rejected the same way. ValidateManagedConfig applies the same invariant to declarative config.yaml / VIRTUAL_MODELS entries, so chained managed configs fail startup too.
This forces duplicating target lists, weights, and strategies across virtual models for any layered routing. Examples:
- An org-wide
cheap alias (round-robin over small models) that should also serve as one fallback leg inside a smart-frugal group.
- Team-scoped variants of a shared base alias — today each team variant restates the full target list.
- A
production alias pointing at a us-east alias that itself load-balances concrete models.
Every change to the underlying targets has to be edited in N places, and the copies drift.
Describe the solution you'd like
Allow a redirect target to reference another virtual model's source, resolved transitively at resolve time. Suggested guard rails:
- Cycle detection: reject chains like
a -> b -> a at upsert and at startup validation for managed config, with the offending chain spelled out in the error message.
- Depth limit: cap chain length (e.g. 8 hops) so resolution stays bounded and cheap.
- Strategy composition: the inner virtual model resolves to a concrete model first; the outer strategy (round-robin / cost / adaptive) then picks among its resolved concrete legs.
- Scoping semantics documented: outer
user_paths keep gating exposure; define how inner redirect scopes and policies interact when chained.
- Disabled leg: a disabled virtual model inside a chain is treated as unavailable, the same as a skipped provider target today.
The structural checks are deliberately catalog-independent (they run before the async model catalog is warm). Cycle and depth validation fits into that same pass.
Describe alternatives you've considered
- Duplicate target lists across virtual models (the current workaround) — works but drifts; one missed edit silently changes routing for one alias but not its siblings.
- Flatten at write time: resolve the referenced virtual model into its concrete targets on upsert and store those. Loses live updates when the inner virtual model changes, which is the main point of chaining.
- Layer outside the gateway (client-side fan-out or a second proxy) — defeats the purpose of a central routing layer.
Additional context
- Enforcement lives in
validateRedirectStructure (internal/virtualmodels/service.go), which rejects both self-targets and cross-redirect targets; ValidateManagedConfig reuses the same invariants for IaC-supplied virtual models.
- Virtual models replaced the legacy aliases /
model_overrides tables (ADR-0008, docs/adr/0008-virtual-models.md). Chaining was not part of that design, so this extends the redirect graph from a star to a DAG — no new entity.
- Resolution is currently single-pass (
ResolveModel, internal/virtualmodels/resolve.go). Chained resolution also needs a defined interaction with per-request override headers and with the Supports / ExposedModels projections (a chained alias should expose its leaf concrete models).
This issue was drafted with AI assistance (Kimi Code CLI). The AI analyzed the codebase at commit b289f466.
Is your feature request related to a problem? Please describe.
Virtual models (redirects) can only target concrete provider models. A redirect whose target is another virtual model's source is rejected at validation time with
target "<name>" refers to another virtual model(internal/virtualmodels/service.go,validateRedirectStructure; covered byTestService_RejectsRedirectTargetingAnotherRedirect). Self-targeting is rejected the same way.ValidateManagedConfigapplies the same invariant to declarativeconfig.yaml/VIRTUAL_MODELSentries, so chained managed configs fail startup too.This forces duplicating target lists, weights, and strategies across virtual models for any layered routing. Examples:
cheapalias (round-robin over small models) that should also serve as one fallback leg inside asmart-frugalgroup.productionalias pointing at aus-eastalias that itself load-balances concrete models.Every change to the underlying targets has to be edited in N places, and the copies drift.
Describe the solution you'd like
Allow a redirect target to reference another virtual model's source, resolved transitively at resolve time. Suggested guard rails:
a -> b -> aat upsert and at startup validation for managed config, with the offending chain spelled out in the error message.user_pathskeep gating exposure; define how inner redirect scopes and policies interact when chained.The structural checks are deliberately catalog-independent (they run before the async model catalog is warm). Cycle and depth validation fits into that same pass.
Describe alternatives you've considered
Additional context
validateRedirectStructure(internal/virtualmodels/service.go), which rejects both self-targets and cross-redirect targets;ValidateManagedConfigreuses the same invariants for IaC-supplied virtual models.model_overridestables (ADR-0008,docs/adr/0008-virtual-models.md). Chaining was not part of that design, so this extends the redirect graph from a star to a DAG — no new entity.ResolveModel,internal/virtualmodels/resolve.go). Chained resolution also needs a defined interaction with per-request override headers and with theSupports/ExposedModelsprojections (a chained alias should expose its leaf concrete models).