923 lines
55 KiB
Swift
923 lines
55 KiB
Swift
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, 0–100.
|
||
/// **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
|
||
}
|
||
}
|