Skip to content

feat(routes): experimental.model_route_settings — route machinery constants as config, defaulted to current values #1141

Closed

Description

@dezren39

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_LIMIT 2_000, SESSION_LIMIT 5_000, CONTINUITY_LIMIT 5_000: LRU caps on process-local maps. Resource-safety invariants, not routing policy.
  • Event buffer caps — recentFailures ring at 200, quota-event history at 50 (recordQuota): bounded-memory invariants sized as headroom over the thresholds they serve. The schema therefore caps demotion.after at 50, so a configured threshold can never exceed the retained history.
  • Usage-accounting internals — BUCKET_MS 6_000, DAY_MS 86_400_000, the 60s last-minute usage window: ledger bucketing granularity/retention, not failover policy.
  • Relational clamps in policy() (slowThreshold ≤ sampleWindow; sampleWindow ≤ 50 mirrors the schema's SampleCount) — 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 Settings class in packages/schema/src/config/model-routes.ts with grouped sub-objects (bench / demotion / quotaGate / escalation / network / continuity / overrides), every field optional and annotated with its default; registered as experimental.model_route_settings in packages/schema/src/config/experimental.ts. The grouped shape survives either schema outcome: it works flat today, parallel to model_routes, and moves under experimental.routes.settings mechanically 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.factor in (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 (the ModelRouteLog.onCaptureErrorBodies precedent), so config reloads are honored without a restart. demotion.after / demotion.windowMs and escalation.enabled / afterGiveUps feed the pre-existing Settings / configure() store; the rest feed a new Constants store with configureConstants() / currentConstants() mirroring configure() / currentSettings().
  • Precedence: compiled defaults ← programmatic configure() / configureConstants() ← config. Per-route health bases still override locally for their route; the bench caps (now bench.cooldownCapMs / bench.quotaCooldownCapMs) bound those local values exactly as the hardcoded caps do today.

Acceptance

  • With no model_route_settings in 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_settings config block feeds every inventoried constant in packages/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

  1. added
    priority/normalOrdinary priority (the default)
    effort/mMedium: expected under 1 day of work
    app/corePackage: core (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    Author

    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.

  3. dezren39 commented on Nov 1, 2026

    @dezren39
    Author

    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:

    • PR feat(routes): experimental.model_route_settings — route machinery constants as config (#1141) #1142 — "feat(routes): experimental.model_route_settings — route machinery constants as config (feat(routes): experimental.model_route_settings — route machinery constants as config, defaulted to current values #1141)" — MERGED 2026-11-01T04:07:45Z, merge commit 855ec25f53e2b2a28ef2ffe2ceac396db2bfc753, which is current main HEAD (verified via gh api repos/developing-today/opencode/commits/main and gh pr view 1142, two independent reads).
    • Spot-verified on main 855ec25f by direct read: packages/schema/src/config/experimental.ts:50 registers model_route_settings: ConfigModelRoutes.Settings; packages/schema/src/config/model-routes.ts:351-458 carries the grouped Settings classes (Bench / Demotion / QuotaGate / Escalation / Network / Continuity / Overrides); packages/core/src/model-route.ts carries the Constants store (DEFAULT_CONSTANTS :1777, configureConstants / currentConstants(), doc at :503 naming experimental.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/corePackage: core (pkgs/ or apps/ in this repo)effort/mMedium: expected under 1 day of workkind/enhancementNew feature or requestmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)status/needs-triageNot yet triaged; needs a first look

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions