Files
nucleic/Sources/NucleicCore/IntelligenceRouting.swift
T

923 lines
55 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import NucleicProtocol
/// The per-model effort capability rules, extracted from the app-layer `ModelCatalog` so the
/// Intelligence router (and its tests) can live in NucleicCore. `ModelCatalog` keeps thin
/// delegating wrappers, so every existing call site — and the wire catalog projection — is
/// unchanged. First slice of the catalog-by-BackendID refactor flagged in
/// docs/GROK_ADAPTER.md §"Known design debt".
public enum EffortLadder {
/// Ordinary effort levels, ordered lowest → highest. A given model supports a *prefix*
/// of these (see `efforts(for:codexPro:)`).
public static let efforts: [String] = ["low", "medium", "high", "xhigh", "max"]
/// Codex's wire value for the Pro-only GPT-5.6 Sol mode (displayed "Pro").
public static let proEffort = "ultra"
/// The single "Auto" reasoning mode used by the ACP wrapper agents: the agent picks its
/// own thinking depth, so the only API-level choice is `auto` (cosmetic — never sent).
public static let autoEfforts: [String] = ["auto"]
/// Effort when nothing else is stored or supported (mirrored by
/// `ModelCatalog.fallbackEffort`).
public static let fallbackEffort = "high"
/// The backends whose agents expose only the single "Auto" reasoning — the ACP wrapper
/// agents, which don't take a `reasoning_effort`-style flag. Individual SKUs on those
/// backends can still opt into a graded ladder (see `gradedReasoningModels`).
public static let autoReasoningBackends: Set<BackendID> = [
.opencode, .openclaw, .hermes, .cursorAgent, .acp,
]
/// SKUs that expose a graded reasoning ladder. Both Grok entries use the first-class
/// headless backend, which passes the selected level through `--reasoning-effort`.
public static let gradedReasoningModels: Set<String> = ["grok-4.5", "grok-build"]
/// The effort levels `sku` actually supports for this account. ACP wrappers expose only
/// "Auto" (except `gradedReasoningModels`); GPT-5.6 models support "max"; a ChatGPT Pro
/// account additionally gets Sol's `ultra` wire mode; older Codex models top out at
/// "xhigh"; Grok 4.5 tops out at "high".
public static func efforts(for sku: String, codexPro: Bool = false) -> [String] {
if !gradedReasoningModels.contains(sku), let backend = BackendID.forModel(sku),
autoReasoningBackends.contains(backend)
{
return autoEfforts
}
if sku == "gpt-5.6-sol", codexPro {
return efforts + [proEffort]
}
guard let cap = effortCap(for: sku), let idx = efforts.firstIndex(of: cap) else {
return efforts
}
return Array(efforts[...idx])
}
/// Highest effort `sku` supports, or `nil` for the full range.
private static func effortCap(for sku: String) -> String? {
if sku.hasPrefix("gpt-5.6-") { return "max" }
// Grok 4.5's ladder stops at "high" — the ceiling the routing matrix uses for the
// lane, and past which xAI exposes no deeper reasoning mode.
if BackendID.forModel(sku) == .grok { return "high" }
return BackendID.forModel(sku) == .codex ? "xhigh" : nil
}
/// Clamp `effort` to what `sku` supports, so switching to a model with a lower cap can't
/// leave an unsupported level selected. Orchestra is an orchestration mode, not an API
/// level — valid for every model (it resolves to `xhigh` on the host), so it's preserved.
public static func clampedEffort(_ effort: String, for sku: String, codexPro: Bool = false) -> String {
if OrchestrationMode.isOrchestra(effort) { return effort }
let supported = efforts(for: sku, codexPro: codexPro)
return supported.contains(effort) ? effort : (supported.last ?? fallbackEffort)
}
}
/// Intelligence-routing provenance carried on a `Session` — pure transparency for the
/// "why this model" UI. It is created with a routed chat and rewritten when the in-session
/// Intelligence slider changes level; a deliberate manual model pick clears it.
public struct RoutingNote: Sendable, Equatable {
public var purpose: PromptPurpose
public var level: IntelligenceLevel
public var reason: String
public init(purpose: PromptPurpose, level: IntelligenceLevel, reason: String) {
self.purpose = purpose
self.level = level
self.reason = reason
}
public init(_ resolution: IntelligenceRouter.Resolution) {
self.init(
purpose: resolution.purpose, level: resolution.level, reason: resolution.reason)
}
}
/// Purpose × level → concrete (model, effort). The routing knowledge is a hand-encoded
/// matrix grounded in July-2026 model research (see the plan's research table and the
/// per-cell comments): Opus 5 carries the heavy Claude lane and is allowed past `high` only
/// at the top of the two rows that reward it (`debugging`/`review` at Max); GPT-5.6 Sol owns
/// the backend lane at high+; Fable 5 is reserved for the two long-horizon Max cells
/// (`planning`, `refactor`); Grok 4.5 is the tertiary lane everywhere and the *preferred* one
/// on the two cheapest quick-fix cells; Luna only where a light model can't hurt. Every cell
/// carries one candidate per lane, so a single-provider user resolves inside its own row.
///
/// **Competence and the swap decision.** Each candidate carries a `competence` score for its
/// cell, and the router uses it for one thing: deciding whether moving off the preferred
/// candidate is *free*. Rank still says what the cell wants; competence says what a swap
/// would cost. Within `marginalCompetenceBand` points the two are interchangeable, and the
/// choice falls to remaining quota and price — so a hot provider sheds load onto a cool one,
/// and an equivalent answer isn't bought a cost tier too high. Outside the band the preferred
/// candidate stands however lopsided the quota picture is: that is the "significant
/// difference" case, where switching would be a downgrade rather than a rebalance.
///
/// `IntelligenceRoutingTests` enforces the invariants (tier monotonicity, per-model caps,
/// valid efforts for every connected set, and that no route lands outside the band).
public enum IntelligenceRouter {
/// One ranked routing option: a SKU, its effort, how capable that pair is on this kind of
/// work, and whether a ChatGPT Pro account upgrades the effort to Sol's `ultra` mode at
/// Max level.
public struct Candidate: Sendable, Equatable {
public var sku: String
public var effort: String
/// How capable this (model, effort) pair is on this cell's kind of work, 0100.
/// **Comparable only within a cell**: it is the research table's read on this task,
/// not a global model ranking, so the same SKU carries different numbers in different
/// rows (Sonnet 5 is 72 on a Quick review and 82 on Light planning). The router uses
/// only *differences between candidates in one cell* — see `marginalCompetenceBand`.
public var competence: Int
public var ultraEligible: Bool
public init(
_ sku: String, _ effort: String, competence: Int, ultraEligible: Bool = false
) {
self.sku = sku
self.effort = effort
self.competence = competence
self.ultraEligible = ultraEligible
}
}
/// How far apart two candidates in the same cell may be before swapping between them
/// stops being free. Within this many competence points the router treats them as
/// interchangeable and is allowed to pick on quota headroom and cost instead; beyond it
/// the matrix's preferred candidate stands no matter how lopsided the quota picture is.
///
/// The band is symmetric on purpose. A *weaker* alternative is excluded because the task
/// would suffer; a much *stronger* one is excluded because the cell already decided that
/// extra capability isn't what this level buys — routing Light quick-fix work to Opus is
/// as much a misroute as routing Max debugging to Luna, just in the expensive direction.
public static let marginalCompetenceBand = 4
/// Capacity assumed for a provider that publishes no quota telemetry (Grok and the ACP
/// wrappers). Deliberately the neutral midpoint: crediting them full headroom would let
/// an unmetered lane win every in-band contest purely for being unmeasured.
static let unmeteredCapacity = 0.5
/// Live quota posture applied on top of the purpose ranking. Reached provider-wide
/// windows remove a whole lane; Claude model-family windows remove only that family so
/// another Claude model can still be the nearest usable match. Sub-limit utilization is
/// retained too: the router uses remaining capacity to break close performance calls
/// before a window is fully exhausted.
public struct Limits: Sendable, Equatable {
/// When a quota window rolls over, paired with how long the window is. The length is
/// what makes "soon" meaningful: thirty minutes out is imminent on a 5-hour session
/// window and irrelevant on a weekly one, so proximity is always judged as a fraction
/// of the window rather than in absolute time.
public struct ResetHorizon: Sendable, Equatable {
public var resetsAt: Date
public var windowLength: TimeInterval
public init(resetsAt: Date, windowLength: TimeInterval) {
self.resetsAt = resetsAt
self.windowLength = windowLength
}
}
public var providers: Set<BackendID>
public var models: Set<String>
/// Percent consumed in each provider-wide quota window, normalized to 0...100.
public var providerUtilization: [BackendID: Double]
/// Percent consumed in a model-family quota window, normalized to 0...100.
public var modelUtilization: [String: Double]
/// When each provider-wide window rolls over, keyed like `providerUtilization`. When a
/// provider has several windows this tracks whichever one is currently *binding* — the
/// one whose percentage set `providerUtilization`, and therefore the one whose reset
/// would actually free the lane.
public var providerResets: [BackendID: ResetHorizon]
/// When each model-family window rolls over, keyed like `modelUtilization`.
public var modelResets: [String: ResetHorizon]
/// Relative plan size per lane, in Claude-Pro units (`SubscriptionPlan.weight`). Lanes
/// absent here are compared neutrally — a missing plan never costs a lane anything.
public var planWeights: [BackendID: Double]
/// When this snapshot was taken. Nil means reset proximity is unknown, so no reset
/// credit is applied and capacity reads exactly as the raw percentages do.
public var asOf: Date?
public init(
providers: Set<BackendID> = [],
models: Set<String> = [],
providerUtilization: [BackendID: Double] = [:],
modelUtilization: [String: Double] = [:],
providerResets: [BackendID: ResetHorizon] = [:],
modelResets: [String: ResetHorizon] = [:],
planWeights: [BackendID: Double] = [:],
asOf: Date? = nil
) {
self.providers = providers
self.models = models
self.providerUtilization = providerUtilization.mapValues(Self.clampedUtilization)
self.modelUtilization = modelUtilization.mapValues(Self.clampedUtilization)
self.providerResets = providerResets
self.modelResets = modelResets
self.planWeights = planWeights
self.asOf = asOf
}
public init(
claudeUsage: SubscriptionUsage?, codexUsage: CodexUsage?,
grokUsage: GrokUsage? = nil,
claudeRateLimit: RateLimit?, now: Date,
claudePlan: SubscriptionPlan? = nil, codexPlan: SubscriptionPlan? = nil
) {
var providers = Set<BackendID>()
var models = Set<String>()
var providerUtilization = [BackendID: Double]()
var modelUtilization = [String: Double]()
var providerResets = [BackendID: ResetHorizon]()
var modelResets = [String: ResetHorizon]()
let claudeWindows = [
claudeUsage?.fiveHour, claudeUsage?.sevenDay,
claudeUsage?.sevenDayFable,
claudeUsage?.sevenDayOpus, claudeUsage?.sevenDaySonnet,
]
let hasPreciseClaudeUsage = claudeWindows.contains { $0 != nil }
func utilization(_ window: UsageWindow?) -> Double? {
window.map { Self.clampedUtilization($0.utilization(at: now)) }
}
func reached(_ window: UsageWindow?) -> Bool {
utilization(window).map { $0 >= 100 } ?? false
}
func horizon(_ window: UsageWindow?, length: TimeInterval) -> ResetHorizon? {
window?.resetsAt.map { ResetHorizon(resetsAt: $0, windowLength: length) }
}
// Claude reports two provider-wide windows. The more-consumed one is what's
// actually constraining the lane, so its percentage *and* its reset are the pair
// that describes the lane's posture — crediting the weekly window's distant reset
// against a nearly-full session window would read the situation backwards.
let claudeProviderWindows: [(Double, ResetHorizon?)] = [
utilization(claudeUsage?.fiveHour)
.map { ($0, horizon(claudeUsage?.fiveHour, length: Self.fiveHourWindow)) },
utilization(claudeUsage?.sevenDay)
.map { ($0, horizon(claudeUsage?.sevenDay, length: Self.weeklyWindow)) },
].compactMap { $0 }
if let binding = claudeProviderWindows.max(by: { $0.0 < $1.0 }) {
providerUtilization[.claudeCode] = binding.0
providerResets[.claudeCode] = binding.1
}
if let weekly = codexUsage?.weekly {
providerUtilization[.codex] = Self.clampedUtilization(weekly.utilization(at: now))
if let resetsAt = weekly.resetsAt {
providerResets[.codex] = ResetHorizon(
resetsAt: resetsAt,
windowLength: weekly.windowMinutes.map { TimeInterval($0) * 60 }
?? Self.weeklyWindow)
}
}
// Grok's allowance is a single credit pool per billing period; its peak (included
// credits vs. the on-demand cap) is the provider-wide number, same as Codex's weekly.
if let grokUsage {
providerUtilization[.grok] = Self.clampedUtilization(max(
grokUsage.utilization(at: now), grokUsage.onDemandUtilization(at: now) ?? 0))
if let resetsAt = grokUsage.resetsAt {
providerResets[.grok] = ResetHorizon(
resetsAt: resetsAt,
windowLength: grokUsage.cycle == "weekly"
? Self.weeklyWindow : Self.monthlyWindow)
}
}
if reached(claudeUsage?.fiveHour) || reached(claudeUsage?.sevenDay) {
providers.insert(.claudeCode)
} else if !hasPreciseClaudeUsage,
claudeRateLimit?.status == "rejected",
claudeRateLimit?.resetsAt.map({ now < $0 }) ?? true {
// The stream event is a coarse Claude-only fallback when the richer OAuth
// snapshot is unavailable.
providers.insert(.claudeCode)
}
if reached(claudeUsage?.sevenDayOpus) {
models.formUnion(IntelligenceRouter.routedModels.filter { $0.hasPrefix("claude-opus") })
}
if reached(claudeUsage?.sevenDayFable) {
models.formUnion(IntelligenceRouter.routedModels.filter { $0.hasPrefix("claude-fable") })
}
if reached(claudeUsage?.sevenDaySonnet) {
models.formUnion(IntelligenceRouter.routedModels.filter { $0.hasPrefix("claude-sonnet") })
}
// Each model-family sub-limit is a weekly window; carry its own reset so a family
// that refreshes tonight isn't treated like one that refreshes on Sunday.
for (prefix, window) in [
("claude-opus", claudeUsage?.sevenDayOpus),
("claude-fable", claudeUsage?.sevenDayFable),
("claude-sonnet", claudeUsage?.sevenDaySonnet),
] {
guard let familyUsage = utilization(window) else { continue }
let familyHorizon = horizon(window, length: Self.weeklyWindow)
for sku in IntelligenceRouter.routedModels where sku.hasPrefix(prefix) {
modelUtilization[sku] = familyUsage
modelResets[sku] = familyHorizon
}
}
if providerUtilization[.codex] ?? 0 >= 100 {
providers.insert(.codex)
}
if providerUtilization[.grok] ?? 0 >= 100 {
providers.insert(.grok)
}
var planWeights = [BackendID: Double]()
planWeights[.claudeCode] = claudePlan?.weight
planWeights[.codex] = codexPlan?.weight
self.init(
providers: providers,
models: models,
providerUtilization: providerUtilization,
modelUtilization: modelUtilization,
providerResets: providerResets,
modelResets: modelResets,
planWeights: planWeights,
asOf: now)
}
public func contains(model sku: String) -> Bool {
if models.contains(sku) || modelUtilization[sku] ?? 0 >= 100 { return true }
guard let backend = BackendID.forModel(sku) else { return false }
let normalized = backend == .codexExec ? BackendID.codex : backend
return providers.contains(normalized)
|| providerUtilization[normalized] ?? 0 >= 100
}
/// Remaining quota capacity for a model, from 0 (exhausted) to 1 (unused). The most
/// constrained applicable window wins. Nil means that provider reports no quantitative
/// quota, which the score treats neutrally rather than guessing that it is full or empty.
public func availableCapacity(forModel sku: String) -> Double? {
guard let backend = BackendID.forModel(sku) else { return nil }
let normalized = backend == .codexExec ? BackendID.codex : backend
let utilization = [
providerUtilization[normalized],
modelUtilization[sku],
].compactMap { $0 }.max()
return utilization.map { (100 - Self.clampedUtilization($0)) / 100 }
}
// MARK: Reset- and plan-aware capacity
static let fiveHourWindow: TimeInterval = 5 * 3600
static let weeklyWindow: TimeInterval = 7 * 86_400
static let monthlyWindow: TimeInterval = 30 * 86_400
/// How close to its reset a window has to be before its remaining capacity is credited,
/// as a fraction of the window's own length. At 0.10 the credit starts about 30 minutes
/// out on a 5-hour session window and about 17 hours out on a weekly one — the same
/// "nearly over" in both cases, which is the point of scaling by the window.
static let resetHorizonFraction = 0.10
/// How strongly plan size bends the capacity comparison. At 1.0 the comparison would be
/// pure absolute compute, which ignores that the smaller plan gets locked out sooner; at
/// 0 plans wouldn't matter at all and a percentage on a Pro plan would count for as much
/// as the same percentage on a Max 20x. The square root sits between the two: a Claude
/// Max 20x lane (weight 6) keeps the edge over a fresh ChatGPT Plus lane (weight 2)
/// until it's about 42% consumed — where a linear weighting would hold it to ~67% and
/// no weighting at all would give it up the moment the first token was spent.
static let planWeightExponent = 0.5
/// The three readings of a lane's headroom, kept separable so the routing note can say
/// *which* of them decided a swap.
struct CapacityBreakdown: Sendable, Equatable {
/// Percent remaining, exactly as the gauges show it.
var raw: Double
/// `raw` after crediting an imminent reset.
var credited: Double
/// `credited` scaled by how large this lane's plan is relative to the others.
var effective: Double
}
/// Remaining headroom for a model, read three ways. Unlike ``availableCapacity(forModel:)``
/// — which stays a plain "percent left" for the gauges and for callers that want the
/// literal number — this is what the router *decides* on, because a percentage alone
/// answers neither "will this refill in a moment?" nor "a percentage of how much?".
///
/// Each applicable window is credited for its own reset before the most-constrained one
/// wins, so a session window minutes from rolling over stops masquerading as scarcity
/// while a genuinely tight weekly window still binds.
func capacityBreakdown(forModel sku: String) -> CapacityBreakdown {
// No telemetry at all: neutral, and never scaled by a plan we can't observe.
let neutral = IntelligenceRouter.unmeteredCapacity
let unmetered = CapacityBreakdown(
raw: neutral, credited: neutral, effective: neutral)
guard let backend = BackendID.forModel(sku) else { return unmetered }
let normalized = backend == .codexExec ? BackendID.codex : backend
let windows: [(Double, ResetHorizon?)] = [
providerUtilization[normalized].map { ($0, providerResets[normalized]) },
modelUtilization[sku].map { ($0, modelResets[sku]) },
].compactMap { $0 }
guard !windows.isEmpty else { return unmetered }
let remaining = windows.map { ((100 - Self.clampedUtilization($0.0)) / 100, $0.1) }
let raw = remaining.map(\.0).min() ?? neutral
let credited = remaining.map { crediting($0.0, for: $0.1) }.min() ?? raw
return CapacityBreakdown(
raw: raw, credited: credited, effective: credited * planFactor(for: normalized))
}
/// The number the balance score uses.
func effectiveCapacity(forModel sku: String) -> Double {
capacityBreakdown(forModel: sku).effective
}
/// Credit a window for an imminent reset: capacity it is about to regain anyway is
/// capacity it effectively has. The credit eases in with the same smoothstep the
/// Intelligence rail's detents use, so nothing jumps as a reset comes into view, and it
/// reaches full only at the reset itself.
private func crediting(_ remaining: Double, for horizon: ResetHorizon?) -> Double {
guard let horizon, let asOf else { return remaining }
let scale = horizon.windowLength * Self.resetHorizonFraction
guard scale > 0 else { return remaining }
let proximity = min(max(1 - horizon.resetsAt.timeIntervalSince(asOf) / scale, 0), 1)
let eased = proximity * proximity * (3 - 2 * proximity)
return remaining + (1 - remaining) * eased
}
/// How this lane's plan compares to the largest one we know about, softened by
/// `planWeightExponent`. A lane with no known plan is placed at the geometric mean of
/// the known ones — genuinely in the middle, rather than at the top (which is what
/// treating it as 1.0 would do) or the bottom.
///
/// With fewer than two known plans there is nothing to compare, so this is the identity
/// and routing behaves exactly as it did before plans were observed.
private func planFactor(for backend: BackendID) -> Double {
let known = planWeights.values.filter { $0 > 0 }
guard known.count > 1, let reference = known.max(), reference > 0 else { return 1 }
let weight = planWeights[backend]
?? exp(known.map(log).reduce(0, +) / Double(known.count))
return min(1, max(0, pow(weight / reference, Self.planWeightExponent)))
}
/// When the binding window for `sku` next frees capacity, if known — used to tell the
/// user how long a hard exclusion will actually last.
func nextReset(forModel sku: String) -> Date? {
guard let backend = BackendID.forModel(sku) else { return nil }
let normalized = backend == .codexExec ? BackendID.codex : backend
return [providerResets[normalized], modelResets[sku]]
.compactMap { $0?.resetsAt }
.min()
}
private static func clampedUtilization(_ utilization: Double) -> Double {
min(100, max(0, utilization))
}
}
/// What the router decided and why — the composer passes `model`/`effort` straight into
/// the existing start path (concrete strings; downstream plumbing unchanged), and the
/// resolved chip + session routing note surface `reason`.
public struct Resolution: Sendable, Equatable {
public var model: String
public var effort: String
public var purpose: PromptPurpose
public var level: IntelligenceLevel
public var reason: String
public var isAvailable: Bool
/// The chosen candidate's `Candidate.competence`, or nil when the route fell through
/// to the app default (which isn't a matrix candidate and has no cell to score in).
public var competence: Int?
public init(
model: String, effort: String, purpose: PromptPurpose,
level: IntelligenceLevel, reason: String, isAvailable: Bool = true,
competence: Int? = nil
) {
self.model = model
self.effort = effort
self.purpose = purpose
self.level = level
self.reason = reason
self.isAvailable = isAvailable
self.competence = competence
}
}
/// Cost tiers for the "a misroute can never cross more than one tier" test invariant, and
/// for the cost half of the in-band cost/benefit call: T0 haiku/luna · T1 terra/sonnet/
/// grok-4.5 · T2 opus-5/sol · T3 fable. Unknown SKUs rank T2 so a future addition fails
/// toward caution in the tests, not silently into the cheap bin.
public static func costTier(of sku: String) -> Int {
if sku.hasPrefix("claude-haiku") || sku == "gpt-5.6-luna" { return 0 }
if sku.hasPrefix("claude-sonnet") || sku == "gpt-5.6-terra" || sku.hasPrefix("grok-") {
return 1
}
if sku.hasPrefix("claude-fable") { return 3 }
return 2
}
/// The routing matrix. Per cell: ranked candidates, preferred lane first, each carrying
/// the `competence` this (model, effort) pair shows on *this* kind of work. Claude lane
/// uses sonnet-5/opus-5/fable-5/haiku-4-5; GPT lane uses luna/terra/sol; the xAI lane is
/// grok-4.5, ranked last as the tertiary fallback (SWE-Bench Pro ceiling and hallucination
/// risk) except on quick fixes, where it leads. Research notes inline where a cell is
/// deliberately *not* the obvious escalation.
///
/// Rank encodes purpose fit; competence encodes raw capability, and the two deliberately
/// disagree in places (Quick-fix Light prefers Grok at 70 over Sonnet at 82 — the cell is
/// buying pass-at-one and token efficiency, not headroom). The router never reorders a
/// cell by competence; it only uses competence to decide whether a *swap* is free.
static let matrix: [PromptPurpose: [IntelligenceLevel: [Candidate]]] = [
.planning: [
.quick: [.init("gpt-5.6-terra", "medium", competence: 79), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "medium", competence: 80)],
.light: [.init("claude-sonnet-5", "medium", competence: 82), .init("gpt-5.6-terra", "high", competence: 83), .init("grok-4.5", "medium", competence: 80)],
.balanced: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.deep: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "xhigh", competence: 94), .init("grok-4.5", "high", competence: 83)],
// The preferred lane here is the long-horizon specialist; max effort is reserved for it.
// Grok serves as the final tertiary fallback due to its SWE-Bench Pro ceiling and hallucination risks.
.max: [.init("claude-fable-5", "max", competence: 98), .init("gpt-5.6-sol", "xhigh", competence: 94, ultraEligible: true), .init("grok-4.5", "high", competence: 83)],
],
.backendImpl: [
.quick: [.init("gpt-5.6-luna", "low", competence: 62), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "low", competence: 70)],
.light: [.init("gpt-5.6-terra", "medium", competence: 79), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "medium", competence: 80)],
// Lane preference flips from Balanced up: the preferred model rewards high+ effort on
// implementation, where the secondary would already be past its medium-effort peak.
.balanced: [.init("gpt-5.6-sol", "high", competence: 91), .init("claude-opus-5", "medium", competence: 88), .init("grok-4.5", "high", competence: 83)],
.deep: [.init("gpt-5.6-sol", "xhigh", competence: 94), .init("claude-opus-5", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.max: [.init("gpt-5.6-sol", "xhigh", competence: 94, ultraEligible: true), .init("claude-opus-5", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
],
.frontendImpl: [
.quick: [.init("gpt-5.6-luna", "low", competence: 62), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "low", competence: 70)],
.light: [.init("gpt-5.6-terra", "medium", competence: 79), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "medium", competence: 80)],
// The preferred model is the consensus UI pick — at medium, its measured peak for routine work.
.balanced: [.init("claude-sonnet-5", "medium", competence: 82), .init("gpt-5.6-terra", "high", competence: 83), .init("grok-4.5", "medium", competence: 80)],
.deep: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.max: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "xhigh", competence: 94), .init("grok-4.5", "high", competence: 83)],
],
.quickFix: [
// Grok 4.5 leads on pass-at-one (SWE Marathon) for quick fixes and brings unbeatable token efficiency and TPS.
.quick: [.init("grok-4.5", "low", competence: 70), .init("gpt-5.6-luna", "low", competence: 62), .init("claude-sonnet-5", "low", competence: 72)],
.light: [.init("grok-4.5", "low", competence: 70), .init("gpt-5.6-terra", "low", competence: 75), .init("claude-sonnet-5", "medium", competence: 82)],
.balanced: [.init("gpt-5.6-terra", "medium", competence: 79), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "medium", competence: 80)],
// Deep tops out at the preferred model's measured peak effort — a "deep" quick fix wants
// care, not scope; and it keeps this row within one cost tier of the heavy rows
// a borderline classification could have landed on (the drift invariant).
.deep: [.init("claude-sonnet-5", "medium", competence: 82), .init("gpt-5.6-terra", "high", competence: 83), .init("grok-4.5", "medium", competence: 80)],
// Even "Max" on a quick fix stops at that same peak — more model would only invite
// out-of-scope refactors (the measured past-peak failure mode).
.max: [.init("claude-sonnet-5", "medium", competence: 82), .init("gpt-5.6-terra", "high", competence: 83), .init("grok-4.5", "medium", competence: 80)],
],
.refactor: [
.quick: [.init("gpt-5.6-terra", "low", competence: 75), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "low", competence: 70)],
.light: [.init("claude-sonnet-5", "medium", competence: 82), .init("gpt-5.6-terra", "medium", competence: 79), .init("grok-4.5", "medium", competence: 80)],
.balanced: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.deep: [.init("claude-opus-5", "medium", competence: 88), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.max: [.init("claude-fable-5", "high", competence: 91), .init("gpt-5.6-sol", "xhigh", competence: 94), .init("grok-4.5", "high", competence: 83)],
],
.debugging: [
.quick: [.init("gpt-5.6-terra", "medium", competence: 79), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "medium", competence: 80)],
.light: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.balanced: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.deep: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "xhigh", competence: 94), .init("grok-4.5", "high", competence: 83)],
// Root-causing gnarly failures is the other lane that genuinely rewards the specialist at max.
.max: [.init("claude-opus-5", "max", competence: 96), .init("gpt-5.6-sol", "xhigh", competence: 94, ultraEligible: true), .init("grok-4.5", "high", competence: 83)],
],
.review: [
.quick: [.init("gpt-5.6-luna", "low", competence: 62), .init("claude-sonnet-5", "low", competence: 72), .init("grok-4.5", "low", competence: 70)],
.light: [.init("gpt-5.6-terra", "low", competence: 75), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "low", competence: 70)],
.balanced: [.init("gpt-5.6-sol", "medium", competence: 88), .init("claude-opus-5", "high", competence: 91), .init("grok-4.5", "medium", competence: 80)],
.deep: [.init("gpt-5.6-sol", "high", competence: 91), .init("claude-opus-5", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.max: [.init("gpt-5.6-sol", "high", competence: 91), .init("claude-opus-5", "xhigh", competence: 94), .init("grok-4.5", "high", competence: 83)],
],
.writing: [
.quick: [.init("gpt-5.6-luna", "low", competence: 62), .init("claude-sonnet-5", "low", competence: 72), .init("grok-4.5", "low", competence: 70)],
.light: [.init("gpt-5.6-terra", "low", competence: 75), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "low", competence: 70)],
// Mid tier (not the bargain tier) from Balanced up: prose prompts routinely
// border on backend/docs mixes, so the row stays within one cost tier of the
// implementation rows a borderline classification could have meant (the drift
// invariant) — and serious writing genuinely reads better off the mid tier.
.balanced: [.init("claude-opus-5", "medium", competence: 88), .init("gpt-5.6-terra", "medium", competence: 79), .init("grok-4.5", "medium", competence: 80)],
.deep: [.init("claude-opus-5", "medium", competence: 88), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.max: [.init("claude-opus-5", "medium", competence: 88), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
],
// The conservative row every failed classification lands on: mid-tier at every stop,
// so "we couldn't tell" can never mean the specialist at max or a light model on a hard task.
.general: [
.quick: [.init("gpt-5.6-luna", "low", competence: 62), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "low", competence: 70)],
.light: [.init("gpt-5.6-terra", "medium", competence: 79), .init("claude-sonnet-5", "medium", competence: 82), .init("grok-4.5", "medium", competence: 80)],
.balanced: [.init("claude-sonnet-5", "medium", competence: 82), .init("gpt-5.6-terra", "high", competence: 83), .init("grok-4.5", "medium", competence: 80)],
.deep: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "high", competence: 91), .init("grok-4.5", "high", competence: 83)],
.max: [.init("claude-opus-5", "high", competence: 91), .init("gpt-5.6-sol", "xhigh", competence: 94), .init("grok-4.5", "high", competence: 83)],
],
]
/// Every SKU the matrix can produce. Kept derived from the matrix so quota-family
/// exclusions automatically cover new cells without a second catalog to maintain.
static var routedModels: Set<String> {
Set(matrix.values.flatMap { $0.values }.flatMap { $0 }.map(\.sku))
}
private static let routedBackendLanes = Set(routedModels.compactMap { sku -> BackendID? in
guard let backend = BackendID.forModel(sku) else { return nil }
return backend == .codexExec ? .codex : backend
})
/// Whether an open chat on `backend` can keep using the Intelligence rail. This is derived
/// from the routing matrix rather than the backend's general reasoning capability: Grok is
/// an auto-reasoning ACP backend for `grok-build`, but its `grok-4.5` lane is still present in
/// every matrix cell. `codexExec` shares Codex's model lane.
public static func supportsInSessionRouting(on backend: BackendID) -> Bool {
let normalized = backend == .codexExec ? BackendID.codex : backend
return routedBackendLanes.contains(normalized)
}
/// The requested cell is the primary ranking. If quota removes all of its usable
/// candidates, walk lower levels of the same purpose from nearest to cheapest before
/// considering higher levels. This preserves the user budget whenever possible while
/// still providing a same-purpose match for a single-provider account.
///
/// Each level stays its own group rather than being flattened, because the cost/benefit
/// swap below must only ever choose *within one cell*. Flattened, "prefer the cheaper
/// in-band candidate" would happily reach down into the Deep row's leftovers while
/// resolving Max and silently undo the slider — the tiers keep a fallback a fallback.
private static func candidateTiers(
purpose: PromptPurpose, level: IntelligenceLevel
) -> [[Candidate]] {
let row = matrix[purpose] ?? matrix[.general] ?? [:]
let lower = IntelligenceLevel.allCases
.filter { $0 <= level }
.sorted { $0.rawValue > $1.rawValue }
let higher = IntelligenceLevel.allCases
.filter { $0 > level }
.sorted { $0.rawValue < $1.rawValue }
var seen = Set<String>()
return (lower + higher)
.map { (row[$0] ?? []).filter { seen.insert($0.sku).inserted } }
.filter { !$0.isEmpty }
}
/// One-line research rationale per model family, composed into `Resolution.reason` so
/// the chip tooltip explains *why* this model, not just which.
static func rationale(for sku: String, effort: String) -> String {
if sku.hasPrefix("claude-fable") {
return "long-horizon specialist — worth its cost on genuinely complex work"
}
if sku.hasPrefix("claude-opus-5") {
return effort == "medium"
? "consensus quality pick at its measured peak effort"
: "near-flagship quality at half the flagship price"
}
if sku == "gpt-5.6-sol" { return "strongest implementation lane at high effort" }
if sku == "gpt-5.6-terra" { return "balanced mid-tier for scoped work" }
if sku == "gpt-5.6-luna" { return "fast lane for clear, contained tasks" }
if sku.hasPrefix("claude-sonnet") { return "balanced daily driver" }
if sku.hasPrefix("claude-haiku") { return "light model — plenty for prose and lookups" }
if sku.hasPrefix("grok-") {
return effort == "low"
? "leads pass-at-one on contained fixes, at the best tokens-per-second"
: "efficient third lane — keeps the other two providers' quota in reserve"
}
return "closest available match"
}
/// The candidates in `cell` that are interchangeable with its preferred (first) entry:
/// those within `marginalCompetenceBand` points of it, in either direction. Everything
/// outside the band is off the table — no amount of quota pressure or cost saving buys a
/// materially worse model, and no cheap-lane exhaustion promotes work to a materially
/// heavier one. The preferred candidate is always in its own band.
static func interchangeable(within cell: [Candidate]) -> [Candidate] {
guard let preferred = cell.first else { return [] }
return cell.filter {
abs($0.competence - preferred.competence) <= marginalCompetenceBand
}
}
/// The cost/benefit call *among candidates already judged interchangeable*. Because the
/// band has already established that quality is not in play, this scores the two things
/// that are: remaining quota (so a hot provider sheds load onto a cool one, and no lane is
/// driven to its limit while another idles) and cost tier (so an equivalent answer is not
/// bought at a higher price). The residual competence edge only breaks what those leave
/// tied — inside a 4-point band it is worth a fraction of a lopsided quota picture, which
/// is what makes this a rebalance rather than a re-ranking.
///
/// "Remaining quota" here is `Limits.effectiveCapacity`, not the raw percentage: a window
/// about to reset is not really scarce, and a percentage means nothing without the size of
/// the plan behind it. All three terms stay normalized 0...1 so the weights below mean
/// what they say.
private static func balanceScore(
for candidate: Candidate, preferred: Candidate, limits: Limits
) -> Double {
let capacity = limits.effectiveCapacity(forModel: candidate.sku)
let costSaving = 1 - (Double(costTier(of: candidate.sku)) / 3)
// Map the ±band window onto 0...1, so the preferred candidate sits at 0.5 and a
// candidate at either edge of the band sits at 0 or 1.
let span = Double(marginalCompetenceBand * 2)
let edge = min(
1,
max(
0,
(Double(candidate.competence - preferred.competence) + Double(marginalCompetenceBand))
/ span))
return (capacity * 0.50) + (costSaving * 0.30) + (edge * 0.20)
}
/// Pick the (model, effort) for `purpose` at `level`.
///
/// - `connected`: backends the user can actually run (`ProviderAvailability`).
/// - `pinned`: the preferred Settings provider. It remains strict while usable, but
/// is temporarily bypassed when quota blocks every candidate on that provider.
/// - `backendLock`: in-session routing. The fixed backend cannot be bypassed, but a
/// model-family limit may choose the nearest model on that same backend.
/// - `degraded`: active provider incidents are demoted, never eliminated.
/// - `limits`: reached provider or model-family quota windows are hard exclusions.
/// - `fallback`: the resolved app default used when routing has no catalog candidate.
public static func route(
purpose: PromptPurpose,
level: IntelligenceLevel,
connected: Set<BackendID>,
pinned: BackendID? = nil,
backendLock: BackendID? = nil,
degraded: Set<BackendID> = [],
limits: Limits = Limits(),
codexPro: Bool = false,
fallback: (model: String, effort: String)
) -> Resolution {
let tiers = candidateTiers(purpose: purpose, level: level)
let ranked = tiers.flatMap { $0 }
func lane(_ backend: BackendID) -> BackendID {
backend == .codexExec ? .codex : backend
}
func candidateLane(_ candidate: Candidate) -> BackendID? {
BackendID.forModel(candidate.sku).map(lane)
}
var reachable = Set(connected.map(lane))
if let lock = backendLock { reachable.insert(lane(lock)) }
let lockedLane = backendLock.map(lane)
func structurallyEligible(_ candidate: Candidate) -> Bool {
guard let backend = candidateLane(candidate), reachable.contains(backend) else { return false }
return lockedLane == nil || backend == lockedLane
}
// Whether the pin holds is a whole-route decision (it depends on there being *no*
// usable candidate on the pinned lane anywhere), so it is settled before walking the
// tiers; the per-tier filter below just applies the verdict.
let pinnedLane = pinned.map(lane)
let quotaEligible = ranked.filter {
structurallyEligible($0) && !limits.contains(model: $0.sku)
}
let limitedOnPin = ranked.contains {
structurallyEligible($0) && candidateLane($0) == pinnedLane
&& limits.contains(model: $0.sku)
}
// A pin expresses preference, not a request to launch against a provider known to be
// rejecting work. Restore it automatically after the reset.
let pinBypassedForLimit =
pinnedLane != nil && limitedOnPin && lockedLane == nil
&& !quotaEligible.contains { candidateLane($0) == pinnedLane }
let honorsPin = pinnedLane != nil && !pinBypassedForLimit
let degradedLanes = Set(degraded.map(lane))
// Walk the tiers (requested cell, then the fallback chain) and stop at the first one
// with a usable candidate. Everything after this point works inside that single cell,
// so a cost/quota swap can never quietly resolve out of a different level.
var demoted = false
var cell: [Candidate] = []
for tier in tiers {
var usable = tier.filter { structurallyEligible($0) && !limits.contains(model: $0.sku) }
if honorsPin { usable = usable.filter { candidateLane($0) == pinnedLane } }
guard !usable.isEmpty else { continue }
// Demote (never drop) degraded lanes. A healthy alternative in the same cell
// wins, but if every lane in it has an incident the purpose ranking stands —
// an incident is a reason to prefer a sibling, not to leave the level.
let healthy = usable.filter { candidate in
candidateLane(candidate).map { !degradedLanes.contains($0) } ?? false
}
if !healthy.isEmpty, healthy.count < usable.count {
demoted = true
usable = healthy
}
cell = usable
break
}
guard let preferred = cell.first else {
let clamped = EffortLadder.clampedEffort(
fallback.effort, for: fallback.model, codexPro: codexPro)
let quotaBlockedRoute = limits.contains(model: fallback.model) || ranked.contains {
structurallyEligible($0) && limits.contains(model: $0.sku)
}
// A usage limit is always temporary, so say how temporary: the soonest window to
// roll over among the blocked candidates is when routing starts working again.
var blocked = "every compatible model is at its usage limit"
let soonestReset = ranked
.filter { structurallyEligible($0) && limits.contains(model: $0.sku) }
.compactMap { limits.nextReset(forModel: $0.sku) }
.min()
if let asOf = limits.asOf, let soonestReset {
let wait = soonestReset.timeIntervalSince(asOf)
if wait > 0 { blocked += " (the first frees up in \(QuotaDuration.short(wait)))" }
}
let reason = quotaBlockedRoute
? "\(purpose.displayName) · \(level.displayName)\(blocked)"
: "\(purpose.displayName) · \(level.displayName) — no routed candidate on a connected provider, using your default"
return Resolution(
model: fallback.model, effort: clamped, purpose: purpose, level: level,
reason: reason, isAvailable: !quotaBlockedRoute)
}
// Only candidates the competence band calls interchangeable are up for the swap; the
// cost/benefit score then decides among them, preserving matrix order on exact ties.
let swappable = interchangeable(within: cell)
let pick =
swappable.enumerated().max { lhs, rhs in
let lhsScore = balanceScore(for: lhs.element, preferred: preferred, limits: limits)
let rhsScore = balanceScore(for: rhs.element, preferred: preferred, limits: limits)
if abs(lhsScore - rhsScore) > 0.000_001 { return lhsScore < rhsScore }
return lhs.offset > rhs.offset
}?.element ?? preferred
var effort = pick.effort
if pick.ultraEligible, codexPro, level == .max {
effort = EffortLadder.proEffort
}
effort = EffortLadder.clampedEffort(effort, for: pick.sku, codexPro: codexPro)
let selectedIndex = ranked.firstIndex(of: pick) ?? 0
let preferredWasLimited = ranked[..<selectedIndex].contains {
structurallyEligible($0) && limits.contains(model: $0.sku)
}
var reason = "\(purpose.displayName) · \(level.displayName)\(rationale(for: pick.sku, effort: effort))"
if pinBypassedForLimit {
reason += " (pinned provider is at its usage limit; using the nearest available provider)"
} else if preferredWasLimited {
reason += " (preferred route is at its usage limit; using the nearest available match)"
}
if pick != preferred {
let gap = abs(pick.competence - preferred.competence)
let pickCapacity = limits.capacityBreakdown(forModel: pick.sku)
let preferredCapacity = limits.capacityBreakdown(forModel: preferred.sku)
let why: String
if pickCapacity.effective > preferredCapacity.effective {
// Name the reading that actually flipped it, so the note explains the decision
// rather than restating that one lane had "more quota" when the percentages
// plainly say otherwise.
if pickCapacity.raw > preferredCapacity.raw {
why = "favored the provider with more available quota"
} else if pickCapacity.credited > preferredCapacity.credited {
why = "its quota window resets shortly"
} else {
why = "more absolute headroom on this plan"
}
} else if costTier(of: pick.sku) < costTier(of: preferred.sku) {
why = "the same result one cost tier cheaper"
} else {
why = "the stronger of the two on this task"
}
reason += " (within \(gap) competence pts of \(preferred.sku)\(why))"
}
if demoted { reason += " (preferred lane has a provider incident)" }
return Resolution(
model: pick.sku, effort: effort, purpose: purpose, level: level, reason: reason,
competence: pick.competence)
}
/// Route a prompt the deep classifier identifies as genuinely mixed-intent. The
/// primary purpose chooses the route, raised when necessary to the secondary's cost
/// tier at the same user-selected level.
public static func routeMixed(
primary: PromptPurpose,
secondary: PromptPurpose,
level: IntelligenceLevel,
connected: Set<BackendID>,
pinned: BackendID? = nil,
backendLock: BackendID? = nil,
degraded: Set<BackendID> = [],
limits: Limits = Limits(),
codexPro: Bool = false,
fallback: (model: String, effort: String)
) -> Resolution {
let primaryRoute = route(
purpose: primary, level: level, connected: connected, pinned: pinned,
backendLock: backendLock, degraded: degraded, limits: limits,
codexPro: codexPro, fallback: fallback)
guard primary != secondary else { return primaryRoute }
let secondaryRoute = route(
purpose: secondary, level: level, connected: connected, pinned: pinned,
backendLock: backendLock, degraded: degraded, limits: limits,
codexPro: codexPro, fallback: fallback)
guard primaryRoute.isAvailable, secondaryRoute.isAvailable else { return primaryRoute }
let secondaryFloor = costTier(of: secondaryRoute.model)
guard costTier(of: primaryRoute.model) < secondaryFloor else {
var result = primaryRoute
result.reason += " (mixed with \(secondary.displayName); primary route already meets its tier)"
return result
}
for candidateLevel in IntelligenceLevel.allCases where candidateLevel >= level {
var candidate = route(
purpose: primary, level: candidateLevel, connected: connected, pinned: pinned,
backendLock: backendLock, degraded: degraded, limits: limits,
codexPro: codexPro, fallback: fallback)
guard candidate.isAvailable, costTier(of: candidate.model) >= secondaryFloor else {
continue
}
// The slider remains the user's budget dial. Difficulty never changes it.
candidate.level = level
candidate.reason =
"\(primary.displayName) + \(secondary.displayName) · \(level.displayName) — "
+ "primary route raised to the secondary intent's cost tier; "
+ rationale(for: candidate.model, effort: candidate.effort)
return candidate
}
// Fail safe if a future matrix leaves the primary row without the required tier.
var result = secondaryRoute
result.purpose = primary
result.reason =
"\(primary.displayName) + \(secondary.displayName) · \(level.displayName) — "
+ "secondary route supplies the mixed-intent cost floor"
return result
}
}