From 3a663121665830038c1a10fc87677878e0de1941 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Fri, 14 Aug 2026 22:23:20 +0000 Subject: [PATCH 1/8] feat: DeepSeek as a first-class provider pool Restructure the two-pool (OpenAI/Anthropic) assumptions into a provider registry so DeepSeek joins routing, usage, and account management as data, not code surgery. - providers.go: single registry table (pools, lanes, labels, colors, buckets, metered/required, special tiers); ordered fallback and advisor pool lists replace every otherPool() flip. - Catalog: __models__ grows a provider column (5/6/7-token rows all parse); the lane facet is catalog-driven, ds-led/ds-only lanes appear only when the catalog carries a D pool. - Generator: N-pool genCombo with pool-set validity, optional-pool ladder fill (one verified D rung is enough), relief tails on heavyweight led/mixed chains, vision purity exception, per-pool advisor tables; generate init scaffolds and bench-probes DeepSeek. - Usage: DeepSeek prepaid balance fetched concurrently with the broker snapshot, rendered as its own group (no bar/reset), cached by value only; api_key never serialized, account pool stays OAuth-only; manager shows display-only API-key rows. - omp catch-up: security-reviewer routed like reviewer everywhere; omp --version probe gates task.agentAdvisor emission (>= 17.3). Two-pool output stays byte-identical outside the __models__ provider column and the security-reviewer rows; goldens pin both. --- README.md | 9 +- docs/configuration.md | 4 +- generate.go | 255 +- generate_init.go | 42 +- generate_test.go | 234 +- main.go | 588 ++- main_test.go | 163 +- manager.go | 43 +- manager_test.go | 9 +- onboarding.go | 2 +- providers.go | 244 ++ selection_state.go | 20 +- suggest.go | 21 +- testdata/two-pool-golden.plain | 7335 ++++++++++++++++++++++++++++++++ vault.go | 146 +- vault_test.go | 122 +- 16 files changed, 8932 insertions(+), 305 deletions(-) create mode 100644 providers.go create mode 100644 testdata/two-pool-golden.plain diff --git a/README.md b/README.md index 2be816f..db17b0f 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,12 @@ Press `enter` and `code` launches oh-my-pi with that setup, as a one-shot overlay — your omp config is never modified. It's made for people who run oh-my-pi with **both Anthropic and OpenAI**: -the whole point is deciding, per task, how to blend the two pools and which -quota to spend. With a single provider you can still launch, but the dials -lose most of their meaning. +the whole point is deciding, per task, how to blend the pools and which +quota to spend. A DeepSeek API key adds a third, pay-as-you-go pool — its +own `ds` lanes, a live balance readout in Usage, and a relief tail at the +end of the heavyweight fallback chains for when the metered windows are +drained. With a single provider you can still launch, but the dials lose +most of their meaning. ## Usage diff --git a/docs/configuration.md b/docs/configuration.md index 7cf37c7..4987d56 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -104,9 +104,9 @@ Each entry under `models:`: | Field | Meaning | |---|---| | `id` | the model id omp routes to | -| `pool` | `O` (OpenAI/Codex) or `A` (Anthropic) | +| `pool` | `O` (OpenAI/Codex), `A` (Anthropic), or `D` (DeepSeek). `O` and `A` must fill tiers 1..3; `D` is optional — one verified model is enough, missing tiers borrow the nearest rung | | `tier` | `1` cheap · `2` regular · `3` smart — the per-pool fallback ladder. `0` (a fast idle-bucket model the `spark` toggle drains) and `4` (a scarce elite the `fable` toggle leads with) are optional | -| `bucket` | the quota window this model draws from (`claude-main`, `claude-fable`, `codex-main`, `codex-spark`). The TUI prefers it over guessing from the model family | +| `bucket` | the quota window this model draws from (`claude-main`, `claude-fable`, `codex-main`, `codex-spark`, `deepseek-main`). The TUI prefers it over guessing from the model family | | `cost_in` / `cost_out` | dollars per 1M tokens; drives the cost meter | | `speed` / `ttft` | output tok/s and seconds to first token; drives the speed meter. Measured by `init`'s probe — a single timed request each, so treat them as one sample rather than a stable benchmark | | `context` | context window, in tokens | diff --git a/generate.go b/generate.go index 8c5148a..ed8847a 100644 --- a/generate.go +++ b/generate.go @@ -8,9 +8,11 @@ package main // pkgs/omp-configured), generalised from that setup's hard-coded model keys to // pure pool/tier logic so it works against anyone's catalog: // -// - pools O (OpenAI/Codex) and A (Anthropic) must each fill tiers 1..3 — -// the per-pool fallback ladder (cheap, regular, smart). code assumes both -// providers are present; generation fails loudly otherwise. +// - required pools (see providerRegistry: O = OpenAI/Codex, A = Anthropic) +// must each fill tiers 1..3 — the per-pool fallback ladder (cheap, +// regular, smart); generation fails loudly otherwise. Optional pools +// (D = DeepSeek) participate when present: one verified rung is enough, +// missing tiers borrow the nearest existing rung. // - tier 0 (an idle-bucket speed model, "spark") and tier 4 (a scarce elite, // "fable") are optional; without them the corresponding facet combos are // simply not generated and the TUI hides the dial. @@ -110,15 +112,19 @@ func loadCatalogBytes(raw []byte, path string) (*catalog, error) { if probedNode == nil || probedNode.Value != "true" { return nil, fmt.Errorf("%s: missing `probed: true` — these models were never verified as callable by your account. Re-run `code generate init --refresh`, which probes every model, or set `probed: true` yourself once you have confirmed each one", path) } - c := &catalog{models: map[string]catModel{}, levels: map[string][]int{}, ladder: map[string][5]string{"O": {}, "A": {}}} + ladder := map[string][5]string{} + for _, p := range providerRegistry { + ladder[p.Pool] = [5]string{} + } + c := &catalog{models: map[string]catModel{}, levels: map[string][]int{}, ladder: ladder} for i := 0; i+1 < len(modelsNode.Content); i += 2 { key := modelsNode.Content[i].Value var m catModel if err := modelsNode.Content[i+1].Decode(&m); err != nil { return nil, fmt.Errorf("%s: model %q: %w", path, key, err) } - if m.Pool != "O" && m.Pool != "A" { - return nil, fmt.Errorf("%s: model %q: pool must be O or A, got %q", path, key, m.Pool) + if providerByPool(m.Pool) == nil { + return nil, fmt.Errorf("%s: model %q: pool must be one of %s, got %q", path, key, strings.Join(fallbackPoolOrder, ", "), m.Pool) } if m.Tier < 0 || m.Tier > 4 { return nil, fmt.Errorf("%s: model %q: tier must be 0..4, got %d", path, key, m.Tier) @@ -137,19 +143,78 @@ func loadCatalogBytes(raw []byte, path string) (*catalog, error) { l[m.Tier] = key c.ladder[m.Pool] = l } - for _, pool := range []string{"O", "A"} { + for _, pool := range fallbackPoolOrder { + prov := providerByPool(pool) + if !prov.Required { + continue + } for t := 1; t <= 3; t++ { if c.ladder[pool][t] == "" { - return nil, fmt.Errorf("%s: pool %s has no tier-%d model — code assumes both an OpenAI and an Anthropic pool with tiers 1..3 filled (cheap, regular, smart)", path, pool, t) + return nil, fmt.Errorf("%s: pool %s has no tier-%d model — code assumes %s with tiers 1..3 filled (cheap, regular, smart)", path, pool, t, requiredProviderNames()) } } } + c.fillOptionalLadders() if err := c.checkLadder(path); err != nil { return nil, err } return c, nil } +// requiredProviderNames names the pools generation cannot proceed without — +// the registry's Required providers, joined for error text. +func requiredProviderNames() string { + var names []string + for _, pool := range fallbackPoolOrder { + if p := providerByPool(pool); p != nil && p.Required { + names = append(names, p.Label) + } + } + return "both " + strings.Join(names, " and ") + " pools" +} + +// pools lists the catalog's usable pools in fallbackPoolOrder: a pool is +// present once tiers 1..3 are filled (fillOptionalLadders completes partial +// optional pools first, so one verified rung is enough to participate). +func (c *catalog) pools() []string { + var out []string + for _, pool := range fallbackPoolOrder { + l := c.ladder[pool] + if l[1] != "" && l[2] != "" && l[3] != "" { + out = append(out, pool) + } + } + return out +} + +// fillOptionalLadders completes a non-Required pool that brought at least one +// ladder rung but not all three: each missing tier borrows the nearest +// existing rung, preferring the lower tier index. Chain dedupe absorbs the +// duplicates, so a one-model pool still yields working lanes. +func (c *catalog) fillOptionalLadders() { + for _, p := range providerRegistry { + if p.Required { + continue + } + l := c.ladder[p.Pool] + if l[1] == "" && l[2] == "" && l[3] == "" { + continue + } + for t := 1; t <= 3; t++ { + if l[t] != "" { + continue + } + for _, s := range []int{t - 1, t + 1, t - 2, t + 2} { + if s >= 1 && s <= 3 && l[s] != "" { + l[t] = l[s] + break + } + } + } + c.ladder[p.Pool] = l + } +} + // checkLadder rejects a ladder whose rungs are out of order. Input price used // to be a fair proxy for capability, so the scaffolder ranked by it — then // providers started repricing new models below predecessors they never @@ -159,7 +224,7 @@ func loadCatalogBytes(raw []byte, path string) (*catalog, error) { // less. Only tiers 1..3 are the capability ladder — tier 0 is a bucket-drain // lead and tier 4 an elite lead, both deliberately off it. func (c *catalog) checkLadder(path string) error { - for _, pool := range []string{"O", "A"} { + for _, pool := range fallbackPoolOrder { for lo := 1; lo <= 3; lo++ { for hi := lo + 1; hi <= 3; hi++ { a, b := c.ladder[pool][lo], c.ladder[pool][hi] @@ -244,11 +309,54 @@ func (c *catalog) clampTh(key, level string) string { return thScale[best] } -func otherPool(p string) string { - if p == "O" { - return "A" +// crossPool is the first non-lead pool the catalog can cross to, in +// fallbackPoolOrder — the generalisation of the old binary O↔A flip. +func (c *catalog) crossPool(pool string) string { + for _, p := range c.pools() { + if p != pool { + return p + } } - return "O" + return "" +} + +// specialKey resolves a special-tier facet ("spark", "fable") to its ladder +// rung — "" when the owning pool never filled that tier. +func (c *catalog) specialKey(facet string) string { + p := providerBySpecial(facet) + if p == nil { + return "" + } + return c.ladder[p.Pool][p.special(facet).Tier] +} + +// advisorPool picks the advisor's context pool: the first advisorPoolOrder +// entry present in the catalog that is not the lead pool. +func (c *catalog) advisorPool(lead string) string { + present := map[string]bool{} + for _, p := range c.pools() { + present[p] = true + } + for _, p := range advisorPoolOrder { + if p != lead && present[p] { + return p + } + } + return lead +} + +// reliefRungs lists each optional pool's regular (tier-2) rung, in +// fallbackPoolOrder, skipping the lead's own pool. +func (c *catalog) reliefRungs(lead string) []string { + leadPool := c.models[lead].Pool + var out []string + for _, pool := range c.pools() { + if pool == leadPool || providerByPool(pool).Required { + continue + } + out = append(out, c.ladder[pool][2]) + } + return out } // sibDown is the same-pool fallback rung below a lead: the elite (tier 4) @@ -265,14 +373,18 @@ func (c *catalog) sibDown(key string) string { return "" } -// cross is the equivalent rung on the opposite pool (elites cross to smart). +// cross is the equivalent rung on the first other pool (elites cross to smart). func (c *catalog) cross(key string) string { m := c.models[key] t := m.Tier if t > 3 { t = 3 } - return c.ladder[otherPool(m.Pool)][t] + cp := c.crossPool(m.Pool) + if cp == "" { + return "" + } + return c.ladder[cp][t] } func dedup(seq []string, lead string) []string { @@ -325,25 +437,39 @@ func (c *catalog) visionLead(pool string, tier int) string { return "" } +// visionCross finds an image-capable rung on any other catalog pool, in +// fallbackPoolOrder — vision correctness beats lane purity, so even a pure +// lane crosses when its own pool is text-only at every rung. +func (c *catalog) visionCross(pool string, tier int) string { + for _, p := range c.pools() { + if p == pool { + continue + } + if k := c.visionLead(p, tier); k != "" { + return k + } + } + return "" +} + // ── the facet grid ──────────────────────────────────────────────────────────── var ( genRoleOrder = []string{"default", "task", "plan", "slow", "designer", "reviewer", - "librarian", "scout", "sonic", "advisor", "vision", "smol", "tiny", "commit"} + "security-reviewer", "librarian", "scout", "sonic", "advisor", "vision", "smol", "tiny", "commit"} // The bundled agents this grid routes: every ●-marked role is mirrored - // into task.agentModelOverrides. omp bundles a seventh since 17.2.1, - // security-reviewer, deliberately unrouted: security scans inject the - // scan's own model into task.agentModelOverrides per session, so a - // generated route would only skew ad-hoc spawns. scout was the one this - // grid never routed, so it silently inherited @smol and never appeared - // in the preview. + // into task.agentModelOverrides. security-reviewer gets reviewer's exact + // routing membership; note `omp security` still injects the scan's own + // model into task.agentModelOverrides at runtime, superseding this value + // inside that workflow — the catalog route covers ad-hoc spawns. genAgentRoles = map[string]bool{"designer": true, "librarian": true, "reviewer": true, - "scout": true, "sonic": true, "task": true} - genDelib = map[string]bool{"plan": true, "slow": true, "designer": true, "reviewer": true} - // Anti-tunnel-vision: on a *-led lane the reviewer crosses to the opposite + "security-reviewer": true, "scout": true, "sonic": true, "task": true} + genDelib = map[string]bool{"plan": true, "slow": true, "designer": true, + "reviewer": true, "security-reviewer": true} + // Anti-tunnel-vision: on a *-led lane the reviewers cross to the opposite // provider so the output always gets an independent second eye (the advisor // crosses too, in its own branch). - genCrossLed = map[string]bool{"reviewer": true} + genCrossLed = map[string]bool{"reviewer": true, "security-reviewer": true} genUtil = map[string]bool{"scout": true, "sonic": true, "smol": true, "tiny": true, "commit": true} // Utility roles respond to the dials but are tier-capped so none can ever // become expensive. @@ -365,21 +491,23 @@ var ( } genTierMap = map[string]int{"fast": 1, "normal": 2, "smart": 3} genBump = map[string]string{"minimal": "low", "low": "medium", "medium": "high", "high": "xhigh", "xhigh": "xhigh"} - genLanes = []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only"} genMTiers = []string{"fast", "normal", "smart"} genThinking = []string{"minimal", "low", "medium", "high", "xhigh", "max"} genExtremes = map[string]bool{"minimal": true, "max": true} + // genRelief marks the heavyweight roles whose led/mixed chains gain a + // relief tail on each optional (unmetered, pay-as-you-go) pool. + genRelief = map[string]bool{"default": true, "task": true, "plan": true, "slow": true} ) +// lanePrimary is the lane's lead pool ("mixed" leads on O, deliberative roles +// aside — see genCombo). func lanePrimary(lane string) string { - if lane == "gpt-only" || lane == "gpt-led" || lane == "mixed" { - return "O" + if p := providerByLane(lane); p != nil { + return p.Pool } - return "A" + return "O" } -func lanePure(lane string) bool { return lane == "gpt-only" || lane == "claude-only" } - type roleRoute struct { lead string // short key; "" = role omitted (advisor off) level string @@ -395,8 +523,8 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain base := genTierMap[mtier] isPure := lanePure(lane) extreme := genExtremes[thinking] - sparkKey := c.ladder["O"][0] - eliteKey := c.ladder["A"][4] + sparkKey := c.specialKey("spark") + eliteKey := c.specialKey("fable") rprov := func(r string) string { if isPure { @@ -409,7 +537,7 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain return "O" } if genCrossLed[r] { - return otherPool(p) + return c.crossPool(p) } return p } @@ -457,8 +585,10 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain vp = "A" } lead := c.visionLead(vp, base) - if lead == "" && !isPure { - lead = c.visionLead(otherPool(vp), base) + if lead == "" { + // No image-capable rung on the lead pool at all: cross pools + // even on a pure lane — vision correctness beats lane purity. + lead = c.visionCross(vp, base) } if lead == "" { out[r] = roleRoute{} @@ -488,7 +618,7 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain // allows crossing — the minimum diversity guarantee. ap := p if !isPure { - ap = otherPool(p) + ap = c.advisorPool(p) } amod := c.ladder[ap][1] if mtier == "smart" { @@ -530,6 +660,13 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain lead = eliteKey } chain := c.buildChain(lead, isPure) + if !isPure && genRelief[r] { + // Relief tails: led/mixed lanes end their heavyweight chains on + // each optional pool's regular rung — pay-as-you-go capacity that + // keeps a session alive when every metered window is drained. + // Pure lanes stay pure. dedup absorbs rungs already in the chain. + chain = dedup(append(chain, c.reliefRungs(lead)...), lead) + } out[r] = roleRoute{lead, th, chain, repeatLvl(th, len(chain))} } return out @@ -557,12 +694,15 @@ func genComboID(lane, mtier, thinking string, spark, fable, fableMain bool) stri return fmt.Sprintf("%s_%s_%s_%s_%s", lane, mtier, thinking, sp, fa) } +// genValid rejects facet combos the lane cannot host: a special-tier facet +// (spark, fable) is valid only when its provider's pool is in the lane's +// pool-set — a pure lane hosts only its own pool. func genValid(lane string, spark, fable, fableMain bool) bool { - if lane == "gpt-only" && fable { - return false // no elite on pure GPT + if fable && !laneHostsSpecial(lane, "fable") { + return false // no elite outside its pool's lanes } - if lane == "claude-only" && spark { - return false // no spark on pure Claude + if spark && !laneHostsSpecial(lane, "spark") { + return false // no spark outside its pool's lanes } if fableMain && !fable { return false // fable-as-main only exists on top of fable @@ -612,18 +752,22 @@ func (c *catalog) renderCombo(lane, mtier, thinking string, spark, fable, fableM return strings.Join(lines, "\n") } -// renderModelFacts emits the per-model table the TUI's meters read. The bucket -// is a trailing optional column: the consumer falls back to guessing from the -// model family when a catalog omits it, so old catalogs keep working. +// renderModelFacts emits the per-model table the TUI's meters read: seven +// fixed columns — id, pricing, speed, ttft, quota bucket, provider id. Old +// catalogs with five- or six-column rows still parse; the consumer falls back +// to guessing bucket and provider from the model family for those. func (c *catalog) renderModelFacts() string { - lines := []string{"__models__ model facts (id in out speed ttft bucket — $/1M in·out, tok/s, s)"} + lines := []string{"__models__ model facts (id in out speed ttft bucket provider — $/1M in·out, tok/s, s)"} for _, k := range c.keys { m := c.models[k] - row := fmt.Sprintf(" %s %s %s %s %s", - m.ID, trimFloat(m.CostIn), trimFloat(m.CostOut), trimFloat(m.Speed), trimFloat(m.TTFT)) - if m.Bucket != "" { - row += " " + m.Bucket - } + prov := providerByPool(m.Pool) + bucket := m.Bucket + if bucket == "" { + bucket = prov.mainBucket() + } + row := fmt.Sprintf(" %s %s %s %s %s %s %s", + m.ID, trimFloat(m.CostIn), trimFloat(m.CostOut), trimFloat(m.Speed), trimFloat(m.TTFT), + bucket, prov.ID) lines = append(lines, row) } lines = append(lines, "") @@ -656,7 +800,8 @@ func (c *catalog) renderAdvisors() string { {"audit", []rung{{3, "high"}, {2, "high"}, {1, "low"}}}, } lines := []string{"__advisors__ advisor dial (level context → chain)"} - for _, ctx := range []struct{ name, pool string }{{"gpt", "O"}, {"claude", "A"}} { + for _, pool := range c.pools() { + ctx := struct{ name, pool string }{providerByPool(pool).Lane, pool} for _, d := range dial { var parts []string for _, rg := range d.chain { @@ -686,9 +831,9 @@ func (c *catalog) renderCatalog() string { " — ● marks a role mirrored into task.agentModelOverrides\n\n") b.WriteString(c.renderAdvisors() + "\n") b.WriteString(c.renderModelFacts() + "\n") - hasSpark := c.ladder["O"][0] != "" - hasElite := c.ladder["A"][4] != "" - for _, lane := range genLanes { + hasSpark := c.specialKey("spark") != "" + hasElite := c.specialKey("fable") != "" + for _, lane := range laneOrderForPools(c.pools()) { for _, mtier := range genMTiers { for _, thinking := range genThinking { for _, spark := range []bool{true, false} { diff --git a/generate_init.go b/generate_init.go index ab4d414..de53ae6 100644 --- a/generate_init.go +++ b/generate_init.go @@ -44,12 +44,12 @@ var ompModelsJSON = func() ([]byte, error) { var datedID = regexp.MustCompile(`-\d{6,8}$`) +// poolOf maps an omp provider id to its catalog pool letter via the registry; +// providers outside the registry (google, groq, ollama, …) map to "" and are +// dropped from the scaffold, as ever. func poolOf(provider string) string { - switch provider { - case "anthropic": - return "A" - case "openai-codex", "openai": - return "O" + if p := providerByID(provider); p != nil { + return p.Pool } return "" } @@ -323,10 +323,7 @@ func matchSpecial(st specialTier, cands []ompModel) string { // bucketName follows the catalog's existing convention: the pool's own quota // window, or a tier-scoped one when the model draws a separate bucket. func bucketName(pool, tier string) string { - base := "codex" - if pool == "A" { - base = "claude" - } + base := providerByPool(pool).BucketBase if tier == "" { return base + "-main" } @@ -535,8 +532,14 @@ func scaffoldModels(raw []byte, probe map[string]benchFact) (string, error) { bucket string } rungs := map[string][]rung{} - for _, pool := range []string{"O", "A"} { + for _, pool := range fallbackPoolOrder { + required := providerByPool(pool).Required cands := supersede(byPool[pool]) + if !required && len(cands) == 0 { + // A missing optional provider is the normal state: the catalog + // simply grows no lanes for its pool. + continue + } // Lift a tier-scoped model out before ranking: it is a lead, not a rung // on the capability ladder. At most one per pool — two would both claim // the same tier and loadCatalog would refuse the file — preferring the @@ -577,9 +580,9 @@ func scaffoldModels(raw []byte, probe map[string]benchFact) (string, error) { c = m.Cost.Input } } - if pool == "A" && c >= top { + if poolDeclaresSpecialTier(pool, 4) && c >= top { specialTierNo = 4 - } else if pool == "O" && c < top { + } else if poolDeclaresSpecialTier(pool, 0) && c < top { specialTierNo = 0 } } @@ -591,7 +594,7 @@ func scaffoldModels(raw []byte, probe map[string]benchFact) (string, error) { // Price is legitimate evidence here. It says nothing about entitlement, // which is why the reachability probe exists, but a model in its own // price class is by definition not the everyday workhorse. - if specialTierNo < 0 && pool == "A" && len(cands) > 1 { + if specialTierNo < 0 && poolDeclaresSpecialTier(pool, 4) && len(cands) > 1 { lead, next := cands[0], 0.0 for _, m := range cands { if m.Cost.Input > lead.Cost.Input { @@ -645,12 +648,9 @@ func scaffoldModels(raw []byte, probe map[string]benchFact) (string, error) { ladderCands = kept } ladder := pickLadder(ladderCands) - if len(ladder) < 3 { - name := "OpenAI/Codex" - if pool == "A" { - name = "Anthropic" - } - hint := "code assumes both Anthropic and OpenAI are set up in omp" + if len(ladder) < 3 && required { + name := providerByPool(pool).Label + hint := "code assumes " + requiredProviderNames() + " are set up in omp" if probe != nil { hint += "; models the provider reported as non-existent were dropped by the probe" } @@ -669,7 +669,7 @@ func scaffoldModels(raw []byte, probe map[string]benchFact) (string, error) { # omp; the tier assignments are derived (newest model per family, then ranked by # thinking ceiling, context and price) and worth a sanity check. # -# pool: O = OpenAI/Codex · A = Anthropic +# pool: O = OpenAI/Codex · A = Anthropic · D = DeepSeek # tier: 1 cheap · 2 regular · 3 smart (the per-pool fallback ladder) # tier 0 = a fast idle-bucket model the 'spark' toggle drains; # tier 4 = a scarce elite the 'fable' toggle leads with. Both are @@ -698,7 +698,7 @@ probed: false } b.WriteString("models:\n") used := map[string]bool{} - for _, pool := range []string{"O", "A"} { + for _, pool := range fallbackPoolOrder { for _, r := range rungs[pool] { key := shortKey(r.m.ID) if used[key] { diff --git a/generate_test.go b/generate_test.go index 331d389..c412d1f 100644 --- a/generate_test.go +++ b/generate_test.go @@ -134,15 +134,15 @@ const goldenAdvisors = `__advisors__ advisor dial (level context → chain) ` // goldenFacts pins the trailing bucket column the TUI's quota meter reads. -const goldenFacts = `__models__ model facts (id in out speed ttft bucket — $/1M in·out, tok/s, s) - gpt-5.6-luna 1 6 52.3 1.18 codex-main - gpt-5.6-terra 2.5 15 51.8 1.74 codex-main - gpt-5.6-sol 5 30 31.5 4.59 codex-main - gpt-5.3-codex-spark 1.75 14 286.7 5.56 codex-spark - claude-haiku-4-5 1 5 48.9 1.7 claude-main - claude-sonnet-5 2 10 35.2 3.84 claude-main - claude-opus-5 5 25 46.6 1.77 claude-main - claude-fable-5 10 50 54 6.9 claude-fable +const goldenFacts = `__models__ model facts (id in out speed ttft bucket provider — $/1M in·out, tok/s, s) + gpt-5.6-luna 1 6 52.3 1.18 codex-main openai-codex + gpt-5.6-terra 2.5 15 51.8 1.74 codex-main openai-codex + gpt-5.6-sol 5 30 31.5 4.59 codex-main openai-codex + gpt-5.3-codex-spark 1.75 14 286.7 5.56 codex-spark openai-codex + claude-haiku-4-5 1 5 48.9 1.7 claude-main anthropic + claude-sonnet-5 2 10 35.2 3.84 claude-main anthropic + claude-opus-5 5 25 46.6 1.77 claude-main anthropic + claude-fable-5 10 50 54 6.9 claude-fable anthropic ` const goldenMixedSmart = `mixed_smart_medium_sp_fa mixed · smart · medium · spark · fable @@ -153,6 +153,7 @@ const goldenMixedSmart = `mixed_smart_medium_sp_fa mixed · smart · medium · slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium @@ -171,6 +172,7 @@ const goldenClaudeMax = `claude-only_normal_max_nosp_famain claude-only · norm slow claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max ● designer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max ● reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh @@ -193,6 +195,7 @@ const goldenClaudeSmart = `claude-only_smart_medium_nosp_nofa claude-only · sm slow claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high ● designer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high ● reviewer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high ● librarian claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium @@ -217,15 +220,16 @@ func TestGoldenModelFacts(t *testing.T) { } } -// A catalog that declares no buckets keeps the old five-column rows, so the -// consumer's fallback path stays exercised. +// A catalog that declares no bucket for a model still renders all seven +// columns: the pool's main window is the derived bucket, so the TUI never has +// to guess from the model family for a freshly generated catalog. func TestModelFactsWithoutBuckets(t *testing.T) { c, err := catalogFrom(t, strings.ReplaceAll(fixtureYML, " bucket: codex-main\n", "")) if err != nil { t.Fatalf("loadCatalog: %v", err) } - if !strings.Contains(c.renderModelFacts(), " gpt-5.6-luna 1 6 52.3 1.18\n") { - t.Errorf("bucketless model row should stop after ttft:\n%s", c.renderModelFacts()) + if !strings.Contains(c.renderModelFacts(), " gpt-5.6-luna 1 6 52.3 1.18 codex-main openai-codex\n") { + t.Errorf("bucketless model row should derive the pool's main bucket:\n%s", c.renderModelFacts()) } } @@ -1109,3 +1113,207 @@ func TestGenerateInitRefusesUnresolvedProbe(t *testing.T) { t.Errorf("no models file may be written from an unresolved probe (stat err: %v)", err) } } + +// fixtureYMLDeepSeek extends the two-pool fixture with a full DeepSeek pool +// (three text-only rungs) — the three-pool grid the ds lanes are generated +// from. +const fixtureYMLDeepSeek = fixtureYML + ` lite: + id: deepseek-v4-lite + pool: D + tier: 1 + bucket: deepseek-main + cost_in: 0.1 + cost_out: 0.4 + speed: 60 + ttft: 1.5 + context: 128000 + thinking: low→high + image: false + v4: + id: deepseek-v4 + pool: D + tier: 2 + bucket: deepseek-main + cost_in: 0.3 + cost_out: 1.2 + speed: 45 + ttft: 2.1 + context: 128000 + thinking: low→high + image: false + pro: + id: deepseek-v4-pro + pool: D + tier: 3 + bucket: deepseek-main + cost_in: 0.6 + cost_out: 2.4 + speed: 38 + ttft: 2.8 + context: 128000 + thinking: low→high + image: false +` + +// fixtureYMLDeepSeekOneRung brings a single DeepSeek model: the optional-pool +// fill must complete tiers 1..3 from it, and chain dedupe must collapse the +// duplicates. +const fixtureYMLDeepSeekOneRung = fixtureYML + ` v4: + id: deepseek-v4 + pool: D + tier: 2 + bucket: deepseek-main + cost_in: 0.3 + cost_out: 1.2 + speed: 45 + ttft: 2.1 + context: 128000 + thinking: low→high + image: false +` + +// TestGoldenCatalogTwoPool is the byte-compat contract of the N-pool +// generalisation: a two-pool models file renders the exact catalog the old +// binary-pool renderer produced (modulo the reviewed additions the golden +// carries: the __models__ bucket+provider columns and the security-reviewer +// role). +func TestGoldenCatalogTwoPool(t *testing.T) { + want, err := os.ReadFile(filepath.Join("testdata", "two-pool-golden.plain")) + if err != nil { + t.Fatal(err) + } + c := fixtureCatalog(t) + if got := c.renderCatalog(); got != string(want) { + t.Errorf("two-pool catalog is no longer byte-identical to the golden (diff it against testdata/two-pool-golden.plain to review)\ngot %d bytes, want %d", len(got), len(want)) + } +} + +// TestThreePoolCatalog locks the DeepSeek pool's grid semantics: the two new +// lanes, special-tier validity, relief tails on led/mixed heavyweight chains +// (and only there), the vision purity exception, and the ds advisor contexts. +func TestThreePoolCatalog(t *testing.T) { + c, err := catalogFrom(t, fixtureYMLDeepSeek) + if err != nil { + t.Fatalf("loadCatalog: %v", err) + } + out := c.renderCatalog() + + for _, lane := range []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ds-led", "ds-only"} { + if !strings.Contains(out, "\n"+lane+"_normal_medium_nosp_nofa ") { + t.Errorf("lane %s missing from the three-pool grid", lane) + } + } + // Special tiers follow their pool: none on ds-only, both on ds-led. + if strings.Contains(out, "\nds-only_normal_medium_sp_") || strings.Contains(out, "_medium_nosp_fa\nds-only") || + strings.Contains(out, "\nds-only_normal_medium_nosp_fa") { + t.Error("ds-only generated spark/fable combos it cannot host") + } + for _, id := range []string{"ds-led_normal_medium_sp_nofa", "ds-led_normal_medium_nosp_fa", "ds-led_normal_medium_nosp_famain"} { + if !strings.Contains(out, "\n"+id+" ") { + t.Errorf("ds-led combo %s missing", id) + } + } + + block := func(id string) string { + i := strings.Index(out, "\n"+id+" ") + if i < 0 { + t.Fatalf("combo %s missing", id) + } + rest := out[i+1:] + if j := strings.Index(rest, "\n\n"); j >= 0 { + rest = rest[:j] + } + return rest + } + row := func(blk, role string) string { + for _, ln := range strings.Split(blk, "\n") { + f := strings.Fields(ln) + if len(f) > 1 && f[0] == "●" { + f = f[1:] + } + if len(f) > 0 && f[0] == role { + return ln + } + } + t.Fatalf("role %s missing from block:\n%s", role, blk) + return "" + } + + // Relief tails: led/mixed heavyweight chains end on the DeepSeek regular + // rung; pure lanes stay pure. + for _, lane := range []string{"gpt-led", "mixed", "claude-led"} { + blk := block(lane + "_normal_medium_nosp_nofa") + for _, role := range []string{"default", "task", "plan", "slow"} { + if r := row(blk, role); !strings.HasSuffix(r, "→ deepseek-v4:medium") && !strings.HasSuffix(r, "→ deepseek-v4:high") { + t.Errorf("%s %s chain lacks the DeepSeek relief tail: %q", lane, role, r) + } + } + if r := row(blk, "reviewer"); strings.Contains(r, "deepseek") { + t.Errorf("%s reviewer must not gain a relief tail: %q", lane, r) + } + } + for _, lane := range []string{"gpt-only", "claude-only"} { + if blk := block(lane + "_normal_medium_nosp_nofa"); strings.Contains(blk, "deepseek") { + t.Errorf("pure lane %s crossed into the DeepSeek pool:\n%s", lane, blk) + } + } + + // Vision purity exception: every DeepSeek rung is text-only, so ds-only's + // vision role must cross pools rather than route images to a text model. + dsOnly := block("ds-only_smart_medium_nosp_nofa") + if r := row(dsOnly, "vision"); strings.Contains(r, "deepseek") || !strings.Contains(r, "gpt-5.6-sol:low") { + t.Errorf("ds-only vision must cross to an image-capable pool: %q", r) + } + // …and every other ds-only role stays on DeepSeek. + for _, role := range []string{"default", "task", "plan", "reviewer", "advisor", "commit"} { + if r := row(dsOnly, role); strings.Contains(r, "gpt") || strings.Contains(r, "claude") { + t.Errorf("ds-only %s left the pool: %q", role, r) + } + } + + // The advisor dial gains one context per pool. + for _, want := range []string{ + " glance ds deepseek-v4-lite:low", + " review ds deepseek-v4:medium → deepseek-v4-lite:low", + " audit ds deepseek-v4-pro:high → deepseek-v4:high → deepseek-v4-lite:low", + } { + if !strings.Contains(out, want+"\n") { + t.Errorf("advisors block lacks %q", want) + } + } + + // The facts table carries the provider column for every pool. + if !strings.Contains(out, " deepseek-v4 0.3 1.2 45 2.1 deepseek-main deepseek\n") { + t.Error("facts table lacks the deepseek provider row") + } +} + +// TestOneRungOptionalPool: a single verified DeepSeek model is enough to grow +// the ds lanes — the fill borrows it for every ladder tier and dedupe keeps +// the chains single-entry. +func TestOneRungOptionalPool(t *testing.T) { + c, err := catalogFrom(t, fixtureYMLDeepSeekOneRung) + if err != nil { + t.Fatalf("loadCatalog: %v", err) + } + for tier := 1; tier <= 3; tier++ { + if got := c.ladder["D"][tier]; got != "v4" { + t.Fatalf("optional-pool fill: ladder[D][%d] = %q, want v4", tier, got) + } + } + out := c.renderCatalog() + blkStart := strings.Index(out, "\nds-only_smart_medium_nosp_nofa ") + if blkStart < 0 { + t.Fatal("one-rung D pool generated no ds-only lane") + } + blk := out[blkStart:] + if i := strings.Index(blk[1:], "\n\n"); i >= 0 { + blk = blk[:i+1] + } + if strings.Contains(blk, "deepseek-v4:medium → deepseek-v4:medium") { + t.Errorf("borrowed rungs must dedupe out of the chains:\n%s", blk) + } + if !strings.Contains(blk, " default deepseek-v4:medium\n") { + t.Errorf("one-rung ds-only default should be the single model, got:\n%s", blk) + } +} diff --git a/main.go b/main.go index a050fe4..87fe477 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "unicode" @@ -123,7 +124,9 @@ var ( pad = clikit.Pad windowList = clikit.WindowList - modelRe = regexp.MustCompile(`(gpt|claude)[A-Za-z0-9._-]*:(minimal|low|medium|high|xhigh|max)`) + // Any id-shaped token with a thinking level is a routing token; which + // provider owns it (and its tint) is the registry's call, not the regex's. + modelRe = regexp.MustCompile(`[A-Za-z][A-Za-z0-9._-]*:(minimal|low|medium|high|xhigh|max)`) ) // ── colourisers ────────────────────────────────────────────────────────────── @@ -168,12 +171,11 @@ func clampByte(x float64) int { func paintModel(tok string) string { i := strings.LastIndex(tok, ":") name, level := tok[:i], tok[i+1:] - var br, bg, bb float64 - if strings.HasPrefix(tok, "gpt") { - br, bg, bb = 110, 170, 240 - } else { - br, bg, bb = 240, 160, 105 + p := providerByModel(name) + if p == nil { + return shortModel(name) + ":" + level // unknown provider: uncoloured } + br, bg, bb := p.PaintRGB[0], p.PaintRGB[1], p.PaintRGB[2] f := 0.60 + float64(lvl(level))*0.088 col := lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", clampByte(br*f), clampByte(bg*f), clampByte(bb*f))) return lipgloss.NewStyle().Foreground(col).Render(shortModel(name) + ":" + level) @@ -184,22 +186,31 @@ func colorizeRoute(line string) string { return modelRe.ReplaceAllStringFunc(lin // bucketOf guesses a quota bucket from a model name. It is the fallback for // catalogs that declare no bucket column, and the only resolver for the bare // facet names ("fable", "spark") the suggest box asks about — prefer -// model.bucketFor wherever a receiver is in reach. +// model.bucketFor wherever a receiver is in reach. An unknown name maps to no +// bucket at all rather than someone else's quota window. func bucketOf(model string) string { m := model if i := strings.IndexByte(m, ':'); i >= 0 { m = m[:i] } - switch { - case strings.Contains(m, "fable"): - return "claude-fable" - case strings.Contains(m, "spark"): - return "codex-spark" - case strings.Contains(m, "claude"), strings.Contains(m, "sonnet"), - strings.Contains(m, "haiku"), strings.Contains(m, "opus"): - return "claude-main" + for _, p := range providerRegistry { + for _, s := range p.Special { + if strings.Contains(m, s.Bucket) { + return p.BucketBase + "-" + s.Bucket + } + } + } + if p := providerByModel(m); p != nil { + return p.mainBucket() + } + for _, p := range providerRegistry { + for _, pre := range p.ModelPrefixes { + if strings.Contains(m, pre) { + return p.mainBucket() + } + } } - return "codex-main" + return "" } // bucketFor resolves a routing token's quota bucket from the catalog, falling @@ -213,8 +224,13 @@ func (m model) bucketFor(name string) string { if i := strings.IndexByte(id, ':'); i >= 0 { id = id[:i] } - if f, ok := m.facts[id]; ok && f.bucket != "" { - return f.bucket + if f, ok := m.facts[id]; ok { + if f.bucket != "" { + return f.bucket + } + if p := providerByID(f.provider); p != nil { + return p.mainBucket() + } } return bucketOf(id) } @@ -283,6 +299,10 @@ type availability struct { accountsOK bool selectionApplied bool accountsStale bool + // deepseek is the DeepSeek prepaid balance: nil when the snapshot carries + // no DeepSeek credential (the group is hidden entirely — an absent API key + // is the normal state, unlike a metered subscription). + deepseek *deepseekBalance } func fetchBrokerUsage(broker brokerConfig) ([]byte, error) { @@ -325,9 +345,17 @@ type usageCacheAccount struct { type usageCacheFile struct { SavedAt int64 `json:"savedAt"` Accounts map[string][]account `json:"accounts"` + Deepseek *usageCacheBalance `json:"deepseekBalance,omitempty"` Usage []usageCacheAccount `json:"usage"` } +// usageCacheBalance caches the DeepSeek balance VALUE only — never the key. +type usageCacheBalance struct { + Currency string `json:"currency"` + Total string `json:"totalBalance"` + FetchedAt int64 `json:"fetchedAt"` +} + func emptyAvailability() availability { return availability{ bucket: map[string]string{}, reset: map[string]int64{}, @@ -422,7 +450,7 @@ func parseAvailability(accounts map[string][]account, accountsOK bool, out []byt break } } - if r.Provider == "openai-codex" { + if r.Provider == openAIProvider { credits := resetCredits{avail: r.ResetCredits.AvailableCount} for _, c := range r.ResetCredits.Credits { if c.Status != "available" { @@ -442,32 +470,53 @@ func parseAvailability(accounts map[string][]account, accountsOK bool, out []byt } } } - for _, b := range []string{"codex-main", "codex-spark", "claude-main", "claude-fable"} { - p := "openai-codex" - if strings.HasPrefix(b, "claude") { - p = "anthropic" + for _, prov := range providerRegistry { + if !prov.Metered { + continue } - if !provSeen[p] { - a.bucket[b] = "unauthed" - } else if _, ok := a.bucket[b]; !ok { - a.bucket[b] = "ok" + for _, b := range prov.buckets() { + if !provSeen[prov.ID] { + a.bucket[b] = "unauthed" + } else if _, ok := a.bucket[b]; !ok { + a.bucket[b] = "ok" + } } } return a } -// loadAvailability reads one central snapshot and one aggregate usage report. +// loadAvailability reads one central snapshot and one aggregate usage report, +// plus — when the snapshot carries a DeepSeek api_key — the upstream prepaid +// balance, fetched concurrently so neither request delays the other. func loadAvailability(broker brokerConfig) availability { accounts, err := loadAccounts(broker) accountsOK := err == nil if !accountsOK { accounts = map[string][]account{} } + var ds *deepseekBalance + var wg sync.WaitGroup + if key := deepseekAPIKey(accounts); key != "" { + wg.Add(1) + go func() { + defer wg.Done() + bal, err := fetchDeepSeekBalance(key) + if err != nil { + // Degrade to an explicit "unavailable" row; other providers + // are unaffected. + bal = deepseekBalance{fetchedAt: time.Now().Unix()} + } + ds = &bal + }() + } out, err := fetchBrokerUsage(broker) if err != nil { out = nil } - return parseAvailability(accounts, accountsOK, out, 0) + wg.Wait() + a := parseAvailability(accounts, accountsOK, out, 0) + a.deepseek = ds + return a } func loadUsageCache(path string) availability { @@ -507,15 +556,22 @@ func loadUsageCache(path string) availability { } } } - for _, bucket := range []string{"codex-main", "codex-spark", "claude-main", "claude-fable"} { - provider := openAIProvider - if strings.HasPrefix(bucket, "claude") { - provider = anthropicProvider + if cached.Deepseek != nil { + a.deepseek = &deepseekBalance{ + ok: true, currency: cached.Deepseek.Currency, total: cached.Deepseek.Total, + fetchedAt: cached.Deepseek.FetchedAt, stale: true, + } + } + for _, prov := range providerRegistry { + if !prov.Metered { + continue } - if !provSeen[provider] { - a.bucket[bucket] = "unauthed" - } else if _, ok := a.bucket[bucket]; !ok { - a.bucket[bucket] = "ok" + for _, bucket := range prov.buckets() { + if !provSeen[prov.ID] { + a.bucket[bucket] = "unauthed" + } else if _, ok := a.bucket[bucket]; !ok { + a.bucket[bucket] = "ok" + } } } return a @@ -527,6 +583,11 @@ func saveUsageCache(path string, a availability) { } now := time.Now().Unix() cached := usageCacheFile{SavedAt: now, Accounts: a.accounts} + if a.deepseek != nil && a.deepseek.ok { + cached.Deepseek = &usageCacheBalance{ + Currency: a.deepseek.currency, Total: a.deepseek.total, FetchedAt: a.deepseek.fetchedAt, + } + } for key, wins := range a.accountUsage { entry := usageCacheAccount{Provider: key.Provider, IdentityKey: key.IdentityKey} for _, win := range wins { @@ -564,21 +625,20 @@ func saveUsageCache(path string, a availability) { _ = atomicPrivateWrite(path, body) } +// bucketForProviderTier maps a usage report's (provider, tier) scope onto the +// quota bucket it constrains: the provider's main window, or a special tier's +// dedicated window. Unmetered and unknown providers own no buckets. func bucketForProviderTier(prov, tier string) string { - if prov == "openai-codex" { - if tier == "spark" { - return "codex-spark" - } - if tier == "" || tier == "-" { - return "codex-main" - } + p := providerByID(prov) + if p == nil || !p.Metered { + return "" } - if prov == "anthropic" { - if tier == "fable" { - return "claude-fable" - } - if tier == "" || tier == "-" { - return "claude-main" + if tier == "" || tier == "-" { + return p.mainBucket() + } + for _, s := range p.Special { + if s.Bucket == tier { + return p.BucketBase + "-" + s.Bucket } } return "" @@ -616,9 +676,16 @@ func reconcileUsage(prev, next availability) (availability, bool) { next.accounts, next.accountsOK, next.accountsStale = prev.accounts, true, true } next.accountUsage = reconcileAccountUsage(prev.accountUsage, next.accountUsage, next.accounts) + if next.deepseek != nil && !next.deepseek.ok && prev.deepseek != nil && prev.deepseek.ok { + // A failed balance refresh keeps the last known value, visibly stale — + // same retention contract as the metered windows. + retained := *prev.deepseek + retained.stale = true + next.deepseek = &retained + } hasClaude, hasFable := false, false for _, w := range next.wins { - if w.prov != "anthropic" { + if w.prov != anthropicProvider { continue } hasClaude = true @@ -630,7 +697,7 @@ func reconcileUsage(prev, next availability) (availability, bool) { return next, false } for _, w := range prev.wins { - if w.prov == "anthropic" && w.tier == "fable" && !w.missing { + if w.prov == anthropicProvider && w.tier == "fable" && !w.missing { w.stale = true next.wins = append(next.wins, w) // Carry the bucket/reset state observed with the retained window: @@ -687,12 +754,12 @@ func reconcileAccountUsage(prev, next map[accountKey][]usageWin, accounts map[st } continue } - if key.Provider != "anthropic" { + if key.Provider != anthropicProvider { continue } hasClaude, hasFable := false, false for _, w := range wins { - if w.prov != "anthropic" { + if w.prov != anthropicProvider { continue } if !w.missing { @@ -707,7 +774,7 @@ func reconcileAccountUsage(prev, next map[accountKey][]usageWin, accounts map[st } retained := false for _, w := range prev[key] { - if w.prov == "anthropic" && w.tier == "fable" && !w.missing { + if w.prov == anthropicProvider && w.tier == "fable" && !w.missing { w.stale = true next[key] = append(next[key], w) retained = true @@ -724,7 +791,7 @@ func reconcileAccountUsage(prev, next map[accountKey][]usageWin, accounts map[st // fablePlaceholder is the never-observed fable window's deterministic // stand-in: the real payload label (so shortWin renders the same "7d fable" // tag) and window length, with no usage numbers to fabricate. -var fablePlaceholder = usageWin{label: "Claude 7 Day (Fable)", tier: "fable", dur: 7 * 24 * 3600, prov: "anthropic", missing: true} +var fablePlaceholder = usageWin{label: "Claude 7 Day (Fable)", tier: "fable", dur: 7 * 24 * 3600, prov: anthropicProvider, missing: true} type usageGroupKey struct { prov string @@ -743,16 +810,16 @@ type usageGroup struct { func knownUsageWindow(w usageWin) bool { label := shortWin(w.label) - switch bucketForProviderTier(w.prov, w.tier) { - case "codex-main", "claude-main": - return label == "5h" || label == "7d" - case "codex-spark": - return label == "5h spark" || label == "7d spark" - case "claude-fable": - return label == "7d fable" - default: + bucket := bucketForProviderTier(w.prov, w.tier) + p := providerByID(w.prov) + if bucket == "" || p == nil { return false } + if bucket == p.mainBucket() { + return label == "5h" || label == "7d" + } + suffix := strings.TrimPrefix(bucket, p.BucketBase+"-") + return label == "5h "+suffix || label == "7d "+suffix } // selectedAvailability derives account-sensitive usage and routing availability @@ -841,15 +908,16 @@ func selectedAvailability(a availability, disabled map[accountKey]bool) availabi selected.wins = append(selected.wins, missing[key]) } } - for _, bucket := range []string{"codex-main", "codex-spark", "claude-main", "claude-fable"} { - provider := "openai-codex" - if strings.HasPrefix(bucket, "claude") { - provider = "anthropic" + for _, prov := range providerRegistry { + if !prov.Metered { + continue } - if enabledProviders[provider] { - selected.bucket[bucket] = "ok" - } else { - selected.bucket[bucket] = "unauthed" + for _, bucket := range prov.buckets() { + if enabledProviders[prov.ID] { + selected.bucket[bucket] = "ok" + } else { + selected.bucket[bucket] = "unauthed" + } } } for _, win := range selected.wins { @@ -963,9 +1031,16 @@ type facet struct { glyph string } +// facetDefs seeds the facet dials. The lane facet starts empty: its values are +// catalog-driven (applyCatalog collects the lanes the catalog actually +// generated), so a two-pool catalog shows exactly the classic five lanes and a +// richer one adds its own. func facetDefs(glyphs map[string]string) []facet { return []facet{ - {"lane", []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only"}, glyphs["lane"]}, + // Seeded with the required pools' lanes so a catalog-less run (the + // onboarding shell, a broken CODE_GENERATED) keeps a working dial; + // applyCatalog replaces the list with the lanes the catalog generated. + {"lane", requiredPoolLanes(), glyphs["lane"]}, {"model", []string{"fast", "normal", "smart"}, glyphs["model"]}, {"thinking", []string{"minimal", "low", "medium", "high", "xhigh", "max"}, glyphs["thinking"]}, // advisor as a power/cost dial: a quick glance, a proper review, or a @@ -1010,6 +1085,7 @@ func parseAdvisors(rows []string) map[string][]string { type modelFact struct { in, out, speed, ttft float64 bucket string + provider string // registry provider id ("" in legacy catalogs) } // effTPS folds ttft into throughput — the effective tok/s for a representative @@ -1025,9 +1101,10 @@ func (f modelFact) effTPS() float64 { } // parseFacts reads the __models__ block (rows: " -// []") into a per-model table, sourced from the catalog so meters and -// routing agree. The bucket column is optional — a catalog that declares none -// for any model omits it entirely, and the name guess covers those rows. +// [ []]") into a per-model table, sourced from the catalog +// so meters and routing agree. The bucket and provider columns are trailing +// optionals so legacy five- and six-token catalogs keep working; the name +// guess covers their rows. func parseFacts(rows []string) map[string]modelFact { out := map[string]modelFact{} for _, r := range rows { @@ -1039,12 +1116,15 @@ func parseFacts(rows []string) map[string]modelFact { outc, e2 := strconv.ParseFloat(f[2], 64) sp, e3 := strconv.ParseFloat(f[3], 64) tt, e4 := strconv.ParseFloat(f[4], 64) - bucket := "" + bucket, provider := "", "" if len(f) >= 6 { bucket = f[5] } + if len(f) >= 7 { + provider = f[6] + } if e1 == nil && e2 == nil && e3 == nil && e4 == nil { - out[f[0]] = modelFact{in, outc, sp, tt, bucket} + out[f[0]] = modelFact{in, outc, sp, tt, bucket, provider} } } return out @@ -1066,6 +1146,9 @@ func parseFacts(rows []string) map[string]modelFact { var roleWeight = map[string]float64{ "default": 10, "task": 6, "reviewer": 3, "sonic": 3, "plan": 3, "advisor": 4, "slow": 2, "designer": 2, "librarian": 2, "scout": 2, "smol": 1, "tiny": 0.5, "commit": 0.5, "vision": 0.5, + // security-reviewer routes like reviewer but is spawned far more rarely + // (ad-hoc scans), so it barely moves the needle. + "security-reviewer": 1, } var thinkMult = map[string]float64{ // reasoning tokens grow with effort → pricier "minimal": 0.6, "low": 0.8, "medium": 1.0, "high": 1.3, "xhigh": 1.6, "max": 2.0, @@ -1129,7 +1212,7 @@ func logScore(idx, lnLo, lnHi float64) int { // costScore rates the current config from 1 (cheap) to 5 (dear). func (m model) costScore() int { - fast := m.sel["fast"] == "on" && m.sel["lane"] != "claude-only" + fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") var num, den float64 m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { c, ok := m.facts[id] @@ -1141,7 +1224,7 @@ func (m model) costScore() int { mult = 1 } cost := (0.25*c.in + 0.75*c.out) * mult - if fast && strings.HasPrefix(id, "gpt-") { + if fast && m.poolOfModel(id) == "O" { cost *= priorityMult } num += w * cost @@ -1155,7 +1238,7 @@ func (m model) costScore() int { // speedScore rates the current config from 1 (slow) to 5 (fast). func (m model) speedScore() int { - fast := m.sel["fast"] == "on" && m.sel["lane"] != "claude-only" + fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") var num, den float64 m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { c, ok := m.facts[id] @@ -1167,7 +1250,7 @@ func (m model) speedScore() int { mult = 1 } sp := c.effTPS() * mult - if fast && strings.HasPrefix(id, "gpt-") { + if fast && m.poolOfModel(id) == "O" { sp *= fastSpeed } num += w * sp @@ -1188,23 +1271,32 @@ func (m model) meter(label, glyph, fill string, n int) string { // advisorChain returns the advisor role's model chain for an intensity, sourced // from the baked __advisors__ table. The advisor is the independent second -// opinion, so it uses the opposite provider to whoever leads the session: GPT -// when the lead is Claude — a Claude-led (or pure-GPT) lane, or fable-as-main -// handing the default role to Fable — and Claude otherwise. Only the pure lanes -// stay on their own provider (gpt-only keeps GPT; claude-only keeps Claude). +// opinion, so it crosses to another provider whenever the lane allows it: the +// first advisorPoolOrder pool that is not the lead's (fable-as-main hands the +// default role to the elite, so the lead becomes the elite's pool). Only the +// pure lanes stay on their own provider. func (m model) advisorChain(level string) []string { lane := m.sel["lane"] - ctx := "claude" - if lane == "gpt-only" || lane == "claude-led" { - ctx = "gpt" + if p := providerByLane(lane); p != nil && lanePure(lane) { + return m.advisors[level+"/"+p.Lane] } - // fable-as-main puts Claude Fable in the default seat, so the second - // opinion flips to GPT — except on claude-only, where the pure-lane rule - // keeps the whole pool (advisor included) on Claude. - if lane != "claude-only" && m.sel["fable"] == "on" && m.sel["main"] == "on" { - ctx = "gpt" + lead := lanePrimary(lane) + if fb := providerBySpecial("fable"); fb != nil && m.sel["fable"] == "on" && m.sel["main"] == "on" { + // fable-as-main puts the elite in the default seat, so the second + // opinion flips away from the elite's own pool. + lead = fb.Pool } - return m.advisors[level+"/"+ctx] + for _, pool := range advisorPoolOrder { + if pool == lead { + continue + } + if p := providerByPool(pool); p != nil { + if chain := m.advisors[level+"/"+p.Lane]; len(chain) > 0 { + return chain + } + } + } + return nil } // roleOf returns the role name of a routing row ("● task" → "task"). @@ -1247,28 +1339,33 @@ func (m model) applyAdvisor(rows []string, level string) []string { } // visibleFacets drops facets that don't apply to the current lane, so the -// generator only ever shows actionable options: no spark/fast on a Claude-only -// pool, no fable on a GPT-only pool. main is fable's sub-setting, so it only -// shows while fable is on (and the lane can host it at all). A dial this catalog -// generated no combo for is dropped the same way — it is not a choice. +// generator only ever shows actionable options: no spark/fast on a pure lane +// of another pool, no fable outside its pool's lanes. main is fable's +// sub-setting, so it only shows while fable is on (and the lane can host it at +// all). A dial this catalog generated no combo for is dropped the same way — +// it is not a choice. func (m model) visibleFacets() []facet { lane := m.sel["lane"] var out []facet for _, f := range m.facets { - if lane == "claude-only" && (f.key == "spark" || f.key == "fast") { - continue - } - if lane == "gpt-only" && (f.key == "fable" || f.key == "main") { - continue - } - if f.key == "spark" && m.noSpark { - continue - } - if (f.key == "fable" || f.key == "main") && m.noFable { - continue - } - if f.key == "main" && m.sel["fable"] != "on" { - continue + switch f.key { + case "spark": + if !laneHostsSpecial(lane, "spark") || m.noSpark { + continue + } + case "fast": + // The fast dial is the priority service tier — an OpenAI pool + // feature, meaningless on a pure lane of any other pool. + if p := providerByLane(lane); lanePure(lane) && p != nil && p.ServiceTier[0] == "" { + continue + } + case "fable", "main": + if !laneHostsSpecial(lane, "fable") || m.noFable { + continue + } + if f.key == "main" && m.sel["fable"] != "on" { + continue + } } out = append(out, f) } @@ -1278,10 +1375,10 @@ func (m model) visibleFacets() []facet { func comboID(sel map[string]string) string { lane := sel["lane"] sp, fb := sel["spark"], sel["fable"] - if lane == "gpt-only" { + if !laneHostsSpecial(lane, "fable") { fb = "off" } - if lane == "claude-only" { + if !laneHostsSpecial(lane, "spark") { sp = "off" } spid, faid := "nosp", "nofa" @@ -1303,6 +1400,10 @@ func comboID(sel map[string]string) string { // written — and a selection persisted against a richer catalog does the same. // Ids are ____, so match whole // segments: "nosp" and "nofa" contain the very substrings being looked for. +// The lane facet's value list is the catalog's too: the distinct lane segments +// of the combo ids, ordered canonically (laneOrderForPools), so a catalog with +// a DeepSeek pool grows ds-led/ds-only dials and an old one shows exactly the +// classic five. func (m *model) applyCatalog() { if len(m.generated) == 0 { return // no catalog read yet: onboarding, or a broken CODE_GENERATED @@ -1319,9 +1420,49 @@ func (m *model) applyCatalog() { } } m.noSpark, m.noFable = !spark, !fable + if lanes := catalogLanes(m.generated); len(lanes) > 0 { + for i := range m.facets { + if m.facets[i].key == "lane" { + m.facets[i].values = lanes + } + } + } m.clampSel() } +// catalogLanes collects the distinct lane values the catalog generated, in +// canonical dial order; unknown lane names (a newer catalog) trail sorted so +// they are still reachable. +func catalogLanes(generated map[string][]string) []string { + seen := map[string]bool{} + for id := range generated { + if strings.HasPrefix(id, "__") { + continue + } + lane, rest, ok := strings.Cut(id, "_") + if !ok || lane == "" || rest == "" { + continue + } + seen[lane] = true + } + if len(seen) == 0 { + return nil + } + var lanes []string + for _, lane := range laneOrderForPools(fallbackPoolOrder) { + if seen[lane] { + lanes = append(lanes, lane) + delete(seen, lane) + } + } + var extra []string + for lane := range seen { + extra = append(extra, lane) + } + sort.Strings(extra) + return append(lanes, extra...) +} + // clampSel turns off every dial the catalog cannot serve. main is fable's // sub-setting and never outlives it. func (m *model) clampSel() { @@ -1332,29 +1473,68 @@ func (m *model) clampSel() { m.sel["fable"] = "off" m.sel["main"] = "off" } + // A persisted lane the applied catalog no longer generates (or one from a + // richer catalog) resets to the dial's first lane. + for _, f := range m.facets { + if f.key != "lane" || len(f.values) == 0 { + continue + } + known := false + for _, v := range f.values { + if v == m.sel["lane"] { + known = true + break + } + } + if !known { + m.sel["lane"] = f.values[0] + } + } } +// laneColor tints the accent by lane: each provider carries a deeper shade for +// its pure lane and a lighter one for its led lane; mixed keeps its purple. func laneColor(lane string) string { - switch lane { - case "gpt-only": - return "#3f8ef0" // deeper blue — pure pool - case "gpt-led": - return "#7ab6ff" // lighter blue — leans GPT - case "mixed": + if lane == "mixed" { return "#aa96e1" - case "claude-led": - return "#ffb277" // lighter orange — leans Claude - case "claude-only": - return "#ff8534" // deeper orange — pure pool + } + if p := providerByLane(lane); p != nil { + if lanePure(lane) { + return p.LaneOnly + } + return p.LaneLed } return "#ff9f52" } -func prefixed(model string) string { - if strings.HasPrefix(model, "claude") { - return "anthropic/" + model +// prefixed qualifies a model id with its omp provider: the catalog's provider +// column when present, else the registry's prefix guess. The unknown-model +// fallback stays openai-codex so a legacy catalog launches exactly as before. +func (m model) prefixed(model string) string { + id, _, _ := strings.Cut(model, ":") + if f, ok := m.facts[id]; ok && f.provider != "" { + if p := providerByID(f.provider); p != nil { + return p.ID + "/" + model + } + } + if p := providerByModel(model); p != nil { + return p.ID + "/" + model + } + return openAIProvider + "/" + model +} + +// poolOfModel is the model's registry pool letter, catalog provider column +// first, prefix guess second; "" when nobody claims it. +func (m model) poolOfModel(id string) string { + if f, ok := m.facts[id]; ok && f.provider != "" { + if p := providerByID(f.provider); p != nil { + return p.Pool + } } - return "openai-codex/" + model + if p := providerByModel(id); p != nil { + return p.Pool + } + return "" } // genConfigYAML reconstructs an omp config (modelRoles, task-agent model @@ -1393,13 +1573,13 @@ func (m model) genConfigYAML() string { if i == 1 && role != "advisor" { // ●-marked agent-backed role: mirror its lead route as the task-agent // model override so spawned agents follow the generated profile. - ao.WriteString(" " + role + ": " + prefixed(models[0]) + "\n") + ao.WriteString(" " + role + ": " + m.prefixed(models[0]) + "\n") } - mr.WriteString(" " + role + ": " + prefixed(models[0]) + "\n") + mr.WriteString(" " + role + ": " + m.prefixed(models[0]) + "\n") if len(models) > 1 { var fbs []string for _, x := range models[1:] { - fbs = append(fbs, prefixed(x)) + fbs = append(fbs, m.prefixed(x)) } fc.WriteString(" " + role + ": [" + strings.Join(fbs, ", ") + "]\n") } @@ -1407,8 +1587,20 @@ func (m model) genConfigYAML() string { var b strings.Builder b.WriteString("modelRoles:\n" + mr.String()) b.WriteString("retry:\n enabled: true\n modelFallback: true\n fallbackRevertPolicy: cooldown-expiry\n fallbackChains:\n" + fc.String()) - if ao.Len() > 0 { - b.WriteString("task:\n agentModelOverrides:\n" + ao.String()) + // task.agentAdvisor (omp ≥ 17.3; earlier omps hard-error on the unknown + // key, and CODE_OMP wrappers can lag the store during a dotfiles rollout, + // so the probed version gates the emission): at the audit dial, spawned + // task agents get their own advisor. Merged into the one task: block — + // overlays are strict YAML and two task: keys would be invalid. + agentAdvisor := m.sel["advisor"] == "audit" && m.ompVersionAtLeast(17, 3) + if ao.Len() > 0 || agentAdvisor { + b.WriteString("task:\n") + if ao.Len() > 0 { + b.WriteString(" agentModelOverrides:\n" + ao.String()) + } + if agentAdvisor { + b.WriteString(" agentAdvisor:\n task: \"on\"\n") + } } b.WriteString("defaultThinkingLevel: " + m.sel["thinking"] + "\n") if advisorOn { @@ -1416,8 +1608,10 @@ func (m model) genConfigYAML() string { } else { b.WriteString("advisor:\n enabled: false\n") } - if m.sel["fast"] == "on" && m.sel["lane"] != "claude-only" { - b.WriteString("tier:\n openai: priority\n") + if m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") { + if p := providerByPool("O"); p != nil && p.ServiceTier[0] != "" { + b.WriteString("tier:\n " + p.ServiceTier[0] + ": " + p.ServiceTier[1] + "\n") + } } return b.String() } @@ -1809,6 +2003,10 @@ type model struct { genConfig string // generated config YAML to launch omp with (generator Enter) firstPrompt string // prompt from the suggest box, forwarded as omp's first message savedSel map[string]string // selection snapshot before a live suggest preview (for revert) + // The probed omp version (omp/ from `omp --version`), fetched + // async at startup. Zero = unknown: 17.3-only overlay keys are omitted so + // a lagging CODE_OMP wrapper never hard-errors at launch. + ompMajor, ompMinor int } // usage auto-refreshes on this cadence; a 1s tick drives the countdown. @@ -1857,11 +2055,53 @@ func barAnimCmd(step int) tea.Cmd { return tea.Tick(barAnimInterval, func(time.Time) tea.Msg { return barAnimMsg{step} }) } +// ompVersionMsg carries the probed omp version; ok=false leaves it unknown. +type ompVersionMsg struct { + major, minor int + ok bool +} + +// ompVersionRe parses `omp/....` anywhere in --version output. +var ompVersionRe = regexp.MustCompile(`omp/(\d+)\.(\d+)`) + +// probeOmpVersionCmd resolves the same binary Enter launches and asks it for +// its version, off the main thread. A drift guard, not a feature flag: any +// failure just reads as "unknown" and version-gated keys stay off. +func probeOmpVersionCmd() tea.Cmd { + return func() tea.Msg { + path, err := resolveLaunchPath("CODE_OMP", []string{"omp"}) + if err != nil { + return ompVersionMsg{} + } + out, err := exec.Command(path, "--version").Output() + if err != nil { + return ompVersionMsg{} + } + match := ompVersionRe.FindSubmatch(out) + if match == nil { + return ompVersionMsg{} + } + major, _ := strconv.Atoi(string(match[1])) + minor, _ := strconv.Atoi(string(match[2])) + return ompVersionMsg{major: major, minor: minor, ok: true} + } +} + +// ompVersionAtLeast reports a probed version ≥ major.minor; unknown is never +// "at least" anything. +func (m model) ompVersionAtLeast(major, minor int) bool { + if m.ompMajor == 0 && m.ompMinor == 0 { + return false + } + return m.ompMajor > major || (m.ompMajor == major && m.ompMinor >= minor) +} + func (m model) Init() tea.Cmd { - if m.broker.URL == "" || m.broker.Token == "" { - return nil + cmds := []tea.Cmd{probeOmpVersionCmd()} + if m.broker.URL != "" && m.broker.Token != "" { + cmds = append(cmds, m.startUsageFetch(), m.spin.Tick, tickCmd()) } - return tea.Batch(m.startUsageFetch(), m.spin.Tick, tickCmd()) + return tea.Batch(cmds...) } func (m model) listW() int { @@ -2073,9 +2313,9 @@ func providerIdentities(a availability, prov string, full bool) []compactProvide // providerHeading keeps the provider's established color and puts compact, // enabled snapshot identities in a dim parenthetical suffix. func providerHeading(prov string, identities []compactProviderIdentity) string { - col, name := "#62a7ff", "Codex" - if prov == "anthropic" { - col, name = "#ff9f52", "Claude" + col, name := "#8a93a6", prov + if p := providerByID(prov); p != nil { + col, name = p.Color, p.Label } heading := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true).Render(name) if len(identities) == 0 { @@ -2139,7 +2379,7 @@ func providerIdentityBlock(a availability, prov string, checking bool) []string // when no usage rows exist. func identityLinesFor(a availability) string { var lines []string - for i, prov := range []string{"anthropic", "openai-codex"} { + for i, prov := range meteredProviderIDs() { if i > 0 { lines = append(lines, "") } @@ -2248,13 +2488,12 @@ func (m *model) usageBodyLayout(w int, stacked bool) string { } wins := append([]usageWin(nil), a.wins...) provOrder := func(p string) int { - switch p { - case "anthropic": - return 0 - case "openai-codex": - return 1 + for i := range providerRegistry { + if providerRegistry[i].ID == p { + return i + } } - return 2 + return len(providerRegistry) } tierOrder := func(t string) int { if t == "" || t == "-" { @@ -2273,7 +2512,7 @@ func (m *model) usageBodyLayout(w int, stacked bool) string { }) blocks := map[string]usageRenderGroup{} var order []string - for _, prov := range []string{"anthropic", "openai-codex"} { + for _, prov := range meteredProviderIDs() { if len(a.accounts[prov]) > 0 { order = append(order, prov) blocks[prov] = usageRenderGroup{prefix: providerIdentityBlockFor(a, prov, false, m.fullUsageIDs)} @@ -2288,15 +2527,22 @@ func (m *model) usageBodyLayout(w int, stacked bool) string { block.rows = append(block.rows, m.usageRowSpec(win, " ")) blocks[win.prov] = block } - if block, ok := blocks["openai-codex"]; ok { - for _, acct := range a.accounts["openai-codex"] { + if block, ok := blocks[openAIProvider]; ok { + for _, acct := range a.accounts[openAIProvider] { key := accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} identity := usageDisplayIdentity(managerAccountLabel(acct), m.fullUsageIDs) if cl := creditLineForAccount(identity, a.accountCredits[key]); cl != "" { block.suffix = append(block.suffix, cl) } } - blocks["openai-codex"] = block + blocks[openAIProvider] = block + } + if a.deepseek != nil { + order = append(order, deepseekProvider) + blocks[deepseekProvider] = usageRenderGroup{ + prefix: []string{padLeft(providerHeading(deepseekProvider, nil), gut)}, + suffix: []string{m.deepseekBalanceRow(*a.deepseek)}, + } } return layoutGroups(w, order, blocks, stacked) } @@ -2362,10 +2608,15 @@ func (m *model) usageLoading() bool { // skeletonWinsByProvider mirrors each provider's stable usage shape. Anthropic // always reserves the Fable row, even before a first successful fetch. -var skeletonWinsByProvider = map[string][]string{ - "openai-codex": {"5h", "7d"}, - "anthropic": {"5h", "7d", "7d fable"}, -} +var skeletonWinsByProvider = func() map[string][]string { + wins := map[string][]string{} + for _, p := range providerRegistry { + if p.Metered { + wins[p.ID] = p.SkeletonWins + } + } + return wins +}() // usageRowSpec is the unrendered canonical Usage-row grammar. Every caller // supplies its indentation and group width, while this one seam reserves the @@ -2467,7 +2718,7 @@ func (m *model) skeletonBody(w int) string { func (m *model) skeletonBodyLayout(w int, stacked bool) string { a := m.selectedUsageAvailability() - order := []string{"openai-codex", "anthropic"} + order := meteredProviderIDs() blocks := map[string]usageRenderGroup{} for _, prov := range order { block := usageRenderGroup{prefix: providerIdentityBlockFor(a, prov, true, m.fullUsageIDs)} @@ -2527,6 +2778,22 @@ func formatCachedAge(observed int64, now time.Time) string { } } +// deepseekBalanceRow renders the DeepSeek group's single body row: the prepaid +// balance — no bar, no window, no reset countdown, because none exist. +func (m *model) deepseekBalanceRow(b deepseekBalance) string { + if !b.ok { + return " " + stWarn.Render("balance unavailable") + } + row := " balance " + stHead.Render("$"+b.total+" "+b.currency) + stDim.Render(" · pay-as-you-go") + if b.stale { + cached := "cached" + if b.fetchedAt > 0 { + cached += " " + formatCachedAge(b.fetchedAt, time.Now()) + } + row += " " + stWarn.Render(cached) + } + return row +} func (m *model) usageRowSpec(w usageWin, indent string) usageRowSpec { if w.missing { // Never-observed windows keep the exact row grammar with dotted values; @@ -2865,6 +3132,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.w, m.h = msg.Width, msg.Height m.relayout() + case ompVersionMsg: + if msg.ok { + m.ompMajor, m.ompMinor = msg.major, msg.minor + } + return m, nil case usageMsg: scoped, scopedStale := reconcileUsage(m.avail, msg.avail) refreshAt := time.Now().Add(refreshEvery) diff --git a/main_test.go b/main_test.go index afba52e..f4bd172 100644 --- a/main_test.go +++ b/main_test.go @@ -124,9 +124,9 @@ func TestParseFactsBucketColumn(t *testing.T) { t.Fatalf("parsed %d rows, want 3: %v", len(facts), facts) } want := map[string]modelFact{ - "gpt-5.6-luna": {1, 6, 52.3, 1.18, "codex-main"}, - "gpt-5.6-terra": {2.5, 15, 41, 1.4, ""}, - "claude-mythos-5": {5, 25, 30, 2, "claude-fable"}, + "gpt-5.6-luna": {1, 6, 52.3, 1.18, "codex-main", ""}, + "gpt-5.6-terra": {2.5, 15, 41, 1.4, "", ""}, + "claude-mythos-5": {5, 25, 30, 2, "claude-fable", ""}, } for id, w := range want { if got := facts[id]; got != w { @@ -141,8 +141,8 @@ func TestParseFactsBucketColumn(t *testing.T) { // would then strike models through against the wrong window. func TestBucketForPrefersCatalog(t *testing.T) { m := model{facts: map[string]modelFact{ - "claude-mythos-5": {5, 25, 30, 2, "claude-fable"}, - "gpt-5.6-luna": {1, 6, 52.3, 1.18, ""}, + "claude-mythos-5": {5, 25, 30, 2, "claude-fable", ""}, + "gpt-5.6-luna": {1, 6, 52.3, 1.18, "", ""}, }} if got := bucketOf("claude-mythos-5"); got != "claude-main" { t.Fatalf("guess baseline moved: bucketOf(claude-mythos-5) = %q", got) @@ -3636,3 +3636,156 @@ func TestSandboxLaunchStripsInheritedBrokerEnvironment(t *testing.T) { t.Errorf("sandbox inherited auth routing: %q", got) } } + +// threePoolModel builds a TUI model over the rendered three-pool catalog — +// the integration seam the launch overlay is generated from. +func threePoolModel(t *testing.T) model { + t.Helper() + c, err := catalogFrom(t, fixtureYMLDeepSeek) + if err != nil { + t.Fatalf("loadCatalog: %v", err) + } + path := filepath.Join(t.TempDir(), "generated.plain") + if err := os.WriteFile(path, []byte(c.renderCatalog()), 0o644); err != nil { + t.Fatal(err) + } + generated := loadBlocks(path) + m := model{ + generated: generated, + advisors: parseAdvisors(generated["__advisors__"]), + facts: parseFacts(generated["__models__"]), + facets: facetDefs(defaultGlyphs()), + sel: defaultSel(), + } + m.applyCatalog() + return m +} + +// TestGenConfigYAMLDeepSeekLane: a ds-led selection launches deepseek-prefixed +// roles, mirrors security-reviewer into the agent overrides, and version-gates +// task.agentAdvisor on the probed omp (17.2 hard-errors on the unknown key). +func TestGenConfigYAMLDeepSeekLane(t *testing.T) { + m := threePoolModel(t) + m.sel["lane"] = "ds-led" + m.sel["spark"], m.sel["fable"], m.sel["fast"] = "off", "off", "off" + m.sel["advisor"] = "audit" + + if _, ok := m.generated[comboID(m.sel)]; !ok { + t.Fatalf("no generated block for %s", comboID(m.sel)) + } + + m.ompMajor, m.ompMinor = 17, 3 + got := m.genConfigYAML() + for _, want := range []string{ + " default: deepseek/deepseek-v4-pro:medium\n", + " security-reviewer: ", + " agentAdvisor:\n task: \"on\"\n", + } { + if !strings.Contains(got, want) { + t.Errorf("ds-led overlay lacks %q:\n%s", want, got) + } + } + // The relief-tail fallback and cross-pool chains must carry their own + // provider prefixes, never a mis-prefixed openai-codex/deepseek-…. + if strings.Contains(got, "openai-codex/deepseek") || strings.Contains(got, "anthropic/deepseek") || + strings.Contains(got, "deepseek/gpt") || strings.Contains(got, "deepseek/claude") { + t.Errorf("mis-prefixed model in overlay:\n%s", got) + } + // ds lanes have no OpenAI priority tier even with fast on. + m.sel["lane"] = "ds-only" + m.sel["fast"] = "on" + if only := m.genConfigYAML(); strings.Contains(only, "tier:") { + t.Errorf("ds-only must not emit the OpenAI priority tier:\n%s", only) + } + + // Version gate: an unknown or 17.2 omp omits the 17.3-only key entirely. + m.sel["lane"] = "ds-led" + for _, v := range []struct{ major, minor int }{{0, 0}, {17, 2}} { + m.ompMajor, m.ompMinor = v.major, v.minor + if got := m.genConfigYAML(); strings.Contains(got, "agentAdvisor") { + t.Errorf("agentAdvisor emitted on omp %d.%d:\n%s", v.major, v.minor, got) + } + } +} + +// TestApplyCatalogGrowsLaneDial: the lane facet's values are the catalog's; +// a three-pool catalog grows ds-led/ds-only, an old two-pool one keeps the +// classic five, and a persisted lane the catalog lacks resets to the first. +func TestApplyCatalogGrowsLaneDial(t *testing.T) { + m := threePoolModel(t) + var lanes []string + for _, f := range m.facets { + if f.key == "lane" { + lanes = f.values + } + } + want := []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ds-led", "ds-only"} + if !reflect.DeepEqual(lanes, want) { + t.Fatalf("three-pool lane dial = %v, want %v", lanes, want) + } + + m.sel["lane"] = "ds-led" + if id := comboID(m.sel); m.generated[id] == nil { + t.Fatalf("ds-led selection resolves to no block: %s", id) + } + + two := model{ + generated: map[string][]string{"gpt-only_fast_low_nosp_nofa": {" default gpt-5.6-luna:low"}}, + facets: facetDefs(defaultGlyphs()), + sel: defaultSel(), + } + two.sel["lane"] = "ds-led" // persisted against a richer catalog + two.applyCatalog() + if two.sel["lane"] != "gpt-only" { + t.Fatalf("vanished lane must reset to the dial's first value, got %q", two.sel["lane"]) + } +} + +// TestUsageBodyDeepSeekBalanceGroup: the DeepSeek usage group renders only +// when a credential exists (an absent API key is the normal state, not "not +// authenticated"), shows the prepaid balance with no bar or reset, and +// degrades to an explicit unavailable row on a failed fetch. +func TestUsageBodyDeepSeekBalanceGroup(t *testing.T) { + m := &model{broker: brokerConfig{URL: "http://broker"}, hadUsage: true} + m.accountSelections = defaultAccountSelectionState() + a := emptyAvailability() + a.ok, a.accountsOK = true, true + a.accounts[anthropicProvider] = []account{{Provider: anthropicProvider, IdentityKey: "k", Email: "a@x.test"}} + a.wins = []usageWin{{label: "Claude 5 Hour", pct: 10, secs: 60, dur: 5 * 3600, prov: anthropicProvider}} + key := accountKey{Provider: anthropicProvider, IdentityKey: "k"} + a.accountUsage[key] = a.wins + m.avail = a + + if body := stripAnsi(m.usageBodyFor(0)); strings.Contains(body, "DeepSeek") { + t.Fatalf("no credential: the DeepSeek group must be hidden entirely:\n%s", body) + } + + m.avail.deepseek = &deepseekBalance{ok: true, currency: "USD", total: "12.34"} + body := stripAnsi(m.usageBodyFor(0)) + if !strings.Contains(body, "DeepSeek") || !strings.Contains(body, "balance $12.34 USD · pay-as-you-go") { + t.Fatalf("balance group missing or malformed:\n%s", body) + } + if strings.Contains(body, "$12.34 USD") && (strings.Contains(body, "% used") && strings.Count(body, "% used") > 1) { + t.Fatalf("the balance row must not grow bar/reset chrome:\n%s", body) + } + + m.avail.deepseek = &deepseekBalance{} + if body := stripAnsi(m.usageBodyFor(0)); !strings.Contains(body, "balance unavailable") { + t.Fatalf("failed fetch must degrade to an unavailable row:\n%s", body) + } + + // A deepseek bucket can never gate a launch: unmetered providers own no + // buckets, so nothing in availability can mark one down. + if b := bucketForProviderTier(deepseekProvider, ""); b != "" { + t.Fatalf("unmetered provider grew a quota bucket: %q", b) + } + sel := selectedAvailability(m.avail, nil) + for bucket := range sel.bucket { + if strings.HasPrefix(bucket, "deepseek") { + t.Fatalf("selected availability seeded a deepseek bucket: %q", bucket) + } + } + if sel.deepseek == nil { + t.Fatal("selected availability dropped the deepseek balance") + } +} diff --git a/manager.go b/manager.go index d537386..fe8305f 100644 --- a/manager.go +++ b/manager.go @@ -10,7 +10,15 @@ import ( "github.com/charmbracelet/lipgloss" ) -var managerProviders = []string{"anthropic", "openai-codex"} +// managerProviders is the account manager's provider order — registry order, +// so it can never disagree with the Usage panel again. +var managerProviders = func() []string { + ids := make([]string, len(providerRegistry)) + for i := range providerRegistry { + ids[i] = providerRegistry[i].ID + } + return ids +}() // managerPresetState holds only transient account-manager UI state. Persisted // selections live in model.accountSelections; a draft is never launch-visible. @@ -382,11 +390,17 @@ type managerProviderRange struct { const ( managerProviderBoxBorderWidth = 2 managerProviderBoxFrameWidth = 4 - managerAnthropicColor = "#ff9f52" - managerOpenAIColor = "#62a7ff" ) func managerAccountLabel(a account) string { + if a.apiKey != "" { + // API-key credentials carry no broker identity; account-pool routing + // is OAuth-only, so this row is display-only. + if a.credentialID != "" { + return "API key · credential #" + a.credentialID + } + return "API key" + } if a.Email != "" { return a.Email } @@ -397,16 +411,16 @@ func managerAccountLabel(a account) string { } func managerProviderColor(provider string) string { - if provider == "openai-codex" { - return managerOpenAIColor + if p := providerByID(provider); p != nil { + return p.Color } - return managerAnthropicColor + return "#8a93a6" } func managerProviderHeading(provider string) string { - label := "Anthropic" - if provider == "openai-codex" { - label = "OpenAI" + label := provider + if p := providerByID(provider); p != nil { + label = p.AccountLabel } return lipgloss.NewStyle(). Foreground(lipgloss.Color(managerProviderColor(provider))). @@ -566,10 +580,15 @@ func (m model) managerLines(width int) []managerLine { selectable := 0 group := 0 for _, provider := range managerProviders { + accounts := m.avail.accounts[provider] + if p := providerByID(provider); p != nil && !p.Metered && len(accounts) == 0 { + // An absent API key is the normal state for an unmetered provider + // — no heading, no "not authenticated" noise. + continue + } lines = append(lines, managerLine{ text: managerProviderHeading(provider), selectable: -1, group: -1, provider: provider, }) - accounts := m.avail.accounts[provider] switch { case !m.avail.accountsOK: lines = append(lines, managerLine{ @@ -591,6 +610,8 @@ func (m model) managerLines(width int) []managerLine { status := "enabled" if disabled { status = "off" + } else if a.apiKey != "" { + status = "api key" } else if a.IdentityKey == "" { status = "unavailable" } @@ -648,7 +669,7 @@ func (m model) managerLines(width int) []managerLine { provider: provider, }) } - if len(usageSpecs) == 0 { + if len(usageSpecs) == 0 && a.apiKey == "" { unavailable := " " + stWarn.Render("usage unavailable") if m.avail.accountsStale { unavailable += " " + stWarn.Render("cached") diff --git a/manager_test.go b/manager_test.go index acb9289..a15b4ec 100644 --- a/manager_test.go +++ b/manager_test.go @@ -1486,7 +1486,14 @@ func TestManagerUsageInlineRequiresExactMeasuredFit(t *testing.T) { footer := clikit.SeparatedSections(m.w, usage, controls) unspacedExact := lipgloss.Height(strings.Repeat("\n", topGap)+unspacedAccounts) + lipgloss.Height(footer) - if got, want := exact-unspacedExact, len(m.managerAccounts())-len(managerProviders); got != want { + rendered := 0 + for _, provider := range managerProviders { + if p := providerByID(provider); p != nil && !p.Metered && len(m.avail.accounts[provider]) == 0 { + continue // unmetered provider with no accounts renders no group + } + rendered++ + } + if got, want := exact-unspacedExact, len(m.managerAccounts())-rendered; got != want { t.Fatalf("exact-fit geometry counted %d inter-account breathing rows, want %d", got, want) } diff --git a/onboarding.go b/onboarding.go index f123e22..d32a625 100644 --- a/onboarding.go +++ b/onboarding.go @@ -112,7 +112,7 @@ func (o onboarding) fail(err error) onboarding { case strings.Contains(o.errMsg, "omp models"): o.remedy = "Install oh-my-pi (omp) and make sure it is on PATH, then press enter to retry." case strings.Contains(o.errMsg, "need 3"): - o.remedy = "code assumes omp has BOTH Anthropic and OpenAI providers set up. Add the missing provider (or hand-write " + o.modelsPath + "), then press enter to retry." + o.remedy = "code assumes omp has " + requiredProviderNames() + " set up. Add the missing provider (or hand-write " + o.modelsPath + "), then press enter to retry." default: o.remedy = "Fix the above (the models file lives at " + o.modelsPath + "), then press enter to retry." } diff --git a/providers.go b/providers.go new file mode 100644 index 0000000..44525d6 --- /dev/null +++ b/providers.go @@ -0,0 +1,244 @@ +package main + +// The provider registry: the single table every provider-aware code path +// consults. Adding a provider is a new entry here (plus a lane pool letter); +// nothing else in the tree may hard-code a provider id, pool letter, model +// prefix, bucket name, or brand color. + +import "strings" + +const ( + anthropicProvider = "anthropic" + openAIProvider = "openai-codex" + deepseekProvider = "deepseek" +) + +// specialFacet is a provider's tier-scoped lead: a facet dial ("spark", +// "fable") that swaps a dedicated ladder tier in as a role lead, drawing a +// separate quota bucket. +type specialFacet struct { + Facet string // facet key: "spark" | "fable" + Tier int // ladder index: 0 | 4 + Bucket string // bucket suffix; full name = BucketBase + "-" + Bucket +} + +type providerDesc struct { + ID string // broker/omp provider id + Aliases []string // extra omp ids mapping to the same pool + Pool string // catalog pool letter + Lane string // lane-name segment used in lane facet values + Label string // UI label (product name: Codex, Claude, DeepSeek) + AccountLabel string // account-manager heading (company name: OpenAI, Anthropic) + Color string // UI heading hex + LaneOnly string // deeper shade for the pure "-only" lane + LaneLed string // lighter shade for the "-led" lane + PaintRGB [3]float64 // routing-token tint base (paintModel) + ModelPrefixes []string // model-id prefix matchers (legacy-catalog fallback) + BucketBase string // bucket name base; main bucket = BucketBase + "-main" + Metered bool // false => no quota windows; never gates launches + Required bool // generate-init: must fill tiers 1..3, else hard error + SkeletonWins []string // usage-panel loading skeleton windows; nil when unmetered + Special []specialFacet + ServiceTier [2]string // omp overlay `tier:` key/value when the `fast` facet is on +} + +// providerRegistry order is the account/usage display order (Claude first, +// matching the established Usage panel). Generator-side pool iteration uses +// fallbackPoolOrder instead, so the two orders are independent knobs. +var providerRegistry = []providerDesc{ + {ID: anthropicProvider, Pool: "A", Lane: "claude", Label: "Claude", AccountLabel: "Anthropic", + Color: "#ff9f52", LaneOnly: "#ff8534", LaneLed: "#ffb277", PaintRGB: [3]float64{240, 160, 105}, + ModelPrefixes: []string{"claude", "sonnet", "haiku", "opus"}, BucketBase: "claude", + Metered: true, Required: true, + SkeletonWins: []string{"5h", "7d", "7d fable"}, + Special: []specialFacet{{Facet: "fable", Tier: 4, Bucket: "fable"}}}, + {ID: openAIProvider, Aliases: []string{"openai"}, Pool: "O", Lane: "gpt", Label: "Codex", AccountLabel: "OpenAI", + Color: "#62a7ff", LaneOnly: "#3f8ef0", LaneLed: "#7ab6ff", PaintRGB: [3]float64{110, 170, 240}, + ModelPrefixes: []string{"gpt", "codex"}, BucketBase: "codex", + Metered: true, Required: true, + SkeletonWins: []string{"5h", "7d"}, + Special: []specialFacet{{Facet: "spark", Tier: 0, Bucket: "spark"}}, + ServiceTier: [2]string{"openai", "priority"}}, + {ID: deepseekProvider, Pool: "D", Lane: "ds", Label: "DeepSeek", AccountLabel: "DeepSeek", + Color: "#4d6bfe", LaneOnly: "#3a55f0", LaneLed: "#7d92ff", PaintRGB: [3]float64{77, 107, 254}, + ModelPrefixes: []string{"deepseek"}, BucketBase: "deepseek"}, +} + +// fallbackPoolOrder is the generator-side pool order: non-lead pools appear in +// fallback chains (and cross-provider role dispatch) in this order. +var fallbackPoolOrder = []string{"O", "A", "D"} + +// advisorPoolOrder decides the advisor's context: the first entry that is not +// the lead pool (pure lanes keep their own pool). +var advisorPoolOrder = []string{"A", "O", "D"} + +// providerByID matches a provider id or alias; nil when unknown. +func providerByID(id string) *providerDesc { + for i := range providerRegistry { + p := &providerRegistry[i] + if p.ID == id { + return p + } + for _, a := range p.Aliases { + if a == id { + return p + } + } + } + return nil +} + +func providerByPool(pool string) *providerDesc { + for i := range providerRegistry { + if providerRegistry[i].Pool == pool { + return &providerRegistry[i] + } + } + return nil +} + +// providerByModel resolves a model id by its longest matching prefix; nil when +// no provider claims it. It is the legacy-catalog fallback — catalogs now +// carry an explicit provider column that wins over this guess. +func providerByModel(modelID string) *providerDesc { + var best *providerDesc + bestLen := 0 + for i := range providerRegistry { + for _, pre := range providerRegistry[i].ModelPrefixes { + if len(pre) > bestLen && strings.HasPrefix(modelID, pre) { + best, bestLen = &providerRegistry[i], len(pre) + } + } + } + return best +} + +// providerByLane maps a lane facet value ("gpt-only", "ds-led") to its +// provider; nil for "mixed" or an unknown lane. +func providerByLane(lane string) *providerDesc { + seg, _, ok := strings.Cut(lane, "-") + if !ok { + return nil + } + for i := range providerRegistry { + if providerRegistry[i].Lane == seg { + return &providerRegistry[i] + } + } + return nil +} + +// providerBySpecial finds the provider owning a special-tier facet ("spark", +// "fable"); nil when no provider declares it. +func providerBySpecial(facet string) *providerDesc { + for i := range providerRegistry { + for _, s := range providerRegistry[i].Special { + if s.Facet == facet { + return &providerRegistry[i] + } + } + } + return nil +} + +// special returns the provider's special-tier declaration for a facet. +func (p *providerDesc) special(facet string) *specialFacet { + for i := range p.Special { + if p.Special[i].Facet == facet { + return &p.Special[i] + } + } + return nil +} + +// mainBucket is the provider's ordinary quota window name. +func (p *providerDesc) mainBucket() string { return p.BucketBase + "-main" } + +// buckets lists every quota bucket the provider draws: main first, then each +// special tier's window. +func (p *providerDesc) buckets() []string { + out := []string{p.mainBucket()} + for _, s := range p.Special { + out = append(out, p.BucketBase+"-"+s.Bucket) + } + return out +} + +// meteredProviderIDs lists the quota-windowed providers in registry (display) +// order — the providers the usage panel, skeleton, and launch gating track. +func meteredProviderIDs() []string { + var out []string + for i := range providerRegistry { + if providerRegistry[i].Metered { + out = append(out, providerRegistry[i].ID) + } + } + return out +} + +// poolDeclaresSpecialTier reports whether the pool's provider declares a +// special facet at the given ladder tier (O's spark at 0, A's fable at 4). +func poolDeclaresSpecialTier(pool string, tier int) bool { + p := providerByPool(pool) + if p == nil { + return false + } + for _, s := range p.Special { + if s.Tier == tier { + return true + } + } + return false +} + +// requiredPoolLanes is the lane list over just the Required pools — the +// classic five-lane dial, and the pre-catalog default. +func requiredPoolLanes() []string { + var pools []string + for _, pool := range fallbackPoolOrder { + if p := providerByPool(pool); p != nil && p.Required { + pools = append(pools, pool) + } + } + return laneOrderForPools(pools) +} + +// lanePure reports whether a lane is a single-pool lane. +func lanePure(lane string) bool { return strings.HasSuffix(lane, "-only") } + +// laneHasPool reports whether a lane's pool-set contains the pool: a pure lane +// hosts only its own pool; every other lane (led, mixed) hosts all pools. +func laneHasPool(lane, pool string) bool { + if !lanePure(lane) { + return true + } + p := providerByLane(lane) + return p != nil && p.Pool == pool +} + +// laneHostsSpecial reports whether a special-tier facet ("spark", "fable") can +// be on for the lane: its provider's pool must be in the lane's pool-set. +func laneHostsSpecial(lane, facet string) bool { + p := providerBySpecial(facet) + return p != nil && laneHasPool(lane, p.Pool) +} + +// laneOrderForPools is the canonical lane-dial order over the given pools +// (subset of fallbackPoolOrder, in that order): the first pool's pure lane +// leads, mixed sits in the middle, the second pool mirrors, and every further +// pool appends its led/only pair. +func laneOrderForPools(pools []string) []string { + var lanes []string + name := func(pool string) string { return providerByPool(pool).Lane } + if len(pools) == 0 { + return nil + } + lanes = append(lanes, name(pools[0])+"-only", name(pools[0])+"-led") + if len(pools) > 1 { + lanes = append(lanes, "mixed", name(pools[1])+"-led", name(pools[1])+"-only") + } + for _, pool := range pools[2:] { + lanes = append(lanes, name(pool)+"-led", name(pool)+"-only") + } + return lanes +} diff --git a/selection_state.go b/selection_state.go index 731d3ef..7ebfbb8 100644 --- a/selection_state.go +++ b/selection_state.go @@ -52,12 +52,22 @@ func facetValues(facets []facet) map[string]map[string]bool { return valid } -// repairPersistedSelection prevents a hidden fable/main choice from being -// resurrected after loading. Other hidden facets retain their ordinary model -// semantics; only main is a subordinate choice that must be explicitly remade. +// repairPersistedSelection prevents a hidden special-tier choice from being +// resurrected after loading: spark/fable are forced off when the persisted +// lane's pool-set cannot host them, and main (a subordinate choice that must +// be explicitly remade) never outlives fable. func repairPersistedSelection(sel map[string]string) { - if sel["lane"] == "gpt-only" { - sel["fable"] = "off" + repairSelectionSpecials(sel) +} + +// repairSelectionSpecials is the one lane-validity rule shared by persisted +// loads and live suggestions: a special-tier facet is forced off iff its +// provider's pool is outside the selected lane's pool-set. +func repairSelectionSpecials(sel map[string]string) { + for _, facet := range []string{"spark", "fable"} { + if !laneHostsSpecial(sel["lane"], facet) { + sel[facet] = "off" + } } if sel["fable"] != "on" { sel["main"] = "off" diff --git a/suggest.go b/suggest.go index f70f049..255ea87 100644 --- a/suggest.go +++ b/suggest.go @@ -96,18 +96,13 @@ func (m model) Commander() clikit.Commander { } // repairConstraints enforces the deterministic rules a suggestion (or selection) -// must never violate — mirroring generate-profiles.py's `valid` plus live quota: -// spark is an OpenAI model, so it can't run on a pure-Claude lane; fable is an -// Anthropic elite, so it can't run on a pure-GPT lane; and neither may be left on -// when its quota bucket is maxed or unauthed. Runs after an applied proposal, so -// the generator can't land on an impossible or unavailable combo. +// must never violate — mirroring the generator's `genValid` plus live quota: +// a special-tier facet (spark, fable) can only run on a lane whose pool-set +// contains its provider's pool, and neither may be left on when its quota +// bucket is maxed or unauthed. Runs after an applied proposal, so the +// generator can't land on an impossible or unavailable combo. func (m *model) repairConstraints() { - switch m.sel["lane"] { - case "claude-only": - m.sel["spark"] = "off" - case "gpt-only": - m.sel["fable"] = "off" - } + repairSelectionSpecials(m.sel) if m.avail.down(bucketOf("fable")) { m.sel["fable"] = "off" } @@ -193,8 +188,8 @@ func (m model) appliedDiff() []clikit.Action { func (m *model) deriveToggles() { tier := m.sel["thinking"] critical := m.sel["model"] == "smart" && (tier == "xhigh" || tier == "max") - claudeLane := m.sel["lane"] != "gpt-only" - if critical && claudeLane && !m.avail.down(bucketOf("fable")) { + fableLane := laneHostsSpecial(m.sel["lane"], "fable") + if critical && fableLane && !m.avail.down(bucketOf("fable")) { m.sel["fable"] = "on" } else { m.sel["fable"] = "off" diff --git a/testdata/two-pool-golden.plain b/testdata/two-pool-golden.plain new file mode 100644 index 0000000..0a1820e --- /dev/null +++ b/testdata/two-pool-golden.plain @@ -0,0 +1,7335 @@ +OMP generated routing — first-principles facet grid +agent-backed roles: task designer reviewer security-reviewer librarian scout sonic — ● marks a role mirrored into task.agentModelOverrides + +__advisors__ advisor dial (level context → chain) + glance gpt gpt-5.6-luna:low + review gpt gpt-5.6-terra:medium → gpt-5.6-luna:low + audit gpt gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:low + glance claude claude-haiku-4-5:low + review claude claude-sonnet-5:medium → claude-haiku-4-5:low + audit claude claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:low + +__models__ model facts (id in out speed ttft bucket provider — $/1M in·out, tok/s, s) + gpt-5.6-luna 1 6 52.3 1.18 codex-main openai-codex + gpt-5.6-terra 2.5 15 51.8 1.74 codex-main openai-codex + gpt-5.6-sol 5 30 31.5 4.59 codex-main openai-codex + gpt-5.3-codex-spark 1.75 14 286.7 5.56 codex-spark openai-codex + claude-haiku-4-5 1 5 48.9 1.7 claude-main anthropic + claude-sonnet-5 2 10 35.2 3.84 claude-main anthropic + claude-opus-5 5 25 46.6 1.77 claude-main anthropic + claude-fable-5 10 50 54 6.9 claude-fable anthropic + +gpt-only_fast_minimal_sp_nofa gpt-only · fast · minimal · spark + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low + ● task gpt-5.6-luna:low + plan gpt-5.6-terra:low → gpt-5.6-luna:low + slow gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_fast_minimal_nosp_nofa gpt-only · fast · minimal + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low + ● task gpt-5.6-luna:low + plan gpt-5.6-terra:low → gpt-5.6-luna:low + slow gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_fast_low_sp_nofa gpt-only · fast · low · spark + thinking low · fallback on · advisor off + default gpt-5.6-luna:low + ● task gpt-5.6-luna:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_fast_low_nosp_nofa gpt-only · fast · low + thinking low · fallback on · advisor off + default gpt-5.6-luna:low + ● task gpt-5.6-luna:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_fast_medium_sp_nofa gpt-only · fast · medium · spark + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium + ● task gpt-5.6-luna:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_fast_medium_nosp_nofa gpt-only · fast · medium + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium + ● task gpt-5.6-luna:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_fast_high_sp_nofa gpt-only · fast · high · spark + thinking high · fallback on · advisor off + default gpt-5.6-luna:high + ● task gpt-5.6-luna:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_fast_high_nosp_nofa gpt-only · fast · high + thinking high · fallback on · advisor off + default gpt-5.6-luna:high + ● task gpt-5.6-luna:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_fast_xhigh_sp_nofa gpt-only · fast · xhigh · spark + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh + ● task gpt-5.6-luna:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_fast_xhigh_nosp_nofa gpt-only · fast · xhigh + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh + ● task gpt-5.6-luna:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_fast_max_sp_nofa gpt-only · fast · max · spark + thinking max · fallback on · advisor off + default gpt-5.6-luna:max + ● task gpt-5.6-luna:max + plan gpt-5.6-terra:max → gpt-5.6-luna:max + slow gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-only_fast_max_nosp_nofa gpt-only · fast · max + thinking max · fallback on · advisor off + default gpt-5.6-luna:max + ● task gpt-5.6-luna:max + plan gpt-5.6-terra:max → gpt-5.6-luna:max + slow gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-only_normal_minimal_sp_nofa gpt-only · normal · minimal · spark + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_normal_minimal_nosp_nofa gpt-only · normal · minimal + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_normal_low_sp_nofa gpt-only · normal · low · spark + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_normal_low_nosp_nofa gpt-only · normal · low + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_normal_medium_sp_nofa gpt-only · normal · medium · spark + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_normal_medium_nosp_nofa gpt-only · normal · medium + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_normal_high_sp_nofa gpt-only · normal · high · spark + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_normal_high_nosp_nofa gpt-only · normal · high + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_normal_xhigh_sp_nofa gpt-only · normal · xhigh · spark + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_normal_xhigh_nosp_nofa gpt-only · normal · xhigh + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-only_normal_max_sp_nofa gpt-only · normal · max · spark + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max + ● task gpt-5.6-terra:max → gpt-5.6-luna:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-only_normal_max_nosp_nofa gpt-only · normal · max + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max + ● task gpt-5.6-terra:max → gpt-5.6-luna:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-only_smart_minimal_sp_nofa gpt-only · smart · minimal · spark + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_smart_minimal_nosp_nofa gpt-only · smart · minimal + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-only_smart_low_sp_nofa gpt-only · smart · low · spark + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_smart_low_nosp_nofa gpt-only · smart · low + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-only_smart_medium_sp_nofa gpt-only · smart · medium · spark + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_smart_medium_nosp_nofa gpt-only · smart · medium + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-only_smart_high_sp_nofa gpt-only · smart · high · spark + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_smart_high_nosp_nofa gpt-only · smart · high + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-only_smart_xhigh_sp_nofa gpt-only · smart · xhigh · spark + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-only_smart_xhigh_nosp_nofa gpt-only · smart · xhigh + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-only_smart_max_sp_nofa gpt-only · smart · max · spark + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-only_smart_max_nosp_nofa gpt-only · smart · max + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → gpt-5.6-luna:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +gpt-led_fast_minimal_sp_fa gpt-led · fast · minimal · spark · fable + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_minimal_sp_famain gpt-led · fast · minimal · spark · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_minimal_sp_nofa gpt-led · fast · minimal · spark + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_minimal_nosp_fa gpt-led · fast · minimal · fable + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_minimal_nosp_famain gpt-led · fast · minimal · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_minimal_nosp_nofa gpt-led · fast · minimal + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_low_sp_fa gpt-led · fast · low · spark · fable + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_low_sp_famain gpt-led · fast · low · spark · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_low_sp_nofa gpt-led · fast · low · spark + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_low_nosp_fa gpt-led · fast · low · fable + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_low_nosp_famain gpt-led · fast · low · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_low_nosp_nofa gpt-led · fast · low + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_medium_sp_fa gpt-led · fast · medium · spark · fable + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_medium_sp_famain gpt-led · fast · medium · spark · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_medium_sp_nofa gpt-led · fast · medium · spark + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_medium_nosp_fa gpt-led · fast · medium · fable + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_medium_nosp_famain gpt-led · fast · medium · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_medium_nosp_nofa gpt-led · fast · medium + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_high_sp_fa gpt-led · fast · high · spark · fable + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_high_sp_famain gpt-led · fast · high · spark · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_high_sp_nofa gpt-led · fast · high · spark + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_high_nosp_fa gpt-led · fast · high · fable + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_high_nosp_famain gpt-led · fast · high · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_high_nosp_nofa gpt-led · fast · high + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_xhigh_sp_fa gpt-led · fast · xhigh · spark · fable + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_xhigh_sp_famain gpt-led · fast · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_xhigh_sp_nofa gpt-led · fast · xhigh · spark + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_fast_xhigh_nosp_fa gpt-led · fast · xhigh · fable + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_xhigh_nosp_famain gpt-led · fast · xhigh · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_xhigh_nosp_nofa gpt-led · fast · xhigh + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_fast_max_sp_fa gpt-led · fast · max · spark · fable + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_fast_max_sp_famain gpt-led · fast · max · spark · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_fast_max_sp_nofa gpt-led · fast · max · spark + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_fast_max_nosp_fa gpt-led · fast · max · fable + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-led_fast_max_nosp_famain gpt-led · fast · max · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-led_fast_max_nosp_nofa gpt-led · fast · max + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-led_normal_minimal_sp_fa gpt-led · normal · minimal · spark · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_minimal_sp_famain gpt-led · normal · minimal · spark · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_minimal_sp_nofa gpt-led · normal · minimal · spark + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_minimal_nosp_fa gpt-led · normal · minimal · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_minimal_nosp_famain gpt-led · normal · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_minimal_nosp_nofa gpt-led · normal · minimal + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_low_sp_fa gpt-led · normal · low · spark · fable + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_low_sp_famain gpt-led · normal · low · spark · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_low_sp_nofa gpt-led · normal · low · spark + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_low_nosp_fa gpt-led · normal · low · fable + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_low_nosp_famain gpt-led · normal · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_low_nosp_nofa gpt-led · normal · low + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_medium_sp_fa gpt-led · normal · medium · spark · fable + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_medium_sp_famain gpt-led · normal · medium · spark · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_medium_sp_nofa gpt-led · normal · medium · spark + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_medium_nosp_fa gpt-led · normal · medium · fable + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_medium_nosp_famain gpt-led · normal · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_medium_nosp_nofa gpt-led · normal · medium + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_high_sp_fa gpt-led · normal · high · spark · fable + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_high_sp_famain gpt-led · normal · high · spark · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_high_sp_nofa gpt-led · normal · high · spark + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_high_nosp_fa gpt-led · normal · high · fable + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_high_nosp_famain gpt-led · normal · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_high_nosp_nofa gpt-led · normal · high + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_xhigh_sp_fa gpt-led · normal · xhigh · spark · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_xhigh_sp_famain gpt-led · normal · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_xhigh_sp_nofa gpt-led · normal · xhigh · spark + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_normal_xhigh_nosp_fa gpt-led · normal · xhigh · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_xhigh_nosp_famain gpt-led · normal · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_xhigh_nosp_nofa gpt-led · normal · xhigh + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +gpt-led_normal_max_sp_fa gpt-led · normal · max · spark · fable + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_normal_max_sp_famain gpt-led · normal · max · spark · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_normal_max_sp_nofa gpt-led · normal · max · spark + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_normal_max_nosp_fa gpt-led · normal · max · fable + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-led_normal_max_nosp_famain gpt-led · normal · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-led_normal_max_nosp_nofa gpt-led · normal · max + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +gpt-led_smart_minimal_sp_fa gpt-led · smart · minimal · spark · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_minimal_sp_famain gpt-led · smart · minimal · spark · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_minimal_sp_nofa gpt-led · smart · minimal · spark + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_minimal_nosp_fa gpt-led · smart · minimal · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_minimal_nosp_famain gpt-led · smart · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_minimal_nosp_nofa gpt-led · smart · minimal + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + slow gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● designer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_low_sp_fa gpt-led · smart · low · spark · fable + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_low_sp_famain gpt-led · smart · low · spark · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_low_sp_nofa gpt-led · smart · low · spark + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_low_nosp_fa gpt-led · smart · low · fable + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_low_nosp_famain gpt-led · smart · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_low_nosp_nofa gpt-led · smart · low + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_medium_sp_fa gpt-led · smart · medium · spark · fable + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_medium_sp_famain gpt-led · smart · medium · spark · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_medium_sp_nofa gpt-led · smart · medium · spark + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_medium_nosp_fa gpt-led · smart · medium · fable + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_medium_nosp_famain gpt-led · smart · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_medium_nosp_nofa gpt-led · smart · medium + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + slow gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● designer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_high_sp_fa gpt-led · smart · high · spark · fable + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_high_sp_famain gpt-led · smart · high · spark · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_high_sp_nofa gpt-led · smart · high · spark + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_high_nosp_fa gpt-led · smart · high · fable + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_high_nosp_famain gpt-led · smart · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_high_nosp_nofa gpt-led · smart · high + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_xhigh_sp_fa gpt-led · smart · xhigh · spark · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_xhigh_sp_famain gpt-led · smart · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_xhigh_sp_nofa gpt-led · smart · xhigh · spark + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +gpt-led_smart_xhigh_nosp_fa gpt-led · smart · xhigh · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_xhigh_nosp_famain gpt-led · smart · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_xhigh_nosp_nofa gpt-led · smart · xhigh + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +gpt-led_smart_max_sp_fa gpt-led · smart · max · spark · fable + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_smart_max_sp_famain gpt-led · smart · max · spark · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_smart_max_sp_nofa gpt-led · smart · max · spark + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +gpt-led_smart_max_nosp_fa gpt-led · smart · max · fable + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +gpt-led_smart_max_nosp_famain gpt-led · smart · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +gpt-led_smart_max_nosp_nofa gpt-led · smart · max + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + slow gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● designer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +mixed_fast_minimal_sp_fa mixed · fast · minimal · spark · fable + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_minimal_sp_famain mixed · fast · minimal · spark · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_minimal_sp_nofa mixed · fast · minimal · spark + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_minimal_nosp_fa mixed · fast · minimal · fable + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_minimal_nosp_famain mixed · fast · minimal · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_minimal_nosp_nofa mixed · fast · minimal + thinking minimal · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● task gpt-5.6-luna:low → claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:minimal + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_low_sp_fa mixed · fast · low · spark · fable + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_low_sp_famain mixed · fast · low · spark · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_low_sp_nofa mixed · fast · low · spark + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_low_nosp_fa mixed · fast · low · fable + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_low_nosp_famain mixed · fast · low · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_low_nosp_nofa mixed · fast · low + thinking low · fallback on · advisor off + default gpt-5.6-luna:low → claude-haiku-4-5:low + ● task gpt-5.6-luna:low → claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● librarian gpt-5.6-luna:low → claude-haiku-4-5:low + ● scout gpt-5.6-luna:low + ● sonic gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_medium_sp_fa mixed · fast · medium · spark · fable + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_medium_sp_famain mixed · fast · medium · spark · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_medium_sp_nofa mixed · fast · medium · spark + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_medium_nosp_fa mixed · fast · medium · fable + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_medium_nosp_famain mixed · fast · medium · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_medium_nosp_nofa mixed · fast · medium + thinking medium · fallback on · advisor off + default gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● task gpt-5.6-luna:medium → claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● librarian gpt-5.6-luna:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_high_sp_fa mixed · fast · high · spark · fable + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_high_sp_famain mixed · fast · high · spark · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_high_sp_nofa mixed · fast · high · spark + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_high_nosp_fa mixed · fast · high · fable + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_high_nosp_famain mixed · fast · high · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_high_nosp_nofa mixed · fast · high + thinking high · fallback on · advisor off + default gpt-5.6-luna:high → claude-haiku-4-5:high + ● task gpt-5.6-luna:high → claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:high → claude-haiku-4-5:high + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_xhigh_sp_fa mixed · fast · xhigh · spark · fable + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_xhigh_sp_famain mixed · fast · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_xhigh_sp_nofa mixed · fast · xhigh · spark + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.3-codex-spark:low → gpt-5.6-luna:low + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_fast_xhigh_nosp_fa mixed · fast · xhigh · fable + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_xhigh_nosp_famain mixed · fast · xhigh · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_xhigh_nosp_nofa mixed · fast · xhigh + thinking xhigh · fallback on · advisor off + default gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● librarian gpt-5.6-luna:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:medium + ● sonic gpt-5.6-luna:medium + vision gpt-5.6-luna:low → claude-haiku-4-5:low + smol gpt-5.6-luna:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_fast_max_sp_fa mixed · fast · max · spark · fable + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_fast_max_sp_famain mixed · fast · max · spark · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_fast_max_sp_nofa mixed · fast · max · spark + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_fast_max_nosp_fa mixed · fast · max · fable + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +mixed_fast_max_nosp_famain mixed · fast · max · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +mixed_fast_max_nosp_nofa mixed · fast · max + thinking max · fallback on · advisor off + default gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-luna:max → claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● librarian gpt-5.6-luna:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-luna:max + ● sonic gpt-5.6-luna:max + vision gpt-5.6-luna:max → claude-haiku-4-5:xhigh + smol gpt-5.6-luna:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +mixed_normal_minimal_sp_fa mixed · normal · minimal · spark · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_minimal_sp_famain mixed · normal · minimal · spark · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_minimal_sp_nofa mixed · normal · minimal · spark + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_minimal_nosp_fa mixed · normal · minimal · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_minimal_nosp_famain mixed · normal · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_minimal_nosp_nofa mixed · normal · minimal + thinking minimal · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:minimal → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_low_sp_fa mixed · normal · low · spark · fable + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_low_sp_famain mixed · normal · low · spark · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_low_sp_nofa mixed · normal · low · spark + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_low_nosp_fa mixed · normal · low · fable + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_low_nosp_famain mixed · normal · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_low_nosp_nofa mixed · normal · low + thinking low · fallback on · advisor on + default gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_medium_sp_fa mixed · normal · medium · spark · fable + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_medium_sp_famain mixed · normal · medium · spark · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_medium_sp_nofa mixed · normal · medium · spark + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_medium_nosp_fa mixed · normal · medium · fable + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_medium_nosp_famain mixed · normal · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_medium_nosp_nofa mixed · normal · medium + thinking medium · fallback on · advisor on + default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:low + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_high_sp_fa mixed · normal · high · spark · fable + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_high_sp_famain mixed · normal · high · spark · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_high_sp_nofa mixed · normal · high · spark + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_high_nosp_fa mixed · normal · high · fable + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_high_nosp_famain mixed · normal · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_high_nosp_nofa mixed · normal · high + thinking high · fallback on · advisor on + default gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_xhigh_sp_fa mixed · normal · xhigh · spark · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_xhigh_sp_famain mixed · normal · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_xhigh_sp_nofa mixed · normal · xhigh · spark + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-luna:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_normal_xhigh_nosp_fa mixed · normal · xhigh · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_xhigh_nosp_famain mixed · normal · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_xhigh_nosp_nofa mixed · normal · xhigh + thinking xhigh · fallback on · advisor on + default gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-haiku-4-5:low → gpt-5.6-luna:low + vision gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-luna:low + commit gpt-5.6-luna:low + +mixed_normal_max_sp_fa mixed · normal · max · spark · fable + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_normal_max_sp_famain mixed · normal · max · spark · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_normal_max_sp_nofa mixed · normal · max · spark + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_normal_max_nosp_fa mixed · normal · max · fable + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +mixed_normal_max_nosp_famain mixed · normal · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +mixed_normal_max_nosp_nofa mixed · normal · max + thinking max · fallback on · advisor on + default gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-haiku-4-5:xhigh → gpt-5.6-luna:max + vision gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol gpt-5.6-terra:max + tiny gpt-5.6-luna:max + commit gpt-5.6-luna:max + +mixed_smart_minimal_sp_fa mixed · smart · minimal · spark · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_minimal_sp_famain mixed · smart · minimal · spark · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_minimal_sp_nofa mixed · smart · minimal · spark + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_minimal_nosp_fa mixed · smart · minimal · fable + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_minimal_nosp_famain mixed · smart · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_minimal_nosp_nofa mixed · smart · minimal + thinking minimal · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_low_sp_fa mixed · smart · low · spark · fable + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_low_sp_famain mixed · smart · low · spark · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_low_sp_nofa mixed · smart · low · spark + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_low_nosp_fa mixed · smart · low · fable + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_low_nosp_famain mixed · smart · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_low_nosp_nofa mixed · smart · low + thinking low · fallback on · advisor on + default gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● task gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● librarian gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● scout gpt-5.6-terra:low → gpt-5.6-luna:low + ● sonic gpt-5.6-terra:low → gpt-5.6-luna:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_medium_sp_fa mixed · smart · medium · spark · fable + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_medium_sp_famain mixed · smart · medium · spark · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_medium_sp_nofa mixed · smart · medium · spark + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_medium_nosp_fa mixed · smart · medium · fable + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_medium_nosp_famain mixed · smart · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_medium_nosp_nofa mixed · smart · medium + thinking medium · fallback on · advisor on + default gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● librarian gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:low + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_high_sp_fa mixed · smart · high · spark · fable + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_high_sp_famain mixed · smart · high · spark · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_high_sp_nofa mixed · smart · high · spark + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_high_nosp_fa mixed · smart · high · fable + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_high_nosp_famain mixed · smart · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_high_nosp_nofa mixed · smart · high + thinking high · fallback on · advisor on + default gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● task gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_xhigh_sp_fa mixed · smart · xhigh · spark · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_xhigh_sp_famain mixed · smart · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_xhigh_sp_nofa mixed · smart · xhigh · spark + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.3-codex-spark:low → gpt-5.6-terra:low + commit gpt-5.3-codex-spark:low → gpt-5.6-luna:low + +mixed_smart_xhigh_nosp_fa mixed · smart · xhigh · fable + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_xhigh_nosp_famain mixed · smart · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_xhigh_nosp_nofa mixed · smart · xhigh + thinking xhigh · fallback on · advisor on + default gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● librarian gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● scout gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● sonic gpt-5.6-terra:medium → gpt-5.6-luna:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol gpt-5.6-terra:medium + tiny gpt-5.6-terra:low + commit gpt-5.6-luna:low + +mixed_smart_max_sp_fa mixed · smart · max · spark · fable + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_smart_max_sp_famain mixed · smart · max · spark · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_smart_max_sp_nofa mixed · smart · max · spark + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol gpt-5.6-terra:max + tiny gpt-5.3-codex-spark:xhigh → gpt-5.6-terra:max + commit gpt-5.3-codex-spark:xhigh → gpt-5.6-luna:max + +mixed_smart_max_nosp_fa mixed · smart · max · fable + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +mixed_smart_max_nosp_famain mixed · smart · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +mixed_smart_max_nosp_nofa mixed · smart · max + thinking max · fallback on · advisor on + default gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● task gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● librarian gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● scout gpt-5.6-terra:max → gpt-5.6-luna:max + ● sonic gpt-5.6-terra:max → gpt-5.6-luna:max + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol gpt-5.6-terra:max + tiny gpt-5.6-terra:max + commit gpt-5.6-luna:max + +claude-led_fast_minimal_sp_fa claude-led · fast · minimal · spark · fable + thinking minimal · fallback on · advisor off + default claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● task claude-haiku-4-5:minimal → gpt-5.6-luna:low + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● scout claude-haiku-4-5:minimal + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal → gpt-5.6-luna:low + smol claude-haiku-4-5:minimal + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_fast_minimal_sp_famain claude-led · fast · minimal · spark · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-haiku-4-5:minimal → gpt-5.6-luna:low + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● scout claude-haiku-4-5:minimal + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal → gpt-5.6-luna:low + smol claude-haiku-4-5:minimal + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_fast_minimal_sp_nofa claude-led · fast · minimal · spark + thinking minimal · fallback on · advisor off + default claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● task claude-haiku-4-5:minimal → gpt-5.6-luna:low + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● scout claude-haiku-4-5:minimal + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal → gpt-5.6-luna:low + smol claude-haiku-4-5:minimal + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_fast_minimal_nosp_fa claude-led · fast · minimal · fable + thinking minimal · fallback on · advisor off + default claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● task claude-haiku-4-5:minimal → gpt-5.6-luna:low + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● scout claude-haiku-4-5:minimal + ● sonic claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal → gpt-5.6-luna:low + smol claude-haiku-4-5:minimal + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_fast_minimal_nosp_famain claude-led · fast · minimal · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-haiku-4-5:minimal → gpt-5.6-luna:low + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● scout claude-haiku-4-5:minimal + ● sonic claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal → gpt-5.6-luna:low + smol claude-haiku-4-5:minimal + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_fast_minimal_nosp_nofa claude-led · fast · minimal + thinking minimal · fallback on · advisor off + default claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● task claude-haiku-4-5:minimal → gpt-5.6-luna:low + plan claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + slow claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal → gpt-5.6-luna:low + ● scout claude-haiku-4-5:minimal + ● sonic claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal → gpt-5.6-luna:low + smol claude-haiku-4-5:minimal + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_fast_low_sp_fa claude-led · fast · low · spark · fable + thinking low · fallback on · advisor off + default claude-haiku-4-5:low → gpt-5.6-luna:low + ● task claude-haiku-4-5:low → gpt-5.6-luna:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low → gpt-5.6-luna:low + ● scout claude-haiku-4-5:low + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_low_sp_famain claude-led · fast · low · spark · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-haiku-4-5:low → gpt-5.6-luna:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low → gpt-5.6-luna:low + ● scout claude-haiku-4-5:low + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_low_sp_nofa claude-led · fast · low · spark + thinking low · fallback on · advisor off + default claude-haiku-4-5:low → gpt-5.6-luna:low + ● task claude-haiku-4-5:low → gpt-5.6-luna:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low → gpt-5.6-luna:low + ● scout claude-haiku-4-5:low + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_low_nosp_fa claude-led · fast · low · fable + thinking low · fallback on · advisor off + default claude-haiku-4-5:low → gpt-5.6-luna:low + ● task claude-haiku-4-5:low → gpt-5.6-luna:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low → gpt-5.6-luna:low + ● scout claude-haiku-4-5:low + ● sonic claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_fast_low_nosp_famain claude-led · fast · low · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-haiku-4-5:low → gpt-5.6-luna:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low → gpt-5.6-luna:low + ● scout claude-haiku-4-5:low + ● sonic claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_fast_low_nosp_nofa claude-led · fast · low + thinking low · fallback on · advisor off + default claude-haiku-4-5:low → gpt-5.6-luna:low + ● task claude-haiku-4-5:low → gpt-5.6-luna:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low → gpt-5.6-luna:low + ● scout claude-haiku-4-5:low + ● sonic claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_fast_medium_sp_fa claude-led · fast · medium · spark · fable + thinking medium · fallback on · advisor off + default claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● task claude-haiku-4-5:medium → gpt-5.6-luna:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_medium_sp_famain claude-led · fast · medium · spark · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-haiku-4-5:medium → gpt-5.6-luna:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_medium_sp_nofa claude-led · fast · medium · spark + thinking medium · fallback on · advisor off + default claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● task claude-haiku-4-5:medium → gpt-5.6-luna:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_medium_nosp_fa claude-led · fast · medium · fable + thinking medium · fallback on · advisor off + default claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● task claude-haiku-4-5:medium → gpt-5.6-luna:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_fast_medium_nosp_famain claude-led · fast · medium · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-haiku-4-5:medium → gpt-5.6-luna:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_fast_medium_nosp_nofa claude-led · fast · medium + thinking medium · fallback on · advisor off + default claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● task claude-haiku-4-5:medium → gpt-5.6-luna:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + slow claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer gpt-5.6-terra:high → gpt-5.6-luna:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium → gpt-5.6-luna:medium + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_fast_high_sp_fa claude-led · fast · high · spark · fable + thinking high · fallback on · advisor off + default claude-haiku-4-5:high → gpt-5.6-luna:high + ● task claude-haiku-4-5:high → gpt-5.6-luna:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high → gpt-5.6-luna:high + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_high_sp_famain claude-led · fast · high · spark · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-haiku-4-5:high → gpt-5.6-luna:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high → gpt-5.6-luna:high + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_high_sp_nofa claude-led · fast · high · spark + thinking high · fallback on · advisor off + default claude-haiku-4-5:high → gpt-5.6-luna:high + ● task claude-haiku-4-5:high → gpt-5.6-luna:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high → gpt-5.6-luna:high + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_high_nosp_fa claude-led · fast · high · fable + thinking high · fallback on · advisor off + default claude-haiku-4-5:high → gpt-5.6-luna:high + ● task claude-haiku-4-5:high → gpt-5.6-luna:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high → gpt-5.6-luna:high + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_fast_high_nosp_famain claude-led · fast · high · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-haiku-4-5:high → gpt-5.6-luna:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high → gpt-5.6-luna:high + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_fast_high_nosp_nofa claude-led · fast · high + thinking high · fallback on · advisor off + default claude-haiku-4-5:high → gpt-5.6-luna:high + ● task claude-haiku-4-5:high → gpt-5.6-luna:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high → gpt-5.6-luna:high + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_fast_xhigh_sp_fa claude-led · fast · xhigh · spark · fable + thinking xhigh · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_xhigh_sp_famain claude-led · fast · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_xhigh_sp_nofa claude-led · fast · xhigh · spark + thinking xhigh · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● scout claude-haiku-4-5:medium + ● sonic gpt-5.3-codex-spark:low → claude-haiku-4-5:low + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_fast_xhigh_nosp_fa claude-led · fast · xhigh · fable + thinking xhigh · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-led_fast_xhigh_nosp_famain claude-led · fast · xhigh · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-led_fast_xhigh_nosp_nofa claude-led · fast · xhigh + thinking xhigh · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:xhigh + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low → gpt-5.6-luna:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-led_fast_max_sp_fa claude-led · fast · max · spark · fable + thinking max · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:max + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● scout claude-haiku-4-5:xhigh + ● sonic gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh → gpt-5.6-luna:max + smol claude-haiku-4-5:xhigh + tiny gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_fast_max_sp_famain claude-led · fast · max · spark · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:max + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● scout claude-haiku-4-5:xhigh + ● sonic gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh → gpt-5.6-luna:max + smol claude-haiku-4-5:xhigh + tiny gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_fast_max_sp_nofa claude-led · fast · max · spark + thinking max · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:max + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● scout claude-haiku-4-5:xhigh + ● sonic gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh → gpt-5.6-luna:max + smol claude-haiku-4-5:xhigh + tiny gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_fast_max_nosp_fa claude-led · fast · max · fable + thinking max · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:max + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● scout claude-haiku-4-5:xhigh + ● sonic claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh → gpt-5.6-luna:max + smol claude-haiku-4-5:xhigh + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-led_fast_max_nosp_famain claude-led · fast · max · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:max + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● scout claude-haiku-4-5:xhigh + ● sonic claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh → gpt-5.6-luna:max + smol claude-haiku-4-5:xhigh + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-led_fast_max_nosp_nofa claude-led · fast · max + thinking max · fallback on · advisor off + default claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● task claude-haiku-4-5:xhigh → gpt-5.6-luna:max + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh → gpt-5.6-luna:max + ● scout claude-haiku-4-5:xhigh + ● sonic claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh → gpt-5.6-luna:max + smol claude-haiku-4-5:xhigh + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-led_normal_minimal_sp_fa claude-led · normal · minimal · spark · fable + thinking minimal · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-luna:low → claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_normal_minimal_sp_famain claude-led · normal · minimal · spark · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-luna:low → claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_normal_minimal_sp_nofa claude-led · normal · minimal · spark + thinking minimal · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-luna:low → claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_normal_minimal_nosp_fa claude-led · normal · minimal · fable + thinking minimal · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-luna:low → claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_normal_minimal_nosp_famain claude-led · normal · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-luna:low → claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_normal_minimal_nosp_nofa claude-led · normal · minimal + thinking minimal · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-luna:low → claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_normal_low_sp_fa claude-led · normal · low · spark · fable + thinking low · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_low_sp_famain claude-led · normal · low · spark · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_low_sp_nofa claude-led · normal · low · spark + thinking low · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_low_nosp_fa claude-led · normal · low · fable + thinking low · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_normal_low_nosp_famain claude-led · normal · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_normal_low_nosp_nofa claude-led · normal · low + thinking low · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-led_normal_medium_sp_fa claude-led · normal · medium · spark · fable + thinking medium · fallback on · advisor on + default claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_medium_sp_famain claude-led · normal · medium · spark · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_medium_sp_nofa claude-led · normal · medium · spark + thinking medium · fallback on · advisor on + default claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_medium_nosp_fa claude-led · normal · medium · fable + thinking medium · fallback on · advisor on + default claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_normal_medium_nosp_famain claude-led · normal · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_normal_medium_nosp_nofa claude-led · normal · medium + thinking medium · fallback on · advisor on + default claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium → gpt-5.6-terra:medium → gpt-5.6-luna:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_normal_high_sp_fa claude-led · normal · high · spark · fable + thinking high · fallback on · advisor on + default claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_high_sp_famain claude-led · normal · high · spark · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_high_sp_nofa claude-led · normal · high · spark + thinking high · fallback on · advisor on + default claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_high_nosp_fa claude-led · normal · high · fable + thinking high · fallback on · advisor on + default claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_normal_high_nosp_famain claude-led · normal · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_normal_high_nosp_nofa claude-led · normal · high + thinking high · fallback on · advisor on + default claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high → gpt-5.6-terra:high → gpt-5.6-luna:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-led_normal_xhigh_sp_fa claude-led · normal · xhigh · spark · fable + thinking xhigh · fallback on · advisor on + default claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_xhigh_sp_famain claude-led · normal · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_xhigh_sp_nofa claude-led · normal · xhigh · spark + thinking xhigh · fallback on · advisor on + default claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-haiku-4-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_normal_xhigh_nosp_fa claude-led · normal · xhigh · fable + thinking xhigh · fallback on · advisor on + default claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-led_normal_xhigh_nosp_famain claude-led · normal · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-led_normal_xhigh_nosp_nofa claude-led · normal · xhigh + thinking xhigh · fallback on · advisor on + default claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh → gpt-5.6-terra:xhigh → gpt-5.6-luna:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-luna:low → claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low → gpt-5.6-terra:low → gpt-5.6-luna:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-led_normal_max_sp_fa claude-led · normal · max · spark · fable + thinking max · fallback on · advisor on + default claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-luna:max → claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + smol claude-sonnet-5:max + tiny gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_normal_max_sp_famain claude-led · normal · max · spark · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-luna:max → claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + smol claude-sonnet-5:max + tiny gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_normal_max_sp_nofa claude-led · normal · max · spark + thinking max · fallback on · advisor on + default claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-luna:max → claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + smol claude-sonnet-5:max + tiny gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_normal_max_nosp_fa claude-led · normal · max · fable + thinking max · fallback on · advisor on + default claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-luna:max → claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + smol claude-sonnet-5:max + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-led_normal_max_nosp_famain claude-led · normal · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-luna:max → claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + smol claude-sonnet-5:max + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-led_normal_max_nosp_nofa claude-led · normal · max + thinking max · fallback on · advisor on + default claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-luna:max → claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh → gpt-5.6-terra:max → gpt-5.6-luna:max + smol claude-sonnet-5:max + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-led_smart_minimal_sp_fa claude-led · smart · minimal · spark · fable + thinking minimal · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_smart_minimal_sp_famain claude-led · smart · minimal · spark · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_smart_minimal_sp_nofa claude-led · smart · minimal · spark + thinking minimal · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:minimal + +claude-led_smart_minimal_nosp_fa claude-led · smart · minimal · fable + thinking minimal · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_minimal_nosp_famain claude-led · smart · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_minimal_nosp_nofa claude-led · smart · minimal + thinking minimal · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + slow claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● designer claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer gpt-5.6-sol:low → gpt-5.6-terra:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor gpt-5.6-terra:low → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_low_sp_fa claude-led · smart · low · spark · fable + thinking low · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_low_sp_famain claude-led · smart · low · spark · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_low_sp_nofa claude-led · smart · low · spark + thinking low · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_low_nosp_fa claude-led · smart · low · fable + thinking low · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_low_nosp_famain claude-led · smart · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_low_nosp_nofa claude-led · smart · low + thinking low · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● task claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + plan claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer gpt-5.6-sol:medium → gpt-5.6-terra:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_medium_sp_fa claude-led · smart · medium · spark · fable + thinking medium · fallback on · advisor on + default claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_medium_sp_famain claude-led · smart · medium · spark · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_medium_sp_nofa claude-led · smart · medium · spark + thinking medium · fallback on · advisor on + default claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_medium_nosp_fa claude-led · smart · medium · fable + thinking medium · fallback on · advisor on + default claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_medium_nosp_famain claude-led · smart · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + plan claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_medium_nosp_nofa claude-led · smart · medium + thinking medium · fallback on · advisor on + default claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + plan claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + slow claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● designer claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer gpt-5.6-sol:high → gpt-5.6-terra:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → gpt-5.6-sol:medium → gpt-5.6-terra:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_high_sp_fa claude-led · smart · high · spark · fable + thinking high · fallback on · advisor on + default claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_high_sp_famain claude-led · smart · high · spark · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_high_sp_nofa claude-led · smart · high · spark + thinking high · fallback on · advisor on + default claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_high_nosp_fa claude-led · smart · high · fable + thinking high · fallback on · advisor on + default claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_high_nosp_famain claude-led · smart · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_high_nosp_nofa claude-led · smart · high + thinking high · fallback on · advisor on + default claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● task claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → gpt-5.6-sol:high → gpt-5.6-terra:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-led_smart_xhigh_sp_fa claude-led · smart · xhigh · spark · fable + thinking xhigh · fallback on · advisor on + default claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_xhigh_sp_famain claude-led · smart · xhigh · spark · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_xhigh_sp_nofa claude-led · smart · xhigh · spark + thinking xhigh · fallback on · advisor on + default claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny gpt-5.3-codex-spark:low → claude-sonnet-5:low + commit gpt-5.3-codex-spark:low → claude-haiku-4-5:low + +claude-led_smart_xhigh_nosp_fa claude-led · smart · xhigh · fable + thinking xhigh · fallback on · advisor on + default claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:low + +claude-led_smart_xhigh_nosp_famain claude-led · smart · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:low + +claude-led_smart_xhigh_nosp_nofa claude-led · smart · xhigh + thinking xhigh · fallback on · advisor on + default claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → gpt-5.6-sol:xhigh → gpt-5.6-terra:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor gpt-5.6-terra:high → gpt-5.6-luna:low → claude-sonnet-5:low → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → gpt-5.6-sol:low → gpt-5.6-terra:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:low + +claude-led_smart_max_sp_fa claude-led · smart · max · spark · fable + thinking max · fallback on · advisor on + default claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol claude-sonnet-5:max + tiny gpt-5.3-codex-spark:xhigh → claude-sonnet-5:max + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_smart_max_sp_famain claude-led · smart · max · spark · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol claude-sonnet-5:max + tiny gpt-5.3-codex-spark:xhigh → claude-sonnet-5:max + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_smart_max_sp_nofa claude-led · smart · max · spark + thinking max · fallback on · advisor on + default claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol claude-sonnet-5:max + tiny gpt-5.3-codex-spark:xhigh → claude-sonnet-5:max + commit gpt-5.3-codex-spark:xhigh → claude-haiku-4-5:xhigh + +claude-led_smart_max_nosp_fa claude-led · smart · max · fable + thinking max · fallback on · advisor on + default claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol claude-sonnet-5:max + tiny claude-sonnet-5:max + commit claude-haiku-4-5:xhigh + +claude-led_smart_max_nosp_famain claude-led · smart · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + plan claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-fable-5:max → claude-opus-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol claude-sonnet-5:max + tiny claude-sonnet-5:max + commit claude-haiku-4-5:xhigh + +claude-led_smart_max_nosp_nofa claude-led · smart · max + thinking max · fallback on · advisor on + default claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● task claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + plan claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + slow claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● designer claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer gpt-5.6-sol:max → gpt-5.6-terra:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor gpt-5.6-terra:max → gpt-5.6-luna:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → gpt-5.6-sol:max → gpt-5.6-terra:max + smol claude-sonnet-5:max + tiny claude-sonnet-5:max + commit claude-haiku-4-5:xhigh + +claude-only_fast_minimal_nosp_fa claude-only · fast · minimal · fable + thinking minimal · fallback on · advisor off + default claude-haiku-4-5:minimal + ● task claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal + slow claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal + ● scout claude-haiku-4-5:minimal + ● sonic claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal + smol claude-haiku-4-5:minimal + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_fast_minimal_nosp_famain claude-only · fast · minimal · fable · main + thinking minimal · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● task claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal + slow claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal + ● scout claude-haiku-4-5:minimal + ● sonic claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal + smol claude-haiku-4-5:minimal + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_fast_minimal_nosp_nofa claude-only · fast · minimal + thinking minimal · fallback on · advisor off + default claude-haiku-4-5:minimal + ● task claude-haiku-4-5:minimal + plan claude-sonnet-5:low → claude-haiku-4-5:minimal + slow claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-haiku-4-5:minimal + ● scout claude-haiku-4-5:minimal + ● sonic claude-haiku-4-5:minimal + vision claude-haiku-4-5:minimal + smol claude-haiku-4-5:minimal + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_fast_low_nosp_fa claude-only · fast · low · fable + thinking low · fallback on · advisor off + default claude-haiku-4-5:low + ● task claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low + ● scout claude-haiku-4-5:low + ● sonic claude-haiku-4-5:low + vision claude-haiku-4-5:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_fast_low_nosp_famain claude-only · fast · low · fable · main + thinking low · fallback on · advisor off + default claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● task claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low + ● scout claude-haiku-4-5:low + ● sonic claude-haiku-4-5:low + vision claude-haiku-4-5:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_fast_low_nosp_nofa claude-only · fast · low + thinking low · fallback on · advisor off + default claude-haiku-4-5:low + ● task claude-haiku-4-5:low + plan claude-sonnet-5:medium → claude-haiku-4-5:medium + slow claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-haiku-4-5:low + ● scout claude-haiku-4-5:low + ● sonic claude-haiku-4-5:low + vision claude-haiku-4-5:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_fast_medium_nosp_fa claude-only · fast · medium · fable + thinking medium · fallback on · advisor off + default claude-haiku-4-5:medium + ● task claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high + slow claude-sonnet-5:high → claude-haiku-4-5:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_fast_medium_nosp_famain claude-only · fast · medium · fable · main + thinking medium · fallback on · advisor off + default claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high + slow claude-sonnet-5:high → claude-haiku-4-5:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_fast_medium_nosp_nofa claude-only · fast · medium + thinking medium · fallback on · advisor off + default claude-haiku-4-5:medium + ● task claude-haiku-4-5:medium + plan claude-sonnet-5:high → claude-haiku-4-5:high + slow claude-sonnet-5:high → claude-haiku-4-5:high + ● designer claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-haiku-4-5:medium + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_fast_high_nosp_fa claude-only · fast · high · fable + thinking high · fallback on · advisor off + default claude-haiku-4-5:high + ● task claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_fast_high_nosp_famain claude-only · fast · high · fable · main + thinking high · fallback on · advisor off + default claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● task claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_fast_high_nosp_nofa claude-only · fast · high + thinking high · fallback on · advisor off + default claude-haiku-4-5:high + ● task claude-haiku-4-5:high + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:high + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_fast_xhigh_nosp_fa claude-only · fast · xhigh · fable + thinking xhigh · fallback on · advisor off + default claude-haiku-4-5:xhigh + ● task claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-only_fast_xhigh_nosp_famain claude-only · fast · xhigh · fable · main + thinking xhigh · fallback on · advisor off + default claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-only_fast_xhigh_nosp_nofa claude-only · fast · xhigh + thinking xhigh · fallback on · advisor off + default claude-haiku-4-5:xhigh + ● task claude-haiku-4-5:xhigh + plan claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh + ● scout claude-haiku-4-5:medium + ● sonic claude-haiku-4-5:medium + vision claude-haiku-4-5:low + smol claude-haiku-4-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-only_fast_max_nosp_fa claude-only · fast · max · fable + thinking max · fallback on · advisor off + default claude-haiku-4-5:xhigh + ● task claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh + ● scout claude-haiku-4-5:xhigh + ● sonic claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh + smol claude-haiku-4-5:xhigh + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-only_fast_max_nosp_famain claude-only · fast · max · fable · main + thinking max · fallback on · advisor off + default claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● task claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh + ● scout claude-haiku-4-5:xhigh + ● sonic claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh + smol claude-haiku-4-5:xhigh + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-only_fast_max_nosp_nofa claude-only · fast · max + thinking max · fallback on · advisor off + default claude-haiku-4-5:xhigh + ● task claude-haiku-4-5:xhigh + plan claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-haiku-4-5:xhigh + ● scout claude-haiku-4-5:xhigh + ● sonic claude-haiku-4-5:xhigh + vision claude-haiku-4-5:xhigh + smol claude-haiku-4-5:xhigh + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-only_normal_minimal_nosp_fa claude-only · normal · minimal · fable + thinking minimal · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + slow claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● designer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_normal_minimal_nosp_famain claude-only · normal · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + slow claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● designer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_normal_minimal_nosp_nofa claude-only · normal · minimal + thinking minimal · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor claude-haiku-4-5:minimal + vision claude-sonnet-5:low → claude-haiku-4-5:minimal + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_normal_low_nosp_fa claude-only · normal · low · fable + thinking low · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_normal_low_nosp_famain claude-only · normal · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_normal_low_nosp_nofa claude-only · normal · low + thinking low · fallback on · advisor on + default claude-sonnet-5:low → claude-haiku-4-5:low + ● task claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-sonnet-5:low → claude-haiku-4-5:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:minimal + commit claude-haiku-4-5:minimal + +claude-only_normal_medium_nosp_fa claude-only · normal · medium · fable + thinking medium · fallback on · advisor on + default claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + slow claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● designer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_normal_medium_nosp_famain claude-only · normal · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + slow claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● designer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_normal_medium_nosp_nofa claude-only · normal · medium + thinking medium · fallback on · advisor on + default claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_normal_high_nosp_fa claude-only · normal · high · fable + thinking high · fallback on · advisor on + default claude-sonnet-5:high → claude-haiku-4-5:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_normal_high_nosp_famain claude-only · normal · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_normal_high_nosp_nofa claude-only · normal · high + thinking high · fallback on · advisor on + default claude-sonnet-5:high → claude-haiku-4-5:high + ● task claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-sonnet-5:high → claude-haiku-4-5:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:minimal + +claude-only_normal_xhigh_nosp_fa claude-only · normal · xhigh · fable + thinking xhigh · fallback on · advisor on + default claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-only_normal_xhigh_nosp_famain claude-only · normal · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-only_normal_xhigh_nosp_nofa claude-only · normal · xhigh + thinking xhigh · fallback on · advisor on + default claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-haiku-4-5:low + vision claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-haiku-4-5:low + commit claude-haiku-4-5:low + +claude-only_normal_max_nosp_fa claude-only · normal · max · fable + thinking max · fallback on · advisor on + default claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + slow claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● designer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol claude-sonnet-5:max + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-only_normal_max_nosp_famain claude-only · normal · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + slow claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● designer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol claude-sonnet-5:max + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-only_normal_max_nosp_nofa claude-only · normal · max + thinking max · fallback on · advisor on + default claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor claude-haiku-4-5:xhigh + vision claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol claude-sonnet-5:max + tiny claude-haiku-4-5:xhigh + commit claude-haiku-4-5:xhigh + +claude-only_smart_minimal_nosp_fa claude-only · smart · minimal · fable + thinking minimal · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + slow claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● designer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_minimal_nosp_famain claude-only · smart · minimal · fable · main + thinking minimal · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● task claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + slow claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● designer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● security-reviewer claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● librarian claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_minimal_nosp_nofa claude-only · smart · minimal + thinking minimal · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● task claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + plan claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + slow claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● designer claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● reviewer claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● security-reviewer claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● librarian claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + ● scout claude-sonnet-5:low → claude-haiku-4-5:minimal + ● sonic claude-sonnet-5:low → claude-haiku-4-5:minimal + advisor claude-sonnet-5:low → claude-haiku-4-5:minimal + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:minimal + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_low_nosp_fa claude-only · smart · low · fable + thinking low · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_low_nosp_famain claude-only · smart · low · fable · main + thinking low · fallback on · advisor on + default claude-fable-5:low → claude-opus-5:low → claude-sonnet-5:low + ● task claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + slow claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● designer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● security-reviewer claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_low_nosp_nofa claude-only · smart · low + thinking low · fallback on · advisor on + default claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● task claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + plan claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + slow claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● designer claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● reviewer claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● security-reviewer claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● librarian claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + ● scout claude-sonnet-5:low → claude-haiku-4-5:low + ● sonic claude-sonnet-5:low → claude-haiku-4-5:low + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_medium_nosp_fa claude-only · smart · medium · fable + thinking medium · fallback on · advisor on + default claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + slow claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● designer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_medium_nosp_famain claude-only · smart · medium · fable · main + thinking medium · fallback on · advisor on + default claude-fable-5:medium → claude-opus-5:medium → claude-sonnet-5:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + slow claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● designer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● security-reviewer claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_medium_nosp_nofa claude-only · smart · medium + thinking medium · fallback on · advisor on + default claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● task claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + plan claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + slow claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● designer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● reviewer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● security-reviewer claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● librarian claude-opus-5:medium → claude-sonnet-5:medium → claude-haiku-4-5:medium + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:low + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_high_nosp_fa claude-only · smart · high · fable + thinking high · fallback on · advisor on + default claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_high_nosp_famain claude-only · smart · high · fable · main + thinking high · fallback on · advisor on + default claude-fable-5:high → claude-opus-5:high → claude-sonnet-5:high + ● task claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_high_nosp_nofa claude-only · smart · high + thinking high · fallback on · advisor on + default claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● task claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-opus-5:high → claude-sonnet-5:high → claude-haiku-4-5:high + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:minimal + +claude-only_smart_xhigh_nosp_fa claude-only · smart · xhigh · fable + thinking xhigh · fallback on · advisor on + default claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:low + +claude-only_smart_xhigh_nosp_famain claude-only · smart · xhigh · fable · main + thinking xhigh · fallback on · advisor on + default claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + slow claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● designer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● security-reviewer claude-fable-5:xhigh → claude-opus-5:xhigh → claude-sonnet-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:low + +claude-only_smart_xhigh_nosp_nofa claude-only · smart · xhigh + thinking xhigh · fallback on · advisor on + default claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● task claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + plan claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + slow claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● designer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● security-reviewer claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● librarian claude-opus-5:xhigh → claude-sonnet-5:xhigh → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:medium → claude-haiku-4-5:medium + ● sonic claude-sonnet-5:medium → claude-haiku-4-5:medium + advisor claude-sonnet-5:high → claude-haiku-4-5:low + vision claude-opus-5:low → claude-sonnet-5:low → claude-haiku-4-5:low + smol claude-sonnet-5:medium + tiny claude-sonnet-5:low + commit claude-haiku-4-5:low + +claude-only_smart_max_nosp_fa claude-only · smart · max · fable + thinking max · fallback on · advisor on + default claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + slow claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● designer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol claude-sonnet-5:max + tiny claude-sonnet-5:max + commit claude-haiku-4-5:xhigh + +claude-only_smart_max_nosp_famain claude-only · smart · max · fable · main + thinking max · fallback on · advisor on + default claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● task claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + slow claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● designer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● security-reviewer claude-fable-5:max → claude-opus-5:max → claude-sonnet-5:max + ● librarian claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol claude-sonnet-5:max + tiny claude-sonnet-5:max + commit claude-haiku-4-5:xhigh + +claude-only_smart_max_nosp_nofa claude-only · smart · max + thinking max · fallback on · advisor on + default claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● task claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + plan claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + slow claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● designer claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● reviewer claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● security-reviewer claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● librarian claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● scout claude-sonnet-5:max → claude-haiku-4-5:xhigh + ● sonic claude-sonnet-5:max → claude-haiku-4-5:xhigh + advisor claude-sonnet-5:max → claude-haiku-4-5:xhigh + vision claude-opus-5:max → claude-sonnet-5:max → claude-haiku-4-5:xhigh + smol claude-sonnet-5:max + tiny claude-sonnet-5:max + commit claude-haiku-4-5:xhigh + diff --git a/vault.go b/vault.go index d1955e7..25ada86 100644 --- a/vault.go +++ b/vault.go @@ -15,15 +15,16 @@ import ( "time" ) -const ( - anthropicProvider = "anthropic" - openAIProvider = "openai-codex" -) - +// account is one broker credential's identity. apiKey is deliberately +// unexported: it exists only in memory for direct balance fetches (DeepSeek) +// and must never reach any serialized artifact — usage cache, account state, +// or the forwarded account pool. type account struct { - Provider string - IdentityKey string - Email string + Provider string + IdentityKey string + Email string + apiKey string + credentialID string } type accountKey struct { @@ -108,12 +109,17 @@ func loadAccounts(broker brokerConfig) (map[string][]account, error) { } func emptyAccounts() map[string][]account { - return map[string][]account{anthropicProvider: {}, openAIProvider: {}} + accounts := make(map[string][]account, len(providerRegistry)) + for _, p := range providerRegistry { + accounts[p.ID] = []account{} + } + return accounts } func parseAccountSnapshot(body []byte) (map[string][]account, error) { var snapshot struct { Credentials *[]struct { + ID json.RawMessage `json:"id"` Provider string `json:"provider"` IdentityKey string `json:"identityKey"` Credential json.RawMessage `json:"credential"` @@ -132,12 +138,13 @@ func parseAccountSnapshot(body []byte) (map[string][]account, error) { accounts := emptyAccounts() seen := make(map[accountKey]bool) for _, item := range *snapshot.Credentials { - if item.Provider != anthropicProvider && item.Provider != openAIProvider { + if providerByID(item.Provider) == nil { continue } var credential struct { Type string `json:"type"` Email string `json:"email"` + Key string `json:"key"` } if len(item.Credential) == 0 || bytes.Equal(item.Credential, []byte("null")) { return nil, fmt.Errorf("broker snapshot account %s/%s has no credential metadata", item.Provider, item.IdentityKey) @@ -145,7 +152,22 @@ func parseAccountSnapshot(body []byte) (map[string][]account, error) { if err := json.Unmarshal(item.Credential, &credential); err != nil { return nil, fmt.Errorf("invalid credential metadata for %s/%s: %w", item.Provider, item.IdentityKey, err) } - if credential.Type != "oauth" { + switch credential.Type { + case "oauth": + case "api_key": + // API-key credentials have no broker identity (identityKey is + // null) and no account-pool routing. For unmetered providers + // (DeepSeek) they are display-only rows whose key is retained in + // memory for direct balance fetches; for metered providers they + // are skipped exactly as before — OAuth is the only usable shape. + if p := providerByID(item.Provider); !p.Metered { + accounts[item.Provider] = append(accounts[item.Provider], account{ + Provider: item.Provider, IdentityKey: item.IdentityKey, + apiKey: credential.Key, credentialID: strings.Trim(string(item.ID), `"`), + }) + } + continue + default: continue } if strings.TrimSpace(item.IdentityKey) == "" { @@ -160,7 +182,8 @@ func parseAccountSnapshot(body []byte) (map[string][]account, error) { Provider: item.Provider, IdentityKey: item.IdentityKey, Email: credential.Email, }) } - for _, provider := range []string{anthropicProvider, openAIProvider} { + for _, p := range providerRegistry { + provider := p.ID sort.Slice(accounts[provider], func(i, j int) bool { return accounts[provider][i].IdentityKey < accounts[provider][j].IdentityKey }) @@ -367,7 +390,7 @@ func decodeAccountStateEntries(entries []accountStateEntry) (map[accountKey]bool } disabled := make(map[accountKey]bool, len(entries)) for _, entry := range entries { - if (entry.Provider != anthropicProvider && entry.Provider != openAIProvider) || entry.IdentityKey == "" { + if providerByID(entry.Provider) == nil || entry.IdentityKey == "" { return nil, false } key := accountKey{Provider: entry.Provider, IdentityKey: entry.IdentityKey} @@ -385,7 +408,7 @@ func encodeAccountStateEntries(disabled map[accountKey]bool) ([]accountStateEntr if !isDisabled { continue } - if (key.Provider != anthropicProvider && key.Provider != openAIProvider) || key.IdentityKey == "" { + if providerByID(key.Provider) == nil || key.IdentityKey == "" { return nil, fmt.Errorf("invalid disabled account %q/%q", key.Provider, key.IdentityKey) } entries = append(entries, accountStateEntry{Provider: key.Provider, IdentityKey: key.IdentityKey}) @@ -488,9 +511,17 @@ func atomicPrivateWrite(path string, body []byte) error { return nil } +// buildAccountPool seeds the account-pool file omp routes OAuth identities +// through. Only metered (OAuth) providers belong: api_key providers have no +// identity keys and no pool routing. func buildAccountPool(accounts map[string][]account, disabled map[accountKey]bool) map[string][]string { - pool := map[string][]string{anthropicProvider: {}, openAIProvider: {}} - for _, provider := range []string{anthropicProvider, openAIProvider} { + pool := make(map[string][]string, len(providerRegistry)) + for _, p := range providerRegistry { + if !p.Metered { + continue + } + provider := p.ID + pool[provider] = []string{} for _, acct := range accounts[provider] { key := accountKey{Provider: provider, IdentityKey: acct.IdentityKey} if acct.Provider == provider && acct.IdentityKey != "" && !disabled[key] { @@ -594,3 +625,86 @@ func stripProfileArgs(args []string) []string { } return clean } + +// ── DeepSeek balance ────────────────────────────────────────────────────────── +// DeepSeek publishes no rate-limit windows (prepaid pay-as-you-go), so omp's +// usage report never carries it. The upstream balance endpoint is the only +// quota-like surface; it is queried directly with the api_key retained from +// the broker snapshot — the key lives in memory only and is never serialized. + +// deepseekBalanceURL is a package var so tests can point it at an httptest +// server. +var deepseekBalanceURL = "https://api.deepseek.com/user/balance" + +// deepseekBalance is the rendered state of the DeepSeek prepaid balance. +// A nil *deepseekBalance means "no DeepSeek credential" (group hidden); +// ok=false means the credential exists but the balance is unavailable. +type deepseekBalance struct { + ok bool + currency string + total string + fetchedAt int64 + stale bool // restored from cache or retained across a failed refresh +} + +// fetchDeepSeekBalance queries the upstream balance endpoint with the +// account's API key. Numeric fields arrive as JSON strings per the DeepSeek +// docs; json.Number tolerates bare numbers too. The USD entry wins when +// present, else the first one; an empty list or is_available=false degrades +// to the unavailable state without error. +func fetchDeepSeekBalance(key string) (deepseekBalance, error) { + req, err := http.NewRequest(http.MethodGet, deepseekBalanceURL, nil) + if err != nil { + return deepseekBalance{}, err + } + req.Header.Set("Authorization", "Bearer "+key) + resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req) + if err != nil { + return deepseekBalance{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, resp.Body) + return deepseekBalance{}, fmt.Errorf("balance endpoint returned %s", resp.Status) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return deepseekBalance{}, err + } + var doc struct { + IsAvailable bool `json:"is_available"` + BalanceInfos []struct { + Currency string `json:"currency"` + TotalBalance json.Number `json:"total_balance"` + } `json:"balance_infos"` + } + if err := json.Unmarshal(body, &doc); err != nil { + return deepseekBalance{}, fmt.Errorf("invalid balance payload: %w", err) + } + now := time.Now().Unix() + if !doc.IsAvailable || len(doc.BalanceInfos) == 0 { + return deepseekBalance{fetchedAt: now}, nil + } + pick := doc.BalanceInfos[0] + for _, info := range doc.BalanceInfos { + if strings.EqualFold(info.Currency, "USD") { + pick = info + break + } + } + return deepseekBalance{ + ok: true, currency: strings.ToUpper(pick.Currency), + total: pick.TotalBalance.String(), fetchedAt: now, + }, nil +} + +// deepseekAPIKey finds the in-memory DeepSeek api_key in a parsed snapshot; +// "" when no such credential exists. +func deepseekAPIKey(accounts map[string][]account) string { + for _, acct := range accounts[deepseekProvider] { + if acct.apiKey != "" { + return acct.apiKey + } + } + return "" +} diff --git a/vault_test.go b/vault_test.go index 897f879..6fa63d2 100644 --- a/vault_test.go +++ b/vault_test.go @@ -38,7 +38,7 @@ func TestLoadAccountsValidatesFiltersAndSortsCentralSnapshot(t *testing.T) { if authorization != "Bearer central-secret" { t.Fatalf("authorization = %q", authorization) } - if len(got) != 2 || len(got[anthropicProvider]) != 3 || len(got[openAIProvider]) != 2 { + if len(got) != len(providerRegistry) || len(got[anthropicProvider]) != 3 || len(got[openAIProvider]) != 2 || len(got[deepseekProvider]) != 0 { t.Fatalf("account groups = %#v", got) } if keys := accountKeys(got[anthropicProvider]); !reflect.DeepEqual(keys, []string{"a-claude", "b-claude", "c-claude"}) { @@ -437,3 +437,123 @@ func strconvQuote(value string) string { body, _ := json.Marshal(value) return string(body) } + +// TestFetchDeepSeekBalance covers the upstream balance contract: numeric +// fields as JSON strings, USD preferred over other currencies, and the +// documented degrade paths (is_available=false, HTTP failure). +func TestFetchDeepSeekBalance(t *testing.T) { + var authorization string + payload := `{"is_available":true,"balance_infos":[ + {"currency":"CNY","total_balance":"110.00","granted_balance":"10.00","topped_up_balance":"100.00"}, + {"currency":"USD","total_balance":"12.34","granted_balance":"0.00","topped_up_balance":"12.34"} + ]}` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authorization = r.Header.Get("Authorization") + _, _ = w.Write([]byte(payload)) + })) + defer server.Close() + saved := deepseekBalanceURL + deepseekBalanceURL = server.URL + defer func() { deepseekBalanceURL = saved }() + + got, err := fetchDeepSeekBalance("sk-test") + if err != nil { + t.Fatal(err) + } + if authorization != "Bearer sk-test" { + t.Fatalf("authorization = %q", authorization) + } + if !got.ok || got.currency != "USD" || got.total != "12.34" { + t.Fatalf("balance = %+v, want the USD entry", got) + } + + payload = `{"is_available":true,"balance_infos":[{"currency":"CNY","total_balance":"7.5"}]}` + if got, err = fetchDeepSeekBalance("sk-test"); err != nil || !got.ok || got.currency != "CNY" || got.total != "7.5" { + t.Fatalf("no-USD fallback = %+v (err %v), want the first entry", got, err) + } + + payload = `{"is_available":false,"balance_infos":[]}` + if got, err = fetchDeepSeekBalance("sk-test"); err != nil || got.ok { + t.Fatalf("unavailable account must degrade without error, got %+v (err %v)", got, err) + } + + server.Close() + if _, err := fetchDeepSeekBalance("sk-test"); err == nil { + t.Fatal("transport failure must surface as an error") + } +} + +// TestFetchDeepSeekBalanceHTTPError: a non-200 answer is an error, not a +// zero balance. +func TestFetchDeepSeekBalanceHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + saved := deepseekBalanceURL + deepseekBalanceURL = server.URL + defer func() { deepseekBalanceURL = saved }() + if _, err := fetchDeepSeekBalance("sk-test"); err == nil { + t.Fatal("HTTP 500 must surface as an error") + } +} + +// TestDeepSeekKeyStaysInMemory is the security boundary of the balance +// feature: the api_key parsed from the broker snapshot must exist only on the +// in-memory account struct and never reach any serialized artifact — neither +// the usage cache (which marshals the accounts map) nor the account state. +func TestDeepSeekKeyStaysInMemory(t *testing.T) { + snapshot := []byte(`{"credentials":[ + {"id":7,"provider":"deepseek","identityKey":null,"credential":{"type":"api_key","key":"sk-secret-x"}}, + {"provider":"anthropic","identityKey":"a-claude","credential":{"type":"oauth","email":"a@example.com"}} + ]}`) + accounts, err := parseAccountSnapshot(snapshot) + if err != nil { + t.Fatal(err) + } + ds := accounts[deepseekProvider] + if len(ds) != 1 || ds[0].apiKey != "sk-secret-x" || ds[0].credentialID != "7" { + t.Fatalf("deepseek account = %+v, want in-memory key and credential id", ds) + } + if got := deepseekAPIKey(accounts); got != "sk-secret-x" { + t.Fatalf("deepseekAPIKey = %q", got) + } + + // The usage cache serializes the accounts map wholesale: the unexported + // key field must not survive the round trip. + body, err := json.Marshal(usageCacheFile{SavedAt: 1, Accounts: accounts}) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(body), "sk-") { + t.Fatalf("api key leaked into the usage cache payload: %s", body) + } + + // The forwarded account pool is OAuth-only; deepseek must not appear. + pool := buildAccountPool(accounts, nil) + if _, ok := pool[deepseekProvider]; ok { + t.Fatalf("account pool grew an api_key provider: %#v", pool) + } + + // Account state persists only {provider, identityKey} pairs of known + // providers; a deepseek OAuth-style entry would be inventable only by + // hand and must round-trip (registry filter, not the old two-literal + // allow-list). + statePath := filepath.Join(t.TempDir(), "state.json") + state := defaultAccountSelectionState() + state.SetManualDisabled(map[accountKey]bool{{Provider: deepseekProvider, IdentityKey: "k"}: true}) + if err := writeAccountSelectionState(statePath, state); err != nil { + t.Fatal(err) + } + raw, err := os.ReadFile(statePath) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(raw), "sk-") { + t.Fatalf("api key leaked into account state: %s", raw) + } + loaded := loadAccountSelectionState(statePath) + if !loaded.CurrentDisabled()[accountKey{Provider: deepseekProvider, IdentityKey: "k"}] { + t.Fatalf("registry-known provider entry did not round-trip: %#v", loaded.CurrentDisabled()) + } +} From 1c887c1fb1506405fb856b5b0f9c541fecf232c8 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Fri, 14 Aug 2026 22:39:28 +0000 Subject: [PATCH 2/8] feat(manager): show DeepSeek balance under the credential row The account manager's DeepSeek box showed only the display-only API-key row; the prepaid balance - its single usage datum - lived in the Usage panel alone. Mirror deepseekBalanceRow under the credential, including the unavailable/stale degrade, so v answers 'how much is left' too. --- manager.go | 10 ++++++++++ manager_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/manager.go b/manager.go index fe8305f..5d9f7ec 100644 --- a/manager.go +++ b/manager.go @@ -691,6 +691,16 @@ func (m model) managerLines(width int) []managerLine { }) } } + // The DeepSeek prepaid balance is the api-key row's only usage + // datum — mirror the Usage panel's row under the credential. + if a.apiKey != "" && provider == deepseekProvider && m.avail.deepseek != nil { + lines = append(lines, managerLine{ + text: managerClipCell(" "+m.deepseekBalanceRow(*m.avail.deepseek), lineWidth), + selectable: -1, + group: group, + provider: provider, + }) + } group++ if accountIndex+1 < len(accounts) { lines = append(lines, managerLine{ diff --git a/manager_test.go b/manager_test.go index a15b4ec..1f4bfcc 100644 --- a/manager_test.go +++ b/manager_test.go @@ -1788,3 +1788,37 @@ func TestManagerUsageControlsFollowContextWithoutPresetSaveCollision(t *testing. t.Fatalf("naming modal leaked Usage controls: %s", naming) } } + +// TestManagerDeepSeekBalanceRow: the DeepSeek box mirrors the Usage panel's +// prepaid balance under the display-only credential row - and degrades to the +// explicit unavailable text rather than dropping the row, so a flaky upstream +// stays visible. +func TestManagerDeepSeekBalanceRow(t *testing.T) { + m := managerTestModel(t) + m.avail.accounts[deepseekProvider] = []account{ + {Provider: deepseekProvider, apiKey: "sk-test", credentialID: "19"}, + } + m.avail.deepseek = &deepseekBalance{ok: true, currency: "USD", total: "18.03"} + + flat := func() string { + var b strings.Builder + for _, ln := range m.managerLines(100) { + b.WriteString(stripAnsi(ln.text)) + b.WriteString("\n") + } + return b.String() + } + + view := flat() + if !strings.Contains(view, "API key · credential #19") { + t.Fatalf("manager lost the DeepSeek credential row:\n%s", view) + } + if !strings.Contains(view, "balance $18.03 USD · pay-as-you-go") { + t.Errorf("manager DeepSeek box lacks the balance row:\n%s", view) + } + + m.avail.deepseek = &deepseekBalance{ok: false} + if view := flat(); !strings.Contains(view, "balance unavailable") { + t.Errorf("failed balance must render as unavailable, not vanish:\n%s", view) + } +} From 198da3fcb1c64192c9e94eb21a719f1dda9fab30 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Fri, 14 Aug 2026 23:02:42 +0000 Subject: [PATCH 3/8] feat: relief dial, quota-aware suggestions, balance guard, off-peak pricing, thinking gauge Design round 2 with the operator (2026-08-14): - relief dial: catalogs with an optional pool double their metered-led blends into _rel/_norel variants; off strips exactly the pay-as-you-go tail. The segment (and dial) exist only in such catalogs, so two-pool output stays byte-identical and old ids keep resolving. - quota-aware lane suggestion: deriveToggles moves a proposal off a lane whose lead pool is maxed/unauthed to the first sibling led lane with headroom (fallbackPoolOrder), gated for DeepSeek by the prepaid balance. Manual dials are never rewritten. - balance guard: under $2 the balance row warns 'low', suggestions stop proposing ds lanes and turn relief off. Unknown balance counts as dry. - off-peak pricing: DeepSeek rungs cost 50% in the meter during UTC 16:30-00:30 and the balance row carries the live discount tag. The classifier deliberately does not chase the discount: spending money while metered quota is free stays an operator decision. - thinking gauge: the six-word dial renders as a level meter with the selected word beside it; arrow behavior and persistence unchanged. --- README.md | 13 ++-- generate.go | 56 ++++++++++++--- generate_test.go | 65 ++++++++++++++---- main.go | 143 +++++++++++++++++++++++++++++++------- main_test.go | 149 +++++++++++++++++++++++++++++++++++++--- providers.go | 14 ++++ selection_state.go | 5 ++ selection_state_test.go | 2 +- suggest.go | 68 ++++++++++++++++++ suggest_test.go | 90 ++++++++++++++++++++++++ 10 files changed, 539 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index db17b0f..a82b9e7 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,10 @@ are not orphaned onto init while still holding their memory. ## Features -- **Dials, not config files** — provider lane, model tier, thinking depth, - advisor level, plus the spark/fable toggles; every combination maps to a - pre-computed routing. +- **Dials, not config files** — provider lane, model tier, a thinking-depth + slider, advisor level, plus the spark/fable toggles; every combination maps + to a pre-computed routing. With a DeepSeek pool, a **relief** dial decides + whether drained metered chains may spill into the pay-as-you-go pool. - **Live preview** — see which model leads every role, and its fallback chain, before anything runs. - **One-shot overlays** — each launch is an ephemeral `--config`; your omp @@ -75,10 +76,14 @@ are not orphaned onto init while still holding their memory. - **Prompt → profile** — `ctrl+o`, describe the task, a small local model rates its difficulty and sets the dials (optional, needs [ollama](https://ollama.com); the prompt is forwarded into the session). + Suggestions are quota-aware: a lane whose lead pool is maxed falls to a + sibling with headroom, and a low DeepSeek balance stops proposals from + spending it. - **Usage at a glance** — quota bars and reset countdowns per provider, before you spend the scarce bucket. - **Account presets** — choose broker accounts and save reusable selections (`v`). -- **Cost & speed meters** — every dial change reprices the session. +- **Cost & speed meters** — every dial change reprices the session; DeepSeek + rungs are priced by the clock during its off-peak discount window. - **Guided first run** — no catalog? `code` builds one from your omp, interactively; `code generate` scripts the same thing. - **Argument passthrough** — `code ` just works. diff --git a/generate.go b/generate.go index ed8847a..7bdb869 100644 --- a/generate.go +++ b/generate.go @@ -359,6 +359,17 @@ func (c *catalog) reliefRungs(lead string) []string { return out } +// hasOptionalPool reports whether the catalog carries any non-required pool — +// the gate for the relief dial and its id segment. +func (c *catalog) hasOptionalPool() bool { + for _, pool := range c.pools() { + if p := providerByPool(pool); p != nil && !p.Required { + return true + } + } + return false +} + // sibDown is the same-pool fallback rung below a lead: the elite (tier 4) // falls to its pool's smart tier, tiers 2..3 fall one rung, the rest have no // same-pool net. @@ -518,7 +529,7 @@ type roleRoute struct { // genCombo computes {role -> route} for one facet combination. A direct port // of generate-profiles.py's gen(), with the hard-coded model keys generalised // to pool/tier lookups. -func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain bool) map[string]roleRoute { +func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain, relief bool) map[string]roleRoute { p := lanePrimary(lane) base := genTierMap[mtier] isPure := lanePure(lane) @@ -660,7 +671,7 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain lead = eliteKey } chain := c.buildChain(lead, isPure) - if !isPure && genRelief[r] { + if !isPure && relief && genRelief[r] { // Relief tails: led/mixed lanes end their heavyweight chains on // each optional pool's regular rung — pay-as-you-go capacity that // keeps a session alive when every metered window is drained. @@ -680,7 +691,7 @@ func repeatLvl(lvl string, n int) []string { return out } -func genComboID(lane, mtier, thinking string, spark, fable, fableMain bool) string { +func genComboID(lane, mtier, thinking string, spark, fable, fableMain, relief, hasRelief bool) string { sp, fa := "nosp", "nofa" if spark { sp = "sp" @@ -691,13 +702,24 @@ func genComboID(lane, mtier, thinking string, spark, fable, fableMain bool) stri fa = "famain" } } - return fmt.Sprintf("%s_%s_%s_%s_%s", lane, mtier, thinking, sp, fa) + id := fmt.Sprintf("%s_%s_%s_%s_%s", lane, mtier, thinking, sp, fa) + if hasRelief { + // The relief segment exists only in catalogs that carry an optional + // pool — two-pool ids stay byte-identical. + if relief { + id += "_rel" + } else { + id += "_norel" + } + } + return id } // genValid rejects facet combos the lane cannot host: a special-tier facet // (spark, fable) is valid only when its provider's pool is in the lane's -// pool-set — a pure lane hosts only its own pool. -func genValid(lane string, spark, fable, fableMain bool) bool { +// pool-set — a pure lane hosts only its own pool. relief=off exists only +// where relief tails could appear at all: a metered-led blend. +func genValid(lane string, spark, fable, fableMain, relief bool) bool { if fable && !laneHostsSpecial(lane, "fable") { return false // no elite outside its pool's lanes } @@ -707,14 +729,17 @@ func genValid(lane string, spark, fable, fableMain bool) bool { if fableMain && !fable { return false // fable-as-main only exists on top of fable } + if !relief && !laneReliefApplies(lane) { + return false // relief is not a choice where no tail is generated + } return true } // ── rendering (byte-compatible with generate-profiles.py) ──────────────────── -func (c *catalog) renderCombo(lane, mtier, thinking string, spark, fable, fableMain bool) string { - roles := c.genCombo(lane, mtier, thinking, spark, fable, fableMain) - cid := genComboID(lane, mtier, thinking, spark, fable, fableMain) +func (c *catalog) renderCombo(lane, mtier, thinking string, spark, fable, fableMain, relief, hasRelief bool) string { + roles := c.genCombo(lane, mtier, thinking, spark, fable, fableMain, relief) + cid := genComboID(lane, mtier, thinking, spark, fable, fableMain, relief, hasRelief) desc := []string{lane, mtier, thinking} if spark { desc = append(desc, "spark") @@ -725,6 +750,9 @@ func (c *catalog) renderCombo(lane, mtier, thinking string, spark, fable, fableM if fable && fableMain { desc = append(desc, "main") } + if !relief { + desc = append(desc, "no-relief") + } lines := []string{fmt.Sprintf("%s %s", cid, strings.Join(desc, " · "))} advOn := roles["advisor"].lead != "" adv := "off" @@ -833,6 +861,7 @@ func (c *catalog) renderCatalog() string { b.WriteString(c.renderModelFacts() + "\n") hasSpark := c.specialKey("spark") != "" hasElite := c.specialKey("fable") != "" + hasRelief := c.hasOptionalPool() for _, lane := range laneOrderForPools(c.pools()) { for _, mtier := range genMTiers { for _, thinking := range genThinking { @@ -845,8 +874,13 @@ func (c *catalog) renderCatalog() string { continue } for _, fableMain := range []bool{false, true} { - if genValid(lane, spark, fable, fableMain) { - b.WriteString(c.renderCombo(lane, mtier, thinking, spark, fable, fableMain) + "\n") + for _, relief := range []bool{true, false} { + if !relief && !hasRelief { + continue + } + if genValid(lane, spark, fable, fableMain, relief) { + b.WriteString(c.renderCombo(lane, mtier, thinking, spark, fable, fableMain, relief, hasRelief) + "\n") + } } } } diff --git a/generate_test.go b/generate_test.go index c412d1f..409f0a9 100644 --- a/generate_test.go +++ b/generate_test.go @@ -240,13 +240,13 @@ func TestGoldenCombos(t *testing.T) { render func() string }{ {"mixed_smart_medium_sp_fa", goldenMixedSmart, func() string { - return c.renderCombo("mixed", "smart", "medium", true, true, false) + return c.renderCombo("mixed", "smart", "medium", true, true, false, true, false) }}, {"claude-only_normal_max_nosp_famain", goldenClaudeMax, func() string { - return c.renderCombo("claude-only", "normal", "max", false, true, true) + return c.renderCombo("claude-only", "normal", "max", false, true, true, true, false) }}, {"claude-only_smart_medium_nosp_nofa", goldenClaudeSmart, func() string { - return c.renderCombo("claude-only", "smart", "medium", false, false, false) + return c.renderCombo("claude-only", "smart", "medium", false, false, false, true, false) }}, } { if got := tc.render(); got != tc.want { @@ -282,7 +282,7 @@ func TestRenderCatalogStructure(t *testing.T) { misses := 0 walk = func(i int) { if i == len(facets) { - id := comboID(sel) + id := comboID(sel, false) if !strings.Contains(out, "\n"+id+" ") { misses++ if misses < 5 { @@ -313,7 +313,7 @@ func TestEveryEmittedRoleIsWeighted(t *testing.T) { // genConfigYAML mirrors it into task.agentModelOverrides. func TestScoutIsAgentBacked(t *testing.T) { c := fixtureCatalog(t) - block := c.renderCombo("mixed", "normal", "medium", false, false, false) + block := c.renderCombo("mixed", "normal", "medium", false, false, false, true, false) if !strings.Contains(block, "● scout ") { t.Errorf("scout must render as an agent-backed role:\n%s", block) } @@ -377,7 +377,7 @@ func TestVisionSkipsTextOnlyModels(t *testing.T) { if lead := c.visionLead("O", 1); lead == "" || c.models[lead].ID != "gpt-5.6-terra" { t.Errorf("visionLead(O, 1) = %q, want the next image-capable rung (terra)", lead) } - block := c.renderCombo("gpt-only", "fast", "medium", false, false, false) + block := c.renderCombo("gpt-only", "fast", "medium", false, false, false, true, false) for _, l := range strings.Split(block, "\n") { if strings.Contains(l, " vision ") && strings.Contains(l, "codex-spark") { t.Errorf("vision must not route to a text-only model: %s", l) @@ -401,7 +401,7 @@ func TestVisionFollowsModelTier(t *testing.T) { {"mixed", "smart", "claude-opus-5"}, } { t.Run(tc.lane+"/"+tc.tier, func(t *testing.T) { - route := c.genCombo(tc.lane, tc.tier, "medium", false, false, false)["vision"] + route := c.genCombo(tc.lane, tc.tier, "medium", false, false, false, true)["vision"] if got := c.models[route.lead].ID; got != tc.want { t.Errorf("vision lead = %q, want %q", got, tc.want) } @@ -1199,7 +1199,7 @@ func TestThreePoolCatalog(t *testing.T) { out := c.renderCatalog() for _, lane := range []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ds-led", "ds-only"} { - if !strings.Contains(out, "\n"+lane+"_normal_medium_nosp_nofa ") { + if !strings.Contains(out, "\n"+lane+"_normal_medium_nosp_nofa_rel ") { t.Errorf("lane %s missing from the three-pool grid", lane) } } @@ -1208,7 +1208,7 @@ func TestThreePoolCatalog(t *testing.T) { strings.Contains(out, "\nds-only_normal_medium_nosp_fa") { t.Error("ds-only generated spark/fable combos it cannot host") } - for _, id := range []string{"ds-led_normal_medium_sp_nofa", "ds-led_normal_medium_nosp_fa", "ds-led_normal_medium_nosp_famain"} { + for _, id := range []string{"ds-led_normal_medium_sp_nofa_rel", "ds-led_normal_medium_nosp_fa_rel", "ds-led_normal_medium_nosp_famain_rel"} { if !strings.Contains(out, "\n"+id+" ") { t.Errorf("ds-led combo %s missing", id) } @@ -1242,7 +1242,7 @@ func TestThreePoolCatalog(t *testing.T) { // Relief tails: led/mixed heavyweight chains end on the DeepSeek regular // rung; pure lanes stay pure. for _, lane := range []string{"gpt-led", "mixed", "claude-led"} { - blk := block(lane + "_normal_medium_nosp_nofa") + blk := block(lane + "_normal_medium_nosp_nofa_rel") for _, role := range []string{"default", "task", "plan", "slow"} { if r := row(blk, role); !strings.HasSuffix(r, "→ deepseek-v4:medium") && !strings.HasSuffix(r, "→ deepseek-v4:high") { t.Errorf("%s %s chain lacks the DeepSeek relief tail: %q", lane, role, r) @@ -1253,14 +1253,14 @@ func TestThreePoolCatalog(t *testing.T) { } } for _, lane := range []string{"gpt-only", "claude-only"} { - if blk := block(lane + "_normal_medium_nosp_nofa"); strings.Contains(blk, "deepseek") { + if blk := block(lane + "_normal_medium_nosp_nofa_rel"); strings.Contains(blk, "deepseek") { t.Errorf("pure lane %s crossed into the DeepSeek pool:\n%s", lane, blk) } } // Vision purity exception: every DeepSeek rung is text-only, so ds-only's // vision role must cross pools rather than route images to a text model. - dsOnly := block("ds-only_smart_medium_nosp_nofa") + dsOnly := block("ds-only_smart_medium_nosp_nofa_rel") if r := row(dsOnly, "vision"); strings.Contains(r, "deepseek") || !strings.Contains(r, "gpt-5.6-sol:low") { t.Errorf("ds-only vision must cross to an image-capable pool: %q", r) } @@ -1302,7 +1302,7 @@ func TestOneRungOptionalPool(t *testing.T) { } } out := c.renderCatalog() - blkStart := strings.Index(out, "\nds-only_smart_medium_nosp_nofa ") + blkStart := strings.Index(out, "\nds-only_smart_medium_nosp_nofa_rel ") if blkStart < 0 { t.Fatal("one-rung D pool generated no ds-only lane") } @@ -1317,3 +1317,42 @@ func TestOneRungOptionalPool(t *testing.T) { t.Errorf("one-rung ds-only default should be the single model, got:\n%s", blk) } } + +// TestReliefToggle: _norel combos exist only on metered-led blends, and the +// off variant strips exactly the DeepSeek tail while everything else in the +// block stays identical. +func TestReliefToggle(t *testing.T) { + c, err := catalogFrom(t, fixtureYMLDeepSeek) + if err != nil { + t.Fatalf("loadCatalog: %v", err) + } + out := c.renderCatalog() + for _, lane := range []string{"gpt-led", "mixed", "claude-led"} { + if !strings.Contains(out, "\n"+lane+"_normal_medium_nosp_nofa_norel ") { + t.Errorf("%s lacks a relief-off combo", lane) + } + } + for _, lane := range []string{"gpt-only", "claude-only", "ds-led", "ds-only"} { + if strings.Contains(out, "\n"+lane+"_normal_medium_nosp_nofa_norel ") { + t.Errorf("%s generated a relief-off combo it cannot use", lane) + } + } + on := c.renderCombo("gpt-led", "normal", "medium", false, false, false, true, true) + off := c.renderCombo("gpt-led", "normal", "medium", false, false, false, false, true) + if !strings.Contains(on, "deepseek") { + t.Fatalf("relief-on block lost its tail:\n%s", on) + } + if strings.Contains(off, "deepseek") { + t.Errorf("relief-off block still spills into DeepSeek:\n%s", off) + } + strip := func(s string) string { + s = strings.ReplaceAll(s, " → deepseek-v4:medium", "") + s = strings.ReplaceAll(s, " → deepseek-v4:high", "") + s = strings.ReplaceAll(s, "_rel ", " ") + s = strings.ReplaceAll(s, "_norel ", " ") + return strings.ReplaceAll(s, " · no-relief", "") + } + if strip(on) != strip(off) { + t.Errorf("relief must only add/remove tails; blocks diverge:\n--- on ---\n%s\n--- off ---\n%s", on, off) + } +} diff --git a/main.go b/main.go index 87fe477..924c731 100644 --- a/main.go +++ b/main.go @@ -76,7 +76,7 @@ var keys = keyMap{ // defaultSel returns a fresh copy of the generator's default facet selection — // used both to seed the model and to restore it via the reset key. func defaultSel() map[string]string { - return map[string]string{"lane": "mixed", "model": "smart", "thinking": "medium", "advisor": "glance", "spark": "on", "fable": "off", "main": "off", "fast": "off"} + return map[string]string{"lane": "mixed", "model": "smart", "thinking": "medium", "advisor": "glance", "spark": "on", "fable": "off", "main": "off", "fast": "off", "relief": "on"} } // ── palette ────────────────────────────────────────────────────────────────── @@ -1053,6 +1053,10 @@ func facetDefs(glyphs map[string]string) []facet { // A sub-setting of fable — only visible while fable is on (see // visibleFacets) and never set by a suggestion (see validFacetActions). {"main", []string{"on", "off"}, glyphs["main"]}, + // relief: whether metered-led chains may spill into a pay-as-you-go + // pool's tail rung. Only rendered when the catalog carries an + // optional pool and the lane is a metered-led blend. + {"relief", []string{"on", "off"}, glyphs["relief"]}, } } @@ -1174,7 +1178,7 @@ const ( // currentRows is the routing block the cost/speed meters score: the generator's // facet combo with the advisor dial applied. func (m model) currentRows() []string { - return m.applyAdvisor(m.generated[comboID(m.sel)], m.sel["advisor"]) + return m.applyAdvisor(m.generated[comboID(m.sel, m.hasRelief)], m.sel["advisor"]) } func (m model) weightedModels(rows []string, fn func(w float64, id, lvl string)) { @@ -1210,6 +1214,19 @@ func logScore(idx, lnLo, lnHi float64) int { return int(math.Round(math.Max(1, math.Min(5, s)))) } +// DeepSeek discounts the pay-as-you-go API during its off-peak window, +// UTC 16:30–00:30 (deepseek.com/pricing). The meter prices D rungs by the +// clock so a cheap window reads as the discount it is. offPeakNow is a var +// for tests only. +const deepseekOffPeakMult = 0.5 + +var offPeakNow = time.Now + +func deepseekOffPeak(utc time.Time) bool { + m := utc.Hour()*60 + utc.Minute() + return m >= 16*60+30 || m < 30 +} + // costScore rates the current config from 1 (cheap) to 5 (dear). func (m model) costScore() int { fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") @@ -1227,6 +1244,9 @@ func (m model) costScore() int { if fast && m.poolOfModel(id) == "O" { cost *= priorityMult } + if m.poolOfModel(id) == "D" && deepseekOffPeak(offPeakNow().UTC()) { + cost *= deepseekOffPeakMult + } num += w * cost den += w }) @@ -1366,13 +1386,17 @@ func (m model) visibleFacets() []facet { if f.key == "main" && m.sel["fable"] != "on" { continue } + case "relief": + if !m.hasRelief || !laneReliefApplies(lane) { + continue + } } out = append(out, f) } return out } -func comboID(sel map[string]string) string { +func comboID(sel map[string]string, hasRelief bool) string { lane := sel["lane"] sp, fb := sel["spark"], sel["fable"] if !laneHostsSpecial(lane, "fable") { @@ -1391,7 +1415,19 @@ func comboID(sel map[string]string) string { faid = "famain" } } - return fmt.Sprintf("%s_%s_%s_%s_%s", lane, sel["model"], sel["thinking"], spid, faid) + id := fmt.Sprintf("%s_%s_%s_%s_%s", lane, sel["model"], sel["thinking"], spid, faid) + if hasRelief { + rel := sel["relief"] + if !laneReliefApplies(lane) { + rel = "on" // the only variant generated there + } + if rel == "off" { + id += "_norel" + } else { + id += "_rel" + } + } + return id } // applyCatalog records which dials this catalog can actually serve, then forces @@ -1408,7 +1444,7 @@ func (m *model) applyCatalog() { if len(m.generated) == 0 { return // no catalog read yet: onboarding, or a broken CODE_GENERATED } - spark, fable := false, false + spark, fable, relief := false, false, false for id := range m.generated { for _, seg := range strings.Split(id, "_") { switch seg { @@ -1416,10 +1452,12 @@ func (m *model) applyCatalog() { spark = true case "fa", "famain": fable = true + case "norel": + relief = true } } } - m.noSpark, m.noFable = !spark, !fable + m.noSpark, m.noFable, m.hasRelief = !spark, !fable, relief if lanes := catalogLanes(m.generated); len(lanes) > 0 { for i := range m.facets { if m.facets[i].key == "lane" { @@ -1473,6 +1511,9 @@ func (m *model) clampSel() { m.sel["fable"] = "off" m.sel["main"] = "off" } + if !m.hasRelief { + m.sel["relief"] = "on" + } // A persisted lane the applied catalog no longer generates (or one from a // richer catalog) resets to the dial's first lane. for _, f := range m.facets { @@ -1545,7 +1586,7 @@ func (m model) poolOfModel(id string) string { // would keep the five agent-backed types pinned regardless of the generated // profile (issue atyrode/dotfiles#173). func (m model) genConfigYAML() string { - rows := m.applyAdvisor(m.generated[comboID(m.sel)], m.sel["advisor"]) + rows := m.applyAdvisor(m.generated[comboID(m.sel, m.hasRelief)], m.sel["advisor"]) var mr, fc, ao strings.Builder advisorOn := false for _, r := range rows { @@ -1963,6 +2004,9 @@ type model struct { // always did. applyCatalog sets these only from a catalog it actually read. noSpark bool // no _sp_ combos exist — hide the spark dial and force it off noFable bool // no _fa_/_famain_ combos — same for fable and its main child + // hasRelief is positive-polarity: the relief segment only exists in + // catalogs with an optional pool, so the zero value keeps old ids intact. + hasRelief bool // _rel_/_norel combos exist — show the relief dial depth int // 0 lead · 1 full collapse bool // p: hide the Routing section @@ -2779,12 +2823,21 @@ func formatCachedAge(observed int64, now time.Time) string { } // deepseekBalanceRow renders the DeepSeek group's single body row: the prepaid -// balance — no bar, no window, no reset countdown, because none exist. +// balance — no bar, no window, no reset countdown, because none exist. A +// balance under the suggestion floor carries an explicit "low" cue (the same +// threshold that stops proposals from spending the pool), and the off-peak +// discount window is surfaced while it is live. func (m *model) deepseekBalanceRow(b deepseekBalance) string { if !b.ok { return " " + stWarn.Render("balance unavailable") } row := " balance " + stHead.Render("$"+b.total+" "+b.currency) + stDim.Render(" · pay-as-you-go") + if v, err := strconv.ParseFloat(b.total, 64); err == nil && v < deepseekLowBalanceUSD { + row += " " + stWarn.Render("low") + } + if deepseekOffPeak(offPeakNow().UTC()) { + row += " " + stDim.Render("off-peak −50%") + } if b.stale { cached := "cached" if b.fetchedAt > 0 { @@ -2963,7 +3016,7 @@ func (m *model) syncPreviewAt(yoff int) { // generator list on the left, so the preview shows only what that selection // produces — the role → model routing itself. var b strings.Builder - id := comboID(m.sel) + id := comboID(m.sel, m.hasRelief) if base, ok := m.generated[id]; ok { _, roles := splitMeta(base) roles = m.applyAdvisor(roles, m.sel["advisor"]) @@ -3297,7 +3350,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // empty, handing omp a session with no routing at all. The preview // already says "no profile for this combination", so the key does // nothing rather than launching something broken. - if _, ok := m.generated[comboID(m.sel)]; !ok { + if _, ok := m.generated[comboID(m.sel, m.hasRelief)]; !ok { return m, nil } m.genConfig = m.genConfigYAML() @@ -3604,22 +3657,26 @@ func (m model) genLines() ([]string, int) { label, childPad, childW = "default", stDim.Render("└ "), 2 } row := fmt.Sprintf("%s%s%s%s", ptr, childPad, gly, stDim.Render(pad(label, 9-childW))) - for _, v := range f.values { - switch { - case v == m.sel[f.key]: - col := acc - if f.key == "lane" { - col = laneColor(v) - } else if (f.key == "spark" || f.key == "fable" || f.key == "main") && v == "on" { - col = cGreen - } - st := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true) - if onRow { // the cursor sits on the selected value of the focused row - st = st.Background(lipgloss.Color(cSelBg)) + if f.key == "thinking" { + row += " " + m.thinkingGauge(f, onRow, acc) + } else { + for _, v := range f.values { + switch { + case v == m.sel[f.key]: + col := acc + if f.key == "lane" { + col = laneColor(v) + } else if (f.key == "spark" || f.key == "fable" || f.key == "main") && v == "on" { + col = cGreen + } + st := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true) + if onRow { // the cursor sits on the selected value of the focused row + st = st.Background(lipgloss.Color(cSelBg)) + } + row += " " + st.Render(" "+v+" ") + default: + row += " " + stDim.Render(v) } - row += " " + st.Render(" "+v+" ") - default: - row += " " + stDim.Render(v) } } switch { @@ -3643,6 +3700,39 @@ func (m model) genLines() ([]string, int) { return lines, cursor } +// thinkingGauge renders the thinking dial as a level meter instead of six +// words: rising bars fill to the selected depth, the current word rides along +// so the level still reads at a glance. ←/→ behave exactly as on a word dial — +// only the rendering differs; the facet's value list is untouched. +func (m model) thinkingGauge(f facet, onRow bool, acc string) string { + bars := []rune("▁▂▃▅▇█") + sel := -1 + for i, v := range f.values { + if v == m.sel[f.key] { + sel = i + } + } + lit := lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Bold(true) + var b strings.Builder + for i := range f.values { + g := string(bars[i*len(bars)/max(1, len(f.values))]) + if i <= sel { + b.WriteString(lit.Render(g)) + } else { + b.WriteString(stDim.Render(g)) + } + } + word := lit + if onRow { + word = word.Background(lipgloss.Color(cSelBg)) + } + label := m.sel[f.key] + if sel < 0 && label == "" { + label = "?" + } + return b.String() + " " + word.Render(" "+label+" ") +} + // defaultGlyphs is the built-in facet-glyph set (Nerd Font, Font Awesome PUA // range), written as explicit \u escapes so the codepoints stay visible and // verifiable in source — a literal PUA glyph is invisible in most editors and @@ -3655,6 +3745,7 @@ func defaultGlyphs() map[string]string { return map[string]string{ "lane": "\uf127", "model": "\uf085", "thinking": "\uf0eb", "advisor": "\uf14e", "spark": "\uf135", "fable": "\uf02d", "main": "\uf140", "fast": "\uf0e7", + "relief": "\uf132", } } @@ -3756,7 +3847,7 @@ func main() { fm.firstPrompt, fm.broker, fm.accountSelections) }) case fm.genConfig != "": - status = withSession(comboID(fm.sel), "CODE_OMP", []string{"omp"}, func() int { + status = withSession(comboID(fm.sel, fm.hasRelief), "CODE_OMP", []string{"omp"}, func() int { return launchGenerated(fm.genConfig, fm.firstPrompt, fm.broker, fm.accountSelections) }) } diff --git a/main_test.go b/main_test.go index f4bd172..a878d0d 100644 --- a/main_test.go +++ b/main_test.go @@ -213,7 +213,7 @@ func TestComboID(t *testing.T) { {map[string]string{"lane": "gpt-only", "model": "smart", "thinking": "high", "spark": "on", "fable": "on", "main": "on"}, "gpt-only_smart_high_sp_nofa"}, } for _, c := range cases { - if got := comboID(c.sel); got != c.want { + if got := comboID(c.sel, false); got != c.want { t.Errorf("comboID(%v) = %q, want %q", c.sel, got, c.want) } } @@ -387,7 +387,7 @@ func TestLaunchKeys(t *testing.T) { } base := model{ sel: defaultSel(), - generated: map[string][]string{comboID(defaultSel()): rows}, + generated: map[string][]string{comboID(defaultSel(), false): rows}, } next, _ := base.Update(tea.KeyMsg{Type: tea.KeyEnter}) @@ -436,7 +436,7 @@ func TestGenConfigYAMLAgentOverrides(t *testing.T) { } m := model{ sel: defaultSel(), - generated: map[string][]string{comboID(defaultSel()): rows}, + generated: map[string][]string{comboID(defaultSel(), false): rows}, } m.sel["advisor"] = "off" got := m.genConfigYAML() @@ -468,6 +468,7 @@ func TestDefaultGlyphs(t *testing.T) { want := map[string]rune{ "lane": 0xf127, "model": 0xf085, "thinking": 0xf0eb, "advisor": 0xf14e, "spark": 0xf135, "fable": 0xf02d, "main": 0xf140, "fast": 0xf0e7, + "relief": 0xf132, } g := defaultGlyphs() if len(g) != len(want) { @@ -581,7 +582,7 @@ func TestApplyAdvisorFableMain(t *testing.T) { // settings-summary line reaches the preview (the dials are visible on the // left). func TestPreviewColumn(t *testing.T) { - id := comboID(defaultSel()) + id := comboID(defaultSel(), false) m := model{ generated: map[string][]string{id: { " thinking medium · fallback on · advisor on", @@ -620,7 +621,7 @@ func TestPreviewColumn(t *testing.T) { // tests exercise the actual compositions rather than skeleton fixtures. func layoutModel() model { glyphs := defaultGlyphs() - id := comboID(defaultSel()) + id := comboID(defaultSel(), false) rows := []string{ " thinking medium · fallback on · advisor on", " default gpt-5.6-terra:medium → gpt-5.6-luna:medium → claude-sonnet-5:medium", @@ -967,7 +968,7 @@ func TestRepeatedResizeCrossingsPreserveState(t *testing.T) { // shrinks the panel; a facet change still resets to the top. func TestResizeScrollClamp(t *testing.T) { m := layoutModel() - id := comboID(defaultSel()) + id := comboID(defaultSel(), false) rows := []string{" thinking medium · fallback on · advisor on"} for i := range 40 { rows = append(rows, fmt.Sprintf(" role%02d gpt-5.6-terra:medium", i)) @@ -1811,7 +1812,7 @@ func TestSectionStatePreservation(t *testing.T) { // Restoring routing recovers the prior scroll position. sc := layoutModel() - id := comboID(defaultSel()) + id := comboID(defaultSel(), false) rows := []string{" thinking medium · fallback on · advisor on"} for i := range 40 { rows = append(rows, fmt.Sprintf(" role%02d gpt-5.6-terra:medium", i)) @@ -2522,7 +2523,7 @@ func TestFirstLoadBarFill(t *testing.T) { // facet selection. func TestRoutingWheelScroll(t *testing.T) { long := layoutModel() - id := comboID(defaultSel()) + id := comboID(defaultSel(), false) rows := []string{" thinking medium · fallback on · advisor on"} for i := range 60 { rows = append(rows, fmt.Sprintf(" role%02d gpt-5.6-terra:medium", i)) @@ -3670,8 +3671,8 @@ func TestGenConfigYAMLDeepSeekLane(t *testing.T) { m.sel["spark"], m.sel["fable"], m.sel["fast"] = "off", "off", "off" m.sel["advisor"] = "audit" - if _, ok := m.generated[comboID(m.sel)]; !ok { - t.Fatalf("no generated block for %s", comboID(m.sel)) + if _, ok := m.generated[comboID(m.sel, m.hasRelief)]; !ok { + t.Fatalf("no generated block for %s", comboID(m.sel, m.hasRelief)) } m.ompMajor, m.ompMinor = 17, 3 @@ -3725,7 +3726,7 @@ func TestApplyCatalogGrowsLaneDial(t *testing.T) { } m.sel["lane"] = "ds-led" - if id := comboID(m.sel); m.generated[id] == nil { + if id := comboID(m.sel, m.hasRelief); m.generated[id] == nil { t.Fatalf("ds-led selection resolves to no block: %s", id) } @@ -3789,3 +3790,129 @@ func TestUsageBodyDeepSeekBalanceGroup(t *testing.T) { t.Fatal("selected availability dropped the deepseek balance") } } + +// TestDeepSeekOffPeakWindow pins the discount window edges (UTC 16:30-00:30) +// and that the cost meter actually prices D rungs down inside it. +func TestDeepSeekOffPeakWindow(t *testing.T) { + for _, tc := range []struct { + hhmm string + want bool + }{ + {"16:29", false}, {"16:30", true}, {"23:59", true}, + {"00:00", true}, {"00:29", true}, {"00:30", false}, {"12:00", false}, + } { + ts, _ := time.Parse("15:04", tc.hhmm) + if got := deepseekOffPeak(ts); got != tc.want { + t.Errorf("deepseekOffPeak(%s) = %v, want %v", tc.hhmm, got, tc.want) + } + } + + m := threePoolModel(t) + m.sel["lane"] = "ds-only" + prev := offPeakNow + defer func() { offPeakNow = prev }() + offPeakNow = func() time.Time { return time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) } + peak := m.costScore() + offPeakNow = func() time.Time { return time.Date(2026, 8, 14, 20, 0, 0, 0, time.UTC) } + if off := m.costScore(); off > peak { + t.Errorf("off-peak cost score %d must not exceed peak %d", off, peak) + } + // The discount must show up in the raw weighted index, even when logScore + // clamps both readings into the same 1..5 bucket for a cheap pool. + idx := func() float64 { + var num, den float64 + m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { + c, ok := m.facts[id] + if !ok { + return + } + cost := 0.25*c.in + 0.75*c.out + if m.poolOfModel(id) == "D" && deepseekOffPeak(offPeakNow().UTC()) { + cost *= deepseekOffPeakMult + } + num += w * cost + den += w + }) + return num / den + } + offIdx := idx() + offPeakNow = func() time.Time { return time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) } + peakIdx := idx() + if offIdx >= peakIdx { + t.Errorf("off-peak weighted cost %v must be under peak %v", offIdx, peakIdx) + } +} + +// TestDeepSeekBalanceRowNotes: the low-balance cue appears exactly under the +// suggestion floor, and the off-peak tag only inside the discount window. +func TestDeepSeekBalanceRowNotes(t *testing.T) { + m := &model{} + prev := offPeakNow + defer func() { offPeakNow = prev }() + + offPeakNow = func() time.Time { return time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) } + row := stripAnsi(m.deepseekBalanceRow(deepseekBalance{ok: true, currency: "USD", total: "1.99"})) + if !strings.Contains(row, "low") { + t.Errorf("balance under the floor must carry the low cue: %q", row) + } + if strings.Contains(row, "off-peak") { + t.Errorf("peak hours must not show the off-peak tag: %q", row) + } + row = stripAnsi(m.deepseekBalanceRow(deepseekBalance{ok: true, currency: "USD", total: "2.00"})) + if strings.Contains(row, "low") { + t.Errorf("balance at the floor is not low: %q", row) + } + offPeakNow = func() time.Time { return time.Date(2026, 8, 14, 20, 0, 0, 0, time.UTC) } + row = stripAnsi(m.deepseekBalanceRow(deepseekBalance{ok: true, currency: "USD", total: "18.03"})) + if !strings.Contains(row, "off-peak −50%") { + t.Errorf("off-peak window must surface the discount: %q", row) + } +} + +// TestThinkingGaugeRendersAsMeter: the thinking dial draws a level meter with +// the selected word beside it, never the six-word list; the fill tracks the +// selected depth so ←/→ reads as a slider. +func TestThinkingGaugeRendersAsMeter(t *testing.T) { + m := model{facets: facetDefs(defaultGlyphs()), sel: defaultSel()} + rowFor := func() string { + lines, _ := m.genLines() + for _, ln := range lines { + if strings.Contains(stripAnsi(ln), "thinking") { + return ln + } + } + t.Fatal("no thinking row rendered") + return "" + } + + m.sel["thinking"] = "medium" + row := rowFor() + plain := stripAnsi(row) + for _, bar := range []string{"▁", "▂", "▃", "▅", "▇", "█"} { + if !strings.Contains(plain, bar) { + t.Fatalf("gauge missing bar %q: %q", bar, plain) + } + } + if !strings.Contains(plain, " medium ") { + t.Errorf("gauge must carry the selected word: %q", plain) + } + for _, word := range []string{"minimal", "xhigh", "max"} { + if strings.Contains(plain, word) { + t.Errorf("gauge must replace the word list, still shows %q: %q", word, plain) + } + } + + // The fill is pure styling (invisible without a color profile); what must + // hold structurally is that the word tracks the dial and the meter keeps a + // constant six-cell width at every depth. + m.sel["thinking"] = "minimal" + minPlain := stripAnsi(rowFor()) + m.sel["thinking"] = "max" + maxPlain := stripAnsi(rowFor()) + if !strings.Contains(minPlain, " minimal ") || !strings.Contains(maxPlain, " max ") { + t.Errorf("gauge word must track the dial:\nmin %q\nmax %q", minPlain, maxPlain) + } + if strings.Count(minPlain, "▁") != 1 || strings.Count(maxPlain, "█") != 1 { + t.Errorf("gauge must keep one cell per level:\nmin %q\nmax %q", minPlain, maxPlain) + } +} diff --git a/providers.go b/providers.go index 44525d6..21289e1 100644 --- a/providers.go +++ b/providers.go @@ -216,6 +216,20 @@ func laneHasPool(lane, pool string) bool { return p != nil && p.Pool == pool } +// laneReliefApplies reports whether relief tails are a real choice on this +// lane: a metered-led blend can spill into a pay-as-you-go pool, so the +// relief dial exists there. Pure lanes never take tails, and a lane led by +// the optional pool already spends it deliberately. +func laneReliefApplies(lane string) bool { + if lanePure(lane) { + return false + } + if p := providerByLane(lane); p != nil && !p.Required { + return false + } + return true +} + // laneHostsSpecial reports whether a special-tier facet ("spark", "fable") can // be on for the lane: its provider's pool must be in the lane's pool-set. func laneHostsSpecial(lane, facet string) bool { diff --git a/selection_state.go b/selection_state.go index 7ebfbb8..38a1ad6 100644 --- a/selection_state.go +++ b/selection_state.go @@ -72,6 +72,11 @@ func repairSelectionSpecials(sel map[string]string) { if sel["fable"] != "on" { sel["main"] = "off" } + // relief is only a choice on a metered-led blend; everywhere else the + // generator writes a single (on) variant, so the selection must match. + if !laneReliefApplies(sel["lane"]) { + sel["relief"] = "on" + } } func selectionChoices(sel map[string]string, facets []facet) map[string]string { diff --git a/selection_state_test.go b/selection_state_test.go index 16d5b7c..542e3eb 100644 --- a/selection_state_test.go +++ b/selection_state_test.go @@ -45,7 +45,7 @@ func TestSelectionStateRoundTripStoresOnlyFacets(t *testing.T) { } if got := loadSelectionState(path, testFacets()); !reflect.DeepEqual(got, map[string]string{ "lane": "claude-led", "model": "smart", "thinking": "xhigh", "advisor": "glance", - "spark": "on", "fable": "off", "main": "off", "fast": "off", + "spark": "on", "fable": "off", "main": "off", "fast": "off", "relief": "on", }) { t.Fatalf("round-trip selection = %v", got) } diff --git a/suggest.go b/suggest.go index 255ea87..c2bccaf 100644 --- a/suggest.go +++ b/suggest.go @@ -2,6 +2,7 @@ package main import ( "os" + "strconv" clikit "github.com/atyrode/cli-kit" "github.com/atyrode/cli-kit/ollama" @@ -186,6 +187,17 @@ func (m model) appliedDiff() []clikit.Action { // task-specific, so it keeps its current value; repairConstraints still turns // it off if its bucket is down or the lane is Claude-only. func (m *model) deriveToggles() { + // Quota-aware lane fallback: a suggestion must not land on a lane whose + // lead pool is drained when a sibling lane has live headroom. + if alt := m.quotaLane(); alt != "" { + m.sel["lane"] = alt + } + // Balance guard: when the pay-as-you-go pool is dry (or its balance is + // unknown), a suggestion stops routing relief tails into it. The manual + // dial stays free — this only shapes proposals. + if m.hasRelief && laneReliefApplies(m.sel["lane"]) && !m.optionalPoolUsable() { + m.sel["relief"] = "off" + } tier := m.sel["thinking"] critical := m.sel["model"] == "smart" && (tier == "xhigh" || tier == "max") fableLane := laneHostsSpecial(m.sel["lane"], "fable") @@ -200,3 +212,59 @@ func (m *model) deriveToggles() { m.sel["fast"] = "off" } } + +// deepseekLowBalanceUSD is the prepaid floor under which suggestions stop +// spending the pay-as-you-go pool: below it, ds lanes are not proposed and +// relief tails are suggested off. +const deepseekLowBalanceUSD = 2.0 + +// optionalPoolUsable reports whether the pay-as-you-go pool can absorb routed +// traffic: a credential exists, the last balance fetch succeeded, and the +// prepaid balance clears the floor. An unknown balance is not usable — the +// guard's whole point is never to discover $0 mid-session. +func (m *model) optionalPoolUsable() bool { + b := m.avail.deepseek + if b == nil || !b.ok { + return false + } + v, err := strconv.ParseFloat(b.total, 64) + return err == nil && v >= deepseekLowBalanceUSD +} + +// quotaLane returns the led lane of the first pool with live headroom when the +// current lane's lead pool is maxed or unauthenticated — the dial move a human +// would make after glancing at the usage panel. "" means stay put: the lead +// pool is fine, no alternative lane exists in this catalog, or none has quota. +func (m *model) quotaLane() string { + lead := providerByPool(lanePrimary(m.sel["lane"])) + if lead == nil || !m.avail.down(lead.mainBucket()) { + return "" + } + lanes := map[string]bool{} + for _, f := range m.facets { + if f.key == "lane" { + for _, v := range f.values { + lanes[v] = true + } + } + } + for _, pool := range fallbackPoolOrder { + p := providerByPool(pool) + if p == nil || p.Pool == lead.Pool { + continue + } + alt := p.Lane + "-led" + if !lanes[alt] { + continue + } + if p.Metered { + if m.avail.down(p.mainBucket()) { + continue + } + } else if !m.optionalPoolUsable() { + continue + } + return alt + } + return "" +} diff --git a/suggest_test.go b/suggest_test.go index 37c34fb..f7ec9c0 100644 --- a/suggest_test.go +++ b/suggest_test.go @@ -194,3 +194,93 @@ func TestEvalSystemPromptIsSizerRole(t *testing.T) { t.Errorf("evalSystemPrompt should pin the difficulty-rating, sizer-only role, got: %q", s) } } + +// suggestModel builds a three-pool-shaped model for suggestion-path tests: +// all seven lanes on the dial, a relief facet, and a controllable quota map. +func suggestModel() model { + m := model{facets: facetDefs(defaultGlyphs()), sel: defaultSel(), hasRelief: true} + for i := range m.facets { + if m.facets[i].key == "lane" { + m.facets[i].values = []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ds-led", "ds-only"} + } + } + m.avail = availability{ok: true, bucket: map[string]string{}, reset: map[string]int64{}} + return m +} + +// TestQuotaLaneSuggestion: a suggestion never lands on a lane whose lead pool +// is drained while a sibling lane has headroom - in fallbackPoolOrder, gated +// by the prepaid balance for the pay-as-you-go pool. +func TestQuotaLaneSuggestion(t *testing.T) { + m := suggestModel() + m.avail.deepseek = &deepseekBalance{ok: true, currency: "USD", total: "18.03"} + + // Lead pool fine: stay put. + m.sel["lane"] = "gpt-led" + m.deriveToggles() + if m.sel["lane"] != "gpt-led" { + t.Fatalf("healthy lead pool must not move the lane, got %q", m.sel["lane"]) + } + + // Codex maxed: the first pool in fallbackPoolOrder with headroom leads. + m.avail.bucket["codex-main"] = "maxed" + m.deriveToggles() + if m.sel["lane"] != "claude-led" { + t.Fatalf("maxed lead pool should fall to claude-led, got %q", m.sel["lane"]) + } + + // Claude maxed too: DeepSeek is the last lane standing (balance is fine). + m.sel["lane"] = "gpt-led" + m.avail.bucket["claude-main"] = "maxed" + m.deriveToggles() + if m.sel["lane"] != "ds-led" { + t.Fatalf("both metered pools maxed should fall to ds-led, got %q", m.sel["lane"]) + } + + // ...but not when the balance is under the floor. + m.sel["lane"] = "gpt-led" + m.avail.deepseek = &deepseekBalance{ok: true, currency: "USD", total: "1.17"} + m.deriveToggles() + if m.sel["lane"] != "gpt-led" { + t.Fatalf("a dry prepaid pool must not be suggested, got %q", m.sel["lane"]) + } + + // A two-pool catalog (no ds lanes on the dial) never proposes one. + m2 := suggestModel() + for i := range m2.facets { + if m2.facets[i].key == "lane" { + m2.facets[i].values = []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only"} + } + } + m2.avail.deepseek = &deepseekBalance{ok: true, currency: "USD", total: "50"} + m2.avail.bucket["codex-main"] = "maxed" + m2.avail.bucket["claude-main"] = "maxed" + m2.sel["lane"] = "gpt-led" + m2.deriveToggles() + if m2.sel["lane"] != "gpt-led" { + t.Fatalf("no alternative lane on the dial: stay put, got %q", m2.sel["lane"]) + } +} + +// TestBalanceGuardRelief: suggestions turn relief off when the prepaid pool is +// dry or its balance unknown, and leave it alone when it is healthy. +func TestBalanceGuardRelief(t *testing.T) { + for _, tc := range []struct { + name string + bal *deepseekBalance + want string + }{ + {"healthy", &deepseekBalance{ok: true, currency: "USD", total: "18.03"}, "on"}, + {"low", &deepseekBalance{ok: true, currency: "USD", total: "0.42"}, "off"}, + {"fetch failed", &deepseekBalance{ok: false}, "off"}, + {"no credential", nil, "off"}, + } { + m := suggestModel() + m.sel["lane"] = "gpt-led" + m.avail.deepseek = tc.bal + m.deriveToggles() + if got := m.sel["relief"]; got != tc.want { + t.Errorf("%s: relief = %q, want %q", tc.name, got, tc.want) + } + } +} From 8a9c9436434400872618e7849454b93d0e9305c3 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Fri, 14 Aug 2026 23:35:14 +0000 Subject: [PATCH 4/8] feat(tui): split lane into lead+blend dials, notched sliders for model/thinking Operator feedback on round 2: - The 7-value lane row overflowed the pane and read as too many choices. It now renders as a lead dial (gpt / mixed / claude / ds - one entry per pool the catalog carries) with a led|only blend child, hidden for mixed. sel["lane"] stays the canonical persisted value: lead/blend are derived in visibleFacets, recomposed in cycleFacet, and filtered from persistence by the facet list. - The half-height bar gauge clashed with the pane's text height. Both the thinking and model dials now draw a notched meter (one segment per step) with the selected word beside it. Arrow behavior, facet values, ids, and persistence are all unchanged. --- README.md | 10 ++-- main.go | 58 ++++++++++++++++----- main_test.go | 108 +++++++++++++++++++++++++++++----------- providers.go | 26 ++++++++++ selection_state_test.go | 6 ++- 5 files changed, 160 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index a82b9e7..df05bc3 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,12 @@ are not orphaned onto init while still holding their memory. ## Features -- **Dials, not config files** — provider lane, model tier, a thinking-depth - slider, advisor level, plus the spark/fable toggles; every combination maps - to a pre-computed routing. With a DeepSeek pool, a **relief** dial decides - whether drained metered chains may spill into the pay-as-you-go pool. +- **Dials, not config files** — a provider **lead** dial with a led/only + blend child (scales past two pools without overflowing), notched sliders + for model tier and thinking depth, advisor level, plus the spark/fable + toggles; every combination maps to a pre-computed routing. With a DeepSeek + pool, a **relief** dial decides whether drained metered chains may spill + into the pay-as-you-go pool. - **Live preview** — see which model leads every role, and its fallback chain, before anything runs. - **One-shot overlays** — each launch is an ephemeral `--config`; your omp diff --git a/main.go b/main.go index 924c731..cc7a421 100644 --- a/main.go +++ b/main.go @@ -1364,10 +1364,34 @@ func (m model) applyAdvisor(rows []string, level string) []string { // sub-setting, so it only shows while fable is on (and the lane can host it at // all). A dial this catalog generated no combo for is dropped the same way — // it is not a choice. +// +// The lane facet renders as two rows: lead (one segment per pool, plus mixed) +// and blend (led | only, hidden for mixed). sel["lane"] stays the canonical +// value — lead/blend are derived here and recomposed by cycleFacet, and are +// never persisted (saveSelectionState filters on m.facets, which carries only +// "lane"). func (m model) visibleFacets() []facet { lane := m.sel["lane"] var out []facet for _, f := range m.facets { + if f.key == "lane" { + lead, blend := laneSplit(lane) + m.sel["lead"], m.sel["blend"] = lead, blend + var leads []string + seen := map[string]bool{} + for _, v := range f.values { + l, _ := laneSplit(v) + if !seen[l] { + seen[l] = true + leads = append(leads, l) + } + } + out = append(out, facet{"lead", leads, f.glyph}) + if lead != "mixed" { + out = append(out, facet{"blend", []string{"led", "only"}, f.glyph}) + } + continue + } switch f.key { case "spark": if !laneHostsSpecial(lane, "spark") || m.noSpark { @@ -3479,6 +3503,11 @@ func (m *model) cycleFacet(dir int) { return } m.sel[f.key] = f.values[next] + // lead/blend are lane's rendered halves: recompose the canonical value + // before anything re-derives them (visibleFacets syncs from lane). + if f.key == "lead" || f.key == "blend" { + m.sel["lane"] = laneJoin(m.sel["lead"], m.sel["blend"]) + } // main is fable's sub-setting: whenever fable leaves "on" it must clear too, // so a later fable re-enable never silently resurrects the (expensive) // fable-as-main escalation — it is re-chosen deliberately every time. @@ -3656,16 +3685,21 @@ func (m model) genLines() ([]string, int) { // tree-style L connector: reads as fable's child, like `tree`. label, childPad, childW = "default", stDim.Render("└ "), 2 } + if f.key == "blend" { + // blend is lane's sub-setting: the lead row picks who drives, + // this child picks how exclusively. + childPad, childW = stDim.Render("└ "), 2 + } row := fmt.Sprintf("%s%s%s%s", ptr, childPad, gly, stDim.Render(pad(label, 9-childW))) - if f.key == "thinking" { - row += " " + m.thinkingGauge(f, onRow, acc) + if f.key == "thinking" || f.key == "model" { + row += " " + m.segmentGauge(f, onRow, acc) } else { for _, v := range f.values { switch { case v == m.sel[f.key]: col := acc - if f.key == "lane" { - col = laneColor(v) + if f.key == "lead" { + col = laneColor(m.sel["lane"]) } else if (f.key == "spark" || f.key == "fable" || f.key == "main") && v == "on" { col = cGreen } @@ -3700,12 +3734,11 @@ func (m model) genLines() ([]string, int) { return lines, cursor } -// thinkingGauge renders the thinking dial as a level meter instead of six -// words: rising bars fill to the selected depth, the current word rides along -// so the level still reads at a glance. ←/→ behave exactly as on a word dial — -// only the rendering differs; the facet's value list is untouched. -func (m model) thinkingGauge(f facet, onRow bool, acc string) string { - bars := []rune("▁▂▃▅▇█") +// segmentGauge renders a stepped dial (thinking, model) as a notched meter: +// one ▰ per step up to the selection, ▱ after, with the selected word riding +// along so the level still reads at a glance. ←/→ behave exactly as on a word +// dial — only the rendering differs; the facet's value list is untouched. +func (m model) segmentGauge(f facet, onRow bool, acc string) string { sel := -1 for i, v := range f.values { if v == m.sel[f.key] { @@ -3715,11 +3748,10 @@ func (m model) thinkingGauge(f facet, onRow bool, acc string) string { lit := lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Bold(true) var b strings.Builder for i := range f.values { - g := string(bars[i*len(bars)/max(1, len(f.values))]) if i <= sel { - b.WriteString(lit.Render(g)) + b.WriteString(lit.Render("▰")) } else { - b.WriteString(stDim.Render(g)) + b.WriteString(stDim.Render("▱")) } } word := lit diff --git a/main_test.go b/main_test.go index a878d0d..15bb8b4 100644 --- a/main_test.go +++ b/main_test.go @@ -3869,50 +3869,100 @@ func TestDeepSeekBalanceRowNotes(t *testing.T) { } } -// TestThinkingGaugeRendersAsMeter: the thinking dial draws a level meter with -// the selected word beside it, never the six-word list; the fill tracks the -// selected depth so ←/→ reads as a slider. -func TestThinkingGaugeRendersAsMeter(t *testing.T) { +// TestSegmentGaugeRendersAsMeter: the thinking and model dials draw a notched +// ▰▱ meter with the selected word beside it, never the word list; one cell per +// step, filled to the selected depth, so ←/→ reads as a slider. +func TestSegmentGaugeRendersAsMeter(t *testing.T) { m := model{facets: facetDefs(defaultGlyphs()), sel: defaultSel()} - rowFor := func() string { + rowFor := func(key string) string { lines, _ := m.genLines() for _, ln := range lines { - if strings.Contains(stripAnsi(ln), "thinking") { - return ln + if strings.Contains(stripAnsi(ln), key) { + return stripAnsi(ln) } } - t.Fatal("no thinking row rendered") + t.Fatalf("no %s row rendered", key) return "" } m.sel["thinking"] = "medium" - row := rowFor() - plain := stripAnsi(row) - for _, bar := range []string{"▁", "▂", "▃", "▅", "▇", "█"} { - if !strings.Contains(plain, bar) { - t.Fatalf("gauge missing bar %q: %q", bar, plain) - } + think := rowFor("thinking") + if got := strings.Count(think, "▰") + strings.Count(think, "▱"); got != 6 { + t.Fatalf("thinking gauge must keep one cell per level, got %d: %q", got, think) } - if !strings.Contains(plain, " medium ") { - t.Errorf("gauge must carry the selected word: %q", plain) + if !strings.Contains(think, " medium ") { + t.Errorf("gauge must carry the selected word: %q", think) } for _, word := range []string{"minimal", "xhigh", "max"} { - if strings.Contains(plain, word) { - t.Errorf("gauge must replace the word list, still shows %q: %q", word, plain) + if strings.Contains(think, word) { + t.Errorf("gauge must replace the word list, still shows %q: %q", word, think) } } - // The fill is pure styling (invisible without a color profile); what must - // hold structurally is that the word tracks the dial and the meter keeps a - // constant six-cell width at every depth. - m.sel["thinking"] = "minimal" - minPlain := stripAnsi(rowFor()) - m.sel["thinking"] = "max" - maxPlain := stripAnsi(rowFor()) - if !strings.Contains(minPlain, " minimal ") || !strings.Contains(maxPlain, " max ") { - t.Errorf("gauge word must track the dial:\nmin %q\nmax %q", minPlain, maxPlain) + m.sel["model"] = "normal" + mdl := rowFor("model") + if got := strings.Count(mdl, "▰") + strings.Count(mdl, "▱"); got != 3 { + t.Fatalf("model gauge must keep one cell per step, got %d: %q", got, mdl) + } + if !strings.Contains(mdl, " normal ") || strings.Contains(mdl, "smart") { + t.Errorf("model gauge must show only the selected word: %q", mdl) + } +} + +// TestLaneSplitDials: the lane facet renders as a lead row plus a blend child +// (hidden for mixed); cycling either recomposes the canonical lane, and the +// persisted state still stores only "lane". +func TestLaneSplitDials(t *testing.T) { + m := model{facets: facetDefs(defaultGlyphs()), sel: defaultSel()} + + // mixed: a single lead row, no blend child, no lane word list. + vf := m.visibleFacets() + if vf[0].key != "lead" { + t.Fatalf("first dial = %q, want lead", vf[0].key) + } + if vf[1].key == "blend" { + t.Fatal("mixed must not render a blend child") + } + wantLeads := []string{"gpt", "mixed", "claude"} + if !reflect.DeepEqual(vf[0].values, wantLeads) { + t.Fatalf("lead values = %v, want %v", vf[0].values, wantLeads) + } + + // Cycling lead off mixed lands on the -led lane and grows the blend child. + m.cycleFacet(1) // mixed → claude (cursor starts on the lead row) + if m.sel["lane"] != "claude-led" { + t.Fatalf("lead change composed lane %q, want claude-led", m.sel["lane"]) + } + vf = m.visibleFacets() + if vf[1].key != "blend" { + t.Fatalf("non-mixed lead must render the blend child, got %q", vf[1].key) + } + + // Cycling blend led → only composes the pure lane. + m.fcur = 1 + m.cycleFacet(1) + if m.sel["lane"] != "claude-only" { + t.Fatalf("blend change composed lane %q, want claude-only", m.sel["lane"]) + } + + // A three-pool dial grows a ds lead; lead/blend never persist. + for i := range m.facets { + if m.facets[i].key == "lane" { + m.facets[i].values = []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ds-led", "ds-only"} + } + } + vf = m.visibleFacets() + if !reflect.DeepEqual(vf[0].values, []string{"gpt", "mixed", "claude", "ds"}) { + t.Fatalf("three-pool lead values = %v", vf[0].values) + } + choices := selectionChoices(m.sel, m.facets) + if _, ok := choices["lead"]; ok { + t.Error("lead is a derived dial and must not persist") + } + if _, ok := choices["blend"]; ok { + t.Error("blend is a derived dial and must not persist") } - if strings.Count(minPlain, "▁") != 1 || strings.Count(maxPlain, "█") != 1 { - t.Errorf("gauge must keep one cell per level:\nmin %q\nmax %q", minPlain, maxPlain) + if choices["lane"] != "claude-only" { + t.Errorf("persisted lane = %q, want claude-only", choices["lane"]) } } diff --git a/providers.go b/providers.go index 21289e1..ff196b2 100644 --- a/providers.go +++ b/providers.go @@ -230,6 +230,32 @@ func laneReliefApplies(lane string) bool { return true } +// laneSplit decomposes a lane into the two dials the TUI renders: the lead +// (a provider's lane segment, or "mixed") and the blend ("led" | "only"). +// mixed has no blend of its own; it reports "led" so a later lead change +// lands on the -led lane. +func laneSplit(lane string) (lead, blend string) { + if lane == "mixed" { + return "mixed", "led" + } + blend = "led" + if lanePure(lane) { + blend = "only" + } + if p := providerByLane(lane); p != nil { + return p.Lane, blend + } + return lane, blend +} + +// laneJoin is laneSplit's inverse: the canonical lane a lead+blend pair names. +func laneJoin(lead, blend string) string { + if lead == "mixed" { + return "mixed" + } + return lead + "-" + blend +} + // laneHostsSpecial reports whether a special-tier facet ("spark", "fable") can // be on for the lane: its provider's pool must be in the lane's pool-set. func laneHostsSpecial(lane, facet string) bool { diff --git a/selection_state_test.go b/selection_state_test.go index 542e3eb..8cf5c83 100644 --- a/selection_state_test.go +++ b/selection_state_test.go @@ -224,8 +224,10 @@ func TestFacetChangeAndResetPersistSelection(t *testing.T) { changedModel, _ := m.Update(tea.KeyMsg{Type: tea.KeyRight}) changed := changedModel.(model) - if got := loadSelectionState(path, testFacets()); !reflect.DeepEqual(got, changed.sel) { - t.Fatalf("facet change persisted %v, want %v", got, changed.sel) + // changed.sel also carries the transient lead/blend derivations of lane; + // what persists (and reloads) is the facet-only projection. + if got := loadSelectionState(path, testFacets()); !reflect.DeepEqual(got, selectionChoices(changed.sel, testFacets())) { + t.Fatalf("facet change persisted %v, want %v", got, selectionChoices(changed.sel, testFacets())) } resetModel, _ := changed.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) From 9e6c57c1a3e4e3dea896e79b7d219d3e79776195 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Sat, 15 Aug 2026 00:06:19 +0000 Subject: [PATCH 5/8] fix(tui): align gauge tracks, put mixed first on the lead dial - Both notched dials now share a fixed 6-cell track (model spans two cells per step), so the meters and the words after them sit in one column; a leading space matches the word dials' cell padding. - mixed anchors the lead dial's left edge: it is the default and the only lead without a blend child. --- main.go | 28 +++++++++++++++++++++------- main_test.go | 46 +++++++++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/main.go b/main.go index cc7a421..8c9bad1 100644 --- a/main.go +++ b/main.go @@ -1377,8 +1377,10 @@ func (m model) visibleFacets() []facet { if f.key == "lane" { lead, blend := laneSplit(lane) m.sel["lead"], m.sel["blend"] = lead, blend - var leads []string - seen := map[string]bool{} + // mixed leads the dial: it is the default and the only lead + // without a blend child, so it anchors the left edge. + leads := []string{"mixed"} + seen := map[string]bool{"mixed": true} for _, v := range f.values { l, _ := laneSplit(v) if !seen[l] { @@ -3734,10 +3736,15 @@ func (m model) genLines() ([]string, int) { return lines, cursor } +// segmentGaugeWidth is the shared track width of every notched dial, so the +// meters and the words after them line up in a column regardless of how many +// steps a dial has (thinking: 6, model: 3 — each step spans width/len cells). +const segmentGaugeWidth = 6 + // segmentGauge renders a stepped dial (thinking, model) as a notched meter: -// one ▰ per step up to the selection, ▱ after, with the selected word riding -// along so the level still reads at a glance. ←/→ behave exactly as on a word -// dial — only the rendering differs; the facet's value list is untouched. +// cells fill up to the selection, with the selected word riding along so the +// level still reads at a glance. ←/→ behave exactly as on a word dial — only +// the rendering differs; the facet's value list is untouched. func (m model) segmentGauge(f facet, onRow bool, acc string) string { sel := -1 for i, v := range f.values { @@ -3745,13 +3752,20 @@ func (m model) segmentGauge(f facet, onRow bool, acc string) string { sel = i } } + per := segmentGaugeWidth / len(f.values) + if per < 1 { + per = 1 + } lit := lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Bold(true) var b strings.Builder + // The leading space mirrors the word dials' selected-cell padding, so the + // track starts on the same column the value words do. + b.WriteString(" ") for i := range f.values { if i <= sel { - b.WriteString(lit.Render("▰")) + b.WriteString(lit.Render(strings.Repeat("▰", per))) } else { - b.WriteString(stDim.Render("▱")) + b.WriteString(stDim.Render(strings.Repeat("▱", per))) } } word := lit diff --git a/main_test.go b/main_test.go index 15bb8b4..8472d85 100644 --- a/main_test.go +++ b/main_test.go @@ -359,20 +359,21 @@ func TestCycleFacetClearsMain(t *testing.T) { } } +// TestCycleFacetClampsAtEndpoints: the lead dial (row 0) clamps rather than +// wraps at either end — mixed is first, the last pool's lead is last. func TestCycleFacetClampsAtEndpoints(t *testing.T) { m := &model{facets: facetDefs(map[string]string{}), sel: defaultSel()} - m.fcur = 0 // lane + m.fcur = 0 // lead - m.sel["lane"] = m.facets[0].values[0] + m.sel["lane"] = "mixed" // lead = mixed, the dial's first value m.cycleFacet(-1) - if got := m.sel["lane"]; got != m.facets[0].values[0] { + if got := m.sel["lane"]; got != "mixed" { t.Fatalf("left at first option wrapped to %q", got) } - last := m.facets[0].values[len(m.facets[0].values)-1] - m.sel["lane"] = last + m.sel["lane"] = "claude-led" // lead = claude, the dial's last value m.cycleFacet(1) - if got := m.sel["lane"]; got != last { + if got := m.sel["lane"]; got != "claude-led" { t.Fatalf("right at last option wrapped to %q", got) } } @@ -1028,7 +1029,7 @@ func TestWheelStepsFacets(t *testing.T) { t.Fatalf("fixture: lane = %q, want mixed", m.sel["lane"]) } m.applyWheelStep(tea.MouseButtonWheelLeft) - if m.sel["lane"] != "claude-led" { + if m.sel["lane"] != "gpt-led" { t.Fatalf("wheel LEFT must cycle to next option: lane = %q", m.sel["lane"]) } m.applyWheelStep(tea.MouseButtonWheelRight) @@ -1108,7 +1109,7 @@ func TestWheelInputFilterRequiresDeliberateBurst(t *testing.T) { left := admit(tea.MouseButtonWheelLeft) nm, _ = m.Update(left) m = nm.(model) - if m.sel["lane"] != "claude-led" { + if m.sel["lane"] != "gpt-led" { t.Fatalf("horizontal burst did not move lane: %q", m.sel["lane"]) } } @@ -1132,8 +1133,8 @@ func TestFilteredWheelPreservesSelectionPersistence(t *testing.T) { } nm, _ := m.Update(msg) m = nm.(model) - if got := loadSelectionState(m.selectionState, m.facets)["lane"]; got != "claude-led" { - t.Fatalf("persisted lane after wheel-left = %q, want claude-led", got) + if got := loadSelectionState(m.selectionState, m.facets)["lane"]; got != "gpt-led" { + t.Fatalf("persisted lane after wheel-left = %q, want gpt-led", got) } filter.last = time.Now().Add(-wheelGestureGap - time.Millisecond) @@ -3901,8 +3902,11 @@ func TestSegmentGaugeRendersAsMeter(t *testing.T) { m.sel["model"] = "normal" mdl := rowFor("model") - if got := strings.Count(mdl, "▰") + strings.Count(mdl, "▱"); got != 3 { - t.Fatalf("model gauge must keep one cell per step, got %d: %q", got, mdl) + if got := strings.Count(mdl, "▰") + strings.Count(mdl, "▱"); got != 6 { + t.Fatalf("model gauge must match the shared track width, got %d cells: %q", got, mdl) + } + if got := strings.Count(mdl, "▰"); got != 4 { + t.Fatalf("model step 2/3 must fill two-thirds of the track, got %d lit: %q", got, mdl) } if !strings.Contains(mdl, " normal ") || strings.Contains(mdl, "smart") { t.Errorf("model gauge must show only the selected word: %q", mdl) @@ -3923,15 +3927,15 @@ func TestLaneSplitDials(t *testing.T) { if vf[1].key == "blend" { t.Fatal("mixed must not render a blend child") } - wantLeads := []string{"gpt", "mixed", "claude"} + wantLeads := []string{"mixed", "gpt", "claude"} if !reflect.DeepEqual(vf[0].values, wantLeads) { t.Fatalf("lead values = %v, want %v", vf[0].values, wantLeads) } // Cycling lead off mixed lands on the -led lane and grows the blend child. - m.cycleFacet(1) // mixed → claude (cursor starts on the lead row) - if m.sel["lane"] != "claude-led" { - t.Fatalf("lead change composed lane %q, want claude-led", m.sel["lane"]) + m.cycleFacet(1) // mixed → gpt (cursor starts on the lead row) + if m.sel["lane"] != "gpt-led" { + t.Fatalf("lead change composed lane %q, want gpt-led", m.sel["lane"]) } vf = m.visibleFacets() if vf[1].key != "blend" { @@ -3941,8 +3945,8 @@ func TestLaneSplitDials(t *testing.T) { // Cycling blend led → only composes the pure lane. m.fcur = 1 m.cycleFacet(1) - if m.sel["lane"] != "claude-only" { - t.Fatalf("blend change composed lane %q, want claude-only", m.sel["lane"]) + if m.sel["lane"] != "gpt-only" { + t.Fatalf("blend change composed lane %q, want gpt-only", m.sel["lane"]) } // A three-pool dial grows a ds lead; lead/blend never persist. @@ -3952,7 +3956,7 @@ func TestLaneSplitDials(t *testing.T) { } } vf = m.visibleFacets() - if !reflect.DeepEqual(vf[0].values, []string{"gpt", "mixed", "claude", "ds"}) { + if !reflect.DeepEqual(vf[0].values, []string{"mixed", "gpt", "claude", "ds"}) { t.Fatalf("three-pool lead values = %v", vf[0].values) } choices := selectionChoices(m.sel, m.facets) @@ -3962,7 +3966,7 @@ func TestLaneSplitDials(t *testing.T) { if _, ok := choices["blend"]; ok { t.Error("blend is a derived dial and must not persist") } - if choices["lane"] != "claude-only" { - t.Errorf("persisted lane = %q, want claude-only", choices["lane"]) + if choices["lane"] != "gpt-only" { + t.Errorf("persisted lane = %q, want gpt-only", choices["lane"]) } } From 4b45e313d26e4c975171145a0a3aac410fd229c3 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Sat, 15 Aug 2026 01:05:17 +0000 Subject: [PATCH 6/8] feat: explain relief inline, give audit the same metered spill logic - The relief row now carries a dim state-specific note ('drained chains spill into DeepSeek' / 'drained chains wait for quota reset') - it was the one dial whose effect was unreadable from the pane. - audit is the advisor's heavyweight setting, so it now follows the same metered logic as the heavyweight roles: with relief on, a metered-led blend's audit chain ends on each optional pool's audit rung (glance and review stay short; optional-led and pure lanes are untouched). The tail flows into both the preview and the launch overlay via applyAdvisor. --- main.go | 35 ++++++++++++++++++++++++++++++++++- main_test.go | 33 +++++++++++++++++++++++++++++++++ providers.go | 12 ++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 8c9bad1..8a70fae 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "regexp" + "slices" "sort" "strconv" "strings" @@ -1312,13 +1313,37 @@ func (m model) advisorChain(level string) []string { } if p := providerByPool(pool); p != nil { if chain := m.advisors[level+"/"+p.Lane]; len(chain) > 0 { - return chain + return m.advisorRelief(level, chain) } } } return nil } +// advisorRelief appends the relief tail to the audit chain: audit is the +// advisor's heavyweight setting, so — exactly like the heavyweight roles — +// its metered chain ends on each optional pool's own audit rung when relief +// is on, and a drained day still gets its second opinion. Lighter levels +// stay short: they are cheap enough that quota rarely blocks them. +func (m model) advisorRelief(level string, chain []string) []string { + if level != "audit" || m.sel["relief"] != "on" || !laneReliefApplies(m.sel["lane"]) { + return chain + } + out := append([]string(nil), chain...) + for i := range providerRegistry { + p := &providerRegistry[i] + if p.Required { + continue + } + relief := m.advisors[level+"/"+p.Lane] + if len(relief) == 0 || slices.Contains(out, relief[0]) { + continue + } + out = append(out, relief[0]) + } + return out +} + // roleOf returns the role name of a routing row ("● task" → "task"). func roleOf(row string) string { f := strings.Fields(row) @@ -3730,6 +3755,14 @@ func (m model) genLines() ([]string, int) { } case f.key == "fast" && m.sel["fast"] == "on": row += " " + stDim.Render("GPT only") + case f.key == "relief": + // relief is the least self-explanatory dial: say what it does, + // in the state it currently does it. + note := "drained chains wait for quota reset" + if m.sel["relief"] == "on" { + note = "drained chains spill into " + optionalPoolLabels() + } + row += " " + stDim.Render(note) } lines = append(lines, row) } diff --git a/main_test.go b/main_test.go index 8472d85..6a1c3ef 100644 --- a/main_test.go +++ b/main_test.go @@ -3970,3 +3970,36 @@ func TestLaneSplitDials(t *testing.T) { t.Errorf("persisted lane = %q, want gpt-only", choices["lane"]) } } + +// TestAdvisorAuditReliefTail: audit is the advisor's heavyweight setting, so +// it follows the same metered logic as the heavyweight roles — with relief on, +// a metered-led blend's audit chain ends on the optional pool's audit rung; +// relief off, lighter levels, and optional-led lanes stay untouched. +func TestAdvisorAuditReliefTail(t *testing.T) { + m := threePoolModel(t) + m.sel["lane"] = "mixed" + m.sel["relief"] = "on" + + audit := m.advisorChain("audit") + if len(audit) == 0 { + t.Fatal("no audit chain on mixed") + } + if !strings.HasPrefix(audit[len(audit)-1], "deepseek-") { + t.Fatalf("relief-on audit chain must tail into the optional pool: %v", audit) + } + + m.sel["relief"] = "off" + if got := m.advisorChain("audit"); strings.HasPrefix(got[len(got)-1], "deepseek-") { + t.Fatalf("relief-off audit chain must stay metered: %v", got) + } + + m.sel["relief"] = "on" + if got := m.advisorChain("review"); strings.HasPrefix(got[len(got)-1], "deepseek-") { + t.Fatalf("lighter advisor levels take no tail: %v", got) + } + + m.sel["lane"] = "ds-led" // optional-led lane already spends DeepSeek deliberately + if got := m.advisorChain("audit"); strings.HasPrefix(got[len(got)-1], "deepseek-") { + t.Fatalf("relief does not apply on an optional-led lane: %v", got) + } +} diff --git a/providers.go b/providers.go index ff196b2..f56f3e2 100644 --- a/providers.go +++ b/providers.go @@ -176,6 +176,18 @@ func meteredProviderIDs() []string { return out } +// optionalPoolLabels names the pay-as-you-go pools ("DeepSeek"), joined for +// display — the pools relief can spill into. +func optionalPoolLabels() string { + var out []string + for i := range providerRegistry { + if !providerRegistry[i].Required { + out = append(out, providerRegistry[i].Label) + } + } + return strings.Join(out, "/") +} + // poolDeclaresSpecialTier reports whether the pool's provider declares a // special facet at the given ladder tier (O's spark at 0, A's fable at 4). func poolDeclaresSpecialTier(pool string, tier int) bool { From 06ea5e02d01e9f1a1638b6a40fc06a3516b062be Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Sat, 15 Aug 2026 12:39:08 +0000 Subject: [PATCH 7/8] feat(tui): advisor as a notched meter, one segment per option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Every gauge now has exactly as many cells as it has selectable options: model ▰▰▰ (3), thinking ▰▰▰▱▱▱ (6), advisor ▰▱▱ (3) - the shared 6-cell track width is gone. - advisor's leading 'off' is the dial's zero: it takes no cell, so off renders an empty track and each level lights exactly one. --- main.go | 36 +++++++++++++++++++----------------- main_test.go | 24 ++++++++++++++++++++---- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/main.go b/main.go index 8a70fae..dd3da7c 100644 --- a/main.go +++ b/main.go @@ -3718,7 +3718,7 @@ func (m model) genLines() ([]string, int) { childPad, childW = stDim.Render("└ "), 2 } row := fmt.Sprintf("%s%s%s%s", ptr, childPad, gly, stDim.Render(pad(label, 9-childW))) - if f.key == "thinking" || f.key == "model" { + if f.key == "thinking" || f.key == "model" || f.key == "advisor" { row += " " + m.segmentGauge(f, onRow, acc) } else { for _, v := range f.values { @@ -3769,15 +3769,12 @@ func (m model) genLines() ([]string, int) { return lines, cursor } -// segmentGaugeWidth is the shared track width of every notched dial, so the -// meters and the words after them line up in a column regardless of how many -// steps a dial has (thinking: 6, model: 3 — each step spans width/len cells). -const segmentGaugeWidth = 6 - -// segmentGauge renders a stepped dial (thinking, model) as a notched meter: -// cells fill up to the selection, with the selected word riding along so the -// level still reads at a glance. ←/→ behave exactly as on a word dial — only -// the rendering differs; the facet's value list is untouched. +// segmentGauge renders a stepped dial (model, thinking, advisor) as a notched +// meter: one cell per selectable option, filled to the selection, with the +// selected word riding along so the level still reads at a glance. A leading +// "off" value is the dial's zero — it takes no cell, so off renders an empty +// track and the first real step lights exactly one. ←/→ behave exactly as on +// a word dial — only the rendering differs; the facet's values are untouched. func (m model) segmentGauge(f facet, onRow bool, acc string) string { sel := -1 for i, v := range f.values { @@ -3785,20 +3782,25 @@ func (m model) segmentGauge(f facet, onRow bool, acc string) string { sel = i } } - per := segmentGaugeWidth / len(f.values) - if per < 1 { - per = 1 + steps := f.values + fill := sel + 1 + if len(steps) > 0 && steps[0] == "off" { + steps = steps[1:] + fill = sel // off (sel 0) lights nothing + } + if fill < 0 { + fill = 0 } lit := lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Bold(true) var b strings.Builder // The leading space mirrors the word dials' selected-cell padding, so the // track starts on the same column the value words do. b.WriteString(" ") - for i := range f.values { - if i <= sel { - b.WriteString(lit.Render(strings.Repeat("▰", per))) + for i := range steps { + if i < fill { + b.WriteString(lit.Render("▰")) } else { - b.WriteString(stDim.Render(strings.Repeat("▱", per))) + b.WriteString(stDim.Render("▱")) } } word := lit diff --git a/main_test.go b/main_test.go index 6a1c3ef..71b31fd 100644 --- a/main_test.go +++ b/main_test.go @@ -3902,15 +3902,31 @@ func TestSegmentGaugeRendersAsMeter(t *testing.T) { m.sel["model"] = "normal" mdl := rowFor("model") - if got := strings.Count(mdl, "▰") + strings.Count(mdl, "▱"); got != 6 { - t.Fatalf("model gauge must match the shared track width, got %d cells: %q", got, mdl) + if got := strings.Count(mdl, "▰") + strings.Count(mdl, "▱"); got != 3 { + t.Fatalf("model gauge must keep one cell per option, got %d cells: %q", got, mdl) } - if got := strings.Count(mdl, "▰"); got != 4 { - t.Fatalf("model step 2/3 must fill two-thirds of the track, got %d lit: %q", got, mdl) + if got := strings.Count(mdl, "▰"); got != 2 { + t.Fatalf("model step 2/3 must light two cells, got %d lit: %q", got, mdl) } if !strings.Contains(mdl, " normal ") || strings.Contains(mdl, "smart") { t.Errorf("model gauge must show only the selected word: %q", mdl) } + + // advisor's leading "off" is the zero mark: no cell of its own, empty + // track when selected, and the levels light one cell each. + m.sel["advisor"] = "off" + adv := rowFor("advisor") + if strings.Count(adv, "▱") != 3 || strings.Count(adv, "▰") != 0 { + t.Fatalf("advisor off must render an empty three-cell track: %q", adv) + } + if !strings.Contains(adv, " off ") { + t.Errorf("advisor gauge must carry the selected word: %q", adv) + } + m.sel["advisor"] = "review" + adv = rowFor("advisor") + if strings.Count(adv, "▰") != 2 || strings.Count(adv, "▱") != 1 { + t.Fatalf("advisor review must light 2 of 3 cells: %q", adv) + } } // TestLaneSplitDials: the lane facet renders as a lead row plus a blend child From e1277334021939e349e1618792fd89064a3eb734 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Sat, 22 Aug 2026 15:12:15 +0000 Subject: [PATCH 8/8] refactor(tui): split the main.go monolith into focused files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure line-range moves, no behavior changes: keys, theme, colorize, facts, usage, render, facets, routing, layout, wheel, model, update, view, launch — main.go keeps only the entrypoint. Section banners already in the file defined the boundaries; goimports settled per-file imports. go vet + full suite green. --- colorize.go | 134 ++ facets.go | 140 ++ facts.go | 39 + keys.go | 45 + launch.go | 135 ++ layout.go | 276 ++++ main.go | 4146 -------------------------------------------------- main_test.go | 11 +- model.go | 172 +++ render.go | 161 ++ routing.go | 693 +++++++++ theme.go | 60 + update.go | 340 +++++ usage.go | 1521 ++++++++++++++++++ view.go | 442 ++++++ wheel.go | 101 ++ 16 files changed, 4265 insertions(+), 4151 deletions(-) create mode 100644 colorize.go create mode 100644 facets.go create mode 100644 facts.go create mode 100644 keys.go create mode 100644 launch.go create mode 100644 layout.go create mode 100644 model.go create mode 100644 render.go create mode 100644 routing.go create mode 100644 theme.go create mode 100644 update.go create mode 100644 usage.go create mode 100644 view.go create mode 100644 wheel.go diff --git a/colorize.go b/colorize.go new file mode 100644 index 0000000..d00b6d5 --- /dev/null +++ b/colorize.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// ── colourisers ────────────────────────────────────────────────────────────── +func lvl(s string) int { + switch s { + case "minimal": + return 0 + case "low": + return 1 + case "medium": + return 2 + case "high": + return 3 + case "xhigh": + return 4 + } + return 5 +} + +func shortModel(name string) string { + if name == "gpt-5.4" { + return name + } + // Slash-scoped ids display without their provider path, and keep their + // full model part — the vendor's own naming is the recognizable bit. + if i := strings.LastIndexByte(name, '/'); i >= 0 { + name = name[i+1:] + if !strings.HasPrefix(name, "claude") { + return name + } + } + p := strings.Split(name, "-") + if strings.HasPrefix(name, "claude") && len(p) > 1 { + return p[1] + } + return p[len(p)-1] +} + +func clampByte(x float64) int { + v := int(x) + if v > 255 { + return 255 + } + if v < 0 { + return 0 + } + return v +} + +func paintModel(tok string) string { + i := strings.LastIndex(tok, ":") + name, level := tok[:i], tok[i+1:] + p := providerByModel(name) + var br, bg, bb float64 + switch { + case p != nil: + br, bg, bb = p.PaintRGB[0], p.PaintRGB[1], p.PaintRGB[2] + case strings.Contains(name, "local-"): + // Free/local runtimes read green — the same family as the ox accents. + br, bg, bb = 96, 211, 150 + default: + return shortModel(name) + ":" + level // unknown provider: uncoloured + } + f := 0.60 + float64(lvl(level))*0.088 + col := lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", clampByte(br*f), clampByte(bg*f), clampByte(bb*f))) + return lipgloss.NewStyle().Foreground(col).Render(shortModel(name) + ":" + level) +} + +func colorizeRoute(line string) string { return modelRe.ReplaceAllStringFunc(line, paintModel) } + +// bucketOf guesses a quota bucket from a model name. It is the fallback for +// catalogs that declare no bucket column, and the only resolver for the bare +// facet names ("fable", "spark") the suggest box asks about — prefer +// model.bucketFor wherever a receiver is in reach. An unknown name maps to no +// bucket at all rather than someone else's quota window. +func bucketOf(model string) string { + m := model + if i := strings.IndexByte(m, ':'); i >= 0 { + m = m[:i] + } + // Provider-scoped ids outside the subscription pools (OpenRouter, + // local runtimes) have no quota window code knows about. An empty bucket + // never reads as down, which is exactly right for a free or local model. + if strings.Contains(m, "/") { + return "" + } + for _, p := range providerRegistry { + for _, s := range p.Special { + if strings.Contains(m, s.Bucket) { + return p.BucketBase + "-" + s.Bucket + } + } + } + if p := providerByModel(m); p != nil { + return p.mainBucket() + } + for _, p := range providerRegistry { + for _, pre := range p.ModelPrefixes { + if strings.Contains(m, pre) { + return p.mainBucket() + } + } + } + return "" +} + +// bucketFor resolves a routing token's quota bucket from the catalog, falling +// back to the name guess only when the catalog declares none. The catalog wins +// because names are not a taxonomy: claude-mythos-5 sits in omp's catalog at +// claude-fable-5's price yet 404s on this account, and every model omp adds +// would otherwise need one more substring arm here before it could be struck +// through correctly. +func (m model) bucketFor(name string) string { + id := name + if i := strings.IndexByte(id, ':'); i >= 0 { + id = id[:i] + } + if f, ok := m.facts[id]; ok { + if f.bucket != "" { + return f.bucket + } + if p := providerByPool(f.pool); p != nil { + return p.mainBucket() + } + } + return bucketOf(id) +} diff --git a/facets.go b/facets.go new file mode 100644 index 0000000..bb135d2 --- /dev/null +++ b/facets.go @@ -0,0 +1,140 @@ +package main + +import ( + "strconv" + "strings" +) + +// ── generator facets ───────────────────────────────────────────────────────── +type facet struct { + key string + values []string + glyph string +} + +// facetDefs seeds the facet dials. The lane facet starts empty: its values are +// catalog-driven (applyCatalog collects the lanes the catalog actually +// generated), so a two-pool catalog shows exactly the classic five lanes and a +// richer one adds its own. +func facetDefs(glyphs map[string]string) []facet { + return []facet{ + // Seeded with the required pools' lanes so a catalog-less run (the + // onboarding shell, a broken CODE_GENERATED) keeps a working dial; + // applyCatalog replaces the list with the lanes the catalog generated. + {"lane", requiredPoolLanes(), glyphs["lane"]}, + {"model", []string{"fast", "normal", "smart"}, glyphs["model"]}, + {"thinking", []string{"minimal", "low", "medium", "high", "xhigh", "max"}, glyphs["thinking"]}, + // advisor as a power/cost dial: a quick glance, a proper review, or a + // deep (expensive) audit — off spends nothing. + {"advisor", []string{"off", "glance", "review", "audit"}, glyphs["advisor"]}, + {"fast", []string{"on", "off"}, glyphs["fast"]}, + {"spark", []string{"on", "off"}, glyphs["spark"]}, + {"fable", []string{"on", "off"}, glyphs["fable"]}, + // fable-as-main: hand the scarce elite the default (main-agent) role too. + // A sub-setting of fable — only visible while fable is on (see + // visibleFacets) and never set by a suggestion (see validFacetActions). + {"main", []string{"on", "off"}, glyphs["main"]}, + // relief: whether metered-led chains may spill into a pay-as-you-go + // pool's tail rung. Only rendered when the catalog carries an + // optional pool and the lane is a metered-led blend. + {"relief", []string{"on", "off"}, glyphs["relief"]}, + } +} + +// parseAdvisors reads the __advisors__ block (rows: " ") +// into a map keyed "level/ctx" — the advisor model table, sourced from +// generate-profiles.py so the catalog stays a single source of truth. +func parseAdvisors(rows []string) map[string][]string { + out := map[string][]string{} + for _, r := range rows { + f := strings.Fields(strings.ReplaceAll(r, "→", " ")) + if len(f) < 3 { + continue + } + var chain []string + for _, t := range f[2:] { + if modelRe.MatchString(t) { + chain = append(chain, t) + } + } + if len(chain) > 0 { + out[f[0]+"/"+f[1]] = chain + } + } + return out +} + +// modelFact is a model's measured facts from omp (via the catalog): pricing +// ($/1M tokens), output throughput (tok/s), time-to-first-token (seconds), the +// quota bucket it draws from ("" when the catalog declares none), and the pool +// it belongs to ("" in catalogs that predate the column — the provider-prefix +// heuristic covers those). +type modelFact struct { + in, out, speed, ttft float64 + bucket string + pool string // catalog pool letter ("" in legacy catalogs — the family guess covers those) +} + +// effTPS folds ttft into throughput — the effective tok/s for a representative +// reply of effTokens: total time = ttft (startup) + tokens/speed (streaming), so +// a blazing-but-slow-to-start model (spark: 287 t/s, 5.6s ttft) reads honestly. +const effTokens = 300.0 + +func (f modelFact) effTPS() float64 { + if f.speed <= 0 { + return 0 + } + return effTokens / (f.ttft + effTokens/f.speed) +} + +// parseFacts reads the __models__ block (rows: " +// [ []]") into a per-model table, sourced from the catalog +// so meters and routing agree. The trailing columns are optionals so legacy +// five- and six-token catalogs keep working: column six is the quota bucket, +// column seven the provider — accepted as the registry id (current renderer) +// or the bare pool letter (older renderers), resolved to a pool either way; +// unknown values fall back to the model-family guess. + +// catalogPool resolves the __models__ trailing column to a pool letter. The +// current renderer writes the registry provider id there; an older one wrote +// the bare pool letter. Either parses; anything unknown falls back to the +// model-family guess so mixed-age catalogs keep working. +func catalogPool(col, id string) string { + if len(col) == 1 { + if providerByPool(col) != nil { + return col + } + } + if p := providerByID(col); p != nil { + return p.Pool + } + if p := providerByModel(id); p != nil { + return p.Pool + } + return "" +} + +func parseFacts(rows []string) map[string]modelFact { + out := map[string]modelFact{} + for _, r := range rows { + f := strings.Fields(r) + if len(f) < 5 { + continue + } + in, e1 := strconv.ParseFloat(f[1], 64) + outc, e2 := strconv.ParseFloat(f[2], 64) + sp, e3 := strconv.ParseFloat(f[3], 64) + tt, e4 := strconv.ParseFloat(f[4], 64) + bucket, pool := "", "" + if len(f) >= 6 { + bucket = f[5] + } + if len(f) >= 7 { + pool = catalogPool(f[6], f[0]) + } + if e1 == nil && e2 == nil && e3 == nil && e4 == nil { + out[f[0]] = modelFact{in, outc, sp, tt, bucket, pool} + } + } + return out +} diff --git a/facts.go b/facts.go new file mode 100644 index 0000000..d702480 --- /dev/null +++ b/facts.go @@ -0,0 +1,39 @@ +package main + +import ( + "bufio" + "os" + "strings" +) + +// ── data ───────────────────────────────────────────────────────────────────── +// loadBlocks parses a generated page into name -> role rows. +func loadBlocks(path string) map[string][]string { + blocks := map[string][]string{} + f, err := os.Open(path) + if err != nil { + return blocks + } + defer f.Close() + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 1024*128), 1024*128) + var cur string + for sc.Scan() { + line := sc.Text() + if line == "" { + cur = "" + continue + } + if line[0] != ' ' { + if fs := strings.Fields(line); len(fs) > 0 { + cur = fs[0] + blocks[cur] = nil + } + continue + } + if cur != "" { + blocks[cur] = append(blocks[cur], line) + } + } + return blocks +} diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..d888dfc --- /dev/null +++ b/keys.go @@ -0,0 +1,45 @@ +package main + +import "github.com/charmbracelet/bubbles/key" + +// ── keybindings (drive both input handling and the bubbles/help footer) ─────── +type keyMap struct { + Move, Change, Reset, Depth, Refresh, Manager, Collapse, Usage, Launch, Managed, Untrusted, Help, Quit key.Binding +} + +// ShortHelp is a static single-line stand-in used only when measuring the +// footer height for mode selection (which would otherwise recurse through the +// state-derived compact help). The rendered compact line comes from +// contextHelp, which derives its bindings from the live model state (atyrode/dotfiles#198). +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Move, k.Change, k.Reset, k.Help, k.Quit} +} +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Move, k.Change, k.Reset}, + {k.Depth, k.Refresh, k.Manager, k.Collapse, k.Usage}, + {k.Launch, k.Managed, k.Untrusted, k.Help, k.Quit}, + } +} + +var keys = keyMap{ + Move: key.NewBinding(key.WithKeys("up", "down", "j", "k"), key.WithHelp("↑↓", "move")), + Change: key.NewBinding(key.WithKeys("left", "right", "h", "l"), key.WithHelp("←→", "change")), + Reset: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", gReset+" defaults")), + Depth: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "primary ⇄ full chains")), + Refresh: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "refresh usage")), + Manager: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "manage accounts")), + Collapse: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "show/hide routing")), + Usage: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "show/hide usage")), + Launch: key.NewBinding(key.WithKeys("enter"), key.WithHelp("⏎", "launch")), + Managed: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "managed omp")), + Untrusted: key.NewBinding(key.WithKeys("u"), key.WithHelp("u", "sandbox")), + Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "more")), + Quit: key.NewBinding(key.WithKeys("q", "esc", "ctrl+c"), key.WithHelp("q", "quit")), +} + +// defaultSel returns a fresh copy of the generator's default facet selection — +// used both to seed the model and to restore it via the reset key. +func defaultSel() map[string]string { + return map[string]string{"lane": "mixed", "model": "smart", "thinking": "medium", "advisor": "glance", "spark": "on", "fable": "off", "main": "off", "fast": "off", "relief": "on"} +} diff --git a/launch.go b/launch.go new file mode 100644 index 0000000..ae1f3c2 --- /dev/null +++ b/launch.go @@ -0,0 +1,135 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" +) + +func forwardArgv(path string, forwarded []string, prompt string) []string { + out := append([]string{path}, stripProfileArgs(forwarded)...) + if prompt != "" { + out = append(out, prompt) + } + return out +} + +func managedLaunchArgv(path string, forwarded []string, prompt string) []string { + return forwardArgv(path, forwarded, prompt) +} + +func sandboxLaunchArgv(path string, forwarded []string, prompt string) []string { + return forwardArgv(path, forwarded, prompt) +} + +func generatedLaunchArgv(path, cfgPath string, forwarded []string, prompt string) []string { + args := append([]string{"--config", cfgPath}, stripProfileArgs(forwarded)...) + out := append([]string{path}, args...) + if prompt != "" { + out = append(out, prompt) + } + return out +} + +func resolveLaunchPath(envName string, fallbacks []string) (string, error) { + if configured := os.Getenv(envName); configured != "" { + return exec.LookPath(configured) + } + var err error + for _, fallback := range fallbacks { + var path string + if path, err = exec.LookPath(fallback); err == nil { + return path, nil + } + } + if err == nil { + err = errors.New("no launcher configured") + } + return "", err +} + +func runChild(path string, argv, env []string) error { + cmd := exec.Command(path, argv[1:]...) + cmd.Args = argv + cmd.Env = env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func childStatus(err error) int { + if err == nil { + return 0 + } + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() >= 0 { + return exitErr.ExitCode() + } + return 1 +} + +func runSandbox(envName string, fallbacks []string, prompt string) int { + path, err := resolveLaunchPath(envName, fallbacks) + if err != nil { + fmt.Fprintln(os.Stderr, "code: sandbox not found:", err) + return 1 + } + err = runChild(path, sandboxLaunchArgv(path, os.Args[1:], prompt), withoutAuthEnv(os.Environ())) + if err != nil { + fmt.Fprintln(os.Stderr, "code: sandbox:", err) + } + return childStatus(err) +} + +func runTrusted(envName string, fallbacks []string, + argv func(string, []string, string) []string, prompt string, + broker brokerConfig, selections accountSelectionState) int { + disabled := selections.CurrentDisabled() + path, err := resolveLaunchPath(envName, fallbacks) + if err != nil { + fmt.Fprintln(os.Stderr, "code: trusted launcher not found:", err) + return 1 + } + accounts, err := loadAccounts(broker) + if err != nil { + fmt.Fprintln(os.Stderr, "code: account snapshot unavailable; refusing unrestricted launch:", err) + return 1 + } + accountPoolPath, cleanup, err := writeAccountPool(accounts, disabled) + if err != nil { + fmt.Fprintln(os.Stderr, "code: account pool unavailable; refusing unrestricted launch:", err) + return 1 + } + defer cleanup() + childEnv := withAuthEnv(os.Environ(), broker, accountPoolPath) + err = runChild(path, argv(path, os.Args[1:], prompt), childEnv) + if err != nil { + fmt.Fprintln(os.Stderr, "code: trusted child:", err) + } + return childStatus(err) +} + +// launchGenerated keeps both immutable launch inputs alive only for the child. +func launchGenerated(cfg, prompt string, broker brokerConfig, selections accountSelectionState) int { + tmp, err := os.CreateTemp("", "code-gen-*.yml") + if err != nil { + fmt.Fprintln(os.Stderr, "code:", err) + return 1 + } + cfgPath := tmp.Name() + defer os.Remove(cfgPath) + if _, err = tmp.WriteString(cfg); err == nil { + err = tmp.Close() + } else { + _ = tmp.Close() + } + if err != nil { + fmt.Fprintln(os.Stderr, "code: generated config:", err) + return 1 + } + return runTrusted("CODE_OMP", []string{"omp"}, func(path string, forwarded []string, prompt string) []string { + return generatedLaunchArgv(path, cfgPath, forwarded, prompt) + }, prompt, broker, selections) +} diff --git a/layout.go b/layout.go new file mode 100644 index 0000000..1e681d8 --- /dev/null +++ b/layout.go @@ -0,0 +1,276 @@ +package main + +import ( + "github.com/charmbracelet/lipgloss" +) + +// ── model ──────────────────────────────────────────────────────────────────── +// layout modes, chosen from the terminal size (unless the user collapses): +// +// split — wide: the focused list on the left, routing preview on the +// right, and Usage spanning the full bottom width +// medium — generator-dominant: the list full width on top (primary), then +// Usage and Routing side by side in a secondary row — Usage's +// provider groups stacked vertically inside its measured left +// column, Routing on the right (and taking the whole row while +// ‹s› hides Usage) +// collapsed — narrow/short or ‹p›: one full-width panel at a time (list, or +// routing w/ showResult) — the Generator stays usable instead of +// compressing every section into an unreadable split +const ( + modeSplit = iota + modeMedium + modeCollapsed +) + +// size classes behind mode(): derived from terminal cells and the measured +// rendered minima of each section (atyrode/dotfiles#197) — never from pixels or a hard-coded +// screenshot width. +const ( + sizeWide = iota + sizeMedium + sizeNarrow +) + +// gut is the left gutter every panel shares, so the whole UI hangs off one +// consistent margin instead of a ragged mix of flush-left and indented rows. +// topGap is the matching vertical breathing room above the section tabs. +// headRows counts the section head (tabs + blank separator) above a list body. +const ( + gut = 2 + topGap = 1 + headRows = 2 + // the launch footer pinned under the list: blank + cost + speed + blank + + // the ⏎ launch action on its own visually separated row. + launchFooterRows = 5 + // routingMinW is the narrowest useful routing column: pane chrome plus room + // for a lead chain — below this a side-by-side routing panel stops earning + // its keep. + routingMinW = 33 + // secSepW is the one-cell border column between medium's adjacent + // secondary panes (Usage left, Routing right) — visible separation, same + // stroke as the wide layout's routing pane border. + secSepW = 1 + // genMinRows is the fewest facet rows the generator list may be windowed to + // before the layout must shed secondary sections instead of compressing it. + genMinRows = 4 + // minRouteRows is the fewest routing viewport rows worth pinning chrome around. + minRouteRows = 4 +) + +// genColMinH is the generator column's minimum useful height: the pinned head, +// a windowed-but-usable slice of the facet list, and the pinned launch footer. +const genColMinH = headRows + genMinRows + launchFooterRows + +// genRowWidth is the width needed to render the widest generator facet row (all +// options) on a single line — the minimum for the left panel. +func (m model) genRowWidth() int { + max := 30 + for _, f := range m.facets { // widest over ALL facets, so width is lane-stable + w := 14 // ▸ + glyph + spaces + padded label + for _, v := range f.values { + w += len(v) + 4 + } + if w > max { + max = w + } + } + return max + 2 +} + +// sizeMode classifies the terminal into the wide / medium / narrow-short +// responsive classes. Widths compare against the measured generator row, +// routing, and usage-column minima; heights against the chrome each composition +// pins on screen — breakpoints track content needs, not screenshot numbers. +func (m model) sizeMode() int { + if m.w >= m.genRowWidth()+routingMinW && m.h >= m.wideMinH() { + return sizeWide + } + if m.w >= m.mediumMinW() && m.h >= m.mediumMinH() { + return sizeMedium + } + return sizeNarrow +} + +func (m model) mode() int { + if m.collapse { + return modeCollapsed + } + switch m.sizeMode() { + case sizeWide: + return modeSplit + case sizeMedium: + return modeMedium + default: + return modeCollapsed + } +} + +// wideMinH is the least height at which the wide composition stays readable: +// a usable generator column above the full-width Usage footer. Shorter than +// this, keeping every section visible would compress them all — shed instead. +func (m model) wideMinH() int { + return topGap + genColMinH + m.footerH(!m.hideUsage) +} + +// mediumMinH stacks the generator over the secondary Routing+Usage row (at its +// measured minimum) with Usage out of the footer. +func (m model) mediumMinH() int { + return topGap + genColMinH + 1 + m.secondaryMinH() + m.footerH(false) +} + +// mediumMinW: the secondary row must seat a useful routing viewport beside the +// measured usage column — plus the one-cell separator between them — without +// clipping either. +func (m model) mediumMinW() int { + if m.hideUsage { + return routingMinW + } + return routingMinW + secSepW + m.usageColW() +} + +// footerH measures the pinned footer for a composition directly from its parts +// — mode selection depends on it, so it must not consult the mode itself. +func (m model) footerH(withUsage bool) int { + h := 1 + lipgloss.Height(padLeft(m.help.View(keys), gut)) + if withUsage { + if p := m.usagePanel(); p != "" { + h += 1 + lipgloss.Height(p) + } + } + return h +} + +// bodyH is the height available above the pinned footer. +func (m model) bodyH() int { + h := m.h - lipgloss.Height(m.footer()) + if h < 1 { + h = 1 + } + return h +} + +// contentH is the height the panels actually fill — bodyH minus the top gap. +func (m model) contentH() int { + h := m.bodyH() - topGap + if h < 1 { + h = 1 + } + return h +} + +// bodyLines renders the generator's scrolling facet list and reports the cursor's +// line index within it. +func (m model) bodyLines() ([]string, int) { + return m.genLines() +} + +// launchFooter is the cost/speed meters for the current facet combo plus the +// enter-to-launch call to action — so the generator always shows what the choice +// costs, how fast it is, and how Enter will launch it. Enter always launches +// the generated profile for the current facets (the untouched default combo is +// a profile like any other); m runs omp-managed on the managed defaults with +// no overlay, and the sandbox (u) key is always offered. A selected runtime +// target gets the same footer shape with an honest summary instead of meters: +// its tokens are free and code has no measurement to quote. +func (m model) launchFooter() []string { + acc := lipgloss.NewStyle().Foreground(lipgloss.Color(m.accent())).Bold(true).Render(" ⏎ launch") + if _, local := m.selectedRuntime(); local { + return []string{ + "", + stDim.Render(" cost free · local inference"), + "", + "", + acc, + } + } + cs, ss := m.costScore(), m.speedScore() + return []string{ + "", + m.meter("cost", "$", meterRamp[cs], cs), // dear → red, cheap → green + m.meter("speed", "»", meterRamp[6-ss], ss), // fast → green, slow → red + "", // breathing room between the meters and the action row + acc + stDim.Render(" m managed omp · u sandbox"), + } +} + +// mediumSplit gives the Routing+Usage row only its measured minimum, then lets +// the primary Generator absorb every remaining row. The medium height threshold +// guarantees both sections fit; taller terminals therefore expand Generator +// instead of leaving slack below the compact secondary content. +func (m model) mediumSplit(bodyH int) (genH, secH int) { + secH = m.secondaryMinH() + genH = bodyH - 1 - secH + if genH < genColMinH { + genH = genColMinH + secH = bodyH - 1 - genH + } + return +} + +// previewDims returns the preview viewport's inner (width, height) for the mode. +// The full-width modes reserve the shared gutter; split leaves the preview's own +// border + padding to do the breathing. Every mode reserves prevChromeRows for +// the pinned pill head above and fallback-display hint below the viewport. +func (m model) previewDims() (int, int) { + bodyH := m.contentH() + switch m.mode() { + case modeCollapsed: + return m.w - gut, bodyH - prevChromeRows + case modeMedium: + _, secH := m.mediumSplit(bodyH) + return m.routingColW() - gut, secH - prevChromeRows + default: // split — the pane draws a border + prevPadL inside its width, so the + // viewport (what renderRoute wraps to) gets the inner text area, not the box. + return m.w - m.listW() - 3 - prevPadL, bodyH - prevChromeRows + } +} + +// usageColShare is the medium secondary row's Usage width: Usage is the +// favored pane — it takes the larger 3/5 proportional share of the row so its +// bars and notes keep breathing room, never dropping below its measured +// stacked minimum. Routing is the pane that shrinks as Usage grows, floored +// at routingMinW (the medium width threshold guarantees both floors seat). +func (m model) usageColShare() int { + avail := m.w - secSepW + uw := avail * 3 / 5 + if min := m.usageColW(); uw < min { + uw = min + } + if avail-uw < routingMinW { + uw = avail - routingMinW + } + return uw +} + +// routingColW is the medium secondary row's routing share: whatever Usage's +// favored share (and the separator between the panes) leaves free. +func (m model) routingColW() int { + w := m.w + if !m.hideUsage { + w -= m.usageColShare() + secSepW + } + if w < routingMinW { + w = routingMinW + } + return w +} + +func (m model) listW() int { + // wide enough for the generator options on one line; capped so a very wide + // terminal doesn't stretch the list needlessly. + w := m.genRowWidth() + if w > m.w-33 { + w = m.w - 33 + } + // The ox lanes widen the lane row past this function's old 80-cell + // aesthetic cap; a wider list beats clipping dial options mid-value. + if w > 116 { + w = 116 + } + return w +} + +// Usage bars naturally measure ten cells, then grow or shrink to the width +// assigned by their row group. Styling is deliberately applied after the +// display-cell geometry is settled, so ANSI sequences never enter the math. diff --git a/main.go b/main.go index fc3d8e9..d4f6b26 100644 --- a/main.go +++ b/main.go @@ -11,4036 +11,16 @@ package main import ( - "bufio" - "encoding/json" - "errors" "fmt" - "io" - "math" - "net/http" "os" "os/exec" - "regexp" - "slices" - "sort" - "strconv" "strings" - "sync" - "time" - "unicode" clikit "github.com/atyrode/cli-kit" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) -// ── keybindings (drive both input handling and the bubbles/help footer) ─────── -type keyMap struct { - Move, Change, Reset, Depth, Refresh, Manager, Collapse, Usage, Launch, Managed, Untrusted, Help, Quit key.Binding -} - -// ShortHelp is a static single-line stand-in used only when measuring the -// footer height for mode selection (which would otherwise recurse through the -// state-derived compact help). The rendered compact line comes from -// contextHelp, which derives its bindings from the live model state (atyrode/dotfiles#198). -func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.Move, k.Change, k.Reset, k.Help, k.Quit} -} -func (k keyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.Move, k.Change, k.Reset}, - {k.Depth, k.Refresh, k.Manager, k.Collapse, k.Usage}, - {k.Launch, k.Managed, k.Untrusted, k.Help, k.Quit}, - } -} - -var keys = keyMap{ - Move: key.NewBinding(key.WithKeys("up", "down", "j", "k"), key.WithHelp("↑↓", "move")), - Change: key.NewBinding(key.WithKeys("left", "right", "h", "l"), key.WithHelp("←→", "change")), - Reset: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", gReset+" defaults")), - Depth: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "primary ⇄ full chains")), - Refresh: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "refresh usage")), - Manager: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "manage accounts")), - Collapse: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "show/hide routing")), - Usage: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "show/hide usage")), - Launch: key.NewBinding(key.WithKeys("enter"), key.WithHelp("⏎", "launch")), - Managed: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "managed omp")), - Untrusted: key.NewBinding(key.WithKeys("u"), key.WithHelp("u", "sandbox")), - Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "more")), - Quit: key.NewBinding(key.WithKeys("q", "esc", "ctrl+c"), key.WithHelp("q", "quit")), -} - -// defaultSel returns a fresh copy of the generator's default facet selection — -// used both to seed the model and to restore it via the reset key. -func defaultSel() map[string]string { - return map[string]string{"lane": "mixed", "model": "smart", "thinking": "medium", "advisor": "glance", "spark": "on", "fable": "off", "main": "off", "fast": "off", "relief": "on"} -} - -// ── palette ────────────────────────────────────────────────────────────────── -// The palette, glyphs, and styles now live in the shared cli-kit; these are -// ergonomic local aliases so the rest of the file reads unchanged. cli-kit is -// the single source both `code` and `atyrode` build on. -const ( - cAcc = clikit.CAcc - cBord = clikit.CBord - cHead = clikit.CHead - cSelBg = clikit.CSelBg - cGreen = clikit.CGreen -) - -const ( - gWarn = clikit.GWarn - gReset = clikit.GReset -) - -var ( - meterRamp = clikit.MeterRamp - - stDim = clikit.StDim - stHead = clikit.StHead - stWarn = clikit.StWarn - stBrk = clikit.StBrk - stStruck = clikit.StStruck - - // stKey renders an inline key cue (r, a, s, p) — visually secondary but - // readable against the background, per the section-chrome convention. - stKey = lipgloss.NewStyle().Foreground(lipgloss.Color(cHead)) - - // Title-local hotkey cues (d · defaults, p · hide, s · hide) are quieter - // than the footer help: terminals have no portable alpha, so these are - // dedicated pre-blended tokens — CHead/CDim mixed ~40% toward the app's - // dark backdrop — applied to the whole cue, key included. Pre-blending is - // used instead of ANSI faint because faint's dimming factor varies wildly - // across terminals (and would double-dim already-muted text). Footer - // recovery cues keep the brighter help styles for readability. - stCueKey = lipgloss.NewStyle().Foreground(lipgloss.Color("#646b76")) // CHead → backdrop - stCue = lipgloss.NewStyle().Foreground(lipgloss.Color("#4f5768")) // CDim → backdrop - - // layout + meter primitives now live in cli-kit - padLeft = clikit.PadLeft - pad = clikit.Pad - windowList = clikit.WindowList - - // Provider-qualified ids: bare catalog ids today (gpt-…, claude-…) plus - // slash-scoped ones (stealth/ox-alpha, local-qwen/qwen3.8-27b). The level - // suffix with its colon is what keeps prose out; the word boundary keeps - // "maxed" from reading as a model. - modelRe = regexp.MustCompile(`([a-z][a-z0-9._/-]*):(minimal|low|medium|high|xhigh|max)\b`) -) - -// ── colourisers ────────────────────────────────────────────────────────────── -func lvl(s string) int { - switch s { - case "minimal": - return 0 - case "low": - return 1 - case "medium": - return 2 - case "high": - return 3 - case "xhigh": - return 4 - } - return 5 -} - -func shortModel(name string) string { - if name == "gpt-5.4" { - return name - } - // Slash-scoped ids display without their provider path, and keep their - // full model part — the vendor's own naming is the recognizable bit. - if i := strings.LastIndexByte(name, '/'); i >= 0 { - name = name[i+1:] - if !strings.HasPrefix(name, "claude") { - return name - } - } - p := strings.Split(name, "-") - if strings.HasPrefix(name, "claude") && len(p) > 1 { - return p[1] - } - return p[len(p)-1] -} - -func clampByte(x float64) int { - v := int(x) - if v > 255 { - return 255 - } - if v < 0 { - return 0 - } - return v -} - -func paintModel(tok string) string { - i := strings.LastIndex(tok, ":") - name, level := tok[:i], tok[i+1:] - p := providerByModel(name) - var br, bg, bb float64 - switch { - case p != nil: - br, bg, bb = p.PaintRGB[0], p.PaintRGB[1], p.PaintRGB[2] - case strings.Contains(name, "local-"): - // Free/local runtimes read green — the same family as the ox accents. - br, bg, bb = 96, 211, 150 - default: - return shortModel(name) + ":" + level // unknown provider: uncoloured - } - f := 0.60 + float64(lvl(level))*0.088 - col := lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", clampByte(br*f), clampByte(bg*f), clampByte(bb*f))) - return lipgloss.NewStyle().Foreground(col).Render(shortModel(name) + ":" + level) -} - -func colorizeRoute(line string) string { return modelRe.ReplaceAllStringFunc(line, paintModel) } - -// bucketOf guesses a quota bucket from a model name. It is the fallback for -// catalogs that declare no bucket column, and the only resolver for the bare -// facet names ("fable", "spark") the suggest box asks about — prefer -// model.bucketFor wherever a receiver is in reach. An unknown name maps to no -// bucket at all rather than someone else's quota window. -func bucketOf(model string) string { - m := model - if i := strings.IndexByte(m, ':'); i >= 0 { - m = m[:i] - } - // Provider-scoped ids outside the subscription pools (OpenRouter, - // local runtimes) have no quota window code knows about. An empty bucket - // never reads as down, which is exactly right for a free or local model. - if strings.Contains(m, "/") { - return "" - } - for _, p := range providerRegistry { - for _, s := range p.Special { - if strings.Contains(m, s.Bucket) { - return p.BucketBase + "-" + s.Bucket - } - } - } - if p := providerByModel(m); p != nil { - return p.mainBucket() - } - for _, p := range providerRegistry { - for _, pre := range p.ModelPrefixes { - if strings.Contains(m, pre) { - return p.mainBucket() - } - } - } - return "" -} - -// bucketFor resolves a routing token's quota bucket from the catalog, falling -// back to the name guess only when the catalog declares none. The catalog wins -// because names are not a taxonomy: claude-mythos-5 sits in omp's catalog at -// claude-fable-5's price yet 404s on this account, and every model omp adds -// would otherwise need one more substring arm here before it could be struck -// through correctly. -func (m model) bucketFor(name string) string { - id := name - if i := strings.IndexByte(id, ':'); i >= 0 { - id = id[:i] - } - if f, ok := m.facts[id]; ok { - if f.bucket != "" { - return f.bucket - } - if p := providerByPool(f.pool); p != nil { - return p.mainBucket() - } - } - return bucketOf(id) -} - -// ── data ───────────────────────────────────────────────────────────────────── -// loadBlocks parses a generated page into name -> role rows. -func loadBlocks(path string) map[string][]string { - blocks := map[string][]string{} - f, err := os.Open(path) - if err != nil { - return blocks - } - defer f.Close() - sc := bufio.NewScanner(f) - sc.Buffer(make([]byte, 1024*128), 1024*128) - var cur string - for sc.Scan() { - line := sc.Text() - if line == "" { - cur = "" - continue - } - if line[0] != ' ' { - if fs := strings.Fields(line); len(fs) > 0 { - cur = fs[0] - blocks[cur] = nil - } - continue - } - if cur != "" { - blocks[cur] = append(blocks[cur], line) - } - } - return blocks -} - -// ── usage + availability ───────────────────────────────────────────────────── -type usageWin struct { - label string - pct int - tier string - secs int64 // seconds until reset (relative) - dur int64 // window length in seconds - prov string - stale bool // retained from the last successful fetch after a refresh omitted this window - missing bool // never observed: rendered as a deterministic placeholder row - observed int64 // Unix timestamp of the last real value; retained across cache fallback -} - -// resetCredits tracks OpenAI reset credits: how many are currently available -// and the seconds until each available credit expires (relative, unsorted). -type resetCredits struct { - avail int - exp []int64 -} - -type availability struct { - bucket map[string]string // bucket -> "ok" | "maxed" | "unauthed" - reset map[string]int64 - wins []usageWin - credits resetCredits - accountCredits map[accountKey]resetCredits - ok bool - accounts map[string][]account - accountUsage map[accountKey][]usageWin - accountsOK bool - selectionApplied bool - accountsStale bool - // deepseek is the DeepSeek prepaid balance: nil when the snapshot carries - // no DeepSeek credential (the group is hidden entirely — an absent API key - // is the normal state, unlike a metered subscription). - deepseek *deepseekBalance -} - -func fetchBrokerUsage(broker brokerConfig) ([]byte, error) { - if broker.URL == "" || broker.Token == "" { - return nil, errors.New("central auth broker is not configured") - } - req, err := http.NewRequest(http.MethodGet, strings.TrimRight(broker.URL, "/")+"/v1/usage", nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", "Bearer "+broker.Token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - _, _ = io.Copy(io.Discard, resp.Body) - return nil, fmt.Errorf("usage endpoint returned %s", resp.Status) - } - return io.ReadAll(resp.Body) -} - -type usageCacheWin struct { - Label string `json:"label"` - Pct int `json:"pct"` - Tier string `json:"tier,omitempty"` - ResetsAt int64 `json:"resetsAt"` - Dur int64 `json:"dur"` - Provider string `json:"provider"` - Observed int64 `json:"observed"` -} - -type usageCacheAccount struct { - Provider string `json:"provider"` - IdentityKey string `json:"identityKey"` - Wins []usageCacheWin `json:"wins"` -} - -type usageCacheFile struct { - SavedAt int64 `json:"savedAt"` - Accounts map[string][]account `json:"accounts"` - Deepseek *usageCacheBalance `json:"deepseekBalance,omitempty"` - Usage []usageCacheAccount `json:"usage"` -} - -// usageCacheBalance caches the DeepSeek balance VALUE only — never the key. -type usageCacheBalance struct { - Currency string `json:"currency"` - Total string `json:"totalBalance"` - FetchedAt int64 `json:"fetchedAt"` -} - -func emptyAvailability() availability { - return availability{ - bucket: map[string]string{}, reset: map[string]int64{}, - accounts: map[string][]account{}, accountUsage: map[accountKey][]usageWin{}, - accountCredits: map[accountKey]resetCredits{}, - } -} - -// parseAvailability associates broker usage with stable identities from the -// same snapshot. observedAt is the cache observation time; reset countdowns -// remain relative to now because the broker payload stores absolute deadlines. -func parseAvailability(accounts map[string][]account, accountsOK bool, out []byte, observedAt int64) availability { - a := emptyAvailability() - a.accounts, a.accountsOK = accounts, accountsOK - type limit struct { - Label string `json:"label"` - Scope struct { - Tier string `json:"tier"` - } `json:"scope"` - Amount struct { - UsedFraction float64 `json:"usedFraction"` - } `json:"amount"` - Window struct { - ResetsAt int64 `json:"resetsAt"` - DurationMs int64 `json:"durationMs"` - } `json:"window"` - } - var doc struct { - Reports []struct { - Provider string `json:"provider"` - Email string `json:"email"` - AccountID string `json:"accountId"` - Metadata struct { - Email string `json:"email"` - AccountID string `json:"accountId"` - } `json:"metadata"` - Limits []limit `json:"limits"` - ResetCredits struct { - AvailableCount int `json:"availableCount"` - Credits []struct { - ExpiresAt string `json:"expiresAt"` - Status string `json:"status"` - } `json:"credits"` - } `json:"resetCredits"` - } `json:"reports"` - } - if len(out) == 0 || json.Unmarshal(out, &doc) != nil { - return a - } - a.ok = true - provSeen := map[string]bool{} - now := time.Now().Unix() - if observedAt <= 0 { - observedAt = now - } - for _, r := range doc.Reports { - provSeen[r.Provider] = true - reportWins := make([]usageWin, 0, len(r.Limits)) - for _, l := range r.Limits { - pct := int(l.Amount.UsedFraction*100 + 0.5) - win := usageWin{label: l.Label, pct: pct, tier: l.Scope.Tier, - secs: l.Window.ResetsAt/1000 - now, dur: l.Window.DurationMs / 1000, - prov: r.Provider, observed: observedAt} - reportWins = append(reportWins, win) - a.wins = append(a.wins, win) - bkt := bucketForProviderTier(r.Provider, l.Scope.Tier) - if bkt == "" { - continue - } - if pct >= 100 { - a.bucket[bkt] = "maxed" - a.reset[bkt] = l.Window.ResetsAt/1000 - now - } else if a.bucket[bkt] != "maxed" { - a.bucket[bkt] = "ok" - } - } - email, accountID := r.Metadata.Email, r.Metadata.AccountID - if email == "" { - email = r.Email - } - if accountID == "" { - accountID = r.AccountID - } - var matchedKey accountKey - matched := false - for _, acct := range a.accounts[r.Provider] { - if (email != "" && acct.Email != "" && strings.EqualFold(email, acct.Email)) || - (accountID != "" && accountID == acct.IdentityKey) { - matchedKey = accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} - a.accountUsage[matchedKey] = append(a.accountUsage[matchedKey], reportWins...) - matched = true - break - } - } - if r.Provider == openAIProvider { - credits := resetCredits{avail: r.ResetCredits.AvailableCount} - for _, c := range r.ResetCredits.Credits { - if c.Status != "available" { - continue - } - if t, err := time.Parse(time.RFC3339, c.ExpiresAt); err == nil { - credits.exp = append(credits.exp, t.Unix()-now) - } - } - a.credits.avail += credits.avail - a.credits.exp = append(a.credits.exp, credits.exp...) - if matched { - attributed := a.accountCredits[matchedKey] - attributed.avail += credits.avail - attributed.exp = append(attributed.exp, credits.exp...) - a.accountCredits[matchedKey] = attributed - } - } - } - for _, prov := range providerRegistry { - if !prov.Metered { - continue - } - for _, b := range prov.buckets() { - if !provSeen[prov.ID] { - a.bucket[b] = "unauthed" - } else if _, ok := a.bucket[b]; !ok { - a.bucket[b] = "ok" - } - } - } - return a -} - -// loadAvailability reads one central snapshot and one aggregate usage report, -// plus — when the snapshot carries a DeepSeek api_key — the upstream prepaid -// balance, fetched concurrently so neither request delays the other. -func loadAvailability(broker brokerConfig) availability { - accounts, err := loadAccounts(broker) - accountsOK := err == nil - if !accountsOK { - accounts = map[string][]account{} - } - var ds *deepseekBalance - var wg sync.WaitGroup - if key := deepseekAPIKey(accounts); key != "" { - wg.Add(1) - go func() { - defer wg.Done() - bal, err := fetchDeepSeekBalance(key) - if err != nil { - // Degrade to an explicit "unavailable" row; other providers - // are unaffected. - bal = deepseekBalance{fetchedAt: time.Now().Unix()} - } - ds = &bal - }() - } - out, err := fetchBrokerUsage(broker) - if err != nil { - out = nil - } - wg.Wait() - a := parseAvailability(accounts, accountsOK, out, 0) - a.deepseek = ds - return a -} - -func loadUsageCache(path string) availability { - a := emptyAvailability() - if path == "" { - return a - } - body, err := os.ReadFile(path) - if err != nil { - return a - } - var cached usageCacheFile - if json.Unmarshal(body, &cached) != nil || cached.SavedAt <= 0 || len(cached.Usage) == 0 { - return a - } - a.accounts, a.accountsOK, a.ok = cached.Accounts, true, true - provSeen := map[string]bool{} - for _, entry := range cached.Usage { - key := accountKey{Provider: entry.Provider, IdentityKey: entry.IdentityKey} - for _, cachedWin := range entry.Wins { - win := usageWin{ - label: cachedWin.Label, pct: cachedWin.Pct, tier: cachedWin.Tier, - secs: cachedWin.ResetsAt - time.Now().Unix(), dur: cachedWin.Dur, prov: cachedWin.Provider, - observed: cachedWin.Observed, stale: true, - } - a.accountUsage[key] = append(a.accountUsage[key], win) - a.wins = append(a.wins, win) - provSeen[win.prov] = true - bucket := bucketForProviderTier(win.prov, win.tier) - if bucket == "" { - continue - } - if win.pct >= 100 { - a.bucket[bucket], a.reset[bucket] = "maxed", win.secs - } else if a.bucket[bucket] != "maxed" { - a.bucket[bucket] = "ok" - } - } - } - if cached.Deepseek != nil { - a.deepseek = &deepseekBalance{ - ok: true, currency: cached.Deepseek.Currency, total: cached.Deepseek.Total, - fetchedAt: cached.Deepseek.FetchedAt, stale: true, - } - } - for _, prov := range providerRegistry { - if !prov.Metered { - continue - } - for _, bucket := range prov.buckets() { - if !provSeen[prov.ID] { - a.bucket[bucket] = "unauthed" - } else if _, ok := a.bucket[bucket]; !ok { - a.bucket[bucket] = "ok" - } - } - } - return a -} - -func saveUsageCache(path string, a availability) { - if path == "" || !a.ok { - return - } - now := time.Now().Unix() - cached := usageCacheFile{SavedAt: now, Accounts: a.accounts} - if a.deepseek != nil && a.deepseek.ok { - cached.Deepseek = &usageCacheBalance{ - Currency: a.deepseek.currency, Total: a.deepseek.total, FetchedAt: a.deepseek.fetchedAt, - } - } - for key, wins := range a.accountUsage { - entry := usageCacheAccount{Provider: key.Provider, IdentityKey: key.IdentityKey} - for _, win := range wins { - if win.missing { - continue - } - observed := win.observed - if observed <= 0 { - observed = now - } - entry.Wins = append(entry.Wins, usageCacheWin{ - Label: win.label, Pct: win.pct, Tier: win.tier, - ResetsAt: observed + win.secs, Dur: win.dur, - Provider: win.prov, Observed: observed, - }) - } - if len(entry.Wins) > 0 { - cached.Usage = append(cached.Usage, entry) - } - } - if len(cached.Usage) == 0 { - return - } - sort.Slice(cached.Usage, func(i, j int) bool { - if cached.Usage[i].Provider != cached.Usage[j].Provider { - return cached.Usage[i].Provider < cached.Usage[j].Provider - } - return cached.Usage[i].IdentityKey < cached.Usage[j].IdentityKey - }) - body, err := json.Marshal(cached) - if err != nil { - return - } - body = append(body, '\n') - _ = atomicPrivateWrite(path, body) -} - -// bucketForProviderTier maps a usage report's (provider, tier) scope onto the -// quota bucket it constrains: the provider's main window, or a special tier's -// dedicated window. Unmetered and unknown providers own no buckets. -func bucketForProviderTier(prov, tier string) string { - p := providerByID(prov) - if p == nil || !p.Metered { - return "" - } - if tier == "" || tier == "-" { - return p.mainBucket() - } - for _, s := range p.Special { - if s.Bucket == tier { - return p.BucketBase + "-" + s.Bucket - } - } - return "" -} - -func (a availability) down(bucket string) bool { - return a.bucket[bucket] == "maxed" || a.bucket[bucket] == "unauthed" -} - -// reconcileUsage folds a freshly fetched availability over the one currently -// shown, so a flaky upstream never wipes known-good data. It returns the -// availability to display plus whether the whole panel is stale: -// -// - a total fetch failure after any prior success keeps the previous -// availability wholesale and reports it stale — the control row shows a -// refresh-failed warning instead of dropping to the unauthenticated error; -// - a successful payload that omits an account's usage retains that -// account's last observed rows, visibly marked stale with their age; -// - a successful Anthropic payload that only omits the flaky Fable window -// retains the last Fable row, along with its bucket/reset routing state; -// - a successful Anthropic payload with no Fable window ever observed -// appends a deterministic unavailable placeholder, so the datum appearing -// on a later refresh never pops the panel geometry. -// -// Fresh values always win; nothing is fabricated — retained rows are visibly -// marked stale and placeholders carry no numbers. -func reconcileUsage(prev, next availability) (availability, bool) { - if !next.ok { - if prev.ok { - return prev, true - } - return next, false - } - if !next.accountsOK && prev.accountsOK { - next.accounts, next.accountsOK, next.accountsStale = prev.accounts, true, true - } - next.accountUsage = reconcileAccountUsage(prev.accountUsage, next.accountUsage, next.accounts) - if next.deepseek != nil && !next.deepseek.ok && prev.deepseek != nil && prev.deepseek.ok { - // A failed balance refresh keeps the last known value, visibly stale — - // same retention contract as the metered windows. - retained := *prev.deepseek - retained.stale = true - next.deepseek = &retained - } - hasClaude, hasFable := false, false - for _, w := range next.wins { - if w.prov != anthropicProvider { - continue - } - hasClaude = true - if w.tier == "fable" { - hasFable = true - } - } - if !hasClaude || hasFable { - return next, false - } - for _, w := range prev.wins { - if w.prov == anthropicProvider && w.tier == "fable" && !w.missing { - w.stale = true - next.wins = append(next.wins, w) - // Carry the bucket/reset state observed with the retained window: - // loadAvailability defaults an unseen bucket to "ok", which would - // route onto a fable the last real datum said was maxed. - if st, ok := prev.bucket["claude-fable"]; ok { - next.bucket["claude-fable"] = st - } - if r, ok := prev.reset["claude-fable"]; ok { - next.reset["claude-fable"] = r - } - return next, false - } - } - next.wins = append(next.wins, fablePlaceholder) - return next, false -} - -func reconcileAccountUsage(prev, next map[accountKey][]usageWin, accounts map[string][]account) map[accountKey][]usageWin { - if next == nil { - next = map[accountKey][]usageWin{} - } - active := map[accountKey]bool{} - for _, providerAccounts := range accounts { - for _, acct := range providerAccounts { - active[accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey}] = true - } - } - if len(active) == 0 { - for key := range next { - active[key] = true - } - } - for key := range active { - wins := next[key] - hasFresh := false - for _, w := range wins { - if !w.missing { - hasFresh = true - break - } - } - if !hasFresh { - retained := make([]usageWin, 0, len(prev[key])) - for _, w := range prev[key] { - if w.missing { - continue - } - w.stale = true - retained = append(retained, w) - } - if len(retained) > 0 { - next[key] = retained - } - continue - } - if key.Provider != anthropicProvider { - continue - } - hasClaude, hasFable := false, false - for _, w := range wins { - if w.prov != anthropicProvider { - continue - } - if !w.missing { - hasClaude = true - } - if w.tier == "fable" { - hasFable = true - } - } - if !hasClaude || hasFable { - continue - } - retained := false - for _, w := range prev[key] { - if w.prov == anthropicProvider && w.tier == "fable" && !w.missing { - w.stale = true - next[key] = append(next[key], w) - retained = true - break - } - } - if !retained { - next[key] = append(next[key], fablePlaceholder) - } - } - return next -} - -// fablePlaceholder is the never-observed fable window's deterministic -// stand-in: the real payload label (so shortWin renders the same "7d fable" -// tag) and window length, with no usage numbers to fabricate. -var fablePlaceholder = usageWin{label: "Claude 7 Day (Fable)", tier: "fable", dur: 7 * 24 * 3600, prov: anthropicProvider, missing: true} - -type usageGroupKey struct { - prov string - tier string - dur int64 - label string -} - -type usageGroup struct { - win usageWin - count int64 - pctSum int64 - secsSum int64 - observed int64 -} - -func knownUsageWindow(w usageWin) bool { - label := shortWin(w.label) - bucket := bucketForProviderTier(w.prov, w.tier) - p := providerByID(w.prov) - if bucket == "" || p == nil { - return false - } - if bucket == p.mainBucket() { - return label == "5h" || label == "7d" - } - suffix := strings.TrimPrefix(bucket, p.BucketBase+"-") - return label == "5h "+suffix || label == "7d "+suffix -} - -// selectedAvailability derives account-sensitive usage and routing availability -// solely from enabled broker identities. Unmatched reports never enter this seam. -func selectedAvailability(a availability, disabled map[accountKey]bool) availability { - selected := a - selected.selectionApplied = true - selected.accounts = map[string][]account{} - selected.accountUsage = map[accountKey][]usageWin{} - selected.accountCredits = map[accountKey]resetCredits{} - selected.bucket = map[string]string{} - selected.reset = map[string]int64{} - selected.wins = nil - selected.credits = resetCredits{} - - enabledProviders := map[string]bool{} - groups := map[usageGroupKey]*usageGroup{} - missing := map[usageGroupKey]usageWin{} - var groupOrder []usageGroupKey - for prov, accounts := range a.accounts { - for _, acct := range accounts { - key := accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} - if disabled[key] { - continue - } - selected.accounts[prov] = append(selected.accounts[prov], acct) - enabledProviders[acct.Provider] = true - wins := a.accountUsage[key] - if credits, ok := a.accountCredits[key]; ok { - selected.accountCredits[key] = credits - selected.credits.avail += credits.avail - selected.credits.exp = append(selected.credits.exp, credits.exp...) - } - for _, win := range wins { - if win.prov != acct.Provider || !knownUsageWindow(win) { - continue - } - selected.accountUsage[key] = append(selected.accountUsage[key], win) - groupKey := usageGroupKey{prov: win.prov, tier: win.tier, dur: win.dur, label: shortWin(win.label)} - if win.missing { - if _, ok := missing[groupKey]; !ok { - placeholder := win - placeholder.label = groupKey.label - missing[groupKey] = placeholder - groupOrder = append(groupOrder, groupKey) - } - continue - } - group := groups[groupKey] - if group == nil { - aggregate := win - aggregate.label = groupKey.label - group = &usageGroup{win: aggregate} - groups[groupKey] = group - groupOrder = append(groupOrder, groupKey) - } - pct, secs := int64(win.pct), win.secs - if pct < 0 { - pct = 0 - } - if secs < 0 { - secs = 0 - } - group.count++ - group.pctSum += pct - group.secsSum += secs - group.win.stale = group.win.stale || win.stale - if win.observed > 0 && (group.observed == 0 || win.observed < group.observed) { - group.observed = win.observed - } - } - } - } - seen := map[usageGroupKey]bool{} - for _, key := range groupOrder { - if seen[key] { - continue - } - seen[key] = true - if group := groups[key]; group != nil { - group.win.pct = int((group.pctSum + group.count/2) / group.count) - group.win.secs = (group.secsSum + group.count/2) / group.count - group.win.observed = group.observed - selected.wins = append(selected.wins, group.win) - } else { - selected.wins = append(selected.wins, missing[key]) - } - } - for _, prov := range providerRegistry { - if !prov.Metered { - continue - } - for _, bucket := range prov.buckets() { - if enabledProviders[prov.ID] { - selected.bucket[bucket] = "ok" - } else { - selected.bucket[bucket] = "unauthed" - } - } - } - for _, win := range selected.wins { - bucket := bucketForProviderTier(win.prov, win.tier) - if bucket == "" || win.missing || win.pct < 100 { - continue - } - selected.bucket[bucket] = "maxed" - // Multiple quota windows can constrain one route. The route becomes - // usable only after the last maxed aggregate resets, so retain the - // longest selected reset rather than whichever account/map came last. - if win.secs > selected.reset[bucket] { - selected.reset[bucket] = win.secs - } - } - return selected -} - -// ── routing render (depth: 0 lead · 1 full) ────────────────────────────────── -// renderRoute lays out each role's chain, wrapping cleanly at `width`: when a -// chain doesn't fit, it breaks after an arrow and the continuation is indented -// to align under the first model, so it reads as one hanging block rather than a -// ragged wrap. Down (maxed/unauthed) models are struck through — which bucket a -// model draws from is a catalog fact, hence the receiver. -func (m model) renderRoute(rows []string, depth int, a availability, width int) string { - if width < 24 { - width = 24 - } - arrow := stDim.Render(" → ") - var out []string - for _, r := range rows { - locs := modelRe.FindAllStringIndex(r, -1) - if len(locs) == 0 { // a note/meta line with no models — pass through - out = append(out, colorizeRoute(r)) - continue - } - label := r[:locs[0][0]] // role + its alignment padding, kept verbatim - labelW := lipgloss.Width(label) - indent := strings.Repeat(" ", labelW) - - type tok struct { - text string - down bool - } - var toks []tok - for _, loc := range locs { - mt := r[loc[0]:loc[1]] - toks = append(toks, tok{mt, a.ok && a.down(m.bucketFor(mt))}) - } - // how many to show: full → all; lead → primary, or up to the first live - // model when the lead is down (the one that actually runs). - last := len(toks) - 1 - if depth == 0 { - last = 0 - for i, t := range toks { - last = i - if !t.down { - break - } - } - } - // render a token as it displays (short name), returning the styled - // string and its display width — struck if the model is down. - render := func(t tok) (string, int) { - c := strings.LastIndexByte(t.text, ':') - short := shortModel(t.text[:c]) + t.text[c:] - if t.down { - s := stStruck.Render(short) - return s, lipgloss.Width(s) - } - s := colorizeRoute(t.text) // paintModel shortens + colours - return s, lipgloss.Width(s) - } - s0, w0 := render(toks[0]) - line, lineW := label+s0, labelW+w0 - for i := 1; i <= last; i++ { - si, wi := render(toks[i]) - // Reserve 2 cols for a trailing " →" whenever more models follow, so a - // break line's continuation arrow always fits within width instead of - // being clipped at the edge; the final model needs no such reserve. - budget := width - if i < last { - budget -= 2 - } - if lineW+3+wi > budget { // won't fit — break after a trailing arrow - out = append(out, line+stDim.Render(" →")) - line, lineW = indent+si, labelW+wi - } else { - line += arrow + si - lineW += 3 + wi - } - } - out = append(out, line) - } - return strings.Join(out, "\n") + "\n" -} - -// splitMeta peels the "thinking … · fallback … · advisor …" summary line off a -// routing block (if present), returning it trimmed plus the remaining role rows. -func splitMeta(rows []string) (string, []string) { - if len(rows) > 0 && strings.Contains(rows[0], "·") && !modelRe.MatchString(rows[0]) { - return strings.TrimSpace(rows[0]), rows[1:] - } - return "", rows -} - -// ── generator facets ───────────────────────────────────────────────────────── -type facet struct { - key string - values []string - glyph string -} - -// facetDefs seeds the facet dials. The lane facet starts empty: its values are -// catalog-driven (applyCatalog collects the lanes the catalog actually -// generated), so a two-pool catalog shows exactly the classic five lanes and a -// richer one adds its own. -func facetDefs(glyphs map[string]string) []facet { - return []facet{ - // Seeded with the required pools' lanes so a catalog-less run (the - // onboarding shell, a broken CODE_GENERATED) keeps a working dial; - // applyCatalog replaces the list with the lanes the catalog generated. - {"lane", requiredPoolLanes(), glyphs["lane"]}, - {"model", []string{"fast", "normal", "smart"}, glyphs["model"]}, - {"thinking", []string{"minimal", "low", "medium", "high", "xhigh", "max"}, glyphs["thinking"]}, - // advisor as a power/cost dial: a quick glance, a proper review, or a - // deep (expensive) audit — off spends nothing. - {"advisor", []string{"off", "glance", "review", "audit"}, glyphs["advisor"]}, - {"fast", []string{"on", "off"}, glyphs["fast"]}, - {"spark", []string{"on", "off"}, glyphs["spark"]}, - {"fable", []string{"on", "off"}, glyphs["fable"]}, - // fable-as-main: hand the scarce elite the default (main-agent) role too. - // A sub-setting of fable — only visible while fable is on (see - // visibleFacets) and never set by a suggestion (see validFacetActions). - {"main", []string{"on", "off"}, glyphs["main"]}, - // relief: whether metered-led chains may spill into a pay-as-you-go - // pool's tail rung. Only rendered when the catalog carries an - // optional pool and the lane is a metered-led blend. - {"relief", []string{"on", "off"}, glyphs["relief"]}, - } -} - -// parseAdvisors reads the __advisors__ block (rows: " ") -// into a map keyed "level/ctx" — the advisor model table, sourced from -// generate-profiles.py so the catalog stays a single source of truth. -func parseAdvisors(rows []string) map[string][]string { - out := map[string][]string{} - for _, r := range rows { - f := strings.Fields(strings.ReplaceAll(r, "→", " ")) - if len(f) < 3 { - continue - } - var chain []string - for _, t := range f[2:] { - if modelRe.MatchString(t) { - chain = append(chain, t) - } - } - if len(chain) > 0 { - out[f[0]+"/"+f[1]] = chain - } - } - return out -} - -// modelFact is a model's measured facts from omp (via the catalog): pricing -// ($/1M tokens), output throughput (tok/s), time-to-first-token (seconds), the -// quota bucket it draws from ("" when the catalog declares none), and the pool -// it belongs to ("" in catalogs that predate the column — the provider-prefix -// heuristic covers those). -type modelFact struct { - in, out, speed, ttft float64 - bucket string - pool string // catalog pool letter ("" in legacy catalogs — the family guess covers those) -} - -// effTPS folds ttft into throughput — the effective tok/s for a representative -// reply of effTokens: total time = ttft (startup) + tokens/speed (streaming), so -// a blazing-but-slow-to-start model (spark: 287 t/s, 5.6s ttft) reads honestly. -const effTokens = 300.0 - -func (f modelFact) effTPS() float64 { - if f.speed <= 0 { - return 0 - } - return effTokens / (f.ttft + effTokens/f.speed) -} - -// parseFacts reads the __models__ block (rows: " -// [ []]") into a per-model table, sourced from the catalog -// so meters and routing agree. The trailing columns are optionals so legacy -// five- and six-token catalogs keep working: column six is the quota bucket, -// column seven the provider — accepted as the registry id (current renderer) -// or the bare pool letter (older renderers), resolved to a pool either way; -// unknown values fall back to the model-family guess. - -// catalogPool resolves the __models__ trailing column to a pool letter. The -// current renderer writes the registry provider id there; an older one wrote -// the bare pool letter. Either parses; anything unknown falls back to the -// model-family guess so mixed-age catalogs keep working. -func catalogPool(col, id string) string { - if len(col) == 1 { - if providerByPool(col) != nil { - return col - } - } - if p := providerByID(col); p != nil { - return p.Pool - } - if p := providerByModel(id); p != nil { - return p.Pool - } - return "" -} - -func parseFacts(rows []string) map[string]modelFact { - out := map[string]modelFact{} - for _, r := range rows { - f := strings.Fields(r) - if len(f) < 5 { - continue - } - in, e1 := strconv.ParseFloat(f[1], 64) - outc, e2 := strconv.ParseFloat(f[2], 64) - sp, e3 := strconv.ParseFloat(f[3], 64) - tt, e4 := strconv.ParseFloat(f[4], 64) - bucket, pool := "", "" - if len(f) >= 6 { - bucket = f[5] - } - if len(f) >= 7 { - pool = catalogPool(f[6], f[0]) - } - if e1 == nil && e2 == nil && e3 == nil && e4 == nil { - out[f[0]] = modelFact{in, outc, sp, tt, bucket, pool} - } - } - return out -} - -// ── cost + speed meters ────────────────────────────────────────────────────── -// A profile's price and pace are dominated by the models on its heaviest roles, -// so each role is weighted by the token volume it drives over a session: the -// default agent and its task sub-agents move the needle; commit/tiny barely -// register — so Fable-as-commit stays cheap while Fable-as-default is dear (and -// slow). Per role, cost blends input+output pricing while speed reads the model's -// effective throughput (tok/s folded with time-to-first-token — see effTPS); both -// scale with thinking effort (more reasoning = pricier + slower) and take OpenAI's -// priority tier under fast mode (pricier but quicker). The weighted averages map -// onto 1..5 log scales (both perceived multiplicatively), calibrated across every -// valid facet × advisor × fast combination. Every role the generator emits must -// appear here: weightedModels silently skips a role it cannot weigh, so an -// omission drops that model out of both meters with no trace. -var roleWeight = map[string]float64{ - "default": 10, "task": 6, "reviewer": 3, "sonic": 3, "plan": 3, "advisor": 4, "slow": 2, - "designer": 2, "librarian": 2, "scout": 2, "smol": 1, "tiny": 0.5, "commit": 0.5, "vision": 0.5, - // security-reviewer routes like reviewer but is spawned far more rarely - // (ad-hoc scans), so it barely moves the needle. - "security-reviewer": 1, -} -var thinkMult = map[string]float64{ // reasoning tokens grow with effort → pricier - "minimal": 0.6, "low": 0.8, "medium": 1.0, "high": 1.3, "xhigh": 1.6, "max": 2.0, -} -var thinkSpeed = map[string]float64{ // more reasoning before the answer → slower - "minimal": 1.4, "low": 1.2, "medium": 1.0, "high": 0.8, "xhigh": 0.65, "max": 0.5, -} - -const ( - priorityMult = 1.9 // OpenAI priority tier costs more under fast mode … - fastSpeed = 1.3 // … but responds quicker -) - -// Ln endpoints of the grid-wide min/max weighted indices, calibrated over every -// valid facet × advisor × fast combination (cost dear→ high, speed fast→ high). -const ( - costLnLo, costLnHi = 1.27, 4.42 - speedLnLo, speedLnHi = 2.49, 4.20 -) - -// weightedModels walks the current config's rows and calls fn(weight, id, level) -// for each role's lead model — the shared basis for both meters. -// currentRows is the routing block the cost/speed meters score: the generator's -// facet combo with the advisor dial applied. -func (m model) currentRows() []string { - return m.applyAdvisor(m.generated[comboID(m.sel, m.hasRelief)], m.sel["advisor"]) -} - -func (m model) weightedModels(rows []string, fn func(w float64, id, lvl string)) { - for _, r := range rows { - f := strings.Fields(strings.ReplaceAll(r, "→", " ")) - i := 0 - if len(f) > 0 && f[0] == "●" { - i = 1 - } - if i >= len(f) { - continue - } - w, ok := roleWeight[f[i]] - if !ok { - continue - } - var lead string - for _, t := range f[i+1:] { - if modelRe.MatchString(t) { - lead = t - break - } - } - id, lvl, _ := strings.Cut(lead, ":") - if id != "" { - fn(w, id, lvl) - } - } -} - -func logScore(idx, lnLo, lnHi float64) int { - s := 1 + 4*(math.Log(idx)-lnLo)/(lnHi-lnLo) - return int(math.Round(math.Max(1, math.Min(5, s)))) -} - -// DeepSeek discounts the pay-as-you-go API during its off-peak window, -// UTC 16:30–00:30 (deepseek.com/pricing). The meter prices D rungs by the -// clock so a cheap window reads as the discount it is. offPeakNow is a var -// for tests only. -const deepseekOffPeakMult = 0.5 - -var offPeakNow = time.Now - -func deepseekOffPeak(utc time.Time) bool { - m := utc.Hour()*60 + utc.Minute() - return m >= 16*60+30 || m < 30 -} - -// costScore rates the current config from 1 (cheap) to 5 (dear). -func (m model) costScore() int { - if _, ok := m.selectedRuntime(); ok { - return 1 - } - fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") - var num, den float64 - m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { - c, ok := m.facts[id] - if !ok { - return - } - mult, ok := thinkMult[lvl] - if !ok { - mult = 1 - } - cost := (0.25*c.in + 0.75*c.out) * mult - if fast && m.poolOfModel(id) == "O" { - cost *= priorityMult - } - if m.poolOfModel(id) == "D" && deepseekOffPeak(offPeakNow().UTC()) { - cost *= deepseekOffPeakMult - } - num += w * cost - den += w - }) - if den == 0 { - return 1 - } - return logScore(num/den, costLnLo, costLnHi) -} - -// speedScore rates the current config from 1 (slow) to 5 (fast). -func (m model) speedScore() int { - if _, ok := m.selectedRuntime(); ok { - return 3 - } - fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") - var num, den float64 - m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { - c, ok := m.facts[id] - if !ok || c.speed == 0 { - return - } - mult, ok := thinkSpeed[lvl] - if !ok { - mult = 1 - } - sp := c.effTPS() * mult - if fast && m.poolOfModel(id) == "O" { - sp *= fastSpeed - } - num += w * sp - den += w - }) - if den == 0 { - return 3 - } - return logScore(num/den, speedLnLo, speedLnHi) -} - -// meter renders a labelled 1..5 scale — n glyphs in the fill colour, the rest in -// the dim "empty" colour — always five glyphs so the fill (and the headroom) read -// at a glance. -func (m model) meter(label, glyph, fill string, n int) string { - return clikit.Meter(label, glyph, fill, n) -} - -// advisorChain returns the advisor role's model chain for an intensity, sourced -// from the baked __advisors__ table. The advisor is the independent second -// opinion, so it crosses to another provider whenever the lane allows it: the -// first advisorPoolOrder pool that is not the lead's (fable-as-main hands the -// default role to the elite, so the lead becomes the elite's pool). Only the -// pure lanes stay on their own provider — ox-only's second opinion is Ox. -// -// On the mixed ox lanes the chain carries a cross-pool net in spend order — -// the other paid pool's cheapest rung, then the free pool itself — so a dead -// quota never leaves the second eye blind. -func (m model) advisorChain(level string) []string { - lane := m.sel["lane"] - if p := providerByLane(lane); p != nil && lanePure(lane) { - return m.advisors[level+"/"+p.Lane] - } - lead := genLanePolicies[m.sel["lane"]].primary - if fb := providerBySpecial("fable"); fb != nil && m.sel["fable"] == "on" && m.sel["main"] == "on" { - // fable-as-main puts the elite in the default seat, so the second - // opinion flips away from the elite's own pool. - lead = fb.Pool - } - for _, pool := range advisorPoolOrder { - if pool == lead { - continue - } - p := providerByPool(pool) - if p == nil { - continue - } - chain := m.advisors[level+"/"+p.Lane] - if len(chain) == 0 { - continue - } - if lane == "ox-led" || lane == "ox-lean" { - chain = m.advisorSpendNet(chain, p.Lane) - } - return m.advisorRelief(level, chain) - } - return nil -} - -// advisorSpendNet appends the mixed ox lanes' cross-pool net in spend order — -// the other paid pool's cheapest rung, then the free pool itself — so a dead -// quota never leaves the second eye blind. -func (m model) advisorSpendNet(chain []string, ctx string) []string { - tail := m.advisors["glance/gpt"] - if ctx == "gpt" { - tail = m.advisors["glance/claude"] - } - out := append([]string(nil), chain...) - for _, t := range append(append([]string{}, tail...), m.advisors["glance/ox"]...) { - if t != "" && !slices.Contains(out, t) { - out = append(out, t) - } - } - return out -} - -// advisorRelief appends the relief tail to the audit chain: audit is the -// advisor's heavyweight setting, so — exactly like the heavyweight roles — -// its metered chain ends on each optional pool's own audit rung when relief -// is on, and a drained day still gets its second opinion. Lighter levels -// stay short: they are cheap enough that quota rarely blocks them. -func (m model) advisorRelief(level string, chain []string) []string { - if level != "audit" || m.sel["relief"] != "on" || !laneReliefApplies(m.sel["lane"]) { - return chain - } - out := append([]string(nil), chain...) - for i := range providerRegistry { - p := &providerRegistry[i] - if p.Required { - continue - } - relief := m.advisors[level+"/"+p.Lane] - if len(relief) == 0 || slices.Contains(out, relief[0]) { - continue - } - out = append(out, relief[0]) - } - return out -} - -// roleOf returns the role name of a routing row ("● task" → "task"). -func roleOf(row string) string { - f := strings.Fields(row) - if len(f) > 0 && f[0] == "●" { - f = f[1:] - } - if len(f) > 0 { - return f[0] - } - return "" -} - -// applyAdvisor replaces the baked advisor row with one synthesised from the -// chosen intensity (dropping it entirely when off), so the generated preview and -// the launched config both reflect the advisor facet. -func (m model) applyAdvisor(rows []string, level string) []string { - chain := m.advisorChain(level) - newRow := "" - if len(chain) > 0 { - newRow = " advisor " + strings.Join(chain, " → ") - } - var out []string - replaced := false - for _, r := range rows { - if roleOf(r) == "advisor" { - replaced = true - if newRow != "" { - out = append(out, newRow) - } - continue - } - out = append(out, r) - } - if !replaced && newRow != "" { - out = append(out, newRow) - } - return out -} - -// visibleFacets drops facets that don't apply to the current lane, so the -// generator only ever shows actionable options: no spark/fast on a pure lane -// of another pool, no fable outside its pool's lanes. main is fable's -// sub-setting, so it only shows while fable is on (and the lane can host it at -// all). A dial this catalog generated no combo for is dropped the same way — -// it is not a choice. -// -// The lane facet renders as two rows: lead (one segment per pool, plus mixed) -// and blend (led | only, hidden for mixed). sel["lane"] stays the canonical -// value — lead/blend are derived here and recomposed by cycleFacet, and are -// never persisted (saveSelectionState filters on m.facets, which carries only -// "lane"). -func (m model) visibleFacets() []facet { - if _, local := m.selectedRuntime(); local { - var out []facet - for _, f := range m.facets { - if f.key == "runtime" || f.key == "thinking" { - out = append(out, f) - } - } - return out - } - lane := m.sel["lane"] - var out []facet - for _, f := range m.facets { - if f.key == "lane" { - lead, blend := laneSplit(lane) - m.sel["lead"], m.sel["blend"] = lead, blend - // mixed leads the dial: it is the default and the only lead - // without a blend child, so it anchors the left edge. - leads := []string{"mixed"} - seen := map[string]bool{"mixed": true} - for _, v := range f.values { - l, _ := laneSplit(v) - if !seen[l] { - seen[l] = true - leads = append(leads, l) - } - } - out = append(out, facet{"lead", leads, f.glyph}) - if lead != "mixed" { - out = append(out, facet{"blend", []string{"led", "only"}, f.glyph}) - } - continue - } - switch f.key { - case "spark": - // genValid refuses every ox-lane spark combo (the drain bucket's - // leads are utility roles those lanes give to their own pools). - if !laneHostsSpecial(lane, "spark") || m.noSpark || - lane == "ox-only" || lane == "ox-led" || lane == "ox-lean" { - continue - } - case "fast": - // The fast dial is the priority service tier — an OpenAI pool - // feature, meaningless on a pure lane of any other pool or where - // no OpenAI token leads the work. - if p := providerByLane(lane); lanePure(lane) && p != nil && p.ServiceTier[0] == "" { - continue - } - if lane == "ox-only" || lane == "ox-led" { - continue - } - case "fable", "main": - if !laneHostsSpecial(lane, "fable") || m.noFable { - continue - } - if f.key == "main" && (m.sel["fable"] != "on" || lane == "ox-led") { - // fable-as-main would defeat ox-led's free worker, and - // genValid refuses that combo outright. - continue - } - case "relief": - if !m.hasRelief || !laneReliefApplies(lane) { - continue - } - } - out = append(out, f) - } - return out -} - -func comboID(sel map[string]string, hasRelief bool) string { - lane := sel["lane"] - sp, fb := sel["spark"], sel["fable"] - if !laneHostsSpecial(lane, "fable") { - fb = "off" - } - if !laneHostsSpecial(lane, "spark") || lane == "ox-led" || lane == "ox-lean" { - sp = "off" - } - spid, faid := "nosp", "nofa" - if sp == "on" { - spid = "sp" - } - if fb == "on" { - faid = "fa" - // ox-led hosts the elite on deliberative roles only; promoting it to - // the default role would defeat the lane, and genValid refuses that - // combo outright. - if sel["main"] == "on" && lane != "ox-led" { - faid = "famain" - } - } - id := fmt.Sprintf("%s_%s_%s_%s_%s", lane, sel["model"], sel["thinking"], spid, faid) - if hasRelief { - rel := sel["relief"] - if !laneReliefApplies(lane) { - rel = "on" // the only variant generated there - } - if rel == "off" { - id += "_norel" - } else { - id += "_rel" - } - } - return id -} - -// applyCatalog records which dials this catalog can actually serve, then forces -// the rest off. A models file with no tier-0 model yields no _sp_ combos at all, -// so the shipped default (spark on) would open the TUI on a combo that was never -// written — and a selection persisted against a richer catalog does the same. -// Ids are ____, so match whole -// segments: "nosp" and "nofa" contain the very substrings being looked for. -// The lane facet's value list is the catalog's too: the distinct lane segments -// of the combo ids, ordered canonically (laneOrderForPools), so a catalog with -// a DeepSeek pool grows ds-led/ds-only dials and an old one shows exactly the -// classic five. -func (m *model) applyCatalog() { - if len(m.generated) == 0 { - return // no catalog read yet: onboarding, or a broken CODE_GENERATED - } - spark, fable, relief := false, false, false - served := map[string]bool{} - for id := range m.generated { - lane := id - if i := strings.IndexByte(id, '_'); i >= 0 { - lane = id[:i] - } - served[lane] = true - for _, seg := range strings.Split(id, "_") { - switch seg { - case "sp": - spark = true - case "fa", "famain": - fable = true - case "norel": - relief = true - } - } - } - m.noSpark, m.noFable, m.hasRelief = !spark, !fable, relief - if lanes := catalogLanes(m.generated); len(lanes) > 0 { - for i := range m.facets { - if m.facets[i].key == "lane" { - m.facets[i].values = lanes - } - } - } - m.trimLanes(served) - - m.clampSel() -} - -// catalogLanes collects the distinct lane values the catalog generated, in -// canonical dial order; unknown lane names (a newer catalog) trail sorted so -// they are still reachable. -func catalogLanes(generated map[string][]string) []string { - seen := map[string]bool{} - for id := range generated { - if strings.HasPrefix(id, "__") { - continue - } - lane, rest, ok := strings.Cut(id, "_") - if !ok || lane == "" || rest == "" { - continue - } - seen[lane] = true - } - if len(seen) == 0 { - return nil - } - var lanes []string - for _, lane := range laneOrderForPools(fallbackPoolOrder) { - if seen[lane] { - lanes = append(lanes, lane) - delete(seen, lane) - } - } - var extra []string - for lane := range seen { - extra = append(extra, lane) - } - sort.Strings(extra) - return append(lanes, extra...) -} - -// trimLanes narrows the lane dial to the lanes this catalog actually serves, -// and lands the selection on a served lane when a persisted or default choice -// points at one that vanished (an older catalog without ox, say). This is the -// consumer side of the optional-pool switches: no ox entries in models.yml -// means no ox values on the dial at all. -func (m *model) trimLanes(served map[string]bool) { - for i, f := range m.facets { - if f.key != "lane" { - continue - } - var values []string - for _, v := range f.values { - if served[v] { - values = append(values, v) - } - } - if len(values) == len(f.values) { - continue // nothing to trim - } - if len(values) > 0 { - m.facets[i].values = values - } - break - } - if !served[m.sel["lane"]] { - for _, fallback := range []string{"mixed", "gpt-only", "claude-only"} { - if served[fallback] { - m.sel["lane"] = fallback - break - } - } - } -} - -// clampSel turns off every dial the catalog cannot serve. main is fable's -// sub-setting and never outlives it. -func (m *model) clampSel() { - if m.noSpark { - m.sel["spark"] = "off" - } - if m.noFable { - m.sel["fable"] = "off" - m.sel["main"] = "off" - } - if !m.hasRelief { - m.sel["relief"] = "on" - } - // A persisted lane the applied catalog no longer generates (or one from a - // richer catalog) resets to the dial's first lane. - for _, f := range m.facets { - if f.key != "lane" || len(f.values) == 0 { - continue - } - known := false - for _, v := range f.values { - if v == m.sel["lane"] { - known = true - break - } - } - if !known { - m.sel["lane"] = f.values[0] - } - } -} - -// laneColor tints the accent by lane: each provider carries a deeper shade for -// its pure lane and a lighter one for its led lane; mixed keeps its purple. -func laneColor(lane string) string { - if lane == "mixed" { - return "#aa96e1" - } - if lane == "ox-lean" { - return "#2dd4bf" // teal — paid work riding the free pool - } - if p := providerByLane(lane); p != nil { - if lanePure(lane) { - return p.LaneOnly - } - return p.LaneLed - } - return "#ff9f52" -} - -// prefixed qualifies a model id with its omp provider: the catalog column when -// present, else the registry's family guess. The unknown-model fallback stays -// openai-codex so a legacy catalog launches exactly as before. -func (m model) prefixed(model string) string { - // Routing tokens carry a thinking level ("id:level"); the catalog is - // keyed on the bare id. Qualify the full token either way. - id := model - if i := strings.IndexByte(id, ':'); i >= 0 { - id = id[:i] - } - if f, ok := m.facts[id]; ok { - if p := providerByPool(f.pool); p != nil { - return p.ID + "/" + model - } - } - if p := providerByModel(model); p != nil { - return p.ID + "/" + model - } - return openAIProvider + "/" + model -} - -// poolOfModel is the model's registry pool letter: the catalog column first -// (leveled tokens are stripped before the lookup), the family guess second; -// "" when nobody claims it. -func (m model) poolOfModel(id string) string { - bare := id - if i := strings.IndexByte(bare, ':'); i >= 0 { - bare = bare[:i] - } - if f, ok := m.facts[bare]; ok && f.pool != "" { - return f.pool - } - if p := providerByModel(bare); p != nil { - return p.Pool - } - return "" -} - -// genConfigYAML reconstructs an omp config (modelRoles, task-agent model -// overrides for the ●-marked agent-backed roles, fallback chains, thinking, -// advisor, and the priority tier when fast is on) from the generated routing -// block for the current facets — what Enter launches omp with. The agent -// overrides mirror the preview: without them the static managed defaults -// would keep the five agent-backed types pinned regardless of the generated -// profile (issue atyrode/dotfiles#173). -func (m model) genConfigYAML() string { - rows := m.applyAdvisor(m.generated[comboID(m.sel, m.hasRelief)], m.sel["advisor"]) - var mr, fc, ao strings.Builder - advisorOn := false - for _, r := range rows { - f := strings.Fields(strings.ReplaceAll(r, "→", " ")) - i := 0 - if len(f) > 0 && f[0] == "●" { - i = 1 - } - if i >= len(f) { - continue - } - role := f[i] - var models []string - for _, t := range f[i+1:] { - if modelRe.MatchString(t) { - models = append(models, t) - } - } - if len(models) == 0 { - continue - } - if role == "advisor" { - advisorOn = true - } - if i == 1 && role != "advisor" { - // ●-marked agent-backed role: mirror its lead route as the task-agent - // model override so spawned agents follow the generated profile. - ao.WriteString(" " + role + ": " + m.prefixed(models[0]) + "\n") - } - mr.WriteString(" " + role + ": " + m.prefixed(models[0]) + "\n") - if len(models) > 1 { - var fbs []string - for _, x := range models[1:] { - fbs = append(fbs, m.prefixed(x)) - } - fc.WriteString(" " + role + ": [" + strings.Join(fbs, ", ") + "]\n") - } - } - var b strings.Builder - b.WriteString("modelRoles:\n" + mr.String()) - b.WriteString("retry:\n enabled: true\n modelFallback: true\n fallbackRevertPolicy: cooldown-expiry\n fallbackChains:\n" + fc.String()) - // task.agentAdvisor (omp ≥ 17.3; earlier omps hard-error on the unknown - // key, and CODE_OMP wrappers can lag the store during a dotfiles rollout, - // so the probed version gates the emission): at the audit dial, spawned - // task agents get their own advisor. Merged into the one task: block — - // overlays are strict YAML and two task: keys would be invalid. - agentAdvisor := m.sel["advisor"] == "audit" && m.ompVersionAtLeast(17, 3) - if ao.Len() > 0 || agentAdvisor { - b.WriteString("task:\n") - if ao.Len() > 0 { - b.WriteString(" agentModelOverrides:\n" + ao.String()) - } - if agentAdvisor { - b.WriteString(" agentAdvisor:\n task: \"on\"\n") - } - } - b.WriteString("defaultThinkingLevel: " + m.sel["thinking"] + "\n") - if advisorOn { - b.WriteString("advisor:\n enabled: true\n") - } else { - b.WriteString("advisor:\n enabled: false\n") - } - if m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") { - if p := providerByPool("O"); p != nil && p.ServiceTier[0] != "" { - b.WriteString("tier:\n " + p.ServiceTier[0] + ": " + p.ServiceTier[1] + "\n") - } - } - return b.String() -} - -// ── model ──────────────────────────────────────────────────────────────────── -// layout modes, chosen from the terminal size (unless the user collapses): -// -// split — wide: the focused list on the left, routing preview on the -// right, and Usage spanning the full bottom width -// medium — generator-dominant: the list full width on top (primary), then -// Usage and Routing side by side in a secondary row — Usage's -// provider groups stacked vertically inside its measured left -// column, Routing on the right (and taking the whole row while -// ‹s› hides Usage) -// collapsed — narrow/short or ‹p›: one full-width panel at a time (list, or -// routing w/ showResult) — the Generator stays usable instead of -// compressing every section into an unreadable split -const ( - modeSplit = iota - modeMedium - modeCollapsed -) - -// size classes behind mode(): derived from terminal cells and the measured -// rendered minima of each section (atyrode/dotfiles#197) — never from pixels or a hard-coded -// screenshot width. -const ( - sizeWide = iota - sizeMedium - sizeNarrow -) - -// gut is the left gutter every panel shares, so the whole UI hangs off one -// consistent margin instead of a ragged mix of flush-left and indented rows. -// topGap is the matching vertical breathing room above the section tabs. -// headRows counts the section head (tabs + blank separator) above a list body. -const ( - gut = 2 - topGap = 1 - headRows = 2 - // the launch footer pinned under the list: blank + cost + speed + blank + - // the ⏎ launch action on its own visually separated row. - launchFooterRows = 5 - // routingMinW is the narrowest useful routing column: pane chrome plus room - // for a lead chain — below this a side-by-side routing panel stops earning - // its keep. - routingMinW = 33 - // secSepW is the one-cell border column between medium's adjacent - // secondary panes (Usage left, Routing right) — visible separation, same - // stroke as the wide layout's routing pane border. - secSepW = 1 - // genMinRows is the fewest facet rows the generator list may be windowed to - // before the layout must shed secondary sections instead of compressing it. - genMinRows = 4 - // minRouteRows is the fewest routing viewport rows worth pinning chrome around. - minRouteRows = 4 -) - -// genColMinH is the generator column's minimum useful height: the pinned head, -// a windowed-but-usable slice of the facet list, and the pinned launch footer. -const genColMinH = headRows + genMinRows + launchFooterRows - -// genRowWidth is the width needed to render the widest generator facet row (all -// options) on a single line — the minimum for the left panel. -func (m model) genRowWidth() int { - max := 30 - for _, f := range m.facets { // widest over ALL facets, so width is lane-stable - w := 14 // ▸ + glyph + spaces + padded label - for _, v := range f.values { - w += len(v) + 4 - } - if w > max { - max = w - } - } - return max + 2 -} - -// sizeMode classifies the terminal into the wide / medium / narrow-short -// responsive classes. Widths compare against the measured generator row, -// routing, and usage-column minima; heights against the chrome each composition -// pins on screen — breakpoints track content needs, not screenshot numbers. -func (m model) sizeMode() int { - if m.w >= m.genRowWidth()+routingMinW && m.h >= m.wideMinH() { - return sizeWide - } - if m.w >= m.mediumMinW() && m.h >= m.mediumMinH() { - return sizeMedium - } - return sizeNarrow -} - -func (m model) mode() int { - if m.collapse { - return modeCollapsed - } - switch m.sizeMode() { - case sizeWide: - return modeSplit - case sizeMedium: - return modeMedium - default: - return modeCollapsed - } -} - -// wideMinH is the least height at which the wide composition stays readable: -// a usable generator column above the full-width Usage footer. Shorter than -// this, keeping every section visible would compress them all — shed instead. -func (m model) wideMinH() int { - return topGap + genColMinH + m.footerH(!m.hideUsage) -} - -// mediumMinH stacks the generator over the secondary Routing+Usage row (at its -// measured minimum) with Usage out of the footer. -func (m model) mediumMinH() int { - return topGap + genColMinH + 1 + m.secondaryMinH() + m.footerH(false) -} - -// mediumMinW: the secondary row must seat a useful routing viewport beside the -// measured usage column — plus the one-cell separator between them — without -// clipping either. -func (m model) mediumMinW() int { - if m.hideUsage { - return routingMinW - } - return routingMinW + secSepW + m.usageColW() -} - -// footerH measures the pinned footer for a composition directly from its parts -// — mode selection depends on it, so it must not consult the mode itself. -func (m model) footerH(withUsage bool) int { - h := 1 + lipgloss.Height(padLeft(m.help.View(keys), gut)) - if withUsage { - if p := m.usagePanel(); p != "" { - h += 1 + lipgloss.Height(p) - } - } - return h -} - -// bodyH is the height available above the pinned footer. -func (m model) bodyH() int { - h := m.h - lipgloss.Height(m.footer()) - if h < 1 { - h = 1 - } - return h -} - -// contentH is the height the panels actually fill — bodyH minus the top gap. -func (m model) contentH() int { - h := m.bodyH() - topGap - if h < 1 { - h = 1 - } - return h -} - -// bodyLines renders the generator's scrolling facet list and reports the cursor's -// line index within it. -func (m model) bodyLines() ([]string, int) { - return m.genLines() -} - -// launchFooter is the cost/speed meters for the current facet combo plus the -// enter-to-launch call to action — so the generator always shows what the choice -// costs, how fast it is, and how Enter will launch it. Enter always launches -// the generated profile for the current facets (the untouched default combo is -// a profile like any other); m runs omp-managed on the managed defaults with -// no overlay, and the sandbox (u) key is always offered. A selected runtime -// target gets the same footer shape with an honest summary instead of meters: -// its tokens are free and code has no measurement to quote. -func (m model) launchFooter() []string { - acc := lipgloss.NewStyle().Foreground(lipgloss.Color(m.accent())).Bold(true).Render(" ⏎ launch") - if _, local := m.selectedRuntime(); local { - return []string{ - "", - stDim.Render(" cost free · local inference"), - "", - "", - acc, - } - } - cs, ss := m.costScore(), m.speedScore() - return []string{ - "", - m.meter("cost", "$", meterRamp[cs], cs), // dear → red, cheap → green - m.meter("speed", "»", meterRamp[6-ss], ss), // fast → green, slow → red - "", // breathing room between the meters and the action row - acc + stDim.Render(" m managed omp · u sandbox"), - } -} - -// mediumSplit gives the Routing+Usage row only its measured minimum, then lets -// the primary Generator absorb every remaining row. The medium height threshold -// guarantees both sections fit; taller terminals therefore expand Generator -// instead of leaving slack below the compact secondary content. -func (m model) mediumSplit(bodyH int) (genH, secH int) { - secH = m.secondaryMinH() - genH = bodyH - 1 - secH - if genH < genColMinH { - genH = genColMinH - secH = bodyH - 1 - genH - } - return -} - -// previewDims returns the preview viewport's inner (width, height) for the mode. -// The full-width modes reserve the shared gutter; split leaves the preview's own -// border + padding to do the breathing. Every mode reserves prevChromeRows for -// the pinned pill head above and fallback-display hint below the viewport. -func (m model) previewDims() (int, int) { - bodyH := m.contentH() - switch m.mode() { - case modeCollapsed: - return m.w - gut, bodyH - prevChromeRows - case modeMedium: - _, secH := m.mediumSplit(bodyH) - return m.routingColW() - gut, secH - prevChromeRows - default: // split — the pane draws a border + prevPadL inside its width, so the - // viewport (what renderRoute wraps to) gets the inner text area, not the box. - return m.w - m.listW() - 3 - prevPadL, bodyH - prevChromeRows - } -} - -// usageColShare is the medium secondary row's Usage width: Usage is the -// favored pane — it takes the larger 3/5 proportional share of the row so its -// bars and notes keep breathing room, never dropping below its measured -// stacked minimum. Routing is the pane that shrinks as Usage grows, floored -// at routingMinW (the medium width threshold guarantees both floors seat). -func (m model) usageColShare() int { - avail := m.w - secSepW - uw := avail * 3 / 5 - if min := m.usageColW(); uw < min { - uw = min - } - if avail-uw < routingMinW { - uw = avail - routingMinW - } - return uw -} - -// routingColW is the medium secondary row's routing share: whatever Usage's -// favored share (and the separator between the panes) leaves free. -func (m model) routingColW() int { - w := m.w - if !m.hideUsage { - w -= m.usageColShare() + secSepW - } - if w < routingMinW { - w = routingMinW - } - return w -} - -// ── trackpad / mouse wheel ─────────────────────────────────────────────────── -// The wheel drives the generator directly: vertical scroll moves the facet -// selection, horizontal scroll changes the selected facet's value. Terminal -// mouse protocols expose direction-only press events rather than trackpad -// distance, so require a small burst before committing one generator step. -// This gives fine Mac trackpad motion room to settle without making every raw -// event select a new row or option. Routing remains continuous and ungated. -const ( - wheelStepEvents = 5 - wheelGestureGap = 200 * time.Millisecond -) - -const ( - wheelAxisNone = iota - wheelAxisV - wheelAxisH -) - -// admittedWheelMsg marks a generator wheel event whose same-direction burst -// crossed the step threshold before Bubble Tea's Update/render cycle. -type admittedWheelMsg struct{ tea.MouseMsg } - -// wheelTarget is the layout state the pre-dispatch filter needs. Keeping this -// interface narrow also lets the raw-input regression test wrap model while -// preserving the exact production filter path. -type wheelTarget interface { - wheelInRouting(int, int) bool - routingWheelCanMove(tea.MouseButton) bool -} - -// wheelInputFilter removes mouse traffic before Bubble Tea's unconditional -// redraw-after-Update. Generator motion accumulates by axis and direction; -// only each complete threshold reaches Update. Motion, non-wheel presses, -// routing horizontal wheel, and clamped routing scroll are dropped because -// none can change the view. -type wheelInputFilter struct { - axis int - button tea.MouseButton - count int - last time.Time -} - -func (f *wheelInputFilter) Filter(app tea.Model, msg tea.Msg) tea.Msg { - - mouse, ok := msg.(tea.MouseMsg) - if !ok { - return msg - } - if mouse.Action != tea.MouseActionPress { - return nil - } - target, ok := app.(wheelTarget) - if !ok { - return msg - } - if target.wheelInRouting(mouse.X, mouse.Y) { - switch mouse.Button { - case tea.MouseButtonWheelUp, tea.MouseButtonWheelDown: - if target.routingWheelCanMove(mouse.Button) { - return mouse - } - } - return nil - } - - axis := wheelAxisNone - switch mouse.Button { - case tea.MouseButtonWheelUp, tea.MouseButtonWheelDown: - axis = wheelAxisV - case tea.MouseButtonWheelLeft, tea.MouseButtonWheelRight: - axis = wheelAxisH - default: - return nil - } - now := time.Now() - if f.count > 0 && now.Sub(f.last) <= wheelGestureGap && axis != f.axis { - // Ignore brief orthogonal trackpad jitter without discarding progress - // along the operator's dominant gesture axis. - return nil - } - if axis != f.axis || mouse.Button != f.button || now.Sub(f.last) > wheelGestureGap { - f.axis = axis - f.button = mouse.Button - f.count = 0 - } - f.last = now - f.count++ - if f.count < wheelStepEvents { - return nil - } - f.count = 0 - return admittedWheelMsg{MouseMsg: mouse} -} - -type model struct { - generated map[string][]string - advisors map[string][]string // "level/ctx" → advisor model chain - facts map[string]modelFact // model id → cost ($/1M) + curated speed (tok/s) - avail availability - glyphs map[string]string - runtimeTargets []runtimeTarget - - // Catalog capability, phrased as absence so the zero value keeps every dial: - // a model with no catalog yet (the onboarding shell, tests) must behave as it - // always did. applyCatalog sets these only from a catalog it actually read. - noSpark bool // no _sp_ combos exist — hide the spark dial and force it off - noFable bool // no _fa_/_famain_ combos — same for fable and its main child - // hasRelief is positive-polarity: the relief segment only exists in - // catalogs with an optional pool, so the zero value keeps old ids intact. - hasRelief bool // _rel_/_norel combos exist — show the relief dial - - depth int // 0 lead · 1 full - collapse bool // p: hide the Routing section - showResult bool // in collapsed mode: show the preview full-width - hideUsage bool // s: hide the Usage section (atyrode/dotfiles#198); fetch state keeps running unseen - showUsage bool // narrow mode: show Usage full-screen instead of silently shedding it - fullUsageIDs bool // i: expand compact Usage identities to full account addresses - - facets []facet - fcur int - sel map[string]string - selectionState string // CODE_SELECTION_STATE; empty keeps standalone runs stateless - - vp viewport.Model - spin spinner.Model - help help.Model - w, h int - rdy bool - - broker brokerConfig - usageCache string - accountState string - accountSelections accountSelectionState - accountErr string - manager bool - mgrCursor int - managerPreset managerPresetState - - fetching bool // a usage fetch is in flight (manual or auto) - nextRefresh time.Time // when the next auto-refresh fires - hadUsage bool // a successful central fetch has landed - usageStale bool // the last central refresh failed; prior data is retained - barAnim int // first-load fill frame (1..barAnimSteps-1 = partial); 0 = inactive, bars at full value - - launchManaged bool // m: run CODE_OMP with no overlay (the managed defaults) - launchUntrusted bool // u: run the CODE_OMP_UNTRUSTED sandbox - launchRuntime string // delegated local runtime target selected via CODE_RUNTIME_BROKER - hasSandbox bool // a sandbox binary exists; gates the u key - genConfig string // generated config YAML to launch omp with (generator Enter) - firstPrompt string // prompt from the suggest box, forwarded as omp's first message - savedSel map[string]string // selection snapshot before a live suggest preview (for revert) - // The probed omp version (omp/ from `omp --version`), fetched - // async at startup. Zero = unknown: 17.3-only overlay keys are omitted so - // a lagging CODE_OMP wrapper never hard-errors at launch. - ompMajor, ompMinor int -} - -// usage auto-refreshes on this cadence; a 1s tick drives the countdown. -const refreshEvery = 5 * time.Minute - -type refreshTickMsg struct{} - -func tickCmd() tea.Cmd { - return tea.Tick(time.Second, func(time.Time) tea.Msg { return refreshTickMsg{} }) -} - -// usageMsg carries the single central account/usage snapshot fetched off the -// main thread so startup and refresh never block the TUI. -type usageMsg struct { - avail availability -} - -func fetchUsageCmd(broker brokerConfig) tea.Cmd { - if broker.URL == "" || broker.Token == "" { - return nil - } - return func() tea.Msg { return usageMsg{avail: loadAvailability(broker)} } -} - -func (m *model) startUsageFetch() tea.Cmd { - cmd := fetchUsageCmd(m.broker) - if cmd != nil { - m.fetching = true - } - return cmd -} - -// First-load bar fill grows each central usage bar from empty when the first -// successful fetch replaces the loading skeleton. A dedicated bounded tick -// sequence renders labels and numbers immediately and animates only the fill. -// Manual and automatic refreshes never re-run it. -const ( - barAnimSteps = 8 - barAnimInterval = 25 * time.Millisecond -) - -// barAnimMsg advances the first-load fill to the given frame (2..barAnimSteps). -type barAnimMsg struct{ step int } - -func barAnimCmd(step int) tea.Cmd { - return tea.Tick(barAnimInterval, func(time.Time) tea.Msg { return barAnimMsg{step} }) -} - -// ompVersionMsg carries the probed omp version; ok=false leaves it unknown. -type ompVersionMsg struct { - major, minor int - ok bool -} - -// ompVersionRe parses `omp/....` anywhere in --version output. -var ompVersionRe = regexp.MustCompile(`omp/(\d+)\.(\d+)`) - -// probeOmpVersionCmd resolves the same binary Enter launches and asks it for -// its version, off the main thread. A drift guard, not a feature flag: any -// failure just reads as "unknown" and version-gated keys stay off. -func probeOmpVersionCmd() tea.Cmd { - return func() tea.Msg { - path, err := resolveLaunchPath("CODE_OMP", []string{"omp"}) - if err != nil { - return ompVersionMsg{} - } - out, err := exec.Command(path, "--version").Output() - if err != nil { - return ompVersionMsg{} - } - match := ompVersionRe.FindSubmatch(out) - if match == nil { - return ompVersionMsg{} - } - major, _ := strconv.Atoi(string(match[1])) - minor, _ := strconv.Atoi(string(match[2])) - return ompVersionMsg{major: major, minor: minor, ok: true} - } -} - -// ompVersionAtLeast reports a probed version ≥ major.minor; unknown is never -// "at least" anything. -func (m model) ompVersionAtLeast(major, minor int) bool { - if m.ompMajor == 0 && m.ompMinor == 0 { - return false - } - return m.ompMajor > major || (m.ompMajor == major && m.ompMinor >= minor) -} - -func (m model) Init() tea.Cmd { - cmds := []tea.Cmd{probeOmpVersionCmd()} - if m.broker.URL != "" && m.broker.Token != "" { - cmds = append(cmds, m.startUsageFetch(), m.spin.Tick, tickCmd()) - } - return tea.Batch(cmds...) -} - -func (m model) listW() int { - // wide enough for the generator options on one line; capped so a very wide - // terminal doesn't stretch the list needlessly. - w := m.genRowWidth() - if w > m.w-33 { - w = m.w - 33 - } - // The ox lanes widen the lane row past this function's old 80-cell - // aesthetic cap; a wider list beats clipping dial options mid-value. - if w > 116 { - w = 116 - } - return w -} - -// Usage bars naturally measure ten cells, then grow or shrink to the width -// assigned by their row group. Styling is deliberately applied after the -// display-cell geometry is settled, so ANSI sequences never enter the math. -const usageBarNaturalW = 10 - -func barStr(p, width int) string { - if width < 0 { - width = 0 - } - var r, g float64 - if p <= 50 { - r, g = 90+float64(p)*3, 200 - } else { - r, g = 235, 200-float64(p-50)*3 - } - if r > 235 { - r = 235 - } - if g < 60 { - g = 60 - } - fill := (p*width + 50) / 100 - if fill > width { - fill = width - } - if fill < 0 { - fill = 0 - } - filled := lipgloss.NewStyle().Foreground(lipgloss.Color(fmt.Sprintf("#%02x%02x46", clampByte(r), clampByte(g)))).Render(strings.Repeat("█", fill)) - return filled + stDim.Render(strings.Repeat("░", width-fill)) -} - -func fmtReset(s int64) string { - if s < 0 { - s = 0 - } - switch { - case s >= 86400: - return fmt.Sprintf("%dd%dh", s/86400, (s%86400)/3600) - case s >= 3600: - return fmt.Sprintf("%dh%dm", s/3600, (s%3600)/60) - } - return fmt.Sprintf("%dm", s/60) -} - -func shortWin(l string) string { - switch l { - case "5 hours", "Claude 5 Hour", "Codex 5 Hour", "OpenAI 5 Hour": - return "5h" - case "7 days", "Claude 7 Day", "Codex 7 Day", "OpenAI 7 Day": - return "7d" - case "5 hours (Spark)", "Codex 5 Hour (Spark)", "OpenAI 5 Hour (Spark)": - return "5h spark" - case "7 days (Spark)", "Codex 7 Day (Spark)", "OpenAI 7 Day (Spark)": - return "7d spark" - case "Claude 7 Day (Fable)": - return "7d fable" - } - return l -} - -// usageCtrlLine is the Usage chrome's bottom action row: central refresh state, -// account-manager access, and any account persistence error. -func (m *model) usageCtrlLine() string { - var parts []string - if m.broker.URL != "" { - switch { - case !m.avail.ok && (m.fetching || m.nextRefresh.IsZero()): - parts = append(parts, stDim.Render(m.spin.View()+" fetching usage…")) - case m.fetching: - parts = append(parts, stWarn.Render(gReset+" refreshing…")) - case m.usageStale: - // A failed refresh kept the previous data on screen: the warning - // takes the countdown's slot (same row, similar width) so the - // measured panel geometry — and with it the medium/collapsed - // breakpoint — barely moves on a flaky refresh. - parts = append(parts, stWarn.Render("refresh failed · stale")+ - stDim.Render(" · ")+stKey.Render("r")+stDim.Render(" retry")) - default: - rem := time.Until(m.nextRefresh) - if rem < 0 { - rem = 0 - } - s := int(rem.Seconds()) - parts = append(parts, - stDim.Render(fmt.Sprintf("next refresh %d:%02d · ", s/60, s%60))+ - stKey.Render("r")+stDim.Render(" now")) - } - } - identityAction := "full ids" - if m.fullUsageIDs { - identityAction = "short ids" - } - parts = append(parts, stKey.Render("i")+stDim.Render(" "+identityAction)) - parts = append(parts, stKey.Render("v")+stDim.Render(" accounts")) - if len(parts) == 0 { - return "" - } - line := " " + strings.Join(parts, stDim.Render(" · ")) - if m.accountErr != "" { - line += "\n" + stBrk.Render(" account update failed: "+m.accountErr) - } - return line -} - -// compactDisplayIdentity produces a deliberately lossy display label. Email -// matching continues to use the untouched broker identity; this helper is only -// for the compact Usage heading. -func compactDisplayIdentity(identity string) string { - normalized := strings.ToLower(strings.TrimSpace(identity)) - at := strings.IndexByte(normalized, '@') - if at > 0 && at == strings.LastIndexByte(normalized, '@') { - local, domain := normalized[:at], normalized[at+1:] - dot := strings.LastIndexByte(domain, '.') - valid := dot > 0 && dot < len(domain)-1 && - !strings.HasPrefix(local, ".") && !strings.HasSuffix(local, ".") && - !strings.Contains(local, "..") && !strings.Contains(domain, "..") && - strings.IndexFunc(normalized, func(r rune) bool { - return unicode.IsSpace(r) || unicode.IsControl(r) - }) < 0 - if valid { - localRunes := []rune(local) - if len(localRunes) > 2 { - localRunes = localRunes[:2] - } - return string(localRunes) + "*" - } - } - if normalized == "" { - return "id unavailable" - } - runes := []rune(normalized) - if len(runes) > 2 { - runes = runes[:2] - } - return string(runes) + "*" -} - -func usageDisplayIdentity(identity string, full bool) string { - if full { - if identity = strings.TrimSpace(identity); identity != "" { - return identity - } - return "id unavailable" - } - return compactDisplayIdentity(identity) -} - -type compactProviderIdentity struct { - label string - reporting bool -} - -// providerIdentities preserves broker snapshot order and collapses repeated -// copies of the same stable account. Compact ambiguity is intentional; pressing -// i reveals full addresses when disambiguation matters. -func providerIdentities(a availability, prov string, full bool) []compactProviderIdentity { - accounts := a.accounts[prov] - identities := make([]compactProviderIdentity, 0, len(accounts)) - seenAccounts := map[string]bool{} - for _, acct := range accounts { - stableID := acct.IdentityKey - if stableID == "" { - stableID = acct.Email - } - if stableID != "" { - stableID = acct.Provider + "\x00" + stableID - if seenAccounts[stableID] { - continue - } - seenAccounts[stableID] = true - } - - identity := acct.Email - if identity == "" { - identity = acct.IdentityKey - } - label := usageDisplayIdentity(identity, full) - - reporting := false - for _, win := range a.accountUsage[accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey}] { - if !win.missing { - reporting = true - break - } - } - identities = append(identities, compactProviderIdentity{ - label: label, reporting: reporting, - }) - } - return identities -} - -// providerHeading keeps the provider's established color and puts compact, -// enabled snapshot identities in a dim parenthetical suffix. -func providerHeading(prov string, identities []compactProviderIdentity) string { - col, name := "#8a93a6", prov - if p := providerByID(prov); p != nil { - col, name = p.Color, p.Label - } - heading := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true).Render(name) - if len(identities) == 0 { - return heading - } - labels := make([]string, 0, len(identities)) - for _, identity := range identities { - labels = append(labels, identity.label) - } - return heading + " " + stDim.Render("("+strings.Join(labels, " + ")+")") -} - -// providerIdentityBlockFor keeps missing usage explicit without spending a -// separate row on accounts already represented by aggregate usage bars. -func providerIdentityBlockFor(a availability, prov string, checking, full bool) []string { - identities := providerIdentities(a, prov, full) - if checking && len(identities) == 0 { - identities = []compactProviderIdentity{{label: "checking account…"}} - } - rows := []string{padLeft(providerHeading(prov, identities), gut)} - if checking { - return rows - } - if !a.accountsOK { - return append(rows, stWarn.Render(" account status unavailable")) - } - if len(identities) == 0 { - if a.selectionApplied { - return append(rows, stDim.Render(" no enabled accounts")) - } - return append(rows, stBrk.Render(" not authenticated")) - } - unavailable := 0 - for _, identity := range identities { - if !identity.reporting { - unavailable++ - } - } - if unavailable > 0 { - status := " usage unavailable" - if len(identities) > 1 { - noun := "account" - if unavailable > 1 { - noun = "accounts" - } - status += fmt.Sprintf(" for %d %s", unavailable, noun) - } - rows = append(rows, stWarn.Render(status)) - } - if a.accountsStale { - rows = append(rows, stWarn.Render(" identity cached")) - } - return rows -} - -func providerIdentityBlock(a availability, prov string, checking bool) []string { - return providerIdentityBlockFor(a, prov, checking, false) -} - -// identityLines keeps provider and broker-reported account state visible even -// when no usage rows exist. -func identityLinesFor(a availability) string { - var lines []string - for i, prov := range meteredProviderIDs() { - if i > 0 { - lines = append(lines, "") - } - lines = append(lines, providerIdentityBlock(a, prov, false)...) - } - return strings.Join(lines, "\n") -} - -func (m *model) selectedLaunchAvailability() availability { - return selectedAvailability(m.avail, m.accountSelections.CurrentDisabled()) -} - -func (m *model) selectedUsageAvailability() availability { - disabled := m.accountSelections.CurrentDisabled() - if m.manager { - disabled = m.managerDisplayedDisabled() - } - return selectedAvailability(m.avail, disabled) -} - -func (m *model) identityLines() string { - return identityLinesFor(m.selectedUsageAvailability()) -} - -// usagePanel is the composition-agnostic Usage band sized for the current -// terminal width — the wide layout's full-width footer form. -func (m *model) usagePanel() string { return m.usagePanelFor(m.w) } - -// usagePanelFor renders central account usage with local visibility and account -// manager cues. There is no selectable vault or profile identity. -func (m *model) usagePanelFor(w int) string { - return m.usagePanelLayout(w, false) -} - -func (m *model) usagePanelStackedFor(w int) string { - return m.usagePanelLayout(w, true) -} - -func (m *model) usagePanelLayout(w int, stacked bool) string { - title := m.pill("usage") - title += " " + stCueKey.Render("s") + stCue.Render(" · hide") - innerWidth := max(0, w-gut) - out := padLeft(title, gut) + "\n" + - "\n" + m.usageBodyLayout(innerWidth, stacked) - if ctrl := m.usageCtrlLine(); ctrl != "" && !m.manager { - out += "\n\n" + ctrl // blank row: air between provider content and the control row - } - return out -} - -// usageRenderGroup keeps provider chrome and usage rows separate until layout -// has assigned the provider its real display width. That is the composition seam -// which lets both stacked and side-by-side layouts grow bars without changing -// headings, notes, or the canonical row grammar. -type usageRenderGroup struct { - prefix []string - rows []usageRowSpec - suffix []string -} - -func (g usageRenderGroup) linesWithUsageLayout(barWidth, noteWidth, prefixHeight int) []string { - lines := make([]string, 0, max(len(g.prefix), prefixHeight)+len(g.rows)+len(g.suffix)) - lines = append(lines, g.prefix...) - for len(lines) < prefixHeight { - lines = append(lines, "") - } - for _, row := range g.rows { - lines = append(lines, row.render(barWidth, noteWidth)) - } - lines = append(lines, g.suffix...) - return lines -} - -func usageRenderWidth(lines []string) int { - width := 0 - for _, line := range lines { - if lineWidth := lipgloss.Width(line); lineWidth > width { - width = lineWidth - } - } - return width -} - -// usageBodyFor renders the provider/account content between the pinned title -// and the bottom control row: the identity-headed usage groups, or the -// loading/unavailable identity block. -func (m *model) usageBodyFor(w int) string { - return m.usageBodyLayout(w, false) -} - -func (m *model) usageBodyLayout(w int, stacked bool) string { - a := m.selectedUsageAvailability() - if !a.ok { - if m.usageLoading() { - return m.skeletonBodyLayout(w, stacked) - } - out := identityLinesFor(a) - if m.broker.URL != "" && !m.fetching && !m.nextRefresh.IsZero() { - out += "\n" + stWarn.Render(" usage unavailable · press v to manage accounts") - } - return out - } - if len(a.wins) == 0 { - return identityLinesFor(a) + "\n" + - stWarn.Render(" no enabled provider usage · press v to manage accounts") - } - wins := append([]usageWin(nil), a.wins...) - provOrder := func(p string) int { - for i := range providerRegistry { - if providerRegistry[i].ID == p { - return i - } - } - return len(providerRegistry) - } - tierOrder := func(t string) int { - if t == "" || t == "-" { - return 0 - } - return 1 - } - sort.SliceStable(wins, func(i, j int) bool { - if o := provOrder(wins[i].prov) - provOrder(wins[j].prov); o != 0 { - return o < 0 - } - if o := tierOrder(wins[i].tier) - tierOrder(wins[j].tier); o != 0 { - return o < 0 - } - return wins[i].dur < wins[j].dur - }) - blocks := map[string]usageRenderGroup{} - var order []string - for _, prov := range meteredProviderIDs() { - if len(a.accounts[prov]) > 0 { - order = append(order, prov) - blocks[prov] = usageRenderGroup{prefix: providerIdentityBlockFor(a, prov, false, m.fullUsageIDs)} - } - } - for _, win := range wins { - block, ok := blocks[win.prov] - if !ok { - order = append(order, win.prov) - block.prefix = providerIdentityBlockFor(a, win.prov, false, m.fullUsageIDs) - } - block.rows = append(block.rows, m.usageRowSpec(win, " ")) - blocks[win.prov] = block - } - if block, ok := blocks[openAIProvider]; ok { - for _, acct := range a.accounts[openAIProvider] { - key := accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} - identity := usageDisplayIdentity(managerAccountLabel(acct), m.fullUsageIDs) - if cl := creditLineForAccount(identity, a.accountCredits[key]); cl != "" { - block.suffix = append(block.suffix, cl) - } - } - blocks[openAIProvider] = block - } - if a.deepseek != nil { - order = append(order, deepseekProvider) - blocks[deepseekProvider] = usageRenderGroup{ - prefix: []string{padLeft(providerHeading(deepseekProvider, nil), gut)}, - suffix: []string{m.deepseekBalanceRow(*a.deepseek)}, - } - } - return layoutGroups(w, order, blocks, stacked) -} - -// layoutGroups assigns provider columns before rendering any usage row. -// Side-by-side groups share the whole panel width; stacked groups each receive -// the whole section width. A zero width is the non-recursive measurement path -// and therefore retains natural ten-cell bars. -func layoutGroups(w int, order []string, blocks map[string]usageRenderGroup, stacked bool) string { - allRows := make([]usageRowSpec, 0) - for _, prov := range order { - allRows = append(allRows, blocks[prov].rows...) - } - naturalBarWidth, noteWidth := usageRowsLayout(0, allRows) - naturalColW := lipgloss.Width(skeletonRow("7d fable")) + 2 - for _, prov := range order { - blockW := usageRenderWidth(blocks[prov].linesWithUsageLayout(naturalBarWidth, noteWidth, 0)) + 2 - if blockW > naturalColW { - naturalColW = blockW - } - } - sideBySide := !stacked && w > 0 && len(order) > 1 && w >= naturalColW*len(order) - layoutWidth := w - if sideBySide { - for i := range order { - colW := w*(i+1)/len(order) - w*i/len(order) - if i == 0 || colW < layoutWidth { - layoutWidth = colW - } - } - } - barWidth, noteWidth := usageRowsLayout(layoutWidth, allRows) - if sideBySide { - prefixHeight := 0 - for _, prov := range order { - if len(blocks[prov].prefix) > prefixHeight { - prefixHeight = len(blocks[prov].prefix) - } - } - cols := make([]string, 0, len(order)) - for i, prov := range order { - colW := w*(i+1)/len(order) - w*i/len(order) - content := strings.Join(blocks[prov].linesWithUsageLayout(barWidth, noteWidth, prefixHeight), "\n") - cols = append(cols, lipgloss.NewStyle().Width(colW).Render(content)) - } - return lipgloss.JoinHorizontal(lipgloss.Top, cols...) - } - var lines []string - for i, prov := range order { - if i > 0 { - lines = append(lines, "") - } - lines = append(lines, blocks[prov].linesWithUsageLayout(barWidth, noteWidth, 0)...) - } - return strings.Join(lines, "\n") -} - -// usageLoading reports the initial central fetch window where the layout-stable -// skeleton replaces the not-yet-known account and usage rows. -func (m *model) usageLoading() bool { - return m.broker.URL != "" && !m.avail.ok && (m.fetching || m.nextRefresh.IsZero()) -} - -// skeletonWinsByProvider mirrors each provider's stable usage shape. Anthropic -// always reserves the Fable row, even before a first successful fetch. -var skeletonWinsByProvider = func() map[string][]string { - wins := map[string][]string{} - for _, p := range providerRegistry { - if p.Metered { - wins[p.ID] = p.SkeletonWins - } - } - return wins -}() - -// usageRowSpec is the unrendered canonical Usage-row grammar. Every caller -// supplies its indentation and group width, while this one seam reserves the -// label, percentage, reset, and actual note before assigning all safe cells to -// the bar. -type usageRowSpec struct { - indent string - label string - barPct int - percentage string - reset string - note string - reserveNote string -} - -func usageRowsNoteWidth(rows []usageRowSpec) int { - width := 0 - for _, row := range rows { - for _, note := range []string{row.note, row.reserveNote} { - if noteWidth := lipgloss.Width(note); noteWidth > width { - width = noteWidth - } - } - } - return width -} - -const usageResetValueWidth = 6 - -func paddedUsageReset(reset string) string { - width := lipgloss.Width(gReset) + 1 + usageResetValueWidth - return reset + strings.Repeat(" ", max(0, width-lipgloss.Width(reset))) -} - -func (r usageRowSpec) render(barWidth, noteWidth int) string { - note := "" - if r.note != "" { - note = " " + r.note + strings.Repeat(" ", max(0, noteWidth-lipgloss.Width(r.note))) - } else if noteWidth > 0 { - note = strings.Repeat(" ", 2+noteWidth) - } - return fmt.Sprintf("%s%-9s %s %s used %s%s", - r.indent, r.label, barStr(r.barPct, barWidth), r.percentage, paddedUsageReset(r.reset), note) -} - -func (r usageRowSpec) reservedWidth(noteWidth int) int { - return lipgloss.Width(r.render(0, noteWidth)) -} - -func usageRowsLayout(width int, rows []usageRowSpec) (barWidth, noteWidth int) { - noteWidth = usageRowsNoteWidth(rows) - if width == 0 { - return usageBarNaturalW, noteWidth - } - reserved := 0 - for _, row := range rows { - if rowW := row.reservedWidth(noteWidth); rowW > reserved { - reserved = rowW - } - } - return max(0, width-reserved), noteWidth -} - -func usageRowsBarWidth(width int, rows []usageRowSpec) int { - barWidth, _ := usageRowsLayout(width, rows) - return barWidth -} - -func renderUsageRows(width int, rows []usageRowSpec) []string { - barWidth, noteWidth := usageRowsLayout(width, rows) - rendered := make([]string, len(rows)) - for i, row := range rows { - rendered[i] = row.render(barWidth, noteWidth) - } - return rendered -} - -func skeletonUsageRowSpec(label, indent string) usageRowSpec { - return usageRowSpec{ - indent: indent, label: label, - percentage: stDim.Render(" ··%"), - reset: stDim.Render(gReset + " ····"), - reserveNote: "unavailable", - } -} - -// skeletonRow is the natural-width placeholder row used by measurement and -// focused tests. Real layout goes through renderUsageRows with its group width. -func skeletonRow(label string) string { - return renderUsageRows(0, []usageRowSpec{skeletonUsageRowSpec(label, " ")})[0] -} - -// skeletonBody is the pre-first-fetch Usage content: provider headings, -// explicit checking state, and generic placeholder window rows, laid out by -// the same group logic as real data so the first result lands predictably. -func (m *model) skeletonBody(w int) string { - return m.skeletonBodyLayout(w, false) -} - -func (m *model) skeletonBodyLayout(w int, stacked bool) string { - a := m.selectedUsageAvailability() - order := meteredProviderIDs() - blocks := map[string]usageRenderGroup{} - for _, prov := range order { - block := usageRenderGroup{prefix: providerIdentityBlockFor(a, prov, true, m.fullUsageIDs)} - for _, label := range skeletonWinsByProvider[prov] { - block.rows = append(block.rows, skeletonUsageRowSpec(label, " ")) - } - blocks[prov] = block - } - return layoutGroups(w, order, blocks, stacked) -} - -// usageColumn is the medium layout's left-hand Usage section: the panel with -// provider groups forced into a vertical stack — the column is deliberately -// too narrow for side-by-side groups. The panel carries its own title chrome. -func (m model) usageColumn() string { - return m.usagePanelStackedFor(0) -} - -// usageColW is the medium usage column's measured width: the widest rendered -// line of the stacked panel (title, controls, bars, notes) — measured, not guessed. -func (m model) usageColW() int { - return lipgloss.Width(m.usageColumn()) -} - -// secondaryMinH is the medium secondary row's minimum height: routing's pinned -// chrome plus a few useful route rows, or the full stacked usage column when -// that is taller — medium only engages when neither column needs clipping. -func (m model) secondaryMinH() int { - h := prevChromeRows + minRouteRows - if m.hideUsage { - return h - } - if u := lipgloss.Height(m.usageColumn()); u > h { - h = u - } - return h -} - -func formatCachedAge(observed int64, now time.Time) string { - seconds := now.Unix() - observed - if seconds < 0 { - seconds = 0 - } - switch { - case seconds < 60: - return "<1m ago" - case seconds < 60*60: - return fmt.Sprintf("%dm ago", seconds/60) - case seconds < 24*60*60: - return fmt.Sprintf("%dh ago", seconds/(60*60)) - case seconds < 7*24*60*60: - return fmt.Sprintf("%dd ago", seconds/(24*60*60)) - case seconds < 365*24*60*60: - return fmt.Sprintf("%dw ago", seconds/(7*24*60*60)) - default: - return fmt.Sprintf("%dy ago", seconds/(365*24*60*60)) - } -} - -// deepseekBalanceRow renders the DeepSeek group's single body row: the prepaid -// balance — no bar, no window, no reset countdown, because none exist. A -// balance under the suggestion floor carries an explicit "low" cue (the same -// threshold that stops proposals from spending the pool), and the off-peak -// discount window is surfaced while it is live. -func (m *model) deepseekBalanceRow(b deepseekBalance) string { - if !b.ok { - return " " + stWarn.Render("balance unavailable") - } - row := " balance " + stHead.Render("$"+b.total+" "+b.currency) + stDim.Render(" · pay-as-you-go") - if v, err := strconv.ParseFloat(b.total, 64); err == nil && v < deepseekLowBalanceUSD { - row += " " + stWarn.Render("low") - } - if deepseekOffPeak(offPeakNow().UTC()) { - row += " " + stDim.Render("off-peak −50%") - } - if b.stale { - cached := "cached" - if b.fetchedAt > 0 { - cached += " " + formatCachedAge(b.fetchedAt, time.Now()) - } - row += " " + stWarn.Render(cached) - } - return row -} -func (m *model) usageRowSpec(w usageWin, indent string) usageRowSpec { - if w.missing { - // Never-observed windows keep the exact row grammar with dotted values; - // only the status text differs from the loading skeleton. - row := skeletonUsageRowSpec(shortWin(w.label), indent) - row.note = stDim.Render("unavailable") - return row - } - note := "" - if w.pct >= 80 { - note = stWarn.Render("tight") - } - if w.pct >= 100 { - note = stBrk.Render("maxed") - } - if w.tier == "spark" && w.pct == 0 { - note = lipgloss.NewStyle().Foreground(lipgloss.Color(cGreen)).Render("idle") - } - if w.stale { - // Retained after a refresh omitted this window; show its age explicitly. - cached := "cached" - if w.observed > 0 { - cached += " " + formatCachedAge(w.observed, time.Now()) - } - if note != "" { - note += " " - } - note += stWarn.Render(cached) - } - resetText := gReset + " " + pad(fmtReset(w.secs), 4) - reset := stDim.Render(resetText) - if w.dur > 0 && w.secs*10 < w.dur { - reset = lipgloss.NewStyle().Foreground(lipgloss.Color("#c8d0dc")).Bold(true).Render(resetText) - } else if w.dur > 0 && w.secs*4 < w.dur { - reset = lipgloss.NewStyle().Foreground(lipgloss.Color("#c8d0dc")).Render(resetText) - } - // During the one-time first-load fill only the bar is scaled toward its - // target; the label, percentage, reset, and note are real from frame one. - barPct := w.pct - if m.barAnim > 0 { - barPct = barPct * m.barAnim / barAnimSteps - } - return usageRowSpec{ - indent: indent, label: shortWin(w.label), barPct: barPct, - percentage: fmt.Sprintf("%3d%%", w.pct), reset: reset, note: note, - } -} - -// usageRow is the natural-width measurement/test path. Composed panels and -// manager account groups render the same spec through renderUsageRows using -// their actual assigned width. -func (m *model) usageRow(w usageWin) string { - return renderUsageRows(0, []usageRowSpec{m.usageRowSpec(w, " ")})[0] -} - -// Reset-credit expiry urgency tints: each individual expiry (`3d`, `12d`, …) -// in the credit line is colored on a muted red→amber→green ramp so soon -// expiries read as warnings and distant ones as headroom, while the icon, -// count, and connecting prose stay dim. The palette is precomputed and -// deliberately desaturated (no per-frame color math, no saturated alarm -// colors inside a dim summary row); the day text itself stays sufficient -// without color. Thresholds are whole days remaining, exactly as fmtDays -// rounds them (up, so later-today = 1): ≤ creditUrgentDays is muted red, -// ≤ creditSoonDays muted amber, anything later muted green. -const ( - creditUrgentDays = 3 // expiring within three days — spend it or lose it - creditSoonDays = 10 // within ten days — plan around it -) - -var ( - stCreditUrgent = lipgloss.NewStyle().Foreground(lipgloss.Color("#b0716f")) // muted red - stCreditSoon = lipgloss.NewStyle().Foreground(lipgloss.Color("#b39c6b")) // muted amber - stCreditSafe = lipgloss.NewStyle().Foreground(lipgloss.Color("#85a883")) // muted green -) - -// creditDayStyle picks the urgency tint for a credit expiring in s seconds, -// bucketing on the same rounded-up whole days fmtDays renders — the color and -// the text can never disagree about which side of a threshold an expiry is on. -func creditDayStyle(s int64) lipgloss.Style { - d := int64(0) - if s > 0 { - d = (s + 86399) / 86400 - } - switch { - case d <= creditUrgentDays: - return stCreditUrgent - case d <= creditSoonDays: - return stCreditSoon - default: - return stCreditSafe - } -} - -// creditSummary renders the OpenAI reset-credit summary: the available count -// and the days remaining until the three soonest credit expirations, ascending. -// Callers own indentation and any account identity prefix. -func creditSummary(c resetCredits) string { - if c.avail == 0 && len(c.exp) == 0 { - return "" - } - exp := append([]int64(nil), c.exp...) - sort.Slice(exp, func(i, j int) bool { return exp[i] < exp[j] }) - if len(exp) > 3 { - exp = exp[:3] - } - noun := "resets" - if c.avail == 1 { - noun = "reset" - } - line := stDim.Render(gReset + " " + fmt.Sprintf("%d %s", c.avail, noun)) - if len(exp) > 0 { - days := make([]string, len(exp)) - for i, s := range exp { - days[i] = creditDayStyle(s).Render(fmtDays(s)) - } - line += stDim.Render(" · expiring in ") + strings.Join(days, stDim.Render(", ")) - } - return line -} - -func creditLineFor(c resetCredits) string { - if summary := creditSummary(c); summary != "" { - return " " + summary - } - return "" -} - -func creditLineForAccount(identity string, c resetCredits) string { - if summary := creditSummary(c); summary != "" { - return " " + stDim.Render(identity+" · ") + summary - } - return "" -} - -func (m *model) creditLine() string { - return creditLineFor(m.selectedUsageAvailability().credits) -} - -// fmtDays renders a relative duration as whole days remaining, rounding up so -// a credit expiring later today still reads 1d. -func fmtDays(s int64) string { - if s <= 0 { - return "0d" - } - return fmt.Sprintf("%dd", (s+86399)/86400) -} - -// syncPreview re-renders the routing content and jumps back to the top — for -// content changes (facet cycling, depth, reset), where the old scroll offset -// points at rows that no longer exist. -func (m *model) syncPreview() { - m.syncPreviewAt(0) -} - -// syncPreviewKeepScroll re-renders while preserving the scroll position where -// still valid — resizes and background usage refreshes must never yank the view. -func (m *model) syncPreviewKeepScroll() { - m.syncPreviewAt(m.vp.YOffset) -} - -func (m *model) syncPreviewAt(yoff int) { - if !m.rdy || m.collapse { - return - } - rw := m.vp.Width - // No settings summary here: every dial is already visible (selected) in the - // generator list on the left, so the preview shows only what that selection - // produces — the role → model routing itself. - var b strings.Builder - if target, local := m.selectedRuntime(); local { - b.WriteString(lipgloss.NewStyle().Bold(true).Render(target.Label) + "\n") - b.WriteString(stDim.Render(target.statusLine()) + "\n\n") - if target.ContextWindow > 0 { - b.WriteString(fmt.Sprintf("context %dk tokens\n\n", target.ContextWindow/1000)) - } - // Same grammar as a hosted profile: every role the broker's generated - // profile routes (all of them but the advisor, which stays off), led by - // the one local model at the dialed thinking — the flag is forwarded - // verbatim and omp clamps it to what the model offers. - rows := []string{fmt.Sprintf(" thinking %s · fallback off · advisor off", m.sel["thinking"])} - for _, r := range genRoleOrder { - if r == "advisor" { - continue - } - marker := " " - if genAgentRoles[r] { - marker = "●" - } - rows = append(rows, fmt.Sprintf(" %s %-10s %s:%s", marker, r, target.Model, m.sel["thinking"])) - } - b.WriteString(m.renderRoute(rows, m.depth, m.selectedLaunchAvailability(), rw)) - b.WriteString("\n" + stDim.Render("broker-owned profile · cloud auth excluded · weights provisioned by the runtime") + "\n") - content := lipgloss.NewStyle().MaxWidth(m.vp.Width).Render(b.String()) - m.vp.SetContent(content) - m.vp.SetYOffset(yoff) - return - } - id := comboID(m.sel, m.hasRelief) - if base, ok := m.generated[id]; ok { - _, roles := splitMeta(base) - roles = m.applyAdvisor(roles, m.sel["advisor"]) - b.WriteString(m.renderRoute(roles, m.depth, m.selectedLaunchAvailability(), rw)) - } else { - b.WriteString(stDim.Render("no profile for this combination") + "\n") - } - // clip (don't wrap) to pane width — renderRoute already wrapped the chains. - content := lipgloss.NewStyle().MaxWidth(m.vp.Width).Render(b.String()) - m.vp.SetContent(content) - m.vp.SetYOffset(yoff) // clamps into the new content and viewport height -} - -// footer is the pinned bottom block: the usage panel (when this composition -// keeps Usage in the footer) then the controls help, each under a rule. Built -// once here so relayout and View stay in sync. -func (m *model) footer() string { - usage := "" - if m.usageInFooter() { - usage = m.usagePanel() - } - return clikit.SeparatedSections( - m.w, - usage, - padLeft(m.help.View(m.contextHelp()), gut), - ) -} - -// usageInFooter says where Usage lives: the wide layout keeps it as the -// full-width bottom band; medium moves it into the secondary column (back to -// the footer while ‹p› hides that row); narrow/short hides it entirely so the -// Generator stays usable first — fetch state and the refresh cadence keep -// running unseen, and nothing is refetched when it reappears. -func (m *model) usageInFooter() bool { - if m.hideUsage { - return false - } - switch m.sizeMode() { - case sizeWide: - return true - case sizeMedium: - return m.collapse - default: - return false - } -} - -// routingShown reports whether the Routing section (and so its title-local -// p · hide cue) is on screen in the current composition. -func (m model) routingShown() bool { - if m.showUsage && m.sizeMode() == sizeNarrow { - return false - } - if m.collapse { - return false - } - if m.mode() == modeCollapsed { - return m.showResult - } - return true -} - -// usageShown reports whether the Usage panel is rendered anywhere — the -// wide/collapsed footer band, medium's secondary column, or narrow's dedicated -// full-screen view. -func (m model) usageShown() bool { - if m.hideUsage { - return false - } - if m.sizeMode() == sizeNarrow { - return m.showUsage - } - return m.usageInFooter() || m.mode() == modeMedium -} - -// usageCanShow reports whether restoring Usage would actually render it. -// Narrow terminals use a dedicated full-width view when they can seat the -// stacked Usage column; extremely small terminals still suppress a dead cue. -func (m model) usageCanShow() bool { - if m.sizeMode() == sizeNarrow { - return m.w >= m.usageColW() - } - t := m - t.hideUsage = false - return t.usageShown() -} - -// generatorShown: the Generator (and its launch footer advertising ⏎/m/u) is -// visible in every composition except a narrow full-screen secondary view. -func (m model) generatorShown() bool { - if m.sizeMode() == sizeNarrow && m.showUsage { - return false - } - return !(m.mode() == modeCollapsed && !m.collapse && m.showResult) -} - -// helpKeys is the state-derived key map handed to the bubbles help view: the -// compact line lists only what contextHelp selected, while ? always exposes -// the complete static reference. -type helpKeys struct { - short []key.Binding - full [][]key.Binding -} - -func (h helpKeys) ShortHelp() []key.Binding { return h.short } -func (h helpKeys) FullHelp() [][]key.Binding { return h.full } - -// contextHelp derives the compact footer from the model state (atyrode/dotfiles#198): -// - navigation, reset, full-help discovery, and quit are always offered; -// - a hidden section contributes its recovery action (show routing/usage) — -// usage only when the current terminal could actually seat it again; -// - account management and manual refresh surface while Usage is off screen, -// with refresh hidden while its one central request is in flight; -// - the launch trio surfaces only while the Generator launch footer is -// hidden (the narrow routing-full-screen swap). -// -// Everything else lives in visible section chrome or behind ?. -func (m model) contextHelp() helpKeys { - short := []key.Binding{keys.Move, keys.Change} - // The generator title advertises d · defaults itself; the compact line - // repeats it only while that chrome is off screen (routing full-screen). - if !m.generatorShown() { - short = append(short, keys.Reset) - } - if !m.routingShown() { - short = append(short, key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "show routing"))) - } - if !m.usageShown() && m.usageCanShow() { - short = append(short, key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "show usage"))) - } - // Keep full-help discovery and quit ahead of optional contextual actions: - // bubbles truncates a compact line from the right on narrow terminals. - short = append(short, keys.Help, keys.Quit) - if !m.usageShown() { - short = append(short, keys.Manager) - if m.broker.URL != "" && !m.fetching { - short = append(short, keys.Refresh) - } - } - if !m.generatorShown() { - short = append(short, keys.Launch, keys.Managed, keys.Untrusted) - } - return helpKeys{short: short, full: keys.FullHelp()} -} - -func (m *model) relayout() { - m.help.Width = m.w - gut // the footer help is gutter-inset on every line; wrap inside the padded width - pw, ph := m.previewDims() - if pw < 10 { - pw = 10 - } - if ph < 3 { - ph = 3 - } - if !m.rdy { - m.vp = viewport.New(pw, ph) - m.rdy = true - } else { - m.vp.Width, m.vp.Height = pw, ph - } - m.syncPreviewKeepScroll() -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.w, m.h = msg.Width, msg.Height - m.relayout() - case ompVersionMsg: - if msg.ok { - m.ompMajor, m.ompMinor = msg.major, msg.minor - } - return m, nil - case usageMsg: - scoped, scopedStale := reconcileUsage(m.avail, msg.avail) - refreshAt := time.Now().Add(refreshEvery) - first := !m.hadUsage && msg.avail.ok - m.avail, m.usageStale = scoped, scopedStale - m.hadUsage = m.hadUsage || msg.avail.ok - m.fetching = false - if msg.avail.ok { - saveUsageCache(m.usageCache, scoped) - } - m.nextRefresh = refreshAt - m.relayout() - if first { - // The first real data replaces the skeleton: run the one-time - // bounded bar fill. Refreshes never reach this branch again. - m.barAnim = 1 - return m, barAnimCmd(2) - } - case barAnimMsg: - // Bounded and self-terminating: apply the frame, arm the next tick, - // and stop at the final step (barAnim 0 = inactive, bars at value). - if m.barAnim == 0 { - return m, nil - } - if msg.step >= barAnimSteps { - m.barAnim = 0 - return m, nil - } - m.barAnim = msg.step - return m, barAnimCmd(msg.step + 1) - case refreshTickMsg: - // re-arm the 1s tick; auto-refresh once the interval elapses. - cmds := []tea.Cmd{tickCmd()} - if !m.fetching && !m.nextRefresh.IsZero() && !time.Now().Before(m.nextRefresh) { - cmds = append(cmds, m.startUsageFetch()) - } - return m, tea.Batch(cmds...) - case spinner.TickMsg: - if m.fetching || !m.avail.ok { - var cmd tea.Cmd - m.spin, cmd = m.spin.Update(msg) - return m, cmd - } - case admittedWheelMsg: - m.applyWheelStep(msg.Button) - return m, nil - case tea.MouseMsg: - // Wheel dispatch by pointer position: inside the visible Routing pane - // the viewport owns vertical scrolling — continuous, ungated, clamped - // by the viewport itself, with horizontal wheel deliberately inert. - // Everywhere else direct Update calls apply one generator step; live - // input is coalesced before dispatch by wheelInputFilter. - if msg.Action == tea.MouseActionPress { - if m.wheelInRouting(msg.X, msg.Y) { - switch msg.Button { - case tea.MouseButtonWheelUp: - m.vp.LineDown(1) // inverted: operator-confirmed trackpad direction - case tea.MouseButtonWheelDown: - m.vp.LineUp(1) - } - return m, nil - } - m.applyWheelStep(msg.Button) - } - case tea.KeyMsg: - if m.manager { - return m.updateManager(msg) - } - switch msg.String() { - case "q", "esc", "ctrl+c": - return m, tea.Quit - case "p": - switch { - case m.showUsage && m.sizeMode() == sizeNarrow: - m.showUsage = false - m.showResult = true - m.collapse = false - case m.collapse: - m.collapse = false // restore the hidden preview - case m.mode() == modeCollapsed: - m.showResult = !m.showResult // narrow+short: swap list ↔ result full-screen - default: - m.collapse = true // split/stacked: hide the preview, list full-screen - } - m.relayout() - case "s": - // Toggle the rendered section, not the pre-toggle size class. Usage - // changes the responsive minima, so a layout that fits while it is - // hidden may become narrow when it returns. In that case open the - // dedicated view atomically. It overlays the existing generator / - // Routing state so closing it restores that exact composition. - switch { - case m.showUsage: - m.showUsage = false - m.hideUsage = true - case !m.usageShown(): - m.hideUsage = false - m.showUsage = m.sizeMode() == sizeNarrow - default: - m.showUsage = false - m.hideUsage = true - } - m.relayout() - case "i": - m.fullUsageIDs = !m.fullUsageIDs - m.relayout() - case "?": - m.help.ShowAll = !m.help.ShowAll - m.relayout() // the taller/shorter footer changes the body height - case "d": - m.sel = defaultSel() - if len(m.runtimeTargets) > 0 { - m.sel["runtime"] = "hosted" - } - m.clampSel() // the defaults assume a full catalog; this one may not be - m.persistSelection() - m.syncPreview() - case "f": - m.depth = (m.depth + 1) % 2 - m.syncPreview() - case "r": - if m.broker.URL != "" && !m.fetching { - return m, m.startUsageFetch() - } - case "v": - m.manager = true - m.clampManagerCursor() - m.relayout() - case "up", "k": - m.moveUp() - case "down", "j": - m.moveDown() - case "left", "h": - m.cycleFacet(-1) - case "right", "l": - m.cycleFacet(1) - case "u": - // Untrusted sandbox: hand off to CODE_OMP_UNTRUSTED (ompu), which owns its - // own routing/policy — no generated --config is passed to it. Inert when - // no sandbox binary exists (the help hides the key too), so a stranger - // can't kill the TUI with a stray keypress. - if !m.hasSandbox { - return m, nil - } - m.launchUntrusted = true - return m, tea.Quit - case "pgup", "ctrl+u": - m.vp.HalfViewUp() // scroll the preview (mouse capture is off, see main) - case "pgdown", "ctrl+d": - m.vp.HalfViewDown() - case "m": - // Managed-defaults omp: omp-managed with no generated overlay. The - // explicit keybind keeps every Enter launch a generated profile. - m.launchManaged = true - return m, tea.Quit - case "enter": - if target, local := m.selectedRuntime(); local { - m.launchRuntime = target.Name - return m, tea.Quit - } - // Enter always launches the generated profile for the current facets — - // the untouched default combo is a generated profile like any other. - // Never for a combo the catalog doesn't carry, though: genConfigYAML - // would walk a nil block and emit an overlay whose modelRoles map is - // empty, handing omp a session with no routing at all. The preview - // already says "no profile for this combination", so the key does - // nothing rather than launching something broken. - if _, ok := m.generated[comboID(m.sel, m.hasRelief)]; !ok { - return m, nil - } - m.genConfig = m.genConfigYAML() - return m, tea.Quit - } - - case clikit.ActionsProposedMsg: - // Live preview: snapshot the current selection, then apply the proposal to - // the generator so the user sees the change while deciding. Report the FULL - // applied diff back to the box (the model's picks plus the derived toggles), - // so its "applied" list reflects everything that changed, not just the three - // facets the model named directly. - m.savedSel = map[string]string{} - for k, v := range m.sel { - m.savedSel[k] = v - } - m.applyActions(msg.Actions) - // applyActions' repair rules know lanes and quota, not catalog contents: - // a "critical" proposal switches fable on even where no fable combo was - // generated. Clamp and re-render before reporting what was applied. - m.clampSel() - m.syncPreview() - return m, func() tea.Msg { return clikit.AppliedActionsMsg{Actions: m.appliedDiff()} } - - case clikit.ActionsConfirmedMsg: - // Kept: the preview stays; remember the prompt for the launched session. - m.savedSel = nil - m.firstPrompt = msg.Prompt - m.persistSelection() - - case clikit.ActionsRevertedMsg: - // Rejected: restore the pre-preview selection. - if m.savedSel != nil { - m.sel = m.savedSel - m.savedSel = nil - m.syncPreview() - } - } - return m, nil -} - -// wheelInRouting reports whether a pointer position (terminal cells, 0-based) -// falls inside the visible Routing pane, whose viewport then owns vertical -// wheel scrolling: the wide split's right pane, medium's lower-right pane, or -// the narrow routing-only swap's full body. Hidden/collapsed routing claims -// nothing, so the generator keeps the wheel everywhere else. -func (m model) wheelInRouting(x, y int) bool { - if !m.routingShown() { - return false - } - ch := m.contentH() - switch m.mode() { - case modeCollapsed: // routing-only swap: the whole body is Routing - return y >= topGap && y < topGap+ch - case modeMedium: // the secondary row's right column, under the divider - genH, secH := m.mediumSplit(ch) - secTop := topGap + genH + 1 - return y >= secTop && y < secTop+secH && x >= m.w-m.routingColW() - default: // split: the right pane, from the list's right edge on - return y >= topGap && y < topGap+ch && x >= m.listW() - } -} - -// routingWheelCanMove reports whether a vertical routing scroll would change -// the viewport. No-op events at either clamp are filtered before redraw. -func (m model) routingWheelCanMove(b tea.MouseButton) bool { - switch b { - case tea.MouseButtonWheelUp: - return m.vp.YOffset < m.vp.TotalLineCount()-m.vp.Height - case tea.MouseButtonWheelDown: - return m.vp.YOffset > 0 - default: - return false - } -} - -// applyWheelStep translates one admitted wheel event into the matching facet -// action: vertical scroll moves the selection, horizontal scroll changes the -// value. The raw mapping is INVERTED on both axes — operator-confirmed trackpad -// direction: WheelUp moves the selection down, WheelDown up; WheelLeft cycles -// to the next (right) option, WheelRight to the previous. Arrow keys keep their -// literal semantics. -func (m *model) applyWheelStep(b tea.MouseButton) { - switch b { - case tea.MouseButtonWheelUp: - m.moveDown() - case tea.MouseButtonWheelDown: - m.moveUp() - case tea.MouseButtonWheelLeft: - m.cycleFacet(1) - case tea.MouseButtonWheelRight: - m.cycleFacet(-1) - } -} - -func (m *model) moveUp() { - if m.fcur > 0 { - m.fcur-- - } -} -func (m *model) moveDown() { - if m.fcur < len(m.visibleFacets())-1 { - m.fcur++ - } -} -func (m *model) cycleFacet(dir int) { - vf := m.visibleFacets() - if m.fcur >= len(vf) { - m.fcur = len(vf) - 1 - } - f := vf[m.fcur] - cur := m.sel[f.key] - idx := 0 - for i, v := range f.values { - if v == cur { - idx = i - } - } - next := idx + dir - if next < 0 { - next = 0 - } else if next >= len(f.values) { - next = len(f.values) - 1 - } - if next == idx { - return - } - m.sel[f.key] = f.values[next] - // lead/blend are lane's rendered halves: recompose the canonical value - // before anything re-derives them (visibleFacets syncs from lane). - if f.key == "lead" || f.key == "blend" { - m.sel["lane"] = laneJoin(m.sel["lead"], m.sel["blend"]) - } - // main is fable's sub-setting: whenever fable leaves "on" it must clear too, - // so a later fable re-enable never silently resurrects the (expensive) - // fable-as-main escalation — it is re-chosen deliberately every time. - if m.sel["fable"] != "on" { - m.sel["main"] = "off" - } - // changing the lane can hide/show facets; keep the cursor in range. - if nv := len(m.visibleFacets()); m.fcur >= nv { - m.fcur = nv - 1 - } - m.syncPreview() - m.persistSelection() -} - -// prevPadL is the split preview pane's left padding. Width() counts it, so the -// viewport's usable text width is the box width minus this — the viewport must -// be sized to that inner area (see previewDims), else lines wrap 1:1 with the -// box and their tail overflows to the pane's left edge. -const prevPadL = 2 - -func (m model) previewPane(w, h int) string { - return lipgloss.NewStyle(). - Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(lipgloss.Color(cBord)).PaddingLeft(prevPadL). - Width(w).Height(h). - Render(m.previewColumn()) -} - -// accent is the context colour — the selected lane in the generator. -// Blue / purple / orange. -func (m model) accent() string { - if _, local := m.selectedRuntime(); local { - return cGreen - } - return laneColor(m.sel["lane"]) -} - -// pill renders an accent-backed section label, coloured by the active lane — -// the shared shape of the generator (left) and routing (right) column heads. -func (m model) pill(label string) string { - return lipgloss.NewStyle().Padding(0, 1). - Background(lipgloss.Color(m.accent())).Foreground(lipgloss.Color("#12161d")).Bold(true). - Render(label) -} - -// sectionTitle is the accent-pilled "generator" label at the top of the left -// column. -func (m model) sectionTitle() string { - return m.pill("generator") -} - -// sectionHead is the gutter-inset title row — the pill plus its local -// reset-to-defaults cue (d · defaults) — and a blank separator (headRows -// tall); it stays pinned above the scrolling facet list. -func (m model) sectionHead() string { - return padLeft(m.sectionTitle()+" "+stCueKey.Render("d")+stCue.Render(" · defaults"), gut) + "\n\n" -} - -// prevChromeRows is the Routing column's pinned chrome around the scrolling -// viewport: the title row with its local collapse cue and a blank separator -// above, plus the fallback-display cue pinned beneath the viewport. -const prevChromeRows = headRows + 1 - -// previewColumn assembles the Routing section: the pinned title row carrying -// the section-local collapse cue (p · hide), the scrolling routing viewport, -// then the fallback-display cue pinned at the section's bottom edge — bottom -// chrome, where the chains it toggles end. The f wording makes clear it only -// changes what is DISPLAYED: the launched profile always keeps its fallback -// chains. -func (m model) previewColumn() string { - verb := "show" - if m.depth == 1 { - verb = "hide" - } - return m.pill("routing") + " " + stCueKey.Render("p") + stCue.Render(" · hide") + "\n\n" + - m.vp.View() + "\n" + - stKey.Render("f") + stDim.Render(" · "+verb+" fallback chains") -} - -// leftColumn renders the pinned section head plus the scrolling list body, the -// whole column inset by the shared gutter, sized to totalH rows and w columns. -func (m model) leftColumn(w, totalH int) string { - iw := w - gut - body, bcur := m.bodyLines() - listH := totalH - headRows - launchFooterRows - if listH < 1 { - listH = 1 - } - list := padLeft(windowList(body, bcur, listH, iw), gut) - footer := padLeft(strings.Join(m.launchFooter(), "\n"), gut) - return m.sectionHead() + list + "\n" + footer -} - -// mediumContent is the generator-dominant layout: the full-width facet list on -// top (primary), a divider, then Usage (left) and Routing (right) side by side -// in a secondary row, separated by a one-cell border column. Usage stacks its -// provider groups vertically inside the measured-width left column; Routing -// keeps its own scrolling viewport in whatever the row leaves free. -func (m model) mediumContent(bodyH int) string { - genH, secH := m.mediumSplit(bodyH) - top := m.leftColumn(m.w, genH) - div := stDim.Render(strings.Repeat("─", m.w)) - rw := m.routingColW() - // clip each column before fixing its width — Width() alone would wrap any - // over-wide line onto an extra physical row and break the row's height. - routing := lipgloss.NewStyle().Width(rw).MaxHeight(secH).Render( - lipgloss.NewStyle().MaxWidth(rw).Render(padLeft(m.previewColumn(), gut))) - sec := routing - if !m.hideUsage { - uw := m.w - rw - secSepW // the measured usage column's share, left of the border - sep := lipgloss.NewStyle().Foreground(lipgloss.Color(cBord)).Render( - strings.TrimSuffix(strings.Repeat("│\n", secH), "\n")) - usage := lipgloss.NewStyle().Width(uw).MaxHeight(secH).Render( - lipgloss.NewStyle().MaxWidth(uw).Render(m.usagePanelStackedFor(uw))) - sec = lipgloss.JoinHorizontal(lipgloss.Top, usage, sep, routing) - } - return lipgloss.JoinVertical(lipgloss.Left, top, div, sec) -} - -func (m model) View() string { - if !m.rdy { - return "loading…" - } - if m.manager { - return m.managerView() - } - foot := m.footer() - bodyH := m.bodyH() - ch := m.contentH() - var content string - switch m.mode() { - case modeCollapsed: - switch { - case m.showUsage && m.sizeMode() == sizeNarrow: - content = padLeft(m.usagePanelFor(m.w-gut), gut) - case m.showResult && !m.collapse: - content = padLeft(m.previewColumn(), gut) - default: - content = m.leftColumn(m.w, ch) - } - case modeMedium: - content = m.mediumContent(ch) - default: // split - content = lipgloss.JoinHorizontal(lipgloss.Top, - m.leftColumn(m.listW(), ch), - m.previewPane(m.w-m.listW()-3, ch)) - } - // Pin the body into a fixed box (top-left) with lipgloss, then clip: any - // stray overflow (e.g. a glyph a terminal renders wider than measured) is - // absorbed here, never pushing the pinned footer off-screen. A top gap above - // the content gives the section tabs vertical breathing room. - body := lipgloss.NewStyle().MaxHeight(bodyH).Render( - strings.Repeat("\n", topGap) + - lipgloss.Place(m.w, ch, lipgloss.Left, lipgloss.Top, content)) - return lipgloss.NewStyle().MaxWidth(m.w).MaxHeight(m.h).Render( - lipgloss.JoinVertical(lipgloss.Left, body, foot)) -} - -func (m model) genLines() ([]string, int) { - acc := m.accent() - var lines []string - cursor := 0 - selected := m.selectedLaunchAvailability() - for i, f := range m.visibleFacets() { - onRow := i == m.fcur - glyCol := laneColor(m.sel["lane"]) - gly := lipgloss.NewStyle().Foreground(lipgloss.Color(glyCol)).Width(2).Render(f.glyph) - ptr := " " - if onRow { - ptr = lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Render("▸ ") - cursor = len(lines) - } - // main renders as fable's tabulated child "default": the indent + the - // default-role row lighting up Fable in the preview explain themselves, - // so it carries no flavor text (which would wrap on narrow panes anyway). - label, childPad, childW := f.key, "", 0 - if f.key == "main" { - // tree-style L connector: reads as fable's child, like `tree`. - label, childPad, childW = "default", stDim.Render("└ "), 2 - } - if f.key == "blend" { - // blend is lane's sub-setting: the lead row picks who drives, - // this child picks how exclusively. - childPad, childW = stDim.Render("└ "), 2 - } - row := fmt.Sprintf("%s%s%s%s", ptr, childPad, gly, stDim.Render(pad(label, 9-childW))) - if f.key == "thinking" || f.key == "model" || f.key == "advisor" { - row += " " + m.segmentGauge(f, onRow, acc) - } else { - for _, v := range f.values { - display := v - if f.key == "runtime" { - display = m.runtimeValueLabel(v) - } - switch { - case v == m.sel[f.key]: - col := acc - if f.key == "lead" { - col = laneColor(m.sel["lane"]) - } else if (f.key == "spark" || f.key == "fable" || f.key == "main") && v == "on" { - col = cGreen - } - st := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true) - if onRow { // the cursor sits on the selected value of the focused row - st = st.Background(lipgloss.Color(cSelBg)) - } - row += " " + st.Render(" "+display+" ") - default: - row += " " + stDim.Render(display) - } - } - } - switch { - case (f.key == "fable" || f.key == "spark") && m.sel[f.key] == "on": - bkt, lbl := "claude-fable", "Fable" - if f.key == "spark" { - bkt, lbl = "codex-spark", "Spark" - } - if selected.down(bkt) { - w := lbl + " maxed · " + gReset + " " + fmtReset(selected.reset[bkt]) - if selected.bucket[bkt] == "unauthed" { - w = lbl + " unavailable" - } - row += " " + stWarn.Render(gWarn+" "+w+" — no usage left") - } - case f.key == "fast" && m.sel["fast"] == "on": - row += " " + stDim.Render("GPT only") - case f.key == "relief": - // relief is the least self-explanatory dial: say what it does, - // in the state it currently does it. - note := "drained chains wait for quota reset" - if m.sel["relief"] == "on" { - note = "drained chains spill into " + optionalPoolLabels() - } - row += " " + stDim.Render(note) - } - lines = append(lines, row) - } - return lines, cursor -} - -// segmentGauge renders a stepped dial (model, thinking, advisor) as a notched -// meter: one cell per selectable option, filled to the selection, with the -// selected word riding along so the level still reads at a glance. A leading -// "off" value is the dial's zero — it takes no cell, so off renders an empty -// track and the first real step lights exactly one. ←/→ behave exactly as on -// a word dial — only the rendering differs; the facet's values are untouched. -func (m model) segmentGauge(f facet, onRow bool, acc string) string { - sel := -1 - for i, v := range f.values { - if v == m.sel[f.key] { - sel = i - } - } - steps := f.values - fill := sel + 1 - if len(steps) > 0 && steps[0] == "off" { - steps = steps[1:] - fill = sel // off (sel 0) lights nothing - } - if fill < 0 { - fill = 0 - } - lit := lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Bold(true) - var b strings.Builder - // The leading space mirrors the word dials' selected-cell padding, so the - // track starts on the same column the value words do. - b.WriteString(" ") - for i := range steps { - if i < fill { - b.WriteString(lit.Render("▰")) - } else { - b.WriteString(stDim.Render("▱")) - } - } - word := lit - if onRow { - word = word.Background(lipgloss.Color(cSelBg)) - } - label := m.sel[f.key] - if sel < 0 && label == "" { - label = "?" - } - return b.String() + " " + word.Render(" "+label+" ") -} - -// defaultGlyphs is the built-in facet-glyph set (Nerd Font, Font Awesome PUA -// range), written as explicit \u escapes so the codepoints stay visible and -// verifiable in source — a literal PUA glyph is invisible in most editors and -// was once wiped by an edit exactly because of that. CODE_FACET_GLYPHS may -// override any entry (see main). -// -// runtime 🖥 (f108) lane ⇄ (f127) model ⚙ (f085) thinking 💡 (f0eb) advisor 🧭 (f14e) -// spark 🚀 (f135) fable 📖 (f02d) default 🎯 (f140) fast ⚡ (f0e7) -func defaultGlyphs() map[string]string { - return map[string]string{ - "runtime": "\uf108", "lane": "\uf127", "model": "\uf085", "thinking": "\uf0eb", "advisor": "\uf14e", - "spark": "\uf135", "fable": "\uf02d", "main": "\uf140", "fast": "\uf0e7", - "relief": "\uf132", - } -} - func main() { // Subcommands are not TUI sessions; any other argv is still forwarded to // the launched omp session as before. @@ -4165,129 +145,3 @@ func main() { // forwardArgv strips every forwarded profile flag. Trusted launches use OMP's // ordinary implicit default state; sandbox routing remains owned by ompu. -func forwardArgv(path string, forwarded []string, prompt string) []string { - out := append([]string{path}, stripProfileArgs(forwarded)...) - if prompt != "" { - out = append(out, prompt) - } - return out -} - -func managedLaunchArgv(path string, forwarded []string, prompt string) []string { - return forwardArgv(path, forwarded, prompt) -} - -func sandboxLaunchArgv(path string, forwarded []string, prompt string) []string { - return forwardArgv(path, forwarded, prompt) -} - -func generatedLaunchArgv(path, cfgPath string, forwarded []string, prompt string) []string { - args := append([]string{"--config", cfgPath}, stripProfileArgs(forwarded)...) - out := append([]string{path}, args...) - if prompt != "" { - out = append(out, prompt) - } - return out -} - -func resolveLaunchPath(envName string, fallbacks []string) (string, error) { - if configured := os.Getenv(envName); configured != "" { - return exec.LookPath(configured) - } - var err error - for _, fallback := range fallbacks { - var path string - if path, err = exec.LookPath(fallback); err == nil { - return path, nil - } - } - if err == nil { - err = errors.New("no launcher configured") - } - return "", err -} - -func runChild(path string, argv, env []string) error { - cmd := exec.Command(path, argv[1:]...) - cmd.Args = argv - cmd.Env = env - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func childStatus(err error) int { - if err == nil { - return 0 - } - var exitErr *exec.ExitError - if errors.As(err, &exitErr) && exitErr.ExitCode() >= 0 { - return exitErr.ExitCode() - } - return 1 -} - -func runSandbox(envName string, fallbacks []string, prompt string) int { - path, err := resolveLaunchPath(envName, fallbacks) - if err != nil { - fmt.Fprintln(os.Stderr, "code: sandbox not found:", err) - return 1 - } - err = runChild(path, sandboxLaunchArgv(path, os.Args[1:], prompt), withoutAuthEnv(os.Environ())) - if err != nil { - fmt.Fprintln(os.Stderr, "code: sandbox:", err) - } - return childStatus(err) -} - -func runTrusted(envName string, fallbacks []string, - argv func(string, []string, string) []string, prompt string, - broker brokerConfig, selections accountSelectionState) int { - disabled := selections.CurrentDisabled() - path, err := resolveLaunchPath(envName, fallbacks) - if err != nil { - fmt.Fprintln(os.Stderr, "code: trusted launcher not found:", err) - return 1 - } - accounts, err := loadAccounts(broker) - if err != nil { - fmt.Fprintln(os.Stderr, "code: account snapshot unavailable; refusing unrestricted launch:", err) - return 1 - } - accountPoolPath, cleanup, err := writeAccountPool(accounts, disabled) - if err != nil { - fmt.Fprintln(os.Stderr, "code: account pool unavailable; refusing unrestricted launch:", err) - return 1 - } - defer cleanup() - childEnv := withAuthEnv(os.Environ(), broker, accountPoolPath) - err = runChild(path, argv(path, os.Args[1:], prompt), childEnv) - if err != nil { - fmt.Fprintln(os.Stderr, "code: trusted child:", err) - } - return childStatus(err) -} - -// launchGenerated keeps both immutable launch inputs alive only for the child. -func launchGenerated(cfg, prompt string, broker brokerConfig, selections accountSelectionState) int { - tmp, err := os.CreateTemp("", "code-gen-*.yml") - if err != nil { - fmt.Fprintln(os.Stderr, "code:", err) - return 1 - } - cfgPath := tmp.Name() - defer os.Remove(cfgPath) - if _, err = tmp.WriteString(cfg); err == nil { - err = tmp.Close() - } else { - _ = tmp.Close() - } - if err != nil { - fmt.Fprintln(os.Stderr, "code: generated config:", err) - return 1 - } - return runTrusted("CODE_OMP", []string{"omp"}, func(path string, forwarded []string, prompt string) []string { - return generatedLaunchArgv(path, cfgPath, forwarded, prompt) - }, prompt, broker, selections) -} diff --git a/main_test.go b/main_test.go index de519ab..0d3a477 100644 --- a/main_test.go +++ b/main_test.go @@ -3,11 +3,6 @@ package main import ( "encoding/json" "fmt" - clikit "github.com/atyrode/cli-kit" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "io" "net/http" "net/http/httptest" @@ -19,6 +14,12 @@ import ( "sync/atomic" "testing" "time" + + clikit "github.com/atyrode/cli-kit" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // ansiRe strips SGR sequences so tests assert on visible text regardless of the diff --git a/model.go b/model.go new file mode 100644 index 0000000..f75056d --- /dev/null +++ b/model.go @@ -0,0 +1,172 @@ +package main + +import ( + "os/exec" + "regexp" + "strconv" + "time" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" +) + +type model struct { + generated map[string][]string + advisors map[string][]string // "level/ctx" → advisor model chain + facts map[string]modelFact // model id → cost ($/1M) + curated speed (tok/s) + avail availability + glyphs map[string]string + runtimeTargets []runtimeTarget + + // Catalog capability, phrased as absence so the zero value keeps every dial: + // a model with no catalog yet (the onboarding shell, tests) must behave as it + // always did. applyCatalog sets these only from a catalog it actually read. + noSpark bool // no _sp_ combos exist — hide the spark dial and force it off + noFable bool // no _fa_/_famain_ combos — same for fable and its main child + // hasRelief is positive-polarity: the relief segment only exists in + // catalogs with an optional pool, so the zero value keeps old ids intact. + hasRelief bool // _rel_/_norel combos exist — show the relief dial + + depth int // 0 lead · 1 full + collapse bool // p: hide the Routing section + showResult bool // in collapsed mode: show the preview full-width + hideUsage bool // s: hide the Usage section (atyrode/dotfiles#198); fetch state keeps running unseen + showUsage bool // narrow mode: show Usage full-screen instead of silently shedding it + fullUsageIDs bool // i: expand compact Usage identities to full account addresses + + facets []facet + fcur int + sel map[string]string + selectionState string // CODE_SELECTION_STATE; empty keeps standalone runs stateless + + vp viewport.Model + spin spinner.Model + help help.Model + w, h int + rdy bool + + broker brokerConfig + usageCache string + accountState string + accountSelections accountSelectionState + accountErr string + manager bool + mgrCursor int + managerPreset managerPresetState + + fetching bool // a usage fetch is in flight (manual or auto) + nextRefresh time.Time // when the next auto-refresh fires + hadUsage bool // a successful central fetch has landed + usageStale bool // the last central refresh failed; prior data is retained + barAnim int // first-load fill frame (1..barAnimSteps-1 = partial); 0 = inactive, bars at full value + + launchManaged bool // m: run CODE_OMP with no overlay (the managed defaults) + launchUntrusted bool // u: run the CODE_OMP_UNTRUSTED sandbox + launchRuntime string // delegated local runtime target selected via CODE_RUNTIME_BROKER + hasSandbox bool // a sandbox binary exists; gates the u key + genConfig string // generated config YAML to launch omp with (generator Enter) + firstPrompt string // prompt from the suggest box, forwarded as omp's first message + savedSel map[string]string // selection snapshot before a live suggest preview (for revert) + // The probed omp version (omp/ from `omp --version`), fetched + // async at startup. Zero = unknown: 17.3-only overlay keys are omitted so + // a lagging CODE_OMP wrapper never hard-errors at launch. + ompMajor, ompMinor int +} + +// usage auto-refreshes on this cadence; a 1s tick drives the countdown. +const refreshEvery = 5 * time.Minute + +type refreshTickMsg struct{} + +func tickCmd() tea.Cmd { + return tea.Tick(time.Second, func(time.Time) tea.Msg { return refreshTickMsg{} }) +} + +// usageMsg carries the single central account/usage snapshot fetched off the +// main thread so startup and refresh never block the TUI. +type usageMsg struct { + avail availability +} + +func fetchUsageCmd(broker brokerConfig) tea.Cmd { + if broker.URL == "" || broker.Token == "" { + return nil + } + return func() tea.Msg { return usageMsg{avail: loadAvailability(broker)} } +} + +func (m *model) startUsageFetch() tea.Cmd { + cmd := fetchUsageCmd(m.broker) + if cmd != nil { + m.fetching = true + } + return cmd +} + +// First-load bar fill grows each central usage bar from empty when the first +// successful fetch replaces the loading skeleton. A dedicated bounded tick +// sequence renders labels and numbers immediately and animates only the fill. +// Manual and automatic refreshes never re-run it. +const ( + barAnimSteps = 8 + barAnimInterval = 25 * time.Millisecond +) + +// barAnimMsg advances the first-load fill to the given frame (2..barAnimSteps). +type barAnimMsg struct{ step int } + +func barAnimCmd(step int) tea.Cmd { + return tea.Tick(barAnimInterval, func(time.Time) tea.Msg { return barAnimMsg{step} }) +} + +// ompVersionMsg carries the probed omp version; ok=false leaves it unknown. +type ompVersionMsg struct { + major, minor int + ok bool +} + +// ompVersionRe parses `omp/....` anywhere in --version output. +var ompVersionRe = regexp.MustCompile(`omp/(\d+)\.(\d+)`) + +// probeOmpVersionCmd resolves the same binary Enter launches and asks it for +// its version, off the main thread. A drift guard, not a feature flag: any +// failure just reads as "unknown" and version-gated keys stay off. +func probeOmpVersionCmd() tea.Cmd { + return func() tea.Msg { + path, err := resolveLaunchPath("CODE_OMP", []string{"omp"}) + if err != nil { + return ompVersionMsg{} + } + out, err := exec.Command(path, "--version").Output() + if err != nil { + return ompVersionMsg{} + } + match := ompVersionRe.FindSubmatch(out) + if match == nil { + return ompVersionMsg{} + } + major, _ := strconv.Atoi(string(match[1])) + minor, _ := strconv.Atoi(string(match[2])) + return ompVersionMsg{major: major, minor: minor, ok: true} + } +} + +// ompVersionAtLeast reports a probed version ≥ major.minor; unknown is never +// "at least" anything. +func (m model) ompVersionAtLeast(major, minor int) bool { + if m.ompMajor == 0 && m.ompMinor == 0 { + return false + } + return m.ompMajor > major || (m.ompMajor == major && m.ompMinor >= minor) +} + +func (m model) Init() tea.Cmd { + cmds := []tea.Cmd{probeOmpVersionCmd()} + if m.broker.URL != "" && m.broker.Token != "" { + cmds = append(cmds, m.startUsageFetch(), m.spin.Tick, tickCmd()) + } + return tea.Batch(cmds...) +} diff --git a/render.go b/render.go new file mode 100644 index 0000000..5f5007c --- /dev/null +++ b/render.go @@ -0,0 +1,161 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// ── routing render (depth: 0 lead · 1 full) ────────────────────────────────── +// renderRoute lays out each role's chain, wrapping cleanly at `width`: when a +// chain doesn't fit, it breaks after an arrow and the continuation is indented +// to align under the first model, so it reads as one hanging block rather than a +// ragged wrap. Down (maxed/unauthed) models are struck through — which bucket a +// model draws from is a catalog fact, hence the receiver. +func (m model) renderRoute(rows []string, depth int, a availability, width int) string { + if width < 24 { + width = 24 + } + arrow := stDim.Render(" → ") + var out []string + for _, r := range rows { + locs := modelRe.FindAllStringIndex(r, -1) + if len(locs) == 0 { // a note/meta line with no models — pass through + out = append(out, colorizeRoute(r)) + continue + } + label := r[:locs[0][0]] // role + its alignment padding, kept verbatim + labelW := lipgloss.Width(label) + indent := strings.Repeat(" ", labelW) + + type tok struct { + text string + down bool + } + var toks []tok + for _, loc := range locs { + mt := r[loc[0]:loc[1]] + toks = append(toks, tok{mt, a.ok && a.down(m.bucketFor(mt))}) + } + // how many to show: full → all; lead → primary, or up to the first live + // model when the lead is down (the one that actually runs). + last := len(toks) - 1 + if depth == 0 { + last = 0 + for i, t := range toks { + last = i + if !t.down { + break + } + } + } + // render a token as it displays (short name), returning the styled + // string and its display width — struck if the model is down. + render := func(t tok) (string, int) { + c := strings.LastIndexByte(t.text, ':') + short := shortModel(t.text[:c]) + t.text[c:] + if t.down { + s := stStruck.Render(short) + return s, lipgloss.Width(s) + } + s := colorizeRoute(t.text) // paintModel shortens + colours + return s, lipgloss.Width(s) + } + s0, w0 := render(toks[0]) + line, lineW := label+s0, labelW+w0 + for i := 1; i <= last; i++ { + si, wi := render(toks[i]) + // Reserve 2 cols for a trailing " →" whenever more models follow, so a + // break line's continuation arrow always fits within width instead of + // being clipped at the edge; the final model needs no such reserve. + budget := width + if i < last { + budget -= 2 + } + if lineW+3+wi > budget { // won't fit — break after a trailing arrow + out = append(out, line+stDim.Render(" →")) + line, lineW = indent+si, labelW+wi + } else { + line += arrow + si + lineW += 3 + wi + } + } + out = append(out, line) + } + return strings.Join(out, "\n") + "\n" +} + +// splitMeta peels the "thinking … · fallback … · advisor …" summary line off a +// routing block (if present), returning it trimmed plus the remaining role rows. +func splitMeta(rows []string) (string, []string) { + if len(rows) > 0 && strings.Contains(rows[0], "·") && !modelRe.MatchString(rows[0]) { + return strings.TrimSpace(rows[0]), rows[1:] + } + return "", rows +} + +func (m *model) syncPreview() { + m.syncPreviewAt(0) +} + +// syncPreviewKeepScroll re-renders while preserving the scroll position where +// still valid — resizes and background usage refreshes must never yank the view. +func (m *model) syncPreviewKeepScroll() { + m.syncPreviewAt(m.vp.YOffset) +} + +func (m *model) syncPreviewAt(yoff int) { + if !m.rdy || m.collapse { + return + } + rw := m.vp.Width + // No settings summary here: every dial is already visible (selected) in the + // generator list on the left, so the preview shows only what that selection + // produces — the role → model routing itself. + var b strings.Builder + if target, local := m.selectedRuntime(); local { + b.WriteString(lipgloss.NewStyle().Bold(true).Render(target.Label) + "\n") + b.WriteString(stDim.Render(target.statusLine()) + "\n\n") + if target.ContextWindow > 0 { + b.WriteString(fmt.Sprintf("context %dk tokens\n\n", target.ContextWindow/1000)) + } + // Same grammar as a hosted profile: every role the broker's generated + // profile routes (all of them but the advisor, which stays off), led by + // the one local model at the dialed thinking — the flag is forwarded + // verbatim and omp clamps it to what the model offers. + rows := []string{fmt.Sprintf(" thinking %s · fallback off · advisor off", m.sel["thinking"])} + for _, r := range genRoleOrder { + if r == "advisor" { + continue + } + marker := " " + if genAgentRoles[r] { + marker = "●" + } + rows = append(rows, fmt.Sprintf(" %s %-10s %s:%s", marker, r, target.Model, m.sel["thinking"])) + } + b.WriteString(m.renderRoute(rows, m.depth, m.selectedLaunchAvailability(), rw)) + b.WriteString("\n" + stDim.Render("broker-owned profile · cloud auth excluded · weights provisioned by the runtime") + "\n") + content := lipgloss.NewStyle().MaxWidth(m.vp.Width).Render(b.String()) + m.vp.SetContent(content) + m.vp.SetYOffset(yoff) + return + } + id := comboID(m.sel, m.hasRelief) + if base, ok := m.generated[id]; ok { + _, roles := splitMeta(base) + roles = m.applyAdvisor(roles, m.sel["advisor"]) + b.WriteString(m.renderRoute(roles, m.depth, m.selectedLaunchAvailability(), rw)) + } else { + b.WriteString(stDim.Render("no profile for this combination") + "\n") + } + // clip (don't wrap) to pane width — renderRoute already wrapped the chains. + content := lipgloss.NewStyle().MaxWidth(m.vp.Width).Render(b.String()) + m.vp.SetContent(content) + m.vp.SetYOffset(yoff) // clamps into the new content and viewport height +} + +// footer is the pinned bottom block: the usage panel (when this composition +// keeps Usage in the footer) then the controls help, each under a rule. Built +// once here so relayout and View stay in sync. diff --git a/routing.go b/routing.go new file mode 100644 index 0000000..2448ca5 --- /dev/null +++ b/routing.go @@ -0,0 +1,693 @@ +package main + +import ( + "fmt" + "math" + "slices" + "sort" + "strings" + "time" + + clikit "github.com/atyrode/cli-kit" +) + +// ── cost + speed meters ────────────────────────────────────────────────────── +// A profile's price and pace are dominated by the models on its heaviest roles, +// so each role is weighted by the token volume it drives over a session: the +// default agent and its task sub-agents move the needle; commit/tiny barely +// register — so Fable-as-commit stays cheap while Fable-as-default is dear (and +// slow). Per role, cost blends input+output pricing while speed reads the model's +// effective throughput (tok/s folded with time-to-first-token — see effTPS); both +// scale with thinking effort (more reasoning = pricier + slower) and take OpenAI's +// priority tier under fast mode (pricier but quicker). The weighted averages map +// onto 1..5 log scales (both perceived multiplicatively), calibrated across every +// valid facet × advisor × fast combination. Every role the generator emits must +// appear here: weightedModels silently skips a role it cannot weigh, so an +// omission drops that model out of both meters with no trace. +var roleWeight = map[string]float64{ + "default": 10, "task": 6, "reviewer": 3, "sonic": 3, "plan": 3, "advisor": 4, "slow": 2, + "designer": 2, "librarian": 2, "scout": 2, "smol": 1, "tiny": 0.5, "commit": 0.5, "vision": 0.5, + // security-reviewer routes like reviewer but is spawned far more rarely + // (ad-hoc scans), so it barely moves the needle. + "security-reviewer": 1, +} +var thinkMult = map[string]float64{ // reasoning tokens grow with effort → pricier + "minimal": 0.6, "low": 0.8, "medium": 1.0, "high": 1.3, "xhigh": 1.6, "max": 2.0, +} +var thinkSpeed = map[string]float64{ // more reasoning before the answer → slower + "minimal": 1.4, "low": 1.2, "medium": 1.0, "high": 0.8, "xhigh": 0.65, "max": 0.5, +} + +const ( + priorityMult = 1.9 // OpenAI priority tier costs more under fast mode … + fastSpeed = 1.3 // … but responds quicker +) + +// Ln endpoints of the grid-wide min/max weighted indices, calibrated over every +// valid facet × advisor × fast combination (cost dear→ high, speed fast→ high). +const ( + costLnLo, costLnHi = 1.27, 4.42 + speedLnLo, speedLnHi = 2.49, 4.20 +) + +// weightedModels walks the current config's rows and calls fn(weight, id, level) +// for each role's lead model — the shared basis for both meters. +// currentRows is the routing block the cost/speed meters score: the generator's +// facet combo with the advisor dial applied. +func (m model) currentRows() []string { + return m.applyAdvisor(m.generated[comboID(m.sel, m.hasRelief)], m.sel["advisor"]) +} + +func (m model) weightedModels(rows []string, fn func(w float64, id, lvl string)) { + for _, r := range rows { + f := strings.Fields(strings.ReplaceAll(r, "→", " ")) + i := 0 + if len(f) > 0 && f[0] == "●" { + i = 1 + } + if i >= len(f) { + continue + } + w, ok := roleWeight[f[i]] + if !ok { + continue + } + var lead string + for _, t := range f[i+1:] { + if modelRe.MatchString(t) { + lead = t + break + } + } + id, lvl, _ := strings.Cut(lead, ":") + if id != "" { + fn(w, id, lvl) + } + } +} + +func logScore(idx, lnLo, lnHi float64) int { + s := 1 + 4*(math.Log(idx)-lnLo)/(lnHi-lnLo) + return int(math.Round(math.Max(1, math.Min(5, s)))) +} + +// DeepSeek discounts the pay-as-you-go API during its off-peak window, +// UTC 16:30–00:30 (deepseek.com/pricing). The meter prices D rungs by the +// clock so a cheap window reads as the discount it is. offPeakNow is a var +// for tests only. +const deepseekOffPeakMult = 0.5 + +var offPeakNow = time.Now + +func deepseekOffPeak(utc time.Time) bool { + m := utc.Hour()*60 + utc.Minute() + return m >= 16*60+30 || m < 30 +} + +// costScore rates the current config from 1 (cheap) to 5 (dear). +func (m model) costScore() int { + if _, ok := m.selectedRuntime(); ok { + return 1 + } + fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") + var num, den float64 + m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { + c, ok := m.facts[id] + if !ok { + return + } + mult, ok := thinkMult[lvl] + if !ok { + mult = 1 + } + cost := (0.25*c.in + 0.75*c.out) * mult + if fast && m.poolOfModel(id) == "O" { + cost *= priorityMult + } + if m.poolOfModel(id) == "D" && deepseekOffPeak(offPeakNow().UTC()) { + cost *= deepseekOffPeakMult + } + num += w * cost + den += w + }) + if den == 0 { + return 1 + } + return logScore(num/den, costLnLo, costLnHi) +} + +// speedScore rates the current config from 1 (slow) to 5 (fast). +func (m model) speedScore() int { + if _, ok := m.selectedRuntime(); ok { + return 3 + } + fast := m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") + var num, den float64 + m.weightedModels(m.currentRows(), func(w float64, id, lvl string) { + c, ok := m.facts[id] + if !ok || c.speed == 0 { + return + } + mult, ok := thinkSpeed[lvl] + if !ok { + mult = 1 + } + sp := c.effTPS() * mult + if fast && m.poolOfModel(id) == "O" { + sp *= fastSpeed + } + num += w * sp + den += w + }) + if den == 0 { + return 3 + } + return logScore(num/den, speedLnLo, speedLnHi) +} + +// meter renders a labelled 1..5 scale — n glyphs in the fill colour, the rest in +// the dim "empty" colour — always five glyphs so the fill (and the headroom) read +// at a glance. +func (m model) meter(label, glyph, fill string, n int) string { + return clikit.Meter(label, glyph, fill, n) +} + +// advisorChain returns the advisor role's model chain for an intensity, sourced +// from the baked __advisors__ table. The advisor is the independent second +// opinion, so it crosses to another provider whenever the lane allows it: the +// first advisorPoolOrder pool that is not the lead's (fable-as-main hands the +// default role to the elite, so the lead becomes the elite's pool). Only the +// pure lanes stay on their own provider — ox-only's second opinion is Ox. +// +// On the mixed ox lanes the chain carries a cross-pool net in spend order — +// the other paid pool's cheapest rung, then the free pool itself — so a dead +// quota never leaves the second eye blind. +func (m model) advisorChain(level string) []string { + lane := m.sel["lane"] + if p := providerByLane(lane); p != nil && lanePure(lane) { + return m.advisors[level+"/"+p.Lane] + } + lead := genLanePolicies[m.sel["lane"]].primary + if fb := providerBySpecial("fable"); fb != nil && m.sel["fable"] == "on" && m.sel["main"] == "on" { + // fable-as-main puts the elite in the default seat, so the second + // opinion flips away from the elite's own pool. + lead = fb.Pool + } + for _, pool := range advisorPoolOrder { + if pool == lead { + continue + } + p := providerByPool(pool) + if p == nil { + continue + } + chain := m.advisors[level+"/"+p.Lane] + if len(chain) == 0 { + continue + } + if lane == "ox-led" || lane == "ox-lean" { + chain = m.advisorSpendNet(chain, p.Lane) + } + return m.advisorRelief(level, chain) + } + return nil +} + +// advisorSpendNet appends the mixed ox lanes' cross-pool net in spend order — +// the other paid pool's cheapest rung, then the free pool itself — so a dead +// quota never leaves the second eye blind. +func (m model) advisorSpendNet(chain []string, ctx string) []string { + tail := m.advisors["glance/gpt"] + if ctx == "gpt" { + tail = m.advisors["glance/claude"] + } + out := append([]string(nil), chain...) + for _, t := range append(append([]string{}, tail...), m.advisors["glance/ox"]...) { + if t != "" && !slices.Contains(out, t) { + out = append(out, t) + } + } + return out +} + +// advisorRelief appends the relief tail to the audit chain: audit is the +// advisor's heavyweight setting, so — exactly like the heavyweight roles — +// its metered chain ends on each optional pool's own audit rung when relief +// is on, and a drained day still gets its second opinion. Lighter levels +// stay short: they are cheap enough that quota rarely blocks them. +func (m model) advisorRelief(level string, chain []string) []string { + if level != "audit" || m.sel["relief"] != "on" || !laneReliefApplies(m.sel["lane"]) { + return chain + } + out := append([]string(nil), chain...) + for i := range providerRegistry { + p := &providerRegistry[i] + if p.Required { + continue + } + relief := m.advisors[level+"/"+p.Lane] + if len(relief) == 0 || slices.Contains(out, relief[0]) { + continue + } + out = append(out, relief[0]) + } + return out +} + +// roleOf returns the role name of a routing row ("● task" → "task"). +func roleOf(row string) string { + f := strings.Fields(row) + if len(f) > 0 && f[0] == "●" { + f = f[1:] + } + if len(f) > 0 { + return f[0] + } + return "" +} + +// applyAdvisor replaces the baked advisor row with one synthesised from the +// chosen intensity (dropping it entirely when off), so the generated preview and +// the launched config both reflect the advisor facet. +func (m model) applyAdvisor(rows []string, level string) []string { + chain := m.advisorChain(level) + newRow := "" + if len(chain) > 0 { + newRow = " advisor " + strings.Join(chain, " → ") + } + var out []string + replaced := false + for _, r := range rows { + if roleOf(r) == "advisor" { + replaced = true + if newRow != "" { + out = append(out, newRow) + } + continue + } + out = append(out, r) + } + if !replaced && newRow != "" { + out = append(out, newRow) + } + return out +} + +// visibleFacets drops facets that don't apply to the current lane, so the +// generator only ever shows actionable options: no spark/fast on a pure lane +// of another pool, no fable outside its pool's lanes. main is fable's +// sub-setting, so it only shows while fable is on (and the lane can host it at +// all). A dial this catalog generated no combo for is dropped the same way — +// it is not a choice. +// +// The lane facet renders as two rows: lead (one segment per pool, plus mixed) +// and blend (led | only, hidden for mixed). sel["lane"] stays the canonical +// value — lead/blend are derived here and recomposed by cycleFacet, and are +// never persisted (saveSelectionState filters on m.facets, which carries only +// "lane"). +func (m model) visibleFacets() []facet { + if _, local := m.selectedRuntime(); local { + var out []facet + for _, f := range m.facets { + if f.key == "runtime" || f.key == "thinking" { + out = append(out, f) + } + } + return out + } + lane := m.sel["lane"] + var out []facet + for _, f := range m.facets { + if f.key == "lane" { + lead, blend := laneSplit(lane) + m.sel["lead"], m.sel["blend"] = lead, blend + // mixed leads the dial: it is the default and the only lead + // without a blend child, so it anchors the left edge. + leads := []string{"mixed"} + seen := map[string]bool{"mixed": true} + for _, v := range f.values { + l, _ := laneSplit(v) + if !seen[l] { + seen[l] = true + leads = append(leads, l) + } + } + out = append(out, facet{"lead", leads, f.glyph}) + if lead != "mixed" { + out = append(out, facet{"blend", []string{"led", "only"}, f.glyph}) + } + continue + } + switch f.key { + case "spark": + // genValid refuses every ox-lane spark combo (the drain bucket's + // leads are utility roles those lanes give to their own pools). + if !laneHostsSpecial(lane, "spark") || m.noSpark || + lane == "ox-only" || lane == "ox-led" || lane == "ox-lean" { + continue + } + case "fast": + // The fast dial is the priority service tier — an OpenAI pool + // feature, meaningless on a pure lane of any other pool or where + // no OpenAI token leads the work. + if p := providerByLane(lane); lanePure(lane) && p != nil && p.ServiceTier[0] == "" { + continue + } + if lane == "ox-only" || lane == "ox-led" { + continue + } + case "fable", "main": + if !laneHostsSpecial(lane, "fable") || m.noFable { + continue + } + if f.key == "main" && (m.sel["fable"] != "on" || lane == "ox-led") { + // fable-as-main would defeat ox-led's free worker, and + // genValid refuses that combo outright. + continue + } + case "relief": + if !m.hasRelief || !laneReliefApplies(lane) { + continue + } + } + out = append(out, f) + } + return out +} + +func comboID(sel map[string]string, hasRelief bool) string { + lane := sel["lane"] + sp, fb := sel["spark"], sel["fable"] + if !laneHostsSpecial(lane, "fable") { + fb = "off" + } + if !laneHostsSpecial(lane, "spark") || lane == "ox-led" || lane == "ox-lean" { + sp = "off" + } + spid, faid := "nosp", "nofa" + if sp == "on" { + spid = "sp" + } + if fb == "on" { + faid = "fa" + // ox-led hosts the elite on deliberative roles only; promoting it to + // the default role would defeat the lane, and genValid refuses that + // combo outright. + if sel["main"] == "on" && lane != "ox-led" { + faid = "famain" + } + } + id := fmt.Sprintf("%s_%s_%s_%s_%s", lane, sel["model"], sel["thinking"], spid, faid) + if hasRelief { + rel := sel["relief"] + if !laneReliefApplies(lane) { + rel = "on" // the only variant generated there + } + if rel == "off" { + id += "_norel" + } else { + id += "_rel" + } + } + return id +} + +// applyCatalog records which dials this catalog can actually serve, then forces +// the rest off. A models file with no tier-0 model yields no _sp_ combos at all, +// so the shipped default (spark on) would open the TUI on a combo that was never +// written — and a selection persisted against a richer catalog does the same. +// Ids are ____, so match whole +// segments: "nosp" and "nofa" contain the very substrings being looked for. +// The lane facet's value list is the catalog's too: the distinct lane segments +// of the combo ids, ordered canonically (laneOrderForPools), so a catalog with +// a DeepSeek pool grows ds-led/ds-only dials and an old one shows exactly the +// classic five. +func (m *model) applyCatalog() { + if len(m.generated) == 0 { + return // no catalog read yet: onboarding, or a broken CODE_GENERATED + } + spark, fable, relief := false, false, false + served := map[string]bool{} + for id := range m.generated { + lane := id + if i := strings.IndexByte(id, '_'); i >= 0 { + lane = id[:i] + } + served[lane] = true + for _, seg := range strings.Split(id, "_") { + switch seg { + case "sp": + spark = true + case "fa", "famain": + fable = true + case "norel": + relief = true + } + } + } + m.noSpark, m.noFable, m.hasRelief = !spark, !fable, relief + if lanes := catalogLanes(m.generated); len(lanes) > 0 { + for i := range m.facets { + if m.facets[i].key == "lane" { + m.facets[i].values = lanes + } + } + } + m.trimLanes(served) + + m.clampSel() +} + +// catalogLanes collects the distinct lane values the catalog generated, in +// canonical dial order; unknown lane names (a newer catalog) trail sorted so +// they are still reachable. +func catalogLanes(generated map[string][]string) []string { + seen := map[string]bool{} + for id := range generated { + if strings.HasPrefix(id, "__") { + continue + } + lane, rest, ok := strings.Cut(id, "_") + if !ok || lane == "" || rest == "" { + continue + } + seen[lane] = true + } + if len(seen) == 0 { + return nil + } + var lanes []string + for _, lane := range laneOrderForPools(fallbackPoolOrder) { + if seen[lane] { + lanes = append(lanes, lane) + delete(seen, lane) + } + } + var extra []string + for lane := range seen { + extra = append(extra, lane) + } + sort.Strings(extra) + return append(lanes, extra...) +} + +// trimLanes narrows the lane dial to the lanes this catalog actually serves, +// and lands the selection on a served lane when a persisted or default choice +// points at one that vanished (an older catalog without ox, say). This is the +// consumer side of the optional-pool switches: no ox entries in models.yml +// means no ox values on the dial at all. +func (m *model) trimLanes(served map[string]bool) { + for i, f := range m.facets { + if f.key != "lane" { + continue + } + var values []string + for _, v := range f.values { + if served[v] { + values = append(values, v) + } + } + if len(values) == len(f.values) { + continue // nothing to trim + } + if len(values) > 0 { + m.facets[i].values = values + } + break + } + if !served[m.sel["lane"]] { + for _, fallback := range []string{"mixed", "gpt-only", "claude-only"} { + if served[fallback] { + m.sel["lane"] = fallback + break + } + } + } +} + +// clampSel turns off every dial the catalog cannot serve. main is fable's +// sub-setting and never outlives it. +func (m *model) clampSel() { + if m.noSpark { + m.sel["spark"] = "off" + } + if m.noFable { + m.sel["fable"] = "off" + m.sel["main"] = "off" + } + if !m.hasRelief { + m.sel["relief"] = "on" + } + // A persisted lane the applied catalog no longer generates (or one from a + // richer catalog) resets to the dial's first lane. + for _, f := range m.facets { + if f.key != "lane" || len(f.values) == 0 { + continue + } + known := false + for _, v := range f.values { + if v == m.sel["lane"] { + known = true + break + } + } + if !known { + m.sel["lane"] = f.values[0] + } + } +} + +// laneColor tints the accent by lane: each provider carries a deeper shade for +// its pure lane and a lighter one for its led lane; mixed keeps its purple. +func laneColor(lane string) string { + if lane == "mixed" { + return "#aa96e1" + } + if lane == "ox-lean" { + return "#2dd4bf" // teal — paid work riding the free pool + } + if p := providerByLane(lane); p != nil { + if lanePure(lane) { + return p.LaneOnly + } + return p.LaneLed + } + return "#ff9f52" +} + +// prefixed qualifies a model id with its omp provider: the catalog column when +// present, else the registry's family guess. The unknown-model fallback stays +// openai-codex so a legacy catalog launches exactly as before. +func (m model) prefixed(model string) string { + // Routing tokens carry a thinking level ("id:level"); the catalog is + // keyed on the bare id. Qualify the full token either way. + id := model + if i := strings.IndexByte(id, ':'); i >= 0 { + id = id[:i] + } + if f, ok := m.facts[id]; ok { + if p := providerByPool(f.pool); p != nil { + return p.ID + "/" + model + } + } + if p := providerByModel(model); p != nil { + return p.ID + "/" + model + } + return openAIProvider + "/" + model +} + +// poolOfModel is the model's registry pool letter: the catalog column first +// (leveled tokens are stripped before the lookup), the family guess second; +// "" when nobody claims it. +func (m model) poolOfModel(id string) string { + bare := id + if i := strings.IndexByte(bare, ':'); i >= 0 { + bare = bare[:i] + } + if f, ok := m.facts[bare]; ok && f.pool != "" { + return f.pool + } + if p := providerByModel(bare); p != nil { + return p.Pool + } + return "" +} + +// genConfigYAML reconstructs an omp config (modelRoles, task-agent model +// overrides for the ●-marked agent-backed roles, fallback chains, thinking, +// advisor, and the priority tier when fast is on) from the generated routing +// block for the current facets — what Enter launches omp with. The agent +// overrides mirror the preview: without them the static managed defaults +// would keep the five agent-backed types pinned regardless of the generated +// profile (issue atyrode/dotfiles#173). +func (m model) genConfigYAML() string { + rows := m.applyAdvisor(m.generated[comboID(m.sel, m.hasRelief)], m.sel["advisor"]) + var mr, fc, ao strings.Builder + advisorOn := false + for _, r := range rows { + f := strings.Fields(strings.ReplaceAll(r, "→", " ")) + i := 0 + if len(f) > 0 && f[0] == "●" { + i = 1 + } + if i >= len(f) { + continue + } + role := f[i] + var models []string + for _, t := range f[i+1:] { + if modelRe.MatchString(t) { + models = append(models, t) + } + } + if len(models) == 0 { + continue + } + if role == "advisor" { + advisorOn = true + } + if i == 1 && role != "advisor" { + // ●-marked agent-backed role: mirror its lead route as the task-agent + // model override so spawned agents follow the generated profile. + ao.WriteString(" " + role + ": " + m.prefixed(models[0]) + "\n") + } + mr.WriteString(" " + role + ": " + m.prefixed(models[0]) + "\n") + if len(models) > 1 { + var fbs []string + for _, x := range models[1:] { + fbs = append(fbs, m.prefixed(x)) + } + fc.WriteString(" " + role + ": [" + strings.Join(fbs, ", ") + "]\n") + } + } + var b strings.Builder + b.WriteString("modelRoles:\n" + mr.String()) + b.WriteString("retry:\n enabled: true\n modelFallback: true\n fallbackRevertPolicy: cooldown-expiry\n fallbackChains:\n" + fc.String()) + // task.agentAdvisor (omp ≥ 17.3; earlier omps hard-error on the unknown + // key, and CODE_OMP wrappers can lag the store during a dotfiles rollout, + // so the probed version gates the emission): at the audit dial, spawned + // task agents get their own advisor. Merged into the one task: block — + // overlays are strict YAML and two task: keys would be invalid. + agentAdvisor := m.sel["advisor"] == "audit" && m.ompVersionAtLeast(17, 3) + if ao.Len() > 0 || agentAdvisor { + b.WriteString("task:\n") + if ao.Len() > 0 { + b.WriteString(" agentModelOverrides:\n" + ao.String()) + } + if agentAdvisor { + b.WriteString(" agentAdvisor:\n task: \"on\"\n") + } + } + b.WriteString("defaultThinkingLevel: " + m.sel["thinking"] + "\n") + if advisorOn { + b.WriteString("advisor:\n enabled: true\n") + } else { + b.WriteString("advisor:\n enabled: false\n") + } + if m.sel["fast"] == "on" && laneHasPool(m.sel["lane"], "O") { + if p := providerByPool("O"); p != nil && p.ServiceTier[0] != "" { + b.WriteString("tier:\n " + p.ServiceTier[0] + ": " + p.ServiceTier[1] + "\n") + } + } + return b.String() +} diff --git a/theme.go b/theme.go new file mode 100644 index 0000000..d78c7eb --- /dev/null +++ b/theme.go @@ -0,0 +1,60 @@ +package main + +import ( + "regexp" + + clikit "github.com/atyrode/cli-kit" + "github.com/charmbracelet/lipgloss" +) + +// ── palette ────────────────────────────────────────────────────────────────── +// The palette, glyphs, and styles now live in the shared cli-kit; these are +// ergonomic local aliases so the rest of the file reads unchanged. cli-kit is +// the single source both `code` and `atyrode` build on. +const ( + cAcc = clikit.CAcc + cBord = clikit.CBord + cHead = clikit.CHead + cSelBg = clikit.CSelBg + cGreen = clikit.CGreen +) + +const ( + gWarn = clikit.GWarn + gReset = clikit.GReset +) + +var ( + meterRamp = clikit.MeterRamp + + stDim = clikit.StDim + stHead = clikit.StHead + stWarn = clikit.StWarn + stBrk = clikit.StBrk + stStruck = clikit.StStruck + + // stKey renders an inline key cue (r, a, s, p) — visually secondary but + // readable against the background, per the section-chrome convention. + stKey = lipgloss.NewStyle().Foreground(lipgloss.Color(cHead)) + + // Title-local hotkey cues (d · defaults, p · hide, s · hide) are quieter + // than the footer help: terminals have no portable alpha, so these are + // dedicated pre-blended tokens — CHead/CDim mixed ~40% toward the app's + // dark backdrop — applied to the whole cue, key included. Pre-blending is + // used instead of ANSI faint because faint's dimming factor varies wildly + // across terminals (and would double-dim already-muted text). Footer + // recovery cues keep the brighter help styles for readability. + stCueKey = lipgloss.NewStyle().Foreground(lipgloss.Color("#646b76")) // CHead → backdrop + stCue = lipgloss.NewStyle().Foreground(lipgloss.Color("#4f5768")) // CDim → backdrop + + // layout + meter primitives now live in cli-kit + padLeft = clikit.PadLeft + pad = clikit.Pad + windowList = clikit.WindowList + + // Provider-qualified ids: bare catalog ids today (gpt-…, claude-…) plus + // slash-scoped ones (stealth/ox-alpha, local-qwen/qwen3.8-27b). The level + // suffix with its colon is what keeps prose out; the word boundary keeps + // "maxed" from reading as a model. + modelRe = regexp.MustCompile(`([a-z][a-z0-9._/-]*):(minimal|low|medium|high|xhigh|max)\b`) +) diff --git a/update.go b/update.go new file mode 100644 index 0000000..a47a5f8 --- /dev/null +++ b/update.go @@ -0,0 +1,340 @@ +package main + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" + + clikit "github.com/atyrode/cli-kit" + "github.com/charmbracelet/bubbles/spinner" +) + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.w, m.h = msg.Width, msg.Height + m.relayout() + case ompVersionMsg: + if msg.ok { + m.ompMajor, m.ompMinor = msg.major, msg.minor + } + return m, nil + case usageMsg: + scoped, scopedStale := reconcileUsage(m.avail, msg.avail) + refreshAt := time.Now().Add(refreshEvery) + first := !m.hadUsage && msg.avail.ok + m.avail, m.usageStale = scoped, scopedStale + m.hadUsage = m.hadUsage || msg.avail.ok + m.fetching = false + if msg.avail.ok { + saveUsageCache(m.usageCache, scoped) + } + m.nextRefresh = refreshAt + m.relayout() + if first { + // The first real data replaces the skeleton: run the one-time + // bounded bar fill. Refreshes never reach this branch again. + m.barAnim = 1 + return m, barAnimCmd(2) + } + case barAnimMsg: + // Bounded and self-terminating: apply the frame, arm the next tick, + // and stop at the final step (barAnim 0 = inactive, bars at value). + if m.barAnim == 0 { + return m, nil + } + if msg.step >= barAnimSteps { + m.barAnim = 0 + return m, nil + } + m.barAnim = msg.step + return m, barAnimCmd(msg.step + 1) + case refreshTickMsg: + // re-arm the 1s tick; auto-refresh once the interval elapses. + cmds := []tea.Cmd{tickCmd()} + if !m.fetching && !m.nextRefresh.IsZero() && !time.Now().Before(m.nextRefresh) { + cmds = append(cmds, m.startUsageFetch()) + } + return m, tea.Batch(cmds...) + case spinner.TickMsg: + if m.fetching || !m.avail.ok { + var cmd tea.Cmd + m.spin, cmd = m.spin.Update(msg) + return m, cmd + } + case admittedWheelMsg: + m.applyWheelStep(msg.Button) + return m, nil + case tea.MouseMsg: + // Wheel dispatch by pointer position: inside the visible Routing pane + // the viewport owns vertical scrolling — continuous, ungated, clamped + // by the viewport itself, with horizontal wheel deliberately inert. + // Everywhere else direct Update calls apply one generator step; live + // input is coalesced before dispatch by wheelInputFilter. + if msg.Action == tea.MouseActionPress { + if m.wheelInRouting(msg.X, msg.Y) { + switch msg.Button { + case tea.MouseButtonWheelUp: + m.vp.LineDown(1) // inverted: operator-confirmed trackpad direction + case tea.MouseButtonWheelDown: + m.vp.LineUp(1) + } + return m, nil + } + m.applyWheelStep(msg.Button) + } + case tea.KeyMsg: + if m.manager { + return m.updateManager(msg) + } + switch msg.String() { + case "q", "esc", "ctrl+c": + return m, tea.Quit + case "p": + switch { + case m.showUsage && m.sizeMode() == sizeNarrow: + m.showUsage = false + m.showResult = true + m.collapse = false + case m.collapse: + m.collapse = false // restore the hidden preview + case m.mode() == modeCollapsed: + m.showResult = !m.showResult // narrow+short: swap list ↔ result full-screen + default: + m.collapse = true // split/stacked: hide the preview, list full-screen + } + m.relayout() + case "s": + // Toggle the rendered section, not the pre-toggle size class. Usage + // changes the responsive minima, so a layout that fits while it is + // hidden may become narrow when it returns. In that case open the + // dedicated view atomically. It overlays the existing generator / + // Routing state so closing it restores that exact composition. + switch { + case m.showUsage: + m.showUsage = false + m.hideUsage = true + case !m.usageShown(): + m.hideUsage = false + m.showUsage = m.sizeMode() == sizeNarrow + default: + m.showUsage = false + m.hideUsage = true + } + m.relayout() + case "i": + m.fullUsageIDs = !m.fullUsageIDs + m.relayout() + case "?": + m.help.ShowAll = !m.help.ShowAll + m.relayout() // the taller/shorter footer changes the body height + case "d": + m.sel = defaultSel() + if len(m.runtimeTargets) > 0 { + m.sel["runtime"] = "hosted" + } + m.clampSel() // the defaults assume a full catalog; this one may not be + m.persistSelection() + m.syncPreview() + case "f": + m.depth = (m.depth + 1) % 2 + m.syncPreview() + case "r": + if m.broker.URL != "" && !m.fetching { + return m, m.startUsageFetch() + } + case "v": + m.manager = true + m.clampManagerCursor() + m.relayout() + case "up", "k": + m.moveUp() + case "down", "j": + m.moveDown() + case "left", "h": + m.cycleFacet(-1) + case "right", "l": + m.cycleFacet(1) + case "u": + // Untrusted sandbox: hand off to CODE_OMP_UNTRUSTED (ompu), which owns its + // own routing/policy — no generated --config is passed to it. Inert when + // no sandbox binary exists (the help hides the key too), so a stranger + // can't kill the TUI with a stray keypress. + if !m.hasSandbox { + return m, nil + } + m.launchUntrusted = true + return m, tea.Quit + case "pgup", "ctrl+u": + m.vp.HalfViewUp() // scroll the preview (mouse capture is off, see main) + case "pgdown", "ctrl+d": + m.vp.HalfViewDown() + case "m": + // Managed-defaults omp: omp-managed with no generated overlay. The + // explicit keybind keeps every Enter launch a generated profile. + m.launchManaged = true + return m, tea.Quit + case "enter": + if target, local := m.selectedRuntime(); local { + m.launchRuntime = target.Name + return m, tea.Quit + } + // Enter always launches the generated profile for the current facets — + // the untouched default combo is a generated profile like any other. + // Never for a combo the catalog doesn't carry, though: genConfigYAML + // would walk a nil block and emit an overlay whose modelRoles map is + // empty, handing omp a session with no routing at all. The preview + // already says "no profile for this combination", so the key does + // nothing rather than launching something broken. + if _, ok := m.generated[comboID(m.sel, m.hasRelief)]; !ok { + return m, nil + } + m.genConfig = m.genConfigYAML() + return m, tea.Quit + } + + case clikit.ActionsProposedMsg: + // Live preview: snapshot the current selection, then apply the proposal to + // the generator so the user sees the change while deciding. Report the FULL + // applied diff back to the box (the model's picks plus the derived toggles), + // so its "applied" list reflects everything that changed, not just the three + // facets the model named directly. + m.savedSel = map[string]string{} + for k, v := range m.sel { + m.savedSel[k] = v + } + m.applyActions(msg.Actions) + // applyActions' repair rules know lanes and quota, not catalog contents: + // a "critical" proposal switches fable on even where no fable combo was + // generated. Clamp and re-render before reporting what was applied. + m.clampSel() + m.syncPreview() + return m, func() tea.Msg { return clikit.AppliedActionsMsg{Actions: m.appliedDiff()} } + + case clikit.ActionsConfirmedMsg: + // Kept: the preview stays; remember the prompt for the launched session. + m.savedSel = nil + m.firstPrompt = msg.Prompt + m.persistSelection() + + case clikit.ActionsRevertedMsg: + // Rejected: restore the pre-preview selection. + if m.savedSel != nil { + m.sel = m.savedSel + m.savedSel = nil + m.syncPreview() + } + } + return m, nil +} + +// wheelInRouting reports whether a pointer position (terminal cells, 0-based) +// falls inside the visible Routing pane, whose viewport then owns vertical +// wheel scrolling: the wide split's right pane, medium's lower-right pane, or +// the narrow routing-only swap's full body. Hidden/collapsed routing claims +// nothing, so the generator keeps the wheel everywhere else. +func (m model) wheelInRouting(x, y int) bool { + if !m.routingShown() { + return false + } + ch := m.contentH() + switch m.mode() { + case modeCollapsed: // routing-only swap: the whole body is Routing + return y >= topGap && y < topGap+ch + case modeMedium: // the secondary row's right column, under the divider + genH, secH := m.mediumSplit(ch) + secTop := topGap + genH + 1 + return y >= secTop && y < secTop+secH && x >= m.w-m.routingColW() + default: // split: the right pane, from the list's right edge on + return y >= topGap && y < topGap+ch && x >= m.listW() + } +} + +// routingWheelCanMove reports whether a vertical routing scroll would change +// the viewport. No-op events at either clamp are filtered before redraw. +func (m model) routingWheelCanMove(b tea.MouseButton) bool { + switch b { + case tea.MouseButtonWheelUp: + return m.vp.YOffset < m.vp.TotalLineCount()-m.vp.Height + case tea.MouseButtonWheelDown: + return m.vp.YOffset > 0 + default: + return false + } +} + +// applyWheelStep translates one admitted wheel event into the matching facet +// action: vertical scroll moves the selection, horizontal scroll changes the +// value. The raw mapping is INVERTED on both axes — operator-confirmed trackpad +// direction: WheelUp moves the selection down, WheelDown up; WheelLeft cycles +// to the next (right) option, WheelRight to the previous. Arrow keys keep their +// literal semantics. +func (m *model) applyWheelStep(b tea.MouseButton) { + switch b { + case tea.MouseButtonWheelUp: + m.moveDown() + case tea.MouseButtonWheelDown: + m.moveUp() + case tea.MouseButtonWheelLeft: + m.cycleFacet(1) + case tea.MouseButtonWheelRight: + m.cycleFacet(-1) + } +} + +func (m *model) moveUp() { + if m.fcur > 0 { + m.fcur-- + } +} +func (m *model) moveDown() { + if m.fcur < len(m.visibleFacets())-1 { + m.fcur++ + } +} +func (m *model) cycleFacet(dir int) { + vf := m.visibleFacets() + if m.fcur >= len(vf) { + m.fcur = len(vf) - 1 + } + f := vf[m.fcur] + cur := m.sel[f.key] + idx := 0 + for i, v := range f.values { + if v == cur { + idx = i + } + } + next := idx + dir + if next < 0 { + next = 0 + } else if next >= len(f.values) { + next = len(f.values) - 1 + } + if next == idx { + return + } + m.sel[f.key] = f.values[next] + // lead/blend are lane's rendered halves: recompose the canonical value + // before anything re-derives them (visibleFacets syncs from lane). + if f.key == "lead" || f.key == "blend" { + m.sel["lane"] = laneJoin(m.sel["lead"], m.sel["blend"]) + } + // main is fable's sub-setting: whenever fable leaves "on" it must clear too, + // so a later fable re-enable never silently resurrects the (expensive) + // fable-as-main escalation — it is re-chosen deliberately every time. + if m.sel["fable"] != "on" { + m.sel["main"] = "off" + } + // changing the lane can hide/show facets; keep the cursor in range. + if nv := len(m.visibleFacets()); m.fcur >= nv { + m.fcur = nv - 1 + } + m.syncPreview() + m.persistSelection() +} + +// prevPadL is the split preview pane's left padding. Width() counts it, so the +// viewport's usable text width is the box width minus this — the viewport must +// be sized to that inner area (see previewDims), else lines wrap 1:1 with the +// box and their tail overflows to the pane's left edge. diff --git a/usage.go b/usage.go new file mode 100644 index 0000000..dd92da8 --- /dev/null +++ b/usage.go @@ -0,0 +1,1521 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + "unicode" + + "github.com/charmbracelet/lipgloss" +) + +// ── usage + availability ───────────────────────────────────────────────────── +type usageWin struct { + label string + pct int + tier string + secs int64 // seconds until reset (relative) + dur int64 // window length in seconds + prov string + stale bool // retained from the last successful fetch after a refresh omitted this window + missing bool // never observed: rendered as a deterministic placeholder row + observed int64 // Unix timestamp of the last real value; retained across cache fallback +} + +// resetCredits tracks OpenAI reset credits: how many are currently available +// and the seconds until each available credit expires (relative, unsorted). +type resetCredits struct { + avail int + exp []int64 +} + +type availability struct { + bucket map[string]string // bucket -> "ok" | "maxed" | "unauthed" + reset map[string]int64 + wins []usageWin + credits resetCredits + accountCredits map[accountKey]resetCredits + ok bool + accounts map[string][]account + accountUsage map[accountKey][]usageWin + accountsOK bool + selectionApplied bool + accountsStale bool + // deepseek is the DeepSeek prepaid balance: nil when the snapshot carries + // no DeepSeek credential (the group is hidden entirely — an absent API key + // is the normal state, unlike a metered subscription). + deepseek *deepseekBalance +} + +func fetchBrokerUsage(broker brokerConfig) ([]byte, error) { + if broker.URL == "" || broker.Token == "" { + return nil, errors.New("central auth broker is not configured") + } + req, err := http.NewRequest(http.MethodGet, strings.TrimRight(broker.URL, "/")+"/v1/usage", nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+broker.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, resp.Body) + return nil, fmt.Errorf("usage endpoint returned %s", resp.Status) + } + return io.ReadAll(resp.Body) +} + +type usageCacheWin struct { + Label string `json:"label"` + Pct int `json:"pct"` + Tier string `json:"tier,omitempty"` + ResetsAt int64 `json:"resetsAt"` + Dur int64 `json:"dur"` + Provider string `json:"provider"` + Observed int64 `json:"observed"` +} + +type usageCacheAccount struct { + Provider string `json:"provider"` + IdentityKey string `json:"identityKey"` + Wins []usageCacheWin `json:"wins"` +} + +type usageCacheFile struct { + SavedAt int64 `json:"savedAt"` + Accounts map[string][]account `json:"accounts"` + Deepseek *usageCacheBalance `json:"deepseekBalance,omitempty"` + Usage []usageCacheAccount `json:"usage"` +} + +// usageCacheBalance caches the DeepSeek balance VALUE only — never the key. +type usageCacheBalance struct { + Currency string `json:"currency"` + Total string `json:"totalBalance"` + FetchedAt int64 `json:"fetchedAt"` +} + +func emptyAvailability() availability { + return availability{ + bucket: map[string]string{}, reset: map[string]int64{}, + accounts: map[string][]account{}, accountUsage: map[accountKey][]usageWin{}, + accountCredits: map[accountKey]resetCredits{}, + } +} + +// parseAvailability associates broker usage with stable identities from the +// same snapshot. observedAt is the cache observation time; reset countdowns +// remain relative to now because the broker payload stores absolute deadlines. +func parseAvailability(accounts map[string][]account, accountsOK bool, out []byte, observedAt int64) availability { + a := emptyAvailability() + a.accounts, a.accountsOK = accounts, accountsOK + type limit struct { + Label string `json:"label"` + Scope struct { + Tier string `json:"tier"` + } `json:"scope"` + Amount struct { + UsedFraction float64 `json:"usedFraction"` + } `json:"amount"` + Window struct { + ResetsAt int64 `json:"resetsAt"` + DurationMs int64 `json:"durationMs"` + } `json:"window"` + } + var doc struct { + Reports []struct { + Provider string `json:"provider"` + Email string `json:"email"` + AccountID string `json:"accountId"` + Metadata struct { + Email string `json:"email"` + AccountID string `json:"accountId"` + } `json:"metadata"` + Limits []limit `json:"limits"` + ResetCredits struct { + AvailableCount int `json:"availableCount"` + Credits []struct { + ExpiresAt string `json:"expiresAt"` + Status string `json:"status"` + } `json:"credits"` + } `json:"resetCredits"` + } `json:"reports"` + } + if len(out) == 0 || json.Unmarshal(out, &doc) != nil { + return a + } + a.ok = true + provSeen := map[string]bool{} + now := time.Now().Unix() + if observedAt <= 0 { + observedAt = now + } + for _, r := range doc.Reports { + provSeen[r.Provider] = true + reportWins := make([]usageWin, 0, len(r.Limits)) + for _, l := range r.Limits { + pct := int(l.Amount.UsedFraction*100 + 0.5) + win := usageWin{label: l.Label, pct: pct, tier: l.Scope.Tier, + secs: l.Window.ResetsAt/1000 - now, dur: l.Window.DurationMs / 1000, + prov: r.Provider, observed: observedAt} + reportWins = append(reportWins, win) + a.wins = append(a.wins, win) + bkt := bucketForProviderTier(r.Provider, l.Scope.Tier) + if bkt == "" { + continue + } + if pct >= 100 { + a.bucket[bkt] = "maxed" + a.reset[bkt] = l.Window.ResetsAt/1000 - now + } else if a.bucket[bkt] != "maxed" { + a.bucket[bkt] = "ok" + } + } + email, accountID := r.Metadata.Email, r.Metadata.AccountID + if email == "" { + email = r.Email + } + if accountID == "" { + accountID = r.AccountID + } + var matchedKey accountKey + matched := false + for _, acct := range a.accounts[r.Provider] { + if (email != "" && acct.Email != "" && strings.EqualFold(email, acct.Email)) || + (accountID != "" && accountID == acct.IdentityKey) { + matchedKey = accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} + a.accountUsage[matchedKey] = append(a.accountUsage[matchedKey], reportWins...) + matched = true + break + } + } + if r.Provider == openAIProvider { + credits := resetCredits{avail: r.ResetCredits.AvailableCount} + for _, c := range r.ResetCredits.Credits { + if c.Status != "available" { + continue + } + if t, err := time.Parse(time.RFC3339, c.ExpiresAt); err == nil { + credits.exp = append(credits.exp, t.Unix()-now) + } + } + a.credits.avail += credits.avail + a.credits.exp = append(a.credits.exp, credits.exp...) + if matched { + attributed := a.accountCredits[matchedKey] + attributed.avail += credits.avail + attributed.exp = append(attributed.exp, credits.exp...) + a.accountCredits[matchedKey] = attributed + } + } + } + for _, prov := range providerRegistry { + if !prov.Metered { + continue + } + for _, b := range prov.buckets() { + if !provSeen[prov.ID] { + a.bucket[b] = "unauthed" + } else if _, ok := a.bucket[b]; !ok { + a.bucket[b] = "ok" + } + } + } + return a +} + +// loadAvailability reads one central snapshot and one aggregate usage report, +// plus — when the snapshot carries a DeepSeek api_key — the upstream prepaid +// balance, fetched concurrently so neither request delays the other. +func loadAvailability(broker brokerConfig) availability { + accounts, err := loadAccounts(broker) + accountsOK := err == nil + if !accountsOK { + accounts = map[string][]account{} + } + var ds *deepseekBalance + var wg sync.WaitGroup + if key := deepseekAPIKey(accounts); key != "" { + wg.Add(1) + go func() { + defer wg.Done() + bal, err := fetchDeepSeekBalance(key) + if err != nil { + // Degrade to an explicit "unavailable" row; other providers + // are unaffected. + bal = deepseekBalance{fetchedAt: time.Now().Unix()} + } + ds = &bal + }() + } + out, err := fetchBrokerUsage(broker) + if err != nil { + out = nil + } + wg.Wait() + a := parseAvailability(accounts, accountsOK, out, 0) + a.deepseek = ds + return a +} + +func loadUsageCache(path string) availability { + a := emptyAvailability() + if path == "" { + return a + } + body, err := os.ReadFile(path) + if err != nil { + return a + } + var cached usageCacheFile + if json.Unmarshal(body, &cached) != nil || cached.SavedAt <= 0 || len(cached.Usage) == 0 { + return a + } + a.accounts, a.accountsOK, a.ok = cached.Accounts, true, true + provSeen := map[string]bool{} + for _, entry := range cached.Usage { + key := accountKey{Provider: entry.Provider, IdentityKey: entry.IdentityKey} + for _, cachedWin := range entry.Wins { + win := usageWin{ + label: cachedWin.Label, pct: cachedWin.Pct, tier: cachedWin.Tier, + secs: cachedWin.ResetsAt - time.Now().Unix(), dur: cachedWin.Dur, prov: cachedWin.Provider, + observed: cachedWin.Observed, stale: true, + } + a.accountUsage[key] = append(a.accountUsage[key], win) + a.wins = append(a.wins, win) + provSeen[win.prov] = true + bucket := bucketForProviderTier(win.prov, win.tier) + if bucket == "" { + continue + } + if win.pct >= 100 { + a.bucket[bucket], a.reset[bucket] = "maxed", win.secs + } else if a.bucket[bucket] != "maxed" { + a.bucket[bucket] = "ok" + } + } + } + if cached.Deepseek != nil { + a.deepseek = &deepseekBalance{ + ok: true, currency: cached.Deepseek.Currency, total: cached.Deepseek.Total, + fetchedAt: cached.Deepseek.FetchedAt, stale: true, + } + } + for _, prov := range providerRegistry { + if !prov.Metered { + continue + } + for _, bucket := range prov.buckets() { + if !provSeen[prov.ID] { + a.bucket[bucket] = "unauthed" + } else if _, ok := a.bucket[bucket]; !ok { + a.bucket[bucket] = "ok" + } + } + } + return a +} + +func saveUsageCache(path string, a availability) { + if path == "" || !a.ok { + return + } + now := time.Now().Unix() + cached := usageCacheFile{SavedAt: now, Accounts: a.accounts} + if a.deepseek != nil && a.deepseek.ok { + cached.Deepseek = &usageCacheBalance{ + Currency: a.deepseek.currency, Total: a.deepseek.total, FetchedAt: a.deepseek.fetchedAt, + } + } + for key, wins := range a.accountUsage { + entry := usageCacheAccount{Provider: key.Provider, IdentityKey: key.IdentityKey} + for _, win := range wins { + if win.missing { + continue + } + observed := win.observed + if observed <= 0 { + observed = now + } + entry.Wins = append(entry.Wins, usageCacheWin{ + Label: win.label, Pct: win.pct, Tier: win.tier, + ResetsAt: observed + win.secs, Dur: win.dur, + Provider: win.prov, Observed: observed, + }) + } + if len(entry.Wins) > 0 { + cached.Usage = append(cached.Usage, entry) + } + } + if len(cached.Usage) == 0 { + return + } + sort.Slice(cached.Usage, func(i, j int) bool { + if cached.Usage[i].Provider != cached.Usage[j].Provider { + return cached.Usage[i].Provider < cached.Usage[j].Provider + } + return cached.Usage[i].IdentityKey < cached.Usage[j].IdentityKey + }) + body, err := json.Marshal(cached) + if err != nil { + return + } + body = append(body, '\n') + _ = atomicPrivateWrite(path, body) +} + +// bucketForProviderTier maps a usage report's (provider, tier) scope onto the +// quota bucket it constrains: the provider's main window, or a special tier's +// dedicated window. Unmetered and unknown providers own no buckets. +func bucketForProviderTier(prov, tier string) string { + p := providerByID(prov) + if p == nil || !p.Metered { + return "" + } + if tier == "" || tier == "-" { + return p.mainBucket() + } + for _, s := range p.Special { + if s.Bucket == tier { + return p.BucketBase + "-" + s.Bucket + } + } + return "" +} + +func (a availability) down(bucket string) bool { + return a.bucket[bucket] == "maxed" || a.bucket[bucket] == "unauthed" +} + +// reconcileUsage folds a freshly fetched availability over the one currently +// shown, so a flaky upstream never wipes known-good data. It returns the +// availability to display plus whether the whole panel is stale: +// +// - a total fetch failure after any prior success keeps the previous +// availability wholesale and reports it stale — the control row shows a +// refresh-failed warning instead of dropping to the unauthenticated error; +// - a successful payload that omits an account's usage retains that +// account's last observed rows, visibly marked stale with their age; +// - a successful Anthropic payload that only omits the flaky Fable window +// retains the last Fable row, along with its bucket/reset routing state; +// - a successful Anthropic payload with no Fable window ever observed +// appends a deterministic unavailable placeholder, so the datum appearing +// on a later refresh never pops the panel geometry. +// +// Fresh values always win; nothing is fabricated — retained rows are visibly +// marked stale and placeholders carry no numbers. +func reconcileUsage(prev, next availability) (availability, bool) { + if !next.ok { + if prev.ok { + return prev, true + } + return next, false + } + if !next.accountsOK && prev.accountsOK { + next.accounts, next.accountsOK, next.accountsStale = prev.accounts, true, true + } + next.accountUsage = reconcileAccountUsage(prev.accountUsage, next.accountUsage, next.accounts) + if next.deepseek != nil && !next.deepseek.ok && prev.deepseek != nil && prev.deepseek.ok { + // A failed balance refresh keeps the last known value, visibly stale — + // same retention contract as the metered windows. + retained := *prev.deepseek + retained.stale = true + next.deepseek = &retained + } + hasClaude, hasFable := false, false + for _, w := range next.wins { + if w.prov != anthropicProvider { + continue + } + hasClaude = true + if w.tier == "fable" { + hasFable = true + } + } + if !hasClaude || hasFable { + return next, false + } + for _, w := range prev.wins { + if w.prov == anthropicProvider && w.tier == "fable" && !w.missing { + w.stale = true + next.wins = append(next.wins, w) + // Carry the bucket/reset state observed with the retained window: + // loadAvailability defaults an unseen bucket to "ok", which would + // route onto a fable the last real datum said was maxed. + if st, ok := prev.bucket["claude-fable"]; ok { + next.bucket["claude-fable"] = st + } + if r, ok := prev.reset["claude-fable"]; ok { + next.reset["claude-fable"] = r + } + return next, false + } + } + next.wins = append(next.wins, fablePlaceholder) + return next, false +} + +func reconcileAccountUsage(prev, next map[accountKey][]usageWin, accounts map[string][]account) map[accountKey][]usageWin { + if next == nil { + next = map[accountKey][]usageWin{} + } + active := map[accountKey]bool{} + for _, providerAccounts := range accounts { + for _, acct := range providerAccounts { + active[accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey}] = true + } + } + if len(active) == 0 { + for key := range next { + active[key] = true + } + } + for key := range active { + wins := next[key] + hasFresh := false + for _, w := range wins { + if !w.missing { + hasFresh = true + break + } + } + if !hasFresh { + retained := make([]usageWin, 0, len(prev[key])) + for _, w := range prev[key] { + if w.missing { + continue + } + w.stale = true + retained = append(retained, w) + } + if len(retained) > 0 { + next[key] = retained + } + continue + } + if key.Provider != anthropicProvider { + continue + } + hasClaude, hasFable := false, false + for _, w := range wins { + if w.prov != anthropicProvider { + continue + } + if !w.missing { + hasClaude = true + } + if w.tier == "fable" { + hasFable = true + } + } + if !hasClaude || hasFable { + continue + } + retained := false + for _, w := range prev[key] { + if w.prov == anthropicProvider && w.tier == "fable" && !w.missing { + w.stale = true + next[key] = append(next[key], w) + retained = true + break + } + } + if !retained { + next[key] = append(next[key], fablePlaceholder) + } + } + return next +} + +// fablePlaceholder is the never-observed fable window's deterministic +// stand-in: the real payload label (so shortWin renders the same "7d fable" +// tag) and window length, with no usage numbers to fabricate. +var fablePlaceholder = usageWin{label: "Claude 7 Day (Fable)", tier: "fable", dur: 7 * 24 * 3600, prov: anthropicProvider, missing: true} + +type usageGroupKey struct { + prov string + tier string + dur int64 + label string +} + +type usageGroup struct { + win usageWin + count int64 + pctSum int64 + secsSum int64 + observed int64 +} + +func knownUsageWindow(w usageWin) bool { + label := shortWin(w.label) + bucket := bucketForProviderTier(w.prov, w.tier) + p := providerByID(w.prov) + if bucket == "" || p == nil { + return false + } + if bucket == p.mainBucket() { + return label == "5h" || label == "7d" + } + suffix := strings.TrimPrefix(bucket, p.BucketBase+"-") + return label == "5h "+suffix || label == "7d "+suffix +} + +// selectedAvailability derives account-sensitive usage and routing availability +// solely from enabled broker identities. Unmatched reports never enter this seam. +func selectedAvailability(a availability, disabled map[accountKey]bool) availability { + selected := a + selected.selectionApplied = true + selected.accounts = map[string][]account{} + selected.accountUsage = map[accountKey][]usageWin{} + selected.accountCredits = map[accountKey]resetCredits{} + selected.bucket = map[string]string{} + selected.reset = map[string]int64{} + selected.wins = nil + selected.credits = resetCredits{} + + enabledProviders := map[string]bool{} + groups := map[usageGroupKey]*usageGroup{} + missing := map[usageGroupKey]usageWin{} + var groupOrder []usageGroupKey + for prov, accounts := range a.accounts { + for _, acct := range accounts { + key := accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} + if disabled[key] { + continue + } + selected.accounts[prov] = append(selected.accounts[prov], acct) + enabledProviders[acct.Provider] = true + wins := a.accountUsage[key] + if credits, ok := a.accountCredits[key]; ok { + selected.accountCredits[key] = credits + selected.credits.avail += credits.avail + selected.credits.exp = append(selected.credits.exp, credits.exp...) + } + for _, win := range wins { + if win.prov != acct.Provider || !knownUsageWindow(win) { + continue + } + selected.accountUsage[key] = append(selected.accountUsage[key], win) + groupKey := usageGroupKey{prov: win.prov, tier: win.tier, dur: win.dur, label: shortWin(win.label)} + if win.missing { + if _, ok := missing[groupKey]; !ok { + placeholder := win + placeholder.label = groupKey.label + missing[groupKey] = placeholder + groupOrder = append(groupOrder, groupKey) + } + continue + } + group := groups[groupKey] + if group == nil { + aggregate := win + aggregate.label = groupKey.label + group = &usageGroup{win: aggregate} + groups[groupKey] = group + groupOrder = append(groupOrder, groupKey) + } + pct, secs := int64(win.pct), win.secs + if pct < 0 { + pct = 0 + } + if secs < 0 { + secs = 0 + } + group.count++ + group.pctSum += pct + group.secsSum += secs + group.win.stale = group.win.stale || win.stale + if win.observed > 0 && (group.observed == 0 || win.observed < group.observed) { + group.observed = win.observed + } + } + } + } + seen := map[usageGroupKey]bool{} + for _, key := range groupOrder { + if seen[key] { + continue + } + seen[key] = true + if group := groups[key]; group != nil { + group.win.pct = int((group.pctSum + group.count/2) / group.count) + group.win.secs = (group.secsSum + group.count/2) / group.count + group.win.observed = group.observed + selected.wins = append(selected.wins, group.win) + } else { + selected.wins = append(selected.wins, missing[key]) + } + } + for _, prov := range providerRegistry { + if !prov.Metered { + continue + } + for _, bucket := range prov.buckets() { + if enabledProviders[prov.ID] { + selected.bucket[bucket] = "ok" + } else { + selected.bucket[bucket] = "unauthed" + } + } + } + for _, win := range selected.wins { + bucket := bucketForProviderTier(win.prov, win.tier) + if bucket == "" || win.missing || win.pct < 100 { + continue + } + selected.bucket[bucket] = "maxed" + // Multiple quota windows can constrain one route. The route becomes + // usable only after the last maxed aggregate resets, so retain the + // longest selected reset rather than whichever account/map came last. + if win.secs > selected.reset[bucket] { + selected.reset[bucket] = win.secs + } + } + return selected +} + +const usageBarNaturalW = 10 + +func barStr(p, width int) string { + if width < 0 { + width = 0 + } + var r, g float64 + if p <= 50 { + r, g = 90+float64(p)*3, 200 + } else { + r, g = 235, 200-float64(p-50)*3 + } + if r > 235 { + r = 235 + } + if g < 60 { + g = 60 + } + fill := (p*width + 50) / 100 + if fill > width { + fill = width + } + if fill < 0 { + fill = 0 + } + filled := lipgloss.NewStyle().Foreground(lipgloss.Color(fmt.Sprintf("#%02x%02x46", clampByte(r), clampByte(g)))).Render(strings.Repeat("█", fill)) + return filled + stDim.Render(strings.Repeat("░", width-fill)) +} + +func fmtReset(s int64) string { + if s < 0 { + s = 0 + } + switch { + case s >= 86400: + return fmt.Sprintf("%dd%dh", s/86400, (s%86400)/3600) + case s >= 3600: + return fmt.Sprintf("%dh%dm", s/3600, (s%3600)/60) + } + return fmt.Sprintf("%dm", s/60) +} + +func shortWin(l string) string { + switch l { + case "5 hours", "Claude 5 Hour", "Codex 5 Hour", "OpenAI 5 Hour": + return "5h" + case "7 days", "Claude 7 Day", "Codex 7 Day", "OpenAI 7 Day": + return "7d" + case "5 hours (Spark)", "Codex 5 Hour (Spark)", "OpenAI 5 Hour (Spark)": + return "5h spark" + case "7 days (Spark)", "Codex 7 Day (Spark)", "OpenAI 7 Day (Spark)": + return "7d spark" + case "Claude 7 Day (Fable)": + return "7d fable" + } + return l +} + +// usageCtrlLine is the Usage chrome's bottom action row: central refresh state, +// account-manager access, and any account persistence error. +func (m *model) usageCtrlLine() string { + var parts []string + if m.broker.URL != "" { + switch { + case !m.avail.ok && (m.fetching || m.nextRefresh.IsZero()): + parts = append(parts, stDim.Render(m.spin.View()+" fetching usage…")) + case m.fetching: + parts = append(parts, stWarn.Render(gReset+" refreshing…")) + case m.usageStale: + // A failed refresh kept the previous data on screen: the warning + // takes the countdown's slot (same row, similar width) so the + // measured panel geometry — and with it the medium/collapsed + // breakpoint — barely moves on a flaky refresh. + parts = append(parts, stWarn.Render("refresh failed · stale")+ + stDim.Render(" · ")+stKey.Render("r")+stDim.Render(" retry")) + default: + rem := time.Until(m.nextRefresh) + if rem < 0 { + rem = 0 + } + s := int(rem.Seconds()) + parts = append(parts, + stDim.Render(fmt.Sprintf("next refresh %d:%02d · ", s/60, s%60))+ + stKey.Render("r")+stDim.Render(" now")) + } + } + identityAction := "full ids" + if m.fullUsageIDs { + identityAction = "short ids" + } + parts = append(parts, stKey.Render("i")+stDim.Render(" "+identityAction)) + parts = append(parts, stKey.Render("v")+stDim.Render(" accounts")) + if len(parts) == 0 { + return "" + } + line := " " + strings.Join(parts, stDim.Render(" · ")) + if m.accountErr != "" { + line += "\n" + stBrk.Render(" account update failed: "+m.accountErr) + } + return line +} + +// compactDisplayIdentity produces a deliberately lossy display label. Email +// matching continues to use the untouched broker identity; this helper is only +// for the compact Usage heading. +func compactDisplayIdentity(identity string) string { + normalized := strings.ToLower(strings.TrimSpace(identity)) + at := strings.IndexByte(normalized, '@') + if at > 0 && at == strings.LastIndexByte(normalized, '@') { + local, domain := normalized[:at], normalized[at+1:] + dot := strings.LastIndexByte(domain, '.') + valid := dot > 0 && dot < len(domain)-1 && + !strings.HasPrefix(local, ".") && !strings.HasSuffix(local, ".") && + !strings.Contains(local, "..") && !strings.Contains(domain, "..") && + strings.IndexFunc(normalized, func(r rune) bool { + return unicode.IsSpace(r) || unicode.IsControl(r) + }) < 0 + if valid { + localRunes := []rune(local) + if len(localRunes) > 2 { + localRunes = localRunes[:2] + } + return string(localRunes) + "*" + } + } + if normalized == "" { + return "id unavailable" + } + runes := []rune(normalized) + if len(runes) > 2 { + runes = runes[:2] + } + return string(runes) + "*" +} + +func usageDisplayIdentity(identity string, full bool) string { + if full { + if identity = strings.TrimSpace(identity); identity != "" { + return identity + } + return "id unavailable" + } + return compactDisplayIdentity(identity) +} + +type compactProviderIdentity struct { + label string + reporting bool +} + +// providerIdentities preserves broker snapshot order and collapses repeated +// copies of the same stable account. Compact ambiguity is intentional; pressing +// i reveals full addresses when disambiguation matters. +func providerIdentities(a availability, prov string, full bool) []compactProviderIdentity { + accounts := a.accounts[prov] + identities := make([]compactProviderIdentity, 0, len(accounts)) + seenAccounts := map[string]bool{} + for _, acct := range accounts { + stableID := acct.IdentityKey + if stableID == "" { + stableID = acct.Email + } + if stableID != "" { + stableID = acct.Provider + "\x00" + stableID + if seenAccounts[stableID] { + continue + } + seenAccounts[stableID] = true + } + + identity := acct.Email + if identity == "" { + identity = acct.IdentityKey + } + label := usageDisplayIdentity(identity, full) + + reporting := false + for _, win := range a.accountUsage[accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey}] { + if !win.missing { + reporting = true + break + } + } + identities = append(identities, compactProviderIdentity{ + label: label, reporting: reporting, + }) + } + return identities +} + +// providerHeading keeps the provider's established color and puts compact, +// enabled snapshot identities in a dim parenthetical suffix. +func providerHeading(prov string, identities []compactProviderIdentity) string { + col, name := "#8a93a6", prov + if p := providerByID(prov); p != nil { + col, name = p.Color, p.Label + } + heading := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true).Render(name) + if len(identities) == 0 { + return heading + } + labels := make([]string, 0, len(identities)) + for _, identity := range identities { + labels = append(labels, identity.label) + } + return heading + " " + stDim.Render("("+strings.Join(labels, " + ")+")") +} + +// providerIdentityBlockFor keeps missing usage explicit without spending a +// separate row on accounts already represented by aggregate usage bars. +func providerIdentityBlockFor(a availability, prov string, checking, full bool) []string { + identities := providerIdentities(a, prov, full) + if checking && len(identities) == 0 { + identities = []compactProviderIdentity{{label: "checking account…"}} + } + rows := []string{padLeft(providerHeading(prov, identities), gut)} + if checking { + return rows + } + if !a.accountsOK { + return append(rows, stWarn.Render(" account status unavailable")) + } + if len(identities) == 0 { + if a.selectionApplied { + return append(rows, stDim.Render(" no enabled accounts")) + } + return append(rows, stBrk.Render(" not authenticated")) + } + unavailable := 0 + for _, identity := range identities { + if !identity.reporting { + unavailable++ + } + } + if unavailable > 0 { + status := " usage unavailable" + if len(identities) > 1 { + noun := "account" + if unavailable > 1 { + noun = "accounts" + } + status += fmt.Sprintf(" for %d %s", unavailable, noun) + } + rows = append(rows, stWarn.Render(status)) + } + if a.accountsStale { + rows = append(rows, stWarn.Render(" identity cached")) + } + return rows +} + +func providerIdentityBlock(a availability, prov string, checking bool) []string { + return providerIdentityBlockFor(a, prov, checking, false) +} + +// identityLines keeps provider and broker-reported account state visible even +// when no usage rows exist. +func identityLinesFor(a availability) string { + var lines []string + for i, prov := range meteredProviderIDs() { + if i > 0 { + lines = append(lines, "") + } + lines = append(lines, providerIdentityBlock(a, prov, false)...) + } + return strings.Join(lines, "\n") +} + +func (m *model) selectedLaunchAvailability() availability { + return selectedAvailability(m.avail, m.accountSelections.CurrentDisabled()) +} + +func (m *model) selectedUsageAvailability() availability { + disabled := m.accountSelections.CurrentDisabled() + if m.manager { + disabled = m.managerDisplayedDisabled() + } + return selectedAvailability(m.avail, disabled) +} + +func (m *model) identityLines() string { + return identityLinesFor(m.selectedUsageAvailability()) +} + +// usagePanel is the composition-agnostic Usage band sized for the current +// terminal width — the wide layout's full-width footer form. +func (m *model) usagePanel() string { return m.usagePanelFor(m.w) } + +// usagePanelFor renders central account usage with local visibility and account +// manager cues. There is no selectable vault or profile identity. +func (m *model) usagePanelFor(w int) string { + return m.usagePanelLayout(w, false) +} + +func (m *model) usagePanelStackedFor(w int) string { + return m.usagePanelLayout(w, true) +} + +func (m *model) usagePanelLayout(w int, stacked bool) string { + title := m.pill("usage") + title += " " + stCueKey.Render("s") + stCue.Render(" · hide") + innerWidth := max(0, w-gut) + out := padLeft(title, gut) + "\n" + + "\n" + m.usageBodyLayout(innerWidth, stacked) + if ctrl := m.usageCtrlLine(); ctrl != "" && !m.manager { + out += "\n\n" + ctrl // blank row: air between provider content and the control row + } + return out +} + +// usageRenderGroup keeps provider chrome and usage rows separate until layout +// has assigned the provider its real display width. That is the composition seam +// which lets both stacked and side-by-side layouts grow bars without changing +// headings, notes, or the canonical row grammar. +type usageRenderGroup struct { + prefix []string + rows []usageRowSpec + suffix []string +} + +func (g usageRenderGroup) linesWithUsageLayout(barWidth, noteWidth, prefixHeight int) []string { + lines := make([]string, 0, max(len(g.prefix), prefixHeight)+len(g.rows)+len(g.suffix)) + lines = append(lines, g.prefix...) + for len(lines) < prefixHeight { + lines = append(lines, "") + } + for _, row := range g.rows { + lines = append(lines, row.render(barWidth, noteWidth)) + } + lines = append(lines, g.suffix...) + return lines +} + +func usageRenderWidth(lines []string) int { + width := 0 + for _, line := range lines { + if lineWidth := lipgloss.Width(line); lineWidth > width { + width = lineWidth + } + } + return width +} + +// usageBodyFor renders the provider/account content between the pinned title +// and the bottom control row: the identity-headed usage groups, or the +// loading/unavailable identity block. +func (m *model) usageBodyFor(w int) string { + return m.usageBodyLayout(w, false) +} + +func (m *model) usageBodyLayout(w int, stacked bool) string { + a := m.selectedUsageAvailability() + if !a.ok { + if m.usageLoading() { + return m.skeletonBodyLayout(w, stacked) + } + out := identityLinesFor(a) + if m.broker.URL != "" && !m.fetching && !m.nextRefresh.IsZero() { + out += "\n" + stWarn.Render(" usage unavailable · press v to manage accounts") + } + return out + } + if len(a.wins) == 0 { + return identityLinesFor(a) + "\n" + + stWarn.Render(" no enabled provider usage · press v to manage accounts") + } + wins := append([]usageWin(nil), a.wins...) + provOrder := func(p string) int { + for i := range providerRegistry { + if providerRegistry[i].ID == p { + return i + } + } + return len(providerRegistry) + } + tierOrder := func(t string) int { + if t == "" || t == "-" { + return 0 + } + return 1 + } + sort.SliceStable(wins, func(i, j int) bool { + if o := provOrder(wins[i].prov) - provOrder(wins[j].prov); o != 0 { + return o < 0 + } + if o := tierOrder(wins[i].tier) - tierOrder(wins[j].tier); o != 0 { + return o < 0 + } + return wins[i].dur < wins[j].dur + }) + blocks := map[string]usageRenderGroup{} + var order []string + for _, prov := range meteredProviderIDs() { + if len(a.accounts[prov]) > 0 { + order = append(order, prov) + blocks[prov] = usageRenderGroup{prefix: providerIdentityBlockFor(a, prov, false, m.fullUsageIDs)} + } + } + for _, win := range wins { + block, ok := blocks[win.prov] + if !ok { + order = append(order, win.prov) + block.prefix = providerIdentityBlockFor(a, win.prov, false, m.fullUsageIDs) + } + block.rows = append(block.rows, m.usageRowSpec(win, " ")) + blocks[win.prov] = block + } + if block, ok := blocks[openAIProvider]; ok { + for _, acct := range a.accounts[openAIProvider] { + key := accountKey{Provider: acct.Provider, IdentityKey: acct.IdentityKey} + identity := usageDisplayIdentity(managerAccountLabel(acct), m.fullUsageIDs) + if cl := creditLineForAccount(identity, a.accountCredits[key]); cl != "" { + block.suffix = append(block.suffix, cl) + } + } + blocks[openAIProvider] = block + } + if a.deepseek != nil { + order = append(order, deepseekProvider) + blocks[deepseekProvider] = usageRenderGroup{ + prefix: []string{padLeft(providerHeading(deepseekProvider, nil), gut)}, + suffix: []string{m.deepseekBalanceRow(*a.deepseek)}, + } + } + return layoutGroups(w, order, blocks, stacked) +} + +// layoutGroups assigns provider columns before rendering any usage row. +// Side-by-side groups share the whole panel width; stacked groups each receive +// the whole section width. A zero width is the non-recursive measurement path +// and therefore retains natural ten-cell bars. +func layoutGroups(w int, order []string, blocks map[string]usageRenderGroup, stacked bool) string { + allRows := make([]usageRowSpec, 0) + for _, prov := range order { + allRows = append(allRows, blocks[prov].rows...) + } + naturalBarWidth, noteWidth := usageRowsLayout(0, allRows) + naturalColW := lipgloss.Width(skeletonRow("7d fable")) + 2 + for _, prov := range order { + blockW := usageRenderWidth(blocks[prov].linesWithUsageLayout(naturalBarWidth, noteWidth, 0)) + 2 + if blockW > naturalColW { + naturalColW = blockW + } + } + sideBySide := !stacked && w > 0 && len(order) > 1 && w >= naturalColW*len(order) + layoutWidth := w + if sideBySide { + for i := range order { + colW := w*(i+1)/len(order) - w*i/len(order) + if i == 0 || colW < layoutWidth { + layoutWidth = colW + } + } + } + barWidth, noteWidth := usageRowsLayout(layoutWidth, allRows) + if sideBySide { + prefixHeight := 0 + for _, prov := range order { + if len(blocks[prov].prefix) > prefixHeight { + prefixHeight = len(blocks[prov].prefix) + } + } + cols := make([]string, 0, len(order)) + for i, prov := range order { + colW := w*(i+1)/len(order) - w*i/len(order) + content := strings.Join(blocks[prov].linesWithUsageLayout(barWidth, noteWidth, prefixHeight), "\n") + cols = append(cols, lipgloss.NewStyle().Width(colW).Render(content)) + } + return lipgloss.JoinHorizontal(lipgloss.Top, cols...) + } + var lines []string + for i, prov := range order { + if i > 0 { + lines = append(lines, "") + } + lines = append(lines, blocks[prov].linesWithUsageLayout(barWidth, noteWidth, 0)...) + } + return strings.Join(lines, "\n") +} + +// usageLoading reports the initial central fetch window where the layout-stable +// skeleton replaces the not-yet-known account and usage rows. +func (m *model) usageLoading() bool { + return m.broker.URL != "" && !m.avail.ok && (m.fetching || m.nextRefresh.IsZero()) +} + +// skeletonWinsByProvider mirrors each provider's stable usage shape. Anthropic +// always reserves the Fable row, even before a first successful fetch. +var skeletonWinsByProvider = func() map[string][]string { + wins := map[string][]string{} + for _, p := range providerRegistry { + if p.Metered { + wins[p.ID] = p.SkeletonWins + } + } + return wins +}() + +// usageRowSpec is the unrendered canonical Usage-row grammar. Every caller +// supplies its indentation and group width, while this one seam reserves the +// label, percentage, reset, and actual note before assigning all safe cells to +// the bar. +type usageRowSpec struct { + indent string + label string + barPct int + percentage string + reset string + note string + reserveNote string +} + +func usageRowsNoteWidth(rows []usageRowSpec) int { + width := 0 + for _, row := range rows { + for _, note := range []string{row.note, row.reserveNote} { + if noteWidth := lipgloss.Width(note); noteWidth > width { + width = noteWidth + } + } + } + return width +} + +const usageResetValueWidth = 6 + +func paddedUsageReset(reset string) string { + width := lipgloss.Width(gReset) + 1 + usageResetValueWidth + return reset + strings.Repeat(" ", max(0, width-lipgloss.Width(reset))) +} + +func (r usageRowSpec) render(barWidth, noteWidth int) string { + note := "" + if r.note != "" { + note = " " + r.note + strings.Repeat(" ", max(0, noteWidth-lipgloss.Width(r.note))) + } else if noteWidth > 0 { + note = strings.Repeat(" ", 2+noteWidth) + } + return fmt.Sprintf("%s%-9s %s %s used %s%s", + r.indent, r.label, barStr(r.barPct, barWidth), r.percentage, paddedUsageReset(r.reset), note) +} + +func (r usageRowSpec) reservedWidth(noteWidth int) int { + return lipgloss.Width(r.render(0, noteWidth)) +} + +func usageRowsLayout(width int, rows []usageRowSpec) (barWidth, noteWidth int) { + noteWidth = usageRowsNoteWidth(rows) + if width == 0 { + return usageBarNaturalW, noteWidth + } + reserved := 0 + for _, row := range rows { + if rowW := row.reservedWidth(noteWidth); rowW > reserved { + reserved = rowW + } + } + return max(0, width-reserved), noteWidth +} + +func usageRowsBarWidth(width int, rows []usageRowSpec) int { + barWidth, _ := usageRowsLayout(width, rows) + return barWidth +} + +func renderUsageRows(width int, rows []usageRowSpec) []string { + barWidth, noteWidth := usageRowsLayout(width, rows) + rendered := make([]string, len(rows)) + for i, row := range rows { + rendered[i] = row.render(barWidth, noteWidth) + } + return rendered +} + +func skeletonUsageRowSpec(label, indent string) usageRowSpec { + return usageRowSpec{ + indent: indent, label: label, + percentage: stDim.Render(" ··%"), + reset: stDim.Render(gReset + " ····"), + reserveNote: "unavailable", + } +} + +// skeletonRow is the natural-width placeholder row used by measurement and +// focused tests. Real layout goes through renderUsageRows with its group width. +func skeletonRow(label string) string { + return renderUsageRows(0, []usageRowSpec{skeletonUsageRowSpec(label, " ")})[0] +} + +// skeletonBody is the pre-first-fetch Usage content: provider headings, +// explicit checking state, and generic placeholder window rows, laid out by +// the same group logic as real data so the first result lands predictably. +func (m *model) skeletonBody(w int) string { + return m.skeletonBodyLayout(w, false) +} + +func (m *model) skeletonBodyLayout(w int, stacked bool) string { + a := m.selectedUsageAvailability() + order := meteredProviderIDs() + blocks := map[string]usageRenderGroup{} + for _, prov := range order { + block := usageRenderGroup{prefix: providerIdentityBlockFor(a, prov, true, m.fullUsageIDs)} + for _, label := range skeletonWinsByProvider[prov] { + block.rows = append(block.rows, skeletonUsageRowSpec(label, " ")) + } + blocks[prov] = block + } + return layoutGroups(w, order, blocks, stacked) +} + +// usageColumn is the medium layout's left-hand Usage section: the panel with +// provider groups forced into a vertical stack — the column is deliberately +// too narrow for side-by-side groups. The panel carries its own title chrome. +func (m model) usageColumn() string { + return m.usagePanelStackedFor(0) +} + +// usageColW is the medium usage column's measured width: the widest rendered +// line of the stacked panel (title, controls, bars, notes) — measured, not guessed. +func (m model) usageColW() int { + return lipgloss.Width(m.usageColumn()) +} + +// secondaryMinH is the medium secondary row's minimum height: routing's pinned +// chrome plus a few useful route rows, or the full stacked usage column when +// that is taller — medium only engages when neither column needs clipping. +func (m model) secondaryMinH() int { + h := prevChromeRows + minRouteRows + if m.hideUsage { + return h + } + if u := lipgloss.Height(m.usageColumn()); u > h { + h = u + } + return h +} + +func formatCachedAge(observed int64, now time.Time) string { + seconds := now.Unix() - observed + if seconds < 0 { + seconds = 0 + } + switch { + case seconds < 60: + return "<1m ago" + case seconds < 60*60: + return fmt.Sprintf("%dm ago", seconds/60) + case seconds < 24*60*60: + return fmt.Sprintf("%dh ago", seconds/(60*60)) + case seconds < 7*24*60*60: + return fmt.Sprintf("%dd ago", seconds/(24*60*60)) + case seconds < 365*24*60*60: + return fmt.Sprintf("%dw ago", seconds/(7*24*60*60)) + default: + return fmt.Sprintf("%dy ago", seconds/(365*24*60*60)) + } +} + +// deepseekBalanceRow renders the DeepSeek group's single body row: the prepaid +// balance — no bar, no window, no reset countdown, because none exist. A +// balance under the suggestion floor carries an explicit "low" cue (the same +// threshold that stops proposals from spending the pool), and the off-peak +// discount window is surfaced while it is live. +func (m *model) deepseekBalanceRow(b deepseekBalance) string { + if !b.ok { + return " " + stWarn.Render("balance unavailable") + } + row := " balance " + stHead.Render("$"+b.total+" "+b.currency) + stDim.Render(" · pay-as-you-go") + if v, err := strconv.ParseFloat(b.total, 64); err == nil && v < deepseekLowBalanceUSD { + row += " " + stWarn.Render("low") + } + if deepseekOffPeak(offPeakNow().UTC()) { + row += " " + stDim.Render("off-peak −50%") + } + if b.stale { + cached := "cached" + if b.fetchedAt > 0 { + cached += " " + formatCachedAge(b.fetchedAt, time.Now()) + } + row += " " + stWarn.Render(cached) + } + return row +} +func (m *model) usageRowSpec(w usageWin, indent string) usageRowSpec { + if w.missing { + // Never-observed windows keep the exact row grammar with dotted values; + // only the status text differs from the loading skeleton. + row := skeletonUsageRowSpec(shortWin(w.label), indent) + row.note = stDim.Render("unavailable") + return row + } + note := "" + if w.pct >= 80 { + note = stWarn.Render("tight") + } + if w.pct >= 100 { + note = stBrk.Render("maxed") + } + if w.tier == "spark" && w.pct == 0 { + note = lipgloss.NewStyle().Foreground(lipgloss.Color(cGreen)).Render("idle") + } + if w.stale { + // Retained after a refresh omitted this window; show its age explicitly. + cached := "cached" + if w.observed > 0 { + cached += " " + formatCachedAge(w.observed, time.Now()) + } + if note != "" { + note += " " + } + note += stWarn.Render(cached) + } + resetText := gReset + " " + pad(fmtReset(w.secs), 4) + reset := stDim.Render(resetText) + if w.dur > 0 && w.secs*10 < w.dur { + reset = lipgloss.NewStyle().Foreground(lipgloss.Color("#c8d0dc")).Bold(true).Render(resetText) + } else if w.dur > 0 && w.secs*4 < w.dur { + reset = lipgloss.NewStyle().Foreground(lipgloss.Color("#c8d0dc")).Render(resetText) + } + // During the one-time first-load fill only the bar is scaled toward its + // target; the label, percentage, reset, and note are real from frame one. + barPct := w.pct + if m.barAnim > 0 { + barPct = barPct * m.barAnim / barAnimSteps + } + return usageRowSpec{ + indent: indent, label: shortWin(w.label), barPct: barPct, + percentage: fmt.Sprintf("%3d%%", w.pct), reset: reset, note: note, + } +} + +// usageRow is the natural-width measurement/test path. Composed panels and +// manager account groups render the same spec through renderUsageRows using +// their actual assigned width. +func (m *model) usageRow(w usageWin) string { + return renderUsageRows(0, []usageRowSpec{m.usageRowSpec(w, " ")})[0] +} + +// Reset-credit expiry urgency tints: each individual expiry (`3d`, `12d`, …) +// in the credit line is colored on a muted red→amber→green ramp so soon +// expiries read as warnings and distant ones as headroom, while the icon, +// count, and connecting prose stay dim. The palette is precomputed and +// deliberately desaturated (no per-frame color math, no saturated alarm +// colors inside a dim summary row); the day text itself stays sufficient +// without color. Thresholds are whole days remaining, exactly as fmtDays +// rounds them (up, so later-today = 1): ≤ creditUrgentDays is muted red, +// ≤ creditSoonDays muted amber, anything later muted green. +const ( + creditUrgentDays = 3 // expiring within three days — spend it or lose it + creditSoonDays = 10 // within ten days — plan around it +) + +var ( + stCreditUrgent = lipgloss.NewStyle().Foreground(lipgloss.Color("#b0716f")) // muted red + stCreditSoon = lipgloss.NewStyle().Foreground(lipgloss.Color("#b39c6b")) // muted amber + stCreditSafe = lipgloss.NewStyle().Foreground(lipgloss.Color("#85a883")) // muted green +) + +// creditDayStyle picks the urgency tint for a credit expiring in s seconds, +// bucketing on the same rounded-up whole days fmtDays renders — the color and +// the text can never disagree about which side of a threshold an expiry is on. +func creditDayStyle(s int64) lipgloss.Style { + d := int64(0) + if s > 0 { + d = (s + 86399) / 86400 + } + switch { + case d <= creditUrgentDays: + return stCreditUrgent + case d <= creditSoonDays: + return stCreditSoon + default: + return stCreditSafe + } +} + +// creditSummary renders the OpenAI reset-credit summary: the available count +// and the days remaining until the three soonest credit expirations, ascending. +// Callers own indentation and any account identity prefix. +func creditSummary(c resetCredits) string { + if c.avail == 0 && len(c.exp) == 0 { + return "" + } + exp := append([]int64(nil), c.exp...) + sort.Slice(exp, func(i, j int) bool { return exp[i] < exp[j] }) + if len(exp) > 3 { + exp = exp[:3] + } + noun := "resets" + if c.avail == 1 { + noun = "reset" + } + line := stDim.Render(gReset + " " + fmt.Sprintf("%d %s", c.avail, noun)) + if len(exp) > 0 { + days := make([]string, len(exp)) + for i, s := range exp { + days[i] = creditDayStyle(s).Render(fmtDays(s)) + } + line += stDim.Render(" · expiring in ") + strings.Join(days, stDim.Render(", ")) + } + return line +} + +func creditLineFor(c resetCredits) string { + if summary := creditSummary(c); summary != "" { + return " " + summary + } + return "" +} + +func creditLineForAccount(identity string, c resetCredits) string { + if summary := creditSummary(c); summary != "" { + return " " + stDim.Render(identity+" · ") + summary + } + return "" +} + +func (m *model) creditLine() string { + return creditLineFor(m.selectedUsageAvailability().credits) +} + +// fmtDays renders a relative duration as whole days remaining, rounding up so +// a credit expiring later today still reads 1d. +func fmtDays(s int64) string { + if s <= 0 { + return "0d" + } + return fmt.Sprintf("%dd", (s+86399)/86400) +} + +// syncPreview re-renders the routing content and jumps back to the top — for +// content changes (facet cycling, depth, reset), where the old scroll offset +// points at rows that no longer exist. diff --git a/view.go b/view.go new file mode 100644 index 0000000..1d8ba6e --- /dev/null +++ b/view.go @@ -0,0 +1,442 @@ +package main + +import ( + "fmt" + "strings" + + clikit "github.com/atyrode/cli-kit" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + "github.com/charmbracelet/lipgloss" +) + +func (m *model) footer() string { + usage := "" + if m.usageInFooter() { + usage = m.usagePanel() + } + return clikit.SeparatedSections( + m.w, + usage, + padLeft(m.help.View(m.contextHelp()), gut), + ) +} + +// usageInFooter says where Usage lives: the wide layout keeps it as the +// full-width bottom band; medium moves it into the secondary column (back to +// the footer while ‹p› hides that row); narrow/short hides it entirely so the +// Generator stays usable first — fetch state and the refresh cadence keep +// running unseen, and nothing is refetched when it reappears. +func (m *model) usageInFooter() bool { + if m.hideUsage { + return false + } + switch m.sizeMode() { + case sizeWide: + return true + case sizeMedium: + return m.collapse + default: + return false + } +} + +// routingShown reports whether the Routing section (and so its title-local +// p · hide cue) is on screen in the current composition. +func (m model) routingShown() bool { + if m.showUsage && m.sizeMode() == sizeNarrow { + return false + } + if m.collapse { + return false + } + if m.mode() == modeCollapsed { + return m.showResult + } + return true +} + +// usageShown reports whether the Usage panel is rendered anywhere — the +// wide/collapsed footer band, medium's secondary column, or narrow's dedicated +// full-screen view. +func (m model) usageShown() bool { + if m.hideUsage { + return false + } + if m.sizeMode() == sizeNarrow { + return m.showUsage + } + return m.usageInFooter() || m.mode() == modeMedium +} + +// usageCanShow reports whether restoring Usage would actually render it. +// Narrow terminals use a dedicated full-width view when they can seat the +// stacked Usage column; extremely small terminals still suppress a dead cue. +func (m model) usageCanShow() bool { + if m.sizeMode() == sizeNarrow { + return m.w >= m.usageColW() + } + t := m + t.hideUsage = false + return t.usageShown() +} + +// generatorShown: the Generator (and its launch footer advertising ⏎/m/u) is +// visible in every composition except a narrow full-screen secondary view. +func (m model) generatorShown() bool { + if m.sizeMode() == sizeNarrow && m.showUsage { + return false + } + return !(m.mode() == modeCollapsed && !m.collapse && m.showResult) +} + +// helpKeys is the state-derived key map handed to the bubbles help view: the +// compact line lists only what contextHelp selected, while ? always exposes +// the complete static reference. +type helpKeys struct { + short []key.Binding + full [][]key.Binding +} + +func (h helpKeys) ShortHelp() []key.Binding { return h.short } +func (h helpKeys) FullHelp() [][]key.Binding { return h.full } + +// contextHelp derives the compact footer from the model state (atyrode/dotfiles#198): +// - navigation, reset, full-help discovery, and quit are always offered; +// - a hidden section contributes its recovery action (show routing/usage) — +// usage only when the current terminal could actually seat it again; +// - account management and manual refresh surface while Usage is off screen, +// with refresh hidden while its one central request is in flight; +// - the launch trio surfaces only while the Generator launch footer is +// hidden (the narrow routing-full-screen swap). +// +// Everything else lives in visible section chrome or behind ?. +func (m model) contextHelp() helpKeys { + short := []key.Binding{keys.Move, keys.Change} + // The generator title advertises d · defaults itself; the compact line + // repeats it only while that chrome is off screen (routing full-screen). + if !m.generatorShown() { + short = append(short, keys.Reset) + } + if !m.routingShown() { + short = append(short, key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "show routing"))) + } + if !m.usageShown() && m.usageCanShow() { + short = append(short, key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "show usage"))) + } + // Keep full-help discovery and quit ahead of optional contextual actions: + // bubbles truncates a compact line from the right on narrow terminals. + short = append(short, keys.Help, keys.Quit) + if !m.usageShown() { + short = append(short, keys.Manager) + if m.broker.URL != "" && !m.fetching { + short = append(short, keys.Refresh) + } + } + if !m.generatorShown() { + short = append(short, keys.Launch, keys.Managed, keys.Untrusted) + } + return helpKeys{short: short, full: keys.FullHelp()} +} + +func (m *model) relayout() { + m.help.Width = m.w - gut // the footer help is gutter-inset on every line; wrap inside the padded width + pw, ph := m.previewDims() + if pw < 10 { + pw = 10 + } + if ph < 3 { + ph = 3 + } + if !m.rdy { + m.vp = viewport.New(pw, ph) + m.rdy = true + } else { + m.vp.Width, m.vp.Height = pw, ph + } + m.syncPreviewKeepScroll() +} + +const prevPadL = 2 + +func (m model) previewPane(w, h int) string { + return lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + BorderForeground(lipgloss.Color(cBord)).PaddingLeft(prevPadL). + Width(w).Height(h). + Render(m.previewColumn()) +} + +// accent is the context colour — the selected lane in the generator. +// Blue / purple / orange. +func (m model) accent() string { + if _, local := m.selectedRuntime(); local { + return cGreen + } + return laneColor(m.sel["lane"]) +} + +// pill renders an accent-backed section label, coloured by the active lane — +// the shared shape of the generator (left) and routing (right) column heads. +func (m model) pill(label string) string { + return lipgloss.NewStyle().Padding(0, 1). + Background(lipgloss.Color(m.accent())).Foreground(lipgloss.Color("#12161d")).Bold(true). + Render(label) +} + +// sectionTitle is the accent-pilled "generator" label at the top of the left +// column. +func (m model) sectionTitle() string { + return m.pill("generator") +} + +// sectionHead is the gutter-inset title row — the pill plus its local +// reset-to-defaults cue (d · defaults) — and a blank separator (headRows +// tall); it stays pinned above the scrolling facet list. +func (m model) sectionHead() string { + return padLeft(m.sectionTitle()+" "+stCueKey.Render("d")+stCue.Render(" · defaults"), gut) + "\n\n" +} + +// prevChromeRows is the Routing column's pinned chrome around the scrolling +// viewport: the title row with its local collapse cue and a blank separator +// above, plus the fallback-display cue pinned beneath the viewport. +const prevChromeRows = headRows + 1 + +// previewColumn assembles the Routing section: the pinned title row carrying +// the section-local collapse cue (p · hide), the scrolling routing viewport, +// then the fallback-display cue pinned at the section's bottom edge — bottom +// chrome, where the chains it toggles end. The f wording makes clear it only +// changes what is DISPLAYED: the launched profile always keeps its fallback +// chains. +func (m model) previewColumn() string { + verb := "show" + if m.depth == 1 { + verb = "hide" + } + return m.pill("routing") + " " + stCueKey.Render("p") + stCue.Render(" · hide") + "\n\n" + + m.vp.View() + "\n" + + stKey.Render("f") + stDim.Render(" · "+verb+" fallback chains") +} + +// leftColumn renders the pinned section head plus the scrolling list body, the +// whole column inset by the shared gutter, sized to totalH rows and w columns. +func (m model) leftColumn(w, totalH int) string { + iw := w - gut + body, bcur := m.bodyLines() + listH := totalH - headRows - launchFooterRows + if listH < 1 { + listH = 1 + } + list := padLeft(windowList(body, bcur, listH, iw), gut) + footer := padLeft(strings.Join(m.launchFooter(), "\n"), gut) + return m.sectionHead() + list + "\n" + footer +} + +// mediumContent is the generator-dominant layout: the full-width facet list on +// top (primary), a divider, then Usage (left) and Routing (right) side by side +// in a secondary row, separated by a one-cell border column. Usage stacks its +// provider groups vertically inside the measured-width left column; Routing +// keeps its own scrolling viewport in whatever the row leaves free. +func (m model) mediumContent(bodyH int) string { + genH, secH := m.mediumSplit(bodyH) + top := m.leftColumn(m.w, genH) + div := stDim.Render(strings.Repeat("─", m.w)) + rw := m.routingColW() + // clip each column before fixing its width — Width() alone would wrap any + // over-wide line onto an extra physical row and break the row's height. + routing := lipgloss.NewStyle().Width(rw).MaxHeight(secH).Render( + lipgloss.NewStyle().MaxWidth(rw).Render(padLeft(m.previewColumn(), gut))) + sec := routing + if !m.hideUsage { + uw := m.w - rw - secSepW // the measured usage column's share, left of the border + sep := lipgloss.NewStyle().Foreground(lipgloss.Color(cBord)).Render( + strings.TrimSuffix(strings.Repeat("│\n", secH), "\n")) + usage := lipgloss.NewStyle().Width(uw).MaxHeight(secH).Render( + lipgloss.NewStyle().MaxWidth(uw).Render(m.usagePanelStackedFor(uw))) + sec = lipgloss.JoinHorizontal(lipgloss.Top, usage, sep, routing) + } + return lipgloss.JoinVertical(lipgloss.Left, top, div, sec) +} + +func (m model) View() string { + if !m.rdy { + return "loading…" + } + if m.manager { + return m.managerView() + } + foot := m.footer() + bodyH := m.bodyH() + ch := m.contentH() + var content string + switch m.mode() { + case modeCollapsed: + switch { + case m.showUsage && m.sizeMode() == sizeNarrow: + content = padLeft(m.usagePanelFor(m.w-gut), gut) + case m.showResult && !m.collapse: + content = padLeft(m.previewColumn(), gut) + default: + content = m.leftColumn(m.w, ch) + } + case modeMedium: + content = m.mediumContent(ch) + default: // split + content = lipgloss.JoinHorizontal(lipgloss.Top, + m.leftColumn(m.listW(), ch), + m.previewPane(m.w-m.listW()-3, ch)) + } + // Pin the body into a fixed box (top-left) with lipgloss, then clip: any + // stray overflow (e.g. a glyph a terminal renders wider than measured) is + // absorbed here, never pushing the pinned footer off-screen. A top gap above + // the content gives the section tabs vertical breathing room. + body := lipgloss.NewStyle().MaxHeight(bodyH).Render( + strings.Repeat("\n", topGap) + + lipgloss.Place(m.w, ch, lipgloss.Left, lipgloss.Top, content)) + return lipgloss.NewStyle().MaxWidth(m.w).MaxHeight(m.h).Render( + lipgloss.JoinVertical(lipgloss.Left, body, foot)) +} + +func (m model) genLines() ([]string, int) { + acc := m.accent() + var lines []string + cursor := 0 + selected := m.selectedLaunchAvailability() + for i, f := range m.visibleFacets() { + onRow := i == m.fcur + glyCol := laneColor(m.sel["lane"]) + gly := lipgloss.NewStyle().Foreground(lipgloss.Color(glyCol)).Width(2).Render(f.glyph) + ptr := " " + if onRow { + ptr = lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Render("▸ ") + cursor = len(lines) + } + // main renders as fable's tabulated child "default": the indent + the + // default-role row lighting up Fable in the preview explain themselves, + // so it carries no flavor text (which would wrap on narrow panes anyway). + label, childPad, childW := f.key, "", 0 + if f.key == "main" { + // tree-style L connector: reads as fable's child, like `tree`. + label, childPad, childW = "default", stDim.Render("└ "), 2 + } + if f.key == "blend" { + // blend is lane's sub-setting: the lead row picks who drives, + // this child picks how exclusively. + childPad, childW = stDim.Render("└ "), 2 + } + row := fmt.Sprintf("%s%s%s%s", ptr, childPad, gly, stDim.Render(pad(label, 9-childW))) + if f.key == "thinking" || f.key == "model" || f.key == "advisor" { + row += " " + m.segmentGauge(f, onRow, acc) + } else { + for _, v := range f.values { + display := v + if f.key == "runtime" { + display = m.runtimeValueLabel(v) + } + switch { + case v == m.sel[f.key]: + col := acc + if f.key == "lead" { + col = laneColor(m.sel["lane"]) + } else if (f.key == "spark" || f.key == "fable" || f.key == "main") && v == "on" { + col = cGreen + } + st := lipgloss.NewStyle().Foreground(lipgloss.Color(col)).Bold(true) + if onRow { // the cursor sits on the selected value of the focused row + st = st.Background(lipgloss.Color(cSelBg)) + } + row += " " + st.Render(" "+display+" ") + default: + row += " " + stDim.Render(display) + } + } + } + switch { + case (f.key == "fable" || f.key == "spark") && m.sel[f.key] == "on": + bkt, lbl := "claude-fable", "Fable" + if f.key == "spark" { + bkt, lbl = "codex-spark", "Spark" + } + if selected.down(bkt) { + w := lbl + " maxed · " + gReset + " " + fmtReset(selected.reset[bkt]) + if selected.bucket[bkt] == "unauthed" { + w = lbl + " unavailable" + } + row += " " + stWarn.Render(gWarn+" "+w+" — no usage left") + } + case f.key == "fast" && m.sel["fast"] == "on": + row += " " + stDim.Render("GPT only") + case f.key == "relief": + // relief is the least self-explanatory dial: say what it does, + // in the state it currently does it. + note := "drained chains wait for quota reset" + if m.sel["relief"] == "on" { + note = "drained chains spill into " + optionalPoolLabels() + } + row += " " + stDim.Render(note) + } + lines = append(lines, row) + } + return lines, cursor +} + +// segmentGauge renders a stepped dial (model, thinking, advisor) as a notched +// meter: one cell per selectable option, filled to the selection, with the +// selected word riding along so the level still reads at a glance. A leading +// "off" value is the dial's zero — it takes no cell, so off renders an empty +// track and the first real step lights exactly one. ←/→ behave exactly as on +// a word dial — only the rendering differs; the facet's values are untouched. +func (m model) segmentGauge(f facet, onRow bool, acc string) string { + sel := -1 + for i, v := range f.values { + if v == m.sel[f.key] { + sel = i + } + } + steps := f.values + fill := sel + 1 + if len(steps) > 0 && steps[0] == "off" { + steps = steps[1:] + fill = sel // off (sel 0) lights nothing + } + if fill < 0 { + fill = 0 + } + lit := lipgloss.NewStyle().Foreground(lipgloss.Color(acc)).Bold(true) + var b strings.Builder + // The leading space mirrors the word dials' selected-cell padding, so the + // track starts on the same column the value words do. + b.WriteString(" ") + for i := range steps { + if i < fill { + b.WriteString(lit.Render("▰")) + } else { + b.WriteString(stDim.Render("▱")) + } + } + word := lit + if onRow { + word = word.Background(lipgloss.Color(cSelBg)) + } + label := m.sel[f.key] + if sel < 0 && label == "" { + label = "?" + } + return b.String() + " " + word.Render(" "+label+" ") +} + +// defaultGlyphs is the built-in facet-glyph set (Nerd Font, Font Awesome PUA +// range), written as explicit \u escapes so the codepoints stay visible and +// verifiable in source — a literal PUA glyph is invisible in most editors and +// was once wiped by an edit exactly because of that. CODE_FACET_GLYPHS may +// override any entry (see main). +// +// runtime 🖥 (f108) lane ⇄ (f127) model ⚙ (f085) thinking 💡 (f0eb) advisor 🧭 (f14e) +// spark 🚀 (f135) fable 📖 (f02d) default 🎯 (f140) fast ⚡ (f0e7) +func defaultGlyphs() map[string]string { + return map[string]string{ + "runtime": "\uf108", "lane": "\uf127", "model": "\uf085", "thinking": "\uf0eb", "advisor": "\uf14e", + "spark": "\uf135", "fable": "\uf02d", "main": "\uf140", "fast": "\uf0e7", + "relief": "\uf132", + } +} diff --git a/wheel.go b/wheel.go new file mode 100644 index 0000000..9aa3dbe --- /dev/null +++ b/wheel.go @@ -0,0 +1,101 @@ +package main + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +// ── trackpad / mouse wheel ─────────────────────────────────────────────────── +// The wheel drives the generator directly: vertical scroll moves the facet +// selection, horizontal scroll changes the selected facet's value. Terminal +// mouse protocols expose direction-only press events rather than trackpad +// distance, so require a small burst before committing one generator step. +// This gives fine Mac trackpad motion room to settle without making every raw +// event select a new row or option. Routing remains continuous and ungated. +const ( + wheelStepEvents = 5 + wheelGestureGap = 200 * time.Millisecond +) + +const ( + wheelAxisNone = iota + wheelAxisV + wheelAxisH +) + +// admittedWheelMsg marks a generator wheel event whose same-direction burst +// crossed the step threshold before Bubble Tea's Update/render cycle. +type admittedWheelMsg struct{ tea.MouseMsg } + +// wheelTarget is the layout state the pre-dispatch filter needs. Keeping this +// interface narrow also lets the raw-input regression test wrap model while +// preserving the exact production filter path. +type wheelTarget interface { + wheelInRouting(int, int) bool + routingWheelCanMove(tea.MouseButton) bool +} + +// wheelInputFilter removes mouse traffic before Bubble Tea's unconditional +// redraw-after-Update. Generator motion accumulates by axis and direction; +// only each complete threshold reaches Update. Motion, non-wheel presses, +// routing horizontal wheel, and clamped routing scroll are dropped because +// none can change the view. +type wheelInputFilter struct { + axis int + button tea.MouseButton + count int + last time.Time +} + +func (f *wheelInputFilter) Filter(app tea.Model, msg tea.Msg) tea.Msg { + + mouse, ok := msg.(tea.MouseMsg) + if !ok { + return msg + } + if mouse.Action != tea.MouseActionPress { + return nil + } + target, ok := app.(wheelTarget) + if !ok { + return msg + } + if target.wheelInRouting(mouse.X, mouse.Y) { + switch mouse.Button { + case tea.MouseButtonWheelUp, tea.MouseButtonWheelDown: + if target.routingWheelCanMove(mouse.Button) { + return mouse + } + } + return nil + } + + axis := wheelAxisNone + switch mouse.Button { + case tea.MouseButtonWheelUp, tea.MouseButtonWheelDown: + axis = wheelAxisV + case tea.MouseButtonWheelLeft, tea.MouseButtonWheelRight: + axis = wheelAxisH + default: + return nil + } + now := time.Now() + if f.count > 0 && now.Sub(f.last) <= wheelGestureGap && axis != f.axis { + // Ignore brief orthogonal trackpad jitter without discarding progress + // along the operator's dominant gesture axis. + return nil + } + if axis != f.axis || mouse.Button != f.button || now.Sub(f.last) > wheelGestureGap { + f.axis = axis + f.button = mouse.Button + f.count = 0 + } + f.last = now + f.count++ + if f.count < wheelStepEvents { + return nil + } + f.count = 0 + return admittedWheelMsg{MouseMsg: mouse} +}