Repository navigation
feat: let an alias point at several models and fail over between them - #8
Merged
Merged
Conversation
The first deployment keeps its id: its row moves to a hidden <model_name>@<provider> name and a new alias row takes the model_name.
The rebase onto main's performance view invalidated the committed bundle.
tito
force-pushed
the
feature/multi-provider-alias-6e5
branch
from
October 8, 2026 19:24
c05b403 to
6663a94
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
An alias points at exactly one model on one provider. When that provider is down, rate limited, or returns 5xx, every request to the alias fails, and the operator must edit the alias by hand to move traffic. Two deployments of the same model (for example on two inference hosts, or on a self-hosted node and a paid API) cannot share one name. The LiteLLM compat endpoint returns 409 for a second deployment under one
model_name, so tooling that relied on LiteLLM load balancing breaks.What
An alias keeps its current meaning, but its target becomes a list: it can point at one model or at several. There is no separate "group" object in the UI, the admin API, or the docs, because the user wants one concept: an alias. When an alias has two or more targets, it has a strategy that picks which target serves a request:
failover(listed order, the default),round_robin(start target rotates),weighted(random start target by weight), orleast_busy(fewest in-flight requests on this replica). With one target, there is no strategy and the alias behaves exactly as today. If the chosen target fails before any response byte reaches the caller (connection error, timeout before headers, 429, or 5xx), the proxy tries the remaining targets in listed order. A failure in the middle of a stream is not retried, because bytes already reached the caller. A target that fails is skipped for a fixed cooldown on that replica, because otherwise every request pays the failed attempt again. If every target is in cooldown, the proxy tries them all anyway. Each failed attempt is recorded as its own usage event with its real outcome and afailed_overflag, because the user wants recovered upstream failures to stay visible on the errors dashboard. Request counts exclude flagged events. The LiteLLM compat endpoint adds a second distinct deployment under onemodel_nameas another target of that alias, because the user wants existing registration tooling to keep working.POST /admin/modelswith{"alias": "ha", "target": "m-ok"}targetis accepted as a one-itemtargetsPOST /admin/modelswith{"alias": "ha", "targets": ["m-dead", "m-ok"], "strategy": "failover"}POST /v1/chat/completionswithmodel: hawhilem-deadis downm-ok,x-llmproxy-providernames it,x-llmproxy-attempts: 2LLMPROXY_FAILOVER_COOLDOWN(30s)x-llmproxy-attempts: 1, dead target skippedx-llmproxy-attempts: 1POST /model/newwith an existingmodel_nameand a newapi_baseweightedDELETE /admin/models/{id}on a model that is one of several targetsmodel_in_use, as for a single targetWhere to look, in reading order:
internal/store/schema.go,migrate.go,types.go: tablemodel_binding_target, columnmodel_binding.strategy, columnusage_event.failed_over.internal/store/providers.go,targets_test.go: the target list is written in the binding's transaction. One target keepstarget_id, two or more use the new table.ListBindingsTargetingreads both forms for the delete guard.internal/catalog/catalog.go:Resolvereturns a plan of routes with the intersected capabilities of the enabled targets.internal/catalog/balance.go,balance_test.go: per-replica ordering by strategy, round-robin counter, in-flight counter, cooldown.internal/server/openai.go,ratelimit.go,usagerec.go,failover_test.go: the attempt loop around the single credential site, header writes moved after the relay decision,x-llmproxy-attempts.internal/store/usage.go: request-count aggregates addfailed_over = 0; the errors dashboard keeps flagged rows.internal/server/admin.go,aliastargets_test.go:targetsandstrategyon create and patch, each target checked for the one-hop rule.internal/server/litellm.go,litellm_test.go: a second deployment under onemodel_namebecomes a target,/model/infoand/model/deletework per deployment.internal/metrics/metrics.go,internal/config/config.go:llmproxy_failover_total{provider, model, reason}andLLMPROXY_FAILOVER_COOLDOWN.ui/src/pages/Models.tsx,ModelsCatalog.tsx,Requests.tsx,lib/strategies.ts: several targets in the alias form, strategy select with two or more, weights and order buttons, failed-over flag in the request log.docs/concepts/architecture.md,guides/models.md,reference/api.md,reference/openapi.yaml,reference/configuration.md.Design changes
An alias target becomes a list. The
target_idcolumn holds one id, and the one-row join inbindingSourcecannot express an ordered list with weights. One-target aliases keeptarget_idunchanged, so no existing row migrates and the shared join stays as it is. An alias with two or more targets stores them inmodel_binding_targetand leavestarget_idempty. This split is internal: the API and the UI only showtargets.target_idpoints at one direct bindingtarget_idfor one target, rows inmodel_binding_targetfor 2+target: "x"targets: ["x", "y"],strategy;targetstill acceptedcatalog.Resolvereturns a plan instead of one route. The server needs the full candidate list to fail over without a second lookup.*Route[]*Route+ strategyResponse headers are written after the attempt decision. Before,
observeRateLimitwrotex-ratelimit-*headers on the first upstream response. A failed attempt must not leak its headers into the final response.x-ratelimit-*headersx-llmproxy-provider,x-llmproxy-modelx-llmproxy-attemptsusage_eventgainsfailed_over. Without the flag, request counts per alias double-count retried requests.failed_over = 1failed_over = 0Results
The alias form with two targets, the strategy select, weights, and order buttons (after; the form had one target combobox before):
3-edit-ha.png, closeup2-form-targets.png.The usage models table with an alias that lists its targets and strategy, and a request count that excludes the failed attempts (after):
4-catalog.png.The screenshots come from the seeded local server.
ghcannot attach images, so they are attached through the web UI after the PR opens.Left alone
<model_name>@<provider>for extra deployments. Operators see rows they did not create by hand. Targets that are not bindings would break the one-hop rule and every listing.How to verify
haand pick both models as targets, dead one first. The strategy select appears, set tofailover. CallPOST /v1/chat/completionswithmodel: ha. The call succeeds,x-llmproxy-providernames the working provider, andx-llmproxy-attemptsis2.x-llmproxy-attemptsis1, because the dead target is in cooldown.unreachableevent flagged as failed over. The request count forhais 2, not 3.x-llmproxy-attemptsis1.round_robinwith two working targets and send 4 requests. Each provider serves 2.hain the UI. The strategy select disappears, and the alias behaves as a plain alias.POST /model/newtwice with the samemodel_nameand two differentapi_base. Both return success.GET /model/infolists two deployments. Calls to the name succeed while oneapi_baseis down.