Repository navigation
feat(routes): experimental.model_route_settings — route machinery constants as config, defaulted to current values #1141
Description
Drew's directive (2026-10-31): "make constants into experimental config defaulted to current values."
Problem
The model-route machinery's tuning constants are hardcoded in packages/core/src/model-route.ts. The per-route health block covers only the two bench bases (60s failure / 900s quota). Everything else — the bench ladder's shape, quota demotion, the quota gate, escalation, the network-suspect heuristic, continuity hedging, the user-preference default — is a literal or an entry in DEFAULT_SETTINGS, for which a code-level ModelRoute.configure() hook exists but no config key feeds it (grep-verified: configure() is called only from tests). Operators cannot tune any of it without a code change and a rebuild.
Constants inventory (all in packages/core/src/model-route.ts unless noted)
New config home for every row: experimental.model_route_settings, grouped so the block can later move under experimental.routes.settings with a mechanical path change when the staged nesting (#1140) lands.
bench — the cooldown ladder
| Symbol / locus | Current value | New key |
|---|---|---|
MAX_COOLDOWN_MS (:344) |
86_400_000 (24h) | bench.maxCooldownMs |
backoff multiplier, inline 2 ** in bench() (:378) |
2 | bench.multiplier |
retry-after floor, inline Math.max(·, 1_000) (:377) |
1_000 | bench.retryAfterMinMs |
failure-base cap, inline in policy() (:197) |
3_600_000 (1h) | bench.cooldownCapMs |
quota-base cap, inline in policy() (:198) |
86_400_000 (24h) | bench.quotaCooldownCapMs |
demotion — quota-error demotion
| Symbol / locus | Current value | New key |
|---|---|---|
DEFAULT_SETTINGS.quota.demoteAfter (in Settings, no config feeder) |
3 | demotion.after |
DEFAULT_SETTINGS.quota.windowMs |
14_400_000 (4h) | demotion.windowMs |
DEMOTED_FACTOR (:1134) |
0.01 | demotion.factor |
quotaGate — exhaustion gates from quota snapshots / attempt reports
| Symbol / locus | Current value | New key |
|---|---|---|
QUOTA_GATE_MAX_MS (:597) |
2_592_000_000 (30d) | quotaGate.maxMs |
QUOTA_GATE_FALLBACK_MS (:601) |
1_800_000 (30m) | quotaGate.fallbackMs |
shouldFeedQuota default ttl (:589) |
60_000 | quotaGate.feedTtlMs |
escalation — racing the next target after give-ups
| Symbol / locus | Current value | New key |
|---|---|---|
DEFAULT_SETTINGS.escalation.enabled (no config feeder) |
true | escalation.enabled |
DEFAULT_SETTINGS.escalation.afterGiveUps |
2 | escalation.afterGiveUps |
OUTCOME_WINDOW (:254) — outcomes the give-up count reads |
5 | escalation.outcomeWindow |
network — network-suspect forgiveness / provider-wide demotion
| Symbol / locus | Current value | New key |
|---|---|---|
NETWORK_WINDOW_MS (:313) |
30_000 | network.windowMs |
NETWORK_PROVIDERS (:314) |
3 | network.providers |
PROVIDER_MODELS (:315) |
2 | network.providerModels |
continuity — hedging back to a failover origin
| Symbol / locus | Current value | New key |
|---|---|---|
ORIGIN_HEDGE_REQUESTS (:1612) |
2 | continuity.hedgeRequests |
overrides — user preference default
| Symbol / locus | Current value | New key |
|---|---|---|
PREFERRED_FACTOR (:1137) |
3 | overrides.preferredFactor |
Deliberately left hardcoded
- Backoff exponent cap (30) in
bench()— an arithmetic overflow guard, not a knob: 2^30 × 1ms ≈ 12.4 days already exceeds the 24h bench cap, so the cap always binds first. - State memory bounds —
HEALTH_LIMIT2_000,SESSION_LIMIT5_000,CONTINUITY_LIMIT5_000: LRU caps on process-local maps. Resource-safety invariants, not routing policy. - Event buffer caps —
recentFailuresring at 200, quota-event history at 50 (recordQuota): bounded-memory invariants sized as headroom over the thresholds they serve. The schema therefore capsdemotion.afterat 50, so a configured threshold can never exceed the retained history. - Usage-accounting internals —
BUCKET_MS6_000,DAY_MS86_400_000, the 60s last-minute usage window: ledger bucketing granularity/retention, not failover policy. - Relational clamps in
policy()(slowThreshold ≤ sampleWindow;sampleWindow ≤ 50mirrors the schema'sSampleCount) — consistency clamps tied to the schema, not standalone values.
Same property, noted as follow-up (out of scope here): model-route-limits.ts (MAX_WINDOW_MS 24h, UNKNOWN_RESET_MS 60s — burst rate-limit holds) and model-route-tuning.ts (MAX_QUOTA_RESET_MS, which mirrors quotaGate.maxMs and should stay in step with it).
Design
- Schema: a
Settingsclass inpackages/schema/src/config/model-routes.tswith grouped sub-objects (bench/demotion/quotaGate/escalation/network/continuity/overrides), every field optional and annotated with its default; registered asexperimental.model_route_settingsinpackages/schema/src/config/experimental.ts. The grouped shape survives either schema outcome: it works flat today, parallel tomodel_routes, and moves underexperimental.routes.settingsmechanically when config: nest routing config under experimental.routes (models, providers, windows, schedules, tuning, log, settings) with a normalize compat shim for the flat keys #1140 lands. - Validation rejects nonsense: durations are positive ints bounded at 90 days;
bench.multiplier≥ 1 (below 1 successive benches would shrink);demotion.factorin (0, 1] (0 would zero a weight; above 1 a quota error would promote);demotion.after≤ 50 (retained-history cap, above);network.providers≥ 2 (1 would forgive every lone failure as a network fault); counts are positive ints. - Wiring: the model-routes config plugin registers a lazy settings reader on
ModelRoute(theModelRouteLog.onCaptureErrorBodiesprecedent), so config reloads are honored without a restart.demotion.after/demotion.windowMsandescalation.enabled/afterGiveUpsfeed the pre-existingSettings/configure()store; the rest feed a newConstantsstore withconfigureConstants()/currentConstants()mirroringconfigure()/currentSettings(). - Precedence: compiled defaults ← programmatic
configure()/configureConstants()← config. Per-routehealthbases still override locally for their route; the bench caps (nowbench.cooldownCapMs/bench.quotaCooldownCapMs) bound those local values exactly as the hardcoded caps do today.
Acceptance
- With no
model_route_settingsin config, behavior is bit-for-bit current behavior: the existing model-route test family passes unchanged (no edits to existing expectations), and new tests assert the resolved defaults equal the current constant values. - New tests show config overrides change behavior: demotion factor, bench max cap, quota-gate fallback (plus defaults + validation coverage).
- Typecheck clean; the routing skill doc (
routing-config.md) documents the block.
Acceptance criteria
- A new
experimental.model_route_settingsconfig block feeds every inventoried constant inpackages/core/src/model-route.ts, grouped as specified in the body. - With no config set, behavior is byte-for-byte the current defaults (existing route tests pass unmodified).
- Setting a value in config observably changes the corresponding machinery constant without a code change.
Activity
feat(routes): experimental.model_route_settings — route machinery con…
feat(routes): experimental.model_route_settings — route machinery con…
Merge pull request #1142 from developing-today/feat/model-route-settings
dezren39 commentedon Nov 1, 2026
Intake review — fleet monitor
Related: #1140 (the staged nesting this block later moves under); PR #1134 (provider_routes rename, staged first). No merged PR references this issue yet at intake.
Notes: body carries a full constants inventory with current values and target keys, and notes configure() is currently test-only — a clean, bounded config-plumbing change.
Suggested approach: wire the config key into the existing ModelRoute.configure() hook rather than adding a second path; default every key to the inventoried current value.
Risks: settings that silently diverge from defaults in existing user configs once the key exists — defaults must be exact.
dezren39 commentedon Nov 1, 2026
Clerical close — already fixed (feeder triage, issue-sweep-rotating, 2026-11-01)
This issue is implemented on current main; closing with evidence rather than enqueueing:
855ec25f53e2b2a28ef2ffe2ceac396db2bfc753, which is current main HEAD (verified viagh api repos/developing-today/opencode/commits/mainandgh pr view 1142, two independent reads).855ec25fby direct read:packages/schema/src/config/experimental.ts:50registersmodel_route_settings: ConfigModelRoutes.Settings;packages/schema/src/config/model-routes.ts:351-458carries the groupedSettingsclasses (Bench / Demotion / QuotaGate / Escalation / Network / Continuity / Overrides);packages/core/src/model-route.tscarries theConstantsstore (DEFAULT_CONSTANTS:1777,configureConstants/currentConstants(), doc at :503 namingexperimental.model_route_settings) and the machinery reads through it (bench ladder :202/:393, quota gate :654/:708, demotion factor :1315, continuity :1741).The PR evidently landed without an auto-close keyword taking effect. No queue packet is needed.