Repository navigation
Description
Drew's directive (2026-10-31)
make synthetic providers a concept which allow ordered round-robin weighted same as models except provider level
The concept
Model routes compose models: a pool (e.g. terra-plan) holds provider/model leaves and applies a selection mode (ordered / round-robin / weighted). Route expansion passes no model context into a nested route, so provider-level composition is impossible today. Tonight proved it: the GPT account pair had to be instantiated as three separate pools — openai-plan-terra, openai-plan-sol, openai-plan-luna in the laptop config — each an ordered [openai.alternate/<model>, openai/<model>] pair, because the account order is a property of the providers, not of any one model.
A synthetic provider is a virtual provider defined by a selection over concrete providers, with the same selection modes as model routes — ordered, round-robin (including the tiered weights semantics of #1122), and weighted. A model route then uses <synthetic-provider>/<model> as a leaf, and resolution picks the member provider per the synthetic provider's selection, yielding a concrete provider/model call.
Canonical first instance: synthetic provider openai-plan = ordered [openai.alternate, openai] — one definition serving every GPT model.
Design answers
Settled from the codebase's actual grammar (packages/core/src/config/plugin/model-routes.ts, packages/core/src/model-route.ts); the implementation follows these.
1. Config grammar. Declared in a new top-level block experimental.synthetic_providers, a record keyed by synthetic provider id (same id pattern as route ids). Each entry: providers (the member provider ids, in configured order), selection (ordered default, round-robin, weighted), and weights (keyed by member provider id; unset weighs 1). A route references a synthetic provider exactly like a concrete one — openai-plan/gpt-6.1-terra, string or object target form, #variant included — because the reference site is what makes the concept useful. That means synthetic ids share the provider-id namespace at reference sites, so collisions are rejected at config load: an id that names a concrete provider in the catalog, the routes provider (opencode-route), or a configured route id is dropped, loudly (a warning naming the id and the reason — never silently, cf. #734). Concrete providers always win a reference.
2. Member eligibility per dispatch. Mirrors the leaf rule exactly. A member contributes a leaf for a requested model only when its catalog lists the model (glob patterns match per member) and it can produce the requested variant — natively, through the target's remap table, or as a suffixed catalog id (#985). Otherwise the member is skipped for that dispatch; it is never an error. A synthetic target whose members contribute no leaf for the model attaches nothing, like a glob that matches nothing.
3. Health interaction. Resolution produces concrete provider/model leaves, and every health mechanism is keyed on concrete targets — per-target and per-credential cooldowns and the bench ladder, quota demotion, quota gates, rate-limit holds, budgets. A benched or walled member leaf is simply absent from the walk's ready set, and selection falls through per the synthetic group's own mode — including #1122's tier rule inside a round-robin synthetic (a demoted member sorts below the live tiers there). No synthetic-level health state exists; member readiness is judged by the same walk that judges nested route groups.
4. Nesting. No in v1. Members must be concrete provider ids; a member naming another synthetic provider (or the routes provider) is dropped at load with a warning. Expansion stays one level deep, like the door-set target (#637). Revisit only when a real composition need appears.
5. Decision records. Expansion gives the synthetic group its own node, marked with the synthetic id. The walk records a decision for that node — route_id is the synthetic id, candidates are the concrete member leaves, chosen is the member that served — and the parent decision names the group by its synthetic reference (openai-plan/gpt-6.1-terra). A walk stays explainable end to end, the way #1122's tiers detail made tiered choices explainable.
6. The route proxy door. The proxy in developing-today/code (bin/opencode-route-proxy.mjs, OpenClaw's door) resolves routes itself from the same config file — its own expand/rotate/weightedOrder, its own providerBaseURL and credential-store reads. It does not share the fork's expansion or selection code. Synthetic providers are therefore v1 server-side only: a route using a synthetic leaf will not resolve through the proxy until the proxy learns the concept. That follow-up belongs to the code repo and is not part of this work.
Implementation shape
- Schema: a
SyntheticProviderclass and aSyntheticProvidersrecord inpackages/schema/src/config/model-routes.ts;experimental.synthetic_providersinpackages/schema/src/config/experimental.ts. - Runtime:
ModelRoute.SyntheticProvider(the validated form); the node tree gains an optional synthetic marker so decisions can name the group (answer 5). - Plugin: a
configuredSyntheticsreader (validation + loud drops, exported likeconfiguredWindows) threaded throughexpand()andretargetDefinition()as an optional parameter, like windows. Expansion treats a synthetic target as a per-model group node — one group per matched model id, so a glob model pattern fans out per model and a group never mixes models — reusing the existing leaf machinery (variant producibility, dedup, ceilings, budgets, cache formats) verbatim. The group's selection and weights are honored when the referencing route uses rules, mirroring how the referencing route's own selection is gated by its autonomy; afixedroute sees the group as ordered with unit weights.
Acceptance criteria
- A config declaring
openai-planonce can serveopenai-plan/<model>leaves in any number of routes, for every model both members offer. - All three selection modes behave per their model-route semantics at provider level, including tiered round-robin after feat(routes): round-robin weights as priority tiers — rotate within a tier, fall through when it is exhausted #1122.
- A member that does not offer the model or the variant is skipped for that dispatch; a benched or demoted member falls through (last-resort rules unchanged).
- A synthetic id colliding with a concrete provider, the routes provider, or a route id is rejected at load with a warning; the concrete provider keeps the reference.
- Decision records show the synthetic resolution (
openai-plan→ the member that served). - Tests cover: ordered synthetic (first ready member; fall-through on failure/bench), round-robin synthetic (even rotation; tiered weights), weighted synthetic, member skipped for a missing model/variant, benched member excluded with fall-through, a synthetic leaf inside a model route resolved end to end, collision rejection, and the decision record.
Activity
feat(core): synthetic providers — provider-level pools with ordered/r…
dezren39 commentedon Nov 1, 2026
Intake review
Still live: filed this run (2026-11-01T02:07Z); no merged PR references it. The body already carries a full design (six settled answers) and an acceptance criteria section — enrichment here is labels only.
Related: #1122 (tiered round-robin weights this builds on), #985 (suffixed catalog ids), #734 (loud-drop precedent), #637 (door-set target).
Notes / risks: v1 is server-side only by design — the code-repo route proxy will not resolve synthetic leaves until it learns the concept (recorded in the issue as a follow-up belonging to developing-today/code, not part of this work). Collision handling (concrete provider wins, loud warning) is the safety-critical piece.
Suggested approach: follow the implementation shape in the body — schema first, then the plugin reader threaded through expand()/retargetDefinition(), reusing leaf machinery verbatim.
Labels applied: kind/enhancement, source/human, status/needs-triage, priority/normal, effort/l, meta/enriched.
refactor(core): rename synthetic providers to provider routes (#1132)
[-]feat(routes): synthetic providers — provider-level pools with ordered/round-robin/weighted selection[/-][+]feat(routes): provider routes — provider-level pools with ordered/round-robin/weighted selection[/+]dezren39 commentedon Nov 1, 2026
Renamed 2026-10-31: synthetic providers → provider routes.
With Drew's approval (2026-10-31), the concept is renamed for symmetry with
model_routes— routes, at provider level. Applied on PR #1134 in commit6958dce851("refactor(core): rename synthetic providers to provider routes (#1132)"):experimental.synthetic_providers→experimental.provider_routesConfig.ModelRoute.SyntheticProvider/.SyntheticProviders→ProviderRoute/ProviderRoutesconfiguredSynthetics→configuredProviderRoutessynthetic→providerRouterouting-config.md) follow the new name throughout.Terminology only — no behavior change. The rename lands while the PR is unmerged, so no deployed config uses the old key and no migration is needed. The follow-up config collapse in the PR description (one
openai-planprovider route replacing the three per-model pools) already uses the new key.