Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,24 @@ fallback chain. Any text-only rung is skipped.

Pool `R` is optional, and its presence is its own switch: with no `R` models
the generator serves only the five base lanes; with a full ladder it also
serves `ox-only` (every role on the free pool) and `ox-led` (the free pool
leads everything high-volume; plan/slow/designer/reviewer cross to Anthropic,
and `fable` may still lead those). A half-declared R ladder is refused. A
one-model family — Ox Alpha is exactly that — declares the same id once per
tier with ascending thinking ceilings (`low→low`, then `low→high`, then
`low→max`); the tier dial then means thinking depth. `code generate init`
never scaffolds pool R: curate those entries by hand and re-confirm
`probed: true` yourself.
serves three ox lanes — `ox-only` (every role on the free pool),
`ox-led` (the free pool leads everything high-volume while plan/slow/
designer/reviewer cross to Anthropic, and `fable` may still lead those), and
`ox-lean` (the mirror: paid providers answer for default/task/librarian while
the free pool absorbs scout/sonic/smol/tiny/commit and vision; `fable` and
fable-as-main stay available, so an elite can take the default seat). A
half-declared R ladder is refused. A one-model family — Ox Alpha is exactly
that — declares the same id once per tier with ascending thinking ceilings;
the tier dial then means thinking depth. `code generate init` never scaffolds
pool R: curate those entries by hand and re-confirm `probed: true` yourself.

The thinking scale is `minimal · low · medium · high · xhigh · max`. Write
`low→max` for a contiguous run, or a comma list when the model skips a level:
claude-opus-4-6 offers `low,medium,high,max` but not `xhigh`, and a range there
would claim a level the API rejects. A single-level model writes `low→low`.
`low→max` ONLY for a genuinely contiguous run — a range claims every level in
between, and requesting one the model doesn't offer sends a level the API may
reject. A model that skips levels must be written as a comma list:
claude-opus-4-6 offers `low,medium,high,max` but not `xhigh`; Ox Alpha offers
only `low,high,max`, so its rungs declare `low→low`, `low,high`, and
`low,high,max`. A single-level model writes `low→low`.

## The `ctrl+o` classifier

Expand Down
106 changes: 65 additions & 41 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,28 +412,66 @@ var (
genExtremes = map[string]bool{"minimal": true, "max": true}
)

func lanePrimary(lane string) string {
if lane == "ox-only" || lane == "ox-led" {
return "R"
}
if lane == "gpt-only" || lane == "gpt-led" || lane == "mixed" {
return "O"
}
return "A"
}

func lanePure(lane string) bool {
return lane == "gpt-only" || lane == "claude-only" || lane == "ox-only"
// lanePolicy is a lane's whole role-mapping, as data. primary answers for
// default/task/librarian; delib hosts plan/slow/designer/reviewer ("" = the
// primary); util hosts scout/sonic/smol/tiny/commit ("" = primary); vision is
// the image rung's pool, with visionSmart overriding it when the model dial
// sits on smart (mixed prefers Claude's tier-3 for that). pure lanes never
// cross: reviewer and advisor stay in-primary.
//
// This table is the whole generator's sense of "a lane". A new setup — a new
// pool pairing, a new emphasis — is a row here, not new branches in genCombo;
// keep it that way. The reviewer always ends up off its lead pool unless the
// lane is pure (the anti-tunnel-vision rule), and the advisor leads on the
// minimum-diversity pool (delib when set, else the crossing target).
type lanePolicy struct {
primary string
delib string
util string
vision string
visionSmart string
pure bool
}

var genLanePolicies = map[string]lanePolicy{
"gpt-only": {primary: "O", pure: true},
"gpt-led": {primary: "O"},
"mixed": {primary: "O", delib: "A", visionSmart: "A"},
"claude-led": {primary: "A"},
"claude-only": {primary: "A", pure: true},
"ox-only": {primary: "R", pure: true},
"ox-led": {primary: "R", delib: "A"},
// The mirror of ox-led: paid providers keep everything that answers for
// the work (default, task, librarian), while the free pool absorbs the
// high-volume background and image description.
"ox-lean": {primary: "O", delib: "A", util: "R", vision: "R"},
}

func (p lanePolicy) pool(role string) string {
if p.pure {
return p.primary
}
if genDelib[role] && p.delib != "" {
return p.delib
}
if genCrossLed[role] {
return otherPool(p.primary)
}
if genUtil[role] && p.util != "" {
return p.util
}
return p.primary
}

// lanes lists the lanes this catalog serves: the five base lanes always, plus
// the ox pair only when the optional OpenRouter ladder is fully declared. This
// is the generator side of the ox on/off switch.
// the ox trio only when the optional OpenRouter ladder is fully declared.
// This is the generator side of the ox on/off switch. Order follows the dial:
// base lanes first, ox lanes appended.
func (c *catalog) lanes() []string {
if !c.hasOxLadder() {
return genBaseLanes
}
return append(append([]string{}, genBaseLanes...), "ox-only", "ox-led")
return append(append([]string{}, genBaseLanes...), "ox-only", "ox-led", "ox-lean")
}

type roleRoute struct {
Expand All @@ -447,35 +485,16 @@ type roleRoute struct {
// 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 {
p := lanePrimary(lane)
pol := genLanePolicies[lane]
p := pol.primary
base := genTierMap[mtier]
isPure := lanePure(lane)
isPure := pol.pure
extreme := genExtremes[thinking]
sparkKey := c.ladder["O"][0]
eliteKey := c.ladder["A"][4]

rprov := func(r string) string {
if isPure {
return p
}
if lane == "mixed" {
if genDelib[r] {
return "A"
}
return "O"
}
// ox-led keeps the free pool on everything high-volume (workers,
// utility, vision) and spends the paid judgment where it pays:
// deliberative roles cross to Anthropic. The reviewer crossing below
// lands there too, which still satisfies the anti-tunnel-vision rule —
// the second eye never shares the lead's pool.
if lane == "ox-led" && genDelib[r] {
return "A"
}
if genCrossLed[r] {
return otherPool(p)
}
return p
return pol.pool(r)
}

out := map[string]roleRoute{}
Expand Down Expand Up @@ -515,10 +534,12 @@ func (c *catalog) genCombo(lane, mtier, thinking string, spark, fable, fableMain
// omp falls back @vision → @default → active model when it needs an
// image described, and describeForTextModels is on by default — so
// this rung must be a model that actually accepts images. Vision
// follows the model tier; mixed smart prefers Claude's smart rung.
vp := p
if lane == "mixed" && mtier == "smart" {
vp = "A"
if pol.vision != "" {
vp = pol.vision
}
if mtier == "smart" && pol.visionSmart != "" {
vp = pol.visionSmart
}
lead := c.visionLead(vp, base)
if lead == "" && !isPure {
Expand Down Expand Up @@ -634,6 +655,9 @@ func genValid(lane string, spark, fable, fableMain bool) bool {
if lane == "ox-led" && (spark || fableMain) {
return false // utility already lives on the free pool; fable-as-main would defeat the lane
}
if lane == "ox-lean" && spark {
return false // the drain bucket's leads are ox here; spark has nothing to drain
}
if fableMain && !fable {
return false // fable-as-main only exists on top of fable
}
Expand Down
49 changes: 43 additions & 6 deletions generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@ const oxEntries = `
speed: 27.4
ttft: 2.1
context: 1048576
thinking: lowhigh
thinking: low,high
oxmax:
id: stealth/ox-alpha
pool: R
Expand All @@ -1161,7 +1161,7 @@ const oxEntries = `
speed: 27.4
ttft: 2.1
context: 1048576
thinking: lowmax
thinking: low,high,max
`

func catalogWithOx(t *testing.T) *catalog {
Expand All @@ -1179,7 +1179,7 @@ func TestOxLadderGatesLanes(t *testing.T) {
t.Errorf("base catalog serves %d lanes, want %d: %v", len(got), len(genBaseLanes), got)
}
withOx := catalogWithOx(t)
want := append(append([]string{}, genBaseLanes...), "ox-only", "ox-led")
want := append(append([]string{}, genBaseLanes...), "ox-only", "ox-led", "ox-lean")
if got := withOx.lanes(); strings.Join(got, ",") != strings.Join(want, ",") {
t.Errorf("ox catalog serves %v, want %v", got, want)
}
Expand All @@ -1203,9 +1203,11 @@ func TestGenValidOxLanes(t *testing.T) {
{"ox-only", true, false, false, false}, // no O drain bucket to lead with
{"ox-only", false, true, false, false}, // no A elite on a pure ox lane
{"ox-led", false, false, false, true},
{"ox-led", true, false, false, false}, // utility already lives on the free pool
{"ox-led", false, true, false, true}, // fable leads the deliberative roles
{"ox-led", false, true, true, false}, // fable-as-main defeats the free worker
{"ox-led", false, true, true, false}, // fable-as-main defeats the free worker
{"ox-lean", false, false, false, true},
{"ox-lean", true, false, false, false}, // utility is ox here; spark has nothing to drain
{"ox-lean", false, true, false, true}, // deliberative roles stay Claude; fable may lead them
{"ox-lean", false, true, true, true}, // fable-as-default is exactly what lean is for
} {
if got := genValid(tc.lane, tc.spark, tc.fable, tc.main_); got != tc.want {
t.Errorf("genValid(%s, sp=%v, fa=%v, famain=%v) = %v, want %v",
Expand Down Expand Up @@ -1250,6 +1252,41 @@ func TestOxLaneRoutingPolicy(t *testing.T) {
}
}

// ox-lean is ox-led's mirror: paid providers answer for the work, the free
// pool absorbs the background. Fable-as-main is allowed — handing the default
// seat to the elite is exactly what an operator on this lane may want.
func TestOxLeanRoutingPolicy(t *testing.T) {
c := catalogWithOx(t)
combo := c.genCombo("ox-lean", "smart", "high", false, true, true)
// fable-as-main hands only the default seat to the elite; task and
// librarian follow the lane's OpenAI primary.
for _, r := range []string{"task", "librarian"} {
if pool := c.models[combo[r].lead].Pool; pool != "O" {
t.Errorf("ox-lean %s lead pool = %s, want O", r, pool)
}
}
if id := c.models[combo["default"].lead].ID; id != "claude-fable-5" {
t.Errorf("ox-lean famain default = %s, want claude-fable-5", id)
}
for _, r := range []string{"scout", "sonic", "smol", "tiny", "commit", "vision"} {
if id := c.models[combo[r].lead].ID; id != "stealth/ox-alpha" {
t.Errorf("ox-lean %s lead = %s, want stealth/ox-alpha", r, id)
}
}
for _, r := range []string{"plan", "slow", "designer", "reviewer"} {
if pool := c.models[combo[r].lead].Pool; pool != "A" {
t.Errorf("ox-lean deliberative %s pool = %s, want A", r, pool)
}
}
// Without fable, workers stay on the OpenAI primary.
base := c.genCombo("ox-lean", "normal", "medium", false, false, false)
for _, r := range []string{"default", "task"} {
if pool := c.models[base[r].lead].Pool; pool != "O" {
t.Errorf("ox-lean %s pool = %s, want O", r, pool)
}
}
}

func TestAdvisorsIncludeOxContext(t *testing.T) {
withOx := catalogWithOx(t)
got := withOx.renderAdvisors()
Expand Down
54 changes: 45 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ func facetDefs(glyphs map[string]string) []facet {
return []facet{
// The ox values are trimmed away by applyCatalog unless the catalog
// serves them — presence in models.yml is what makes them appear.
{"lane", []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ox-only", "ox-led"}, glyphs["lane"]},
{"lane", []string{"gpt-only", "gpt-led", "mixed", "claude-led", "claude-only", "ox-only", "ox-led", "ox-lean"}, 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
Expand Down Expand Up @@ -1226,12 +1226,18 @@ func (m model) meter(label, glyph, fill string, n int) string {
// 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).
// handing the default role to Fable — and Claude otherwise. Pure lanes keep
// their own provider, including the ox lane: 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"]
ctx := "claude"
if lane == "gpt-only" || lane == "claude-led" {
if lane == "ox-only" {
ctx = "ox"
} else if lane == "gpt-only" || lane == "claude-led" {
ctx = "gpt"
}
// fable-as-main puts Claude Fable in the default seat, so the second
Expand All @@ -1240,7 +1246,26 @@ func (m model) advisorChain(level string) []string {
if lane != "claude-only" && m.sel["fable"] == "on" && m.sel["main"] == "on" {
ctx = "gpt"
}
return m.advisors[level+"/"+ctx]
chain := m.advisors[level+"/"+ctx]
if lane == "ox-led" || lane == "ox-lean" {
tail := m.advisors["glance/gpt"]
if ctx == "gpt" {
tail = m.advisors["glance/claude"]
}
for _, t := range append(append([]string{}, tail...), m.advisors["glance/ox"]...) {
dup := false
for _, c := range chain {
if c == t {
dup = true
break
}
}
if !dup && t != "" {
chain = append(chain, t)
}
}
}
return chain
}

// roleOf returns the role name of a routing row ("● task" → "task").
Expand Down Expand Up @@ -1314,6 +1339,9 @@ func (m model) visibleFacets() []facet {
if lane == "ox-led" && (f.key == "spark" || f.key == "main" || f.key == "fast") {
continue
}
if lane == "ox-lean" && (f.key == "spark") {
continue
}
if f.key == "spark" && m.noSpark {
continue
}
Expand All @@ -1334,7 +1362,7 @@ func comboID(sel map[string]string) string {
if lane == "gpt-only" || lane == "ox-only" {
fb = "off"
}
if lane == "claude-only" || lane == "ox-only" || lane == "ox-led" {
if lane == "claude-only" || lane == "ox-only" || lane == "ox-led" || lane == "ox-lean" {
sp = "off"
}
spid, faid := "nosp", "nofa"
Expand Down Expand Up @@ -1437,6 +1465,8 @@ func laneColor(lane string) string {
return "#1f9d5b" // deeper green — pure free pool
case "ox-led":
return "#5fce96" // lighter green — leans Ox Alpha
case "ox-lean":
return "#2dd4bf" // teal — paid work riding the free pool
case "gpt-only":
return "#3f8ef0" // deeper blue — pure pool
case "gpt-led":
Expand All @@ -1455,7 +1485,13 @@ func laneColor(lane string) string {
// through. The catalog's pool column is authoritative; the name heuristic is
// only for catalogs that predate it.
func (m model) prefixed(model string) string {
if f, ok := m.facts[model]; ok && f.pool != "" {
// 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 && f.pool != "" {
switch f.pool {
case "O":
return "openai-codex/" + model
Expand Down Expand Up @@ -2000,8 +2036,8 @@ func (m model) listW() int {
}
// 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 > 104 {
w = 104
if w > 116 {
w = 116
}
return w
}
Expand Down
Loading
Loading