1732faf3f3
* feat(host): answer pre-launch model listings by probing the real harnesses
The pre-launch pickers were fed by catalog reconstruction — for a
Databricks-gateway codex host, serving-endpoint name enumeration: id
spellings the gateway's codex surface does not route, chat-only traps
(gpt-oss), no display names or effort ladders. The harness itself is
the only authority on what its /model picker would offer, so the host
now asks the harnesses:
- codex-native: probe_codex_model_options boots codex app-server with
the SAME Databricks materialization a session launch gets (shared
_databricks_launch_materialization, extracted from
build_codex_native_server so the two cannot drift), a persistent
probe CODEX_HOME (codex's own models_cache ETag makes refreshes
cheap), and passes model/list rows through verbatim with a single
default marker (launch pin first, else codex's own). Scoped to
Databricks-profile launches; everything else — and every probe
failure — falls open to the existing catalog path unchanged.
- claude-native: session launches (and the probe) now opt in to Claude
Code's gateway model discovery
(CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 in the ucode env; the
fetch 404s harmlessly until the gateway serves /v1/models).
probe_claude_gateway_models runs claude -p "/model" with the launch
env so the harness executes its own discovery, then reads the
harness-written gateway-models.json artifact — no discovery
semantics replicated. Rows union with the configured tier rows,
exact-id deduped. The nonessential-traffic kill-switch is stripped
from the probe env (Claude treats it as covering discovery).
- claude-sdk: SDK-mode claude is a pass-through client with no catalog
of its own, so the endpoint listing is the harness truth — served
via the existing list_models_for_worker in the exact wire spelling
the SDK sends.
Serving stays off the probe path: a new host-side cache
(omnigent/host/model_options_cache.py) keys results by a resolved-
config fingerprint, serves stale-while-revalidating with single-flight
probes, and is prewarmed per tunnel connection — measured 65ms at the
REST route warm, ~1.3s joining the prewarm probe cold. The
model-options frame is now answered from a tracked task instead of
inline on the tunnel receive loop (a cold probe there stalled every
frame — same class as a83cc707); a filesystem frame answered in 22ms
mid-probe. The REST route also stops dropping the routable_models the
frame already carries (openapi regenerated).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(host): always probe Claude Code itself for the model list
claude -p "/model" makes the harness print its own alias enumeration
headlessly, so the curated static subscription list demotes from
first resort to failure fallback. probe_claude_gateway_models
generalizes to probe_claude_model_options: it runs for every config
shape (bare subscription launches included), parses the printed
"Available:" aliases verbatim (no alias names known to the parser, so
new Claude releases flow through), and still reads the discovery
artifact when the env opts in. The host lane serves configured tier
rows (the rich spelling for pinned aliases) unioned with the
harness's printed aliases and discovered gateway rows, exact-id
deduped; the configured/static rows stand alone only when the probe
itself fails.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(host): probe Codex for every launch shape, not just Databricks routing
Same mandate as the Claude lane: the harness always answers. The probe
drops its Databricks-profile gate — a non-profile launch boots codex
app-server with whatever -c overrides the launch resolved (provider
routing, the dismissal pin, or nothing) and reads model/list verbatim,
so subscription/CLI-login and custom-provider shapes get Codex's real
visible catalog instead of the static curated list (the stale
hyphenated-id class of bug) or the raw enumeration. With no
launch-pinned model, Codex's own default marker stands. The legacy
catalog paths remain solely as the probe-failure fallback, pinned by
the existing handler tests now running with a failing probe stub.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(host): resolve Claude model aliases to concrete versions in the picker
The probed alias list answered WHICH aliases exist but not what they land
on — 'opus' could be Opus 5.0 or 4.8 and the picker couldn't say. Ask the
harness that too: each printed alias gets its own headless
'--model <alias> -p /model' run in stream-json mode, whose init event
carries the exact resolved id and whose printed 'Current model:' line
carries the human label (only the effort suffix stripped). Rows become
{id: alias, model: exact id, displayName: 'alias — label'}; the web
picker already renders displayName, so no frontend change.
Resolution runs share the enumeration run's invocation assembly so the
two cannot drift, fan out under one bounded budget (startup dominates
and stretches with box load — measured 0.7s-17s for the same command —
so one wave covers a whole alias set), and fail per-alias back to the
bare row, never the probe. Live run resolves all 10 aliases in ~6s and
surfaces facts worth not guessing: fable[1m] resolves to plain
claude-fable-5, and 'best' pins to Fable rather than Opus.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(host): show Claude picker rows as resolved versions only
Presentation pass on the resolved alias rows: the display is the
harness's resolved label alone (the alias prefix was noise), 1M-context
resolutions always say '(1M context)' even where the harness's label
omits it (sonnet[1m] prints just 'Sonnet 5'), the 'default' alias never
becomes a row (the picker renders its own Default choice, so it was a
duplicate), and aliases resolving to an earlier row's exact (model,
label) are dropped — which removes 'best' and 'fable[1m]' as the
duplicates of fable's row they currently are, without hardcoding any
alias name. Launch ids are untouched; only displayName and row
membership change.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(host): dedupe Claude picker rows by resolved model alone
opusplan resolves to claude-sonnet-5 — a model the sonnet row already
lists — so the same duplicate-model rule that removes best and
fable[1m] now covers it: one picker row per resolved model, no alias
names hardcoded. A composite-mode alias would reappear only if it ever
resolved to a model no other alias offers.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(models): drop curated picker fallbacks; sessions ride the probe
The release-curated picker stand-ins in model_fallbacks are gone — the
live harness probes are the source of truth everywhere, and a path that
cannot probe now reports nothing rather than a plausible-but-stale list
(the codex entries even carried hyphenated spellings codex itself does
not use). Smart Routing's tables stay: rankings, arm menus, and probed
exclusions are router contract data no discovery API can provide, and
the ownership test now guards those records.
Companions so nothing regresses to empty:
- The subscription sonnet_5 pick degrades to Claude's own 'sonnet'
alias instead of hunting a static list — the harness resolves it.
- The claude-sdk pre-launch lane rides the claude probe whenever the
endpoint listing is empty (the SDK drives the claude CLI, so the
CLI's aliases are its truth on subscription boxes).
- Existing sessions now match the new-session picker: the runner's
claude-model-options endpoint resolves configured rows ∪ probe once
per session via the new shared claude_model_options_with_probe (the
host lane uses the same composition, so the two cannot drift),
answering 503-pending while the probe is in flight (the server fetch
already retries those) and falling back to configured rows past a
grace so the catalog is never empty.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): friendly composer label for Claude [1m] aliases off-catalog
The composer chip prefers the session catalog's display name, but a
Claude bracket alias the catalog doesn't list (a pick made before the
catalog carried the row, e.g. on a session launched by an older runner)
fell through to the raw id — 'sonnet[1m] High'. Render that case as
'Sonnet (1M context)': title-cased family plus the context marker, no
version claimed, since only the harness knows which Sonnet the alias
lands on. Catalog hits keep the probed display name verbatim.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): version-agnostic 'Sonnet' fallback label kills the 4.6 flash
Cold-loading a claude session painted the composer chip 'Sonnet 4.6'
for the window before the session catalog arrived, then corrected to
'Sonnet 5' — the fallback label list pinned a version that only the
harness can know (reproduced via Playwright: 'Sonnet 4.6 High' at
3.96s → 'Sonnet 5 High' at 4.70s). The fallback now says just
'Sonnet'; the catalog's display name supersedes it wherever one has
arrived, so the pre-catalog window shows a coarser label, never a
wrong one. Same honesty for the sandbox new-chat picker and the
scheduled-task model dropdown, which render the same list.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(web): retire two redundant uses of the local Claude alias list
The composer chip's pre-catalog fallback now formats alias-shaped ids
mechanically (title-case family, '_N' → ' N', '[1m]' → ' (1M context)')
instead of looking them up in CLAUDE_NATIVE_MODELS — same rendering,
zero model knowledge. The sticky-model compatibility check collapses to
session-catalog membership alone: its isClaudeNativeModel conjunct was
subsumed by the catalog check it was AND-ed with, and would have
rejected catalog rows whose ids don't look Claude-ish even though the
session's own catalog offered them. The now-orphaned guard is deleted;
the list itself stays for the genuinely hostless surfaces (sandbox
picker, unpinned scheduled tasks, schema enums).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(e2e_ui): composer label stays version-free until the catalog speaks
Covers the label behaviors the model-listing work changed: with the
session catalog held back, the composer chip renders the alias
mechanically ('sonnet[1m]' → 'Sonnet (1M context)'), and only the
arriving catalog upgrades it to its display name ('Sonnet 5 (1M
context)'). Every painted label is recorded via a MutationObserver so a
transient raw id or invented version ('Sonnet 4.6') cannot hide from a
retrying expect().
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(web): offer Smart Routing in the in-session gear for native panes
The in-session composer gear withheld the Smart Routing model option
from native Claude Code / Codex sessions under a stale premise ('their
CLI bakes the model at launch') — the server has routed native panes
per turn via /model injection since the create-time gear gained the
option, and validates routing-on creates with a per-family rule. The
in-session gate now mirrors that exact rule: a router must answer for
the session's family — the external AI-Gateway router only when the
host runs the family through the gateway (read off the session's host
row; absent rows fail open like the landing), the built-in judge
anywhere. SDK/bundle sessions keep their existing flag-only gate.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* style(web): prettier over the routing-gate and label changes
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(runner): sys_list_models subscription row is an honest empty listing
The curated claude stand-ins are gone from the static subscription
path; the dispatch test now pins the empty-models shape with the
probing note, matching the model-catalog contract.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): composer gear rides the host probe until the session catalog lands
A fresh codex session's gear showed a sparse Model row and no Effort row
for ~15s: effort levels come from the session catalog's
supportedReasoningEfforts, and that catalog only resolves once codex
app-server answers model/list. The session's host already probed the
same harness for the new-chat picker, so the gear (and the composer
chip) now falls back to those cached rows — same ids the launch accepts,
~90ms warm — whenever the session's own catalog is empty; the runner's
per-session catalog supersedes them the moment it arrives. Claude
sessions get the same pre-catalog Model list for free (their effort
levels were already static).
Verified live on a fresh codex session: Effort visible 0.5s after
create+load with the session catalog still empty, offering the host
row's levels.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): keep model-options identity stable when the host fallback is idle
The pre-catalog host fallback returned a fresh empty array whenever the
session catalog was empty and no host rows existed — for EVERY session
shape, native or not. That new identity per render re-rendered each
options consumer (composer, gear, agent-info popover) on every
streaming/liveness tick, which under CI load tipped the agent-info
hover-open grace race (shard 1 failed the same popover test twice).
Substitute only when host rows actually exist; otherwise the store's own
stable array reference flows through untouched, restoring the exact
pre-fallback behavior for every session the feature doesn't apply to.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(claude-native): apply picked model aliases verbatim, never the default
Picking Fable in a gateway session's composer switched the pane to
Opus: resolve_claude_native_model_selection swapped an unpinned family
alias for the provider's default model (a degrade from the era when
the picker always showed every family alias), the vocabulary re-spelled
that default as its pinned alias, so the runner injected '/model opus'
for a 'fable' pick — and the statusLine mirror then recorded the wrong
model as the session override. Bracket aliases had sibling failures:
'/model sonnet[1m]' 503'd on a pinned env (no spelling for it) and
silently dropped the [1m] marker on a bare login (family-segment
step-down).
Picker rows are pin-backed or probe-vouched now, so a pick passes
through verbatim and Claude owns resolution:
- the resolver's no-pin gateway degrade is gone (an out-of-band
unpinned pick now fails visibly at inference instead of silently
running the default);
- bracket variants of the family aliases are their own /model
arguments in the vocabulary — the harness enumerates them itself;
- the configured∪probe union drops probe rows whose resolved model is
a bare canonical Anthropic id on an endpoint that routes its own ids
only: the pick could never work there, so the row is not offered
(a pinned family resolves to the endpoint's spelling and stays).
Reproduced at the runner layer (events → resolver → injected command):
picking 'fable' asserted '/model fable' and got '/model opus' before
the fix. An e2e_ui guard pins that the web PATCHes the picked row id
verbatim — the client layer was innocent.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(claude-native): mirror pane model switches in the catalog's vocabulary
Live verification of the verbatim-alias fix exposed the last surface in
the same family: after a web pick of 'sonnet[1m]' correctly switched the
pane, the statusLine mirror collapsed the observed model back to the
LEGACY picker vocabulary — 'databricks-claude-sonnet-5[1m]' became
'sonnet_5' — stomping the just-saved override with an id the session's
catalog doesn't list (and which a relaunch would resolve through the
custom-tier branch, silently dropping the 1M context).
_model_alias_for now speaks the catalog's row ids: 1M resolutions keep
their bracket marker ('sonnet[1m]'), and the legacy 'sonnet_5' opt-in
row is mirrored only on a config whose custom slot actually pins it —
read off the session's launch pins — since everywhere else the generic
sonnet row IS that model. This also restores the designed web→TUI
round-trip no-op: the mirrored alias now equals the persisted override,
so the server-side dedupe skips the write.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(codex-native): report the model a Default launch actually runs
A Default codex launch names no model, so nothing pinned the session's
config.toml — yet the profile still resolved a concrete model and passed it
as `-c model=`, which outranks the config copied from the user's shared
~/.codex home. The pane ran the resolved model while the session reported
the shared file's leftover one: the create dialog promised
"Default (GPT-5.6-Luna)" and the session then said GPT-5.4.
Pin the profile-resolved model in codex's own spelling, so the forwarder
mirror and the cost gate read the model this session runs. Mark that model
as the catalog default too — codex's own isDefault is its built-in
preference and named GPT-5.6-Sol on a session running Luna, which also fed
the composer gear an effort ladder the running model rejects.
Web side: fold catalog and codex spellings when resolving a session's model
onto a picker row, and stop borrowing the default row's effort levels for an
unresolved model.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(runner): keep the codex model-options test off the real launch
Session create launches Codex for real, and that launch owns the bridge
dir: it clears the state and its forwarder task rewrites both the state
and CODEX_HOME/config.toml after the response returns. On a machine
where Codex and a Databricks profile resolve, that wiped the seeded
state no matter which side of create seeded it, so the endpoint answered
503. Stub the launch; the endpoint, the bridge-state read, the
CODEX_HOME read, and the fake app-server client all stay real.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): name Codex's Default the same in both model gears
The composer gear and the new-session gear each built their own copy for
the Model row, so one session read a bare "Default" in the composer and
"Default (gpt-5.6-luna)" on the landing page, and the landing page
listed raw catalog ids where the composer listed display names. Neither
gear told the user which model Codex would actually run.
Move both labels into HarnessConfigControls next to the sentinels they
belong to and read them from there in both callers. Row ids are
untouched, so picks still submit the harness's own spelling.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(claude-native): read the custom slot instead of guessing the model row
The terminal->web mirror mapped a concrete model id onto a picker row by
looking for a family name inside the id, so a routed Opus 4.9 landed on
the `opus` row that holds 4.8: the web showed the wrong model, and posting
that row back stepped the session off its launch pin. Resolve rows by
exact comparison against the launch pins instead, and read Claude Code's
one custom model slot to name its row rather than inferring it from the
model's spelling. A `[1m]` resolution stays a distinct row from its
non-bracket sibling.
The legacy `sonnet_5` row id and the substring spellings it used to be
matched by move into claude_model_vocabulary with a 0.10.0 removal note;
the substring leg now runs only when the exact comparison misses.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(web): drop the substring model-row match the picker never calls
`isModelImplicitlySelected` guessed which picker row a bound model belonged
to by searching for the row id inside the model name, which is why `sonnet`
matched `sonnet-5` and needed a special case per generation. Its only
caller sat in the branch taken when a session has no server-supplied model
list, and every native picker kind is on that list, so the branch ran with
an empty list and the call could not select anything.
Delete the function and collapse the caller to the server-list path. The
two suites that covered it go with it; nothing else exercised it.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* docs(claude-native): say plainly that gateway model discovery never fires
The launch env asks Claude Code to discover the gateway's model inventory,
and the comment claimed the only thing holding it back was a gateway that
did not serve `/v1/models` yet. The gateway serves it now, but the same env
sets CLAUDE_CODE_USE_GATEWAY, and the CLI fires that fetch only on its
first-party provider path — so the artifact is never written and the rows
read from it are always empty.
Name that in all three places a reader lands: the flag, the probe's
env-unset list (popping the nonessential-traffic switch is not enough), and
the artifact read itself.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(model-flows): add the harness-truth e2e suite, red-first
The model-flows design lands test-first: this suite encodes the target
behavior for every flow (pre-launch picker, default labels, create→launch
pane truth, gear parity, confirmed switching, terminal-side mirroring) and
is deliberately red today in the ways the analysis measured.
Two tiers. The hermetic tier drives the real SPA over the spawned server
with the session snapshot shaped at the browser edge and SSE frames pushed
through a captured stream controller; it runs in the normal e2e_ui lane.
The live tier (`live_model_flows` marker, opt-in via
OMNIGENT_E2E_MODEL_FLOWS=1) boots a real server + host from any checkout —
OMNIGENT_E2E_MODEL_FLOWS_REPO selects which, so the identical tests
produce the red-on-main matrix — flips provider shapes the way setup
writes default claims, launches real claude/codex TUIs, and asserts pane
truth over tmux.
Recorded pre-implementation: hermetic 4 red / 2 guard-green (the design's
predicted set exactly); live rows 1 and 5 red against unmodified main
(the frozen "Sonnet 4.6" static list; the empty/erroring codex pre-launch
answer).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(models): verbatim reported_model as the single display authority
Landing-order step 3 of the model-flows design. Sessions gain a
reported_model — the model the harness last said it is actually on, in
the harness's own spelling — stored as a new key in the existing
session_overrides blob (no DDL) and served on the snapshot's llm_model
field with precedence reported ?? spec model. external_model_change
writes and dedupes against it verbatim; the user's request
(model_override) is untouched, because requests and reports are separate
roles and only reports are ever displayed.
The claude forwarder now posts the status file's model byte-for-byte:
the alias-collapse mapper (_model_alias_for / _custom_slot_row_id) is
deleted — collapsing a routed Opus 4.9 onto the opus row holding 4.8 is
the bug class this kills — and the first-observation-silent-seed rule is
gone, so the launch's own model reports within seconds of spawn and the
composer is never blank-forever. The codex forwarder already posted raw
ids and needed no change.
The web renders and highlights models from the reported value alone:
exact id/model match against the catalog, with an off-catalog report
appended as its own raw row rather than relabeled onto a same-family
row. The sticky model becomes a pure preference — the silent bind-time
and delayed-catalog model_override PATCHes are removed (they wrote
requests the pane was never asked to honor), and session.model events
land on llmModel instead of the picker selection. Cost attribution
prefers the reported model too.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(models): explicit launches from the shared catalog store
Landing-order step 4. Every native launch now pins its model explicitly,
resolved through the new on-disk catalog store
(omnigent/model_catalog_store.py — one probe result under a
launch-config fingerprint, read by every consumer), so nothing is left
to invisible CLI-private state and a stale config line can never govern
a session.
Claude: the enumeration probe runs in stream-json and captures its own
init-event model — the truthful Default — so claude_model_catalog marks
exactly one isDefault row (appending an off-list default, e.g. a
settings.json pin, as its own launchable row; never appending a bare
Anthropic spelling on an endpoint that rejects it). A Default
subscription launch passes --model with that default; an explicit
request is validated against the catalog and fails the launch loudly
when the list no longer carries it. The runner also records the launch
vocabulary onto the bridge after config resolution
(record_model_vocabulary), closing the model_env gap that made
mid-session /model conversion read the runner's ambient env.
Codex: the session-shaped probe home now links the account's real
auth.json (the catalog must answer for the account that will run — the
Sol-promised/Terra-offered mismatch dies here), a Default launch on
codex's own login resolves the account's real default instead of
inheriting the copied config line (the stale-gpt-5.4 400 class), and
build_codex_native_server emits -c model= alongside the config-copy pin
from one resolved value on every shape. A guard test pins the
argv/config-pin agreement across all provider shapes; on the profile
shape the file deliberately keeps codex's own spelling and the guard
asserts same-model rather than same-bytes (addendum).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Serve every model surface from the shared harness catalog
One catalog file per (harness, launch-config fingerprint) now backs the
pre-launch picker, launch resolution, and the in-session gear:
- Host: catalog-backed model-options handlers with a concurrent, detached
boot prewarm; probe failures answer ok+[] plus an error string the web
new-session dialog displays. The in-memory ModelOptionsCache module is
removed.
- Runner: unified GET /v1/sessions/{id}/model-options (harness-named
routes stay as deprecated aliases until 0.11.0); the claude route waits
briefly on the store's single-flight probe (503-pending past that) and
the codex route writes live listings back to the store.
- Server: model-options loads go unified-first and fall back to the
legacy route on 404; the hosts API forwards the host's error string.
- Deletions: static claude alias table, gateway-discovery artifact
machinery, the configured-union composition, and the host's codex
catalog reconstruction lanes.
- Tests isolate the catalog store per test so suites cannot touch the
developer's real ~/.omnigent cache or boot real harness CLIs.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Confirm model switches through the harness before claiming them
Switching is now ask -> pending -> harness-confirmed on every lane:
- Runner (claude): after typing /model, verify against the statusLine
snapshot the forwarder already polls (10s budget) — expected spellings
come from the session's own catalog rows; a pane that never switches
answers 503 so the server surfaces the swallowed-dialog case instead
of the row silently claiming the pick. A shape with no snapshot stays
unverifiable-but-successful.
- Runner (codex): the awaited thread/settings/update RPC is the
confirmation; a missing Codex bridge now answers 503 instead of a
silent 204, and plan-mode updates re-assert the reported model rather
than a stale override.
- Server: the visible model_change_not_applied notice now carries the
runner's own detail string.
- Web: a transient pendingModelChange marks the ask (spinner beside the
composer chip); the chip keeps the reported model until session.model
confirms, and the not-applied error (or a switch/bind) settles the
indicator.
Also repairs tests/runner/conftest.py's REAL_CLAUDE_LAUNCH_CATALOG
export, which the previous commit's lint autofix stripped after its
consumers had been verified.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Name the true Default on every picker and pin gateway models
- The web's claude lane keeps the catalog's isDefault marker and all
Default rows (new-chat select, its summary line, the session gear)
label through the one shared defaultModelLabel — both harnesses now
read "Default (X)" where X is the model a bare launch actually runs.
- Provider entries' models map (the existing flat tier keys — opus,
sonnet, haiku, fable — beside default) now pins the claude alias
vocabulary: the launch env derives ANTHROPIC_DEFAULT_*_MODEL from the
declared tiers, models.default pins its own family when that family
has no explicit key, and the declared ids become the config's
routable set. Aliases on gateway endpoints resolve inside the
gateway's own catalog instead of falling back to canonical Anthropic
ids the gateway rejects.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Make the live model-flow rig trustworthy end to end
The step-8 before/after pass surfaced six defects in the live CUJ
scaffolding itself; with them fixed the suite is 16/16 green against the
implementation branch and red against main in the documented modes:
- Drop tests/conftest.py's inherited OMNIGENT_DISABLE_CATALOG_LOOKUP for
the rig's spawned server/host — the databricks catalog was empty only
inside the rig.
- Wait for the post-create navigation with page.wait_for_url: the sync
Playwright API pumps events only inside playwright calls, so the old
time.sleep poll read a page.url frozen at the landing route forever.
- Re-read a model dropdown opened during the host's boot-probe warm-up
until rows (or the settled error) appear.
- Resolve a codex session's private CODEX_HOME through the bridge's own
state.json (the dir is named by a runner-generated bridge id).
- Row 17: no Escape after a Radix select pick (it closes the whole gear
modal), pick an effort that differs from the machine's global default,
and poll for persistence while the browser is still open (the save's
model leg holds until the pane confirms, so the effort PATCH is sent
by the page seconds later).
- Snapshot and restore ~/.claude/settings.json around the suite: the
real /model switches run under the real HOME and Claude persists every
switch as the developer's global default.
Also: useHostModelOptions retries with backoff so a picker opened during
the boot-probe warm-up fills in when the single-flight probe completes
instead of pinning the transient error until reopened.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Wait out the boot-probe warm-up when reading Default labels
The codex launch pin now resolves through live Unity-Catalog discovery
(seconds on a cold host), so a landing model label read immediately
after opening the config renders the bare sentinel while the web's
retry loop is still filling the catalog. Give the label the same
warm-up wait the dropdown read already has.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Mark a model ask pending before its PATCH, not after
The PATCH is held open while the runner drives and confirms the switch,
so the harness's session.model report usually arrives before the PATCH
resolves. Setting pendingModelChange from the response overwrote the
report's clear and stranded the spinner until the hygiene timer. The ask
is now marked pending up front (and cleared if the PATCH throws); a
store test pins the report-beats-PATCH ordering.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Type full model ids verbatim on unpinned claude sessions
Picking a full-id catalog row (e.g. the appended default
'Opus 4.8 (1M context)') stepped down to '/model opus' — the family
alias resolves to claude's CURRENT generation, silently switching to
Opus 5 instead. The confirm layer caught and surfaced it; the
translation now passes claude-* full ids verbatim on envs with no alias
pins (claude's /model accepts full ids — the probe resolves them the
same way), while pinned envs keep exact-pin-or-fail-loud.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Defer mid-turn model switches instead of failing them
A /model typed during an active turn queues in Claude's composer and
applies when the turn settles — past the 10s confirm window — so the
runner surfaced a false 'was not switched' error for a switch still on
its way, and the injection's short dialog watch could leave the late
confirm dialog parked on the pane.
The confirm loop now answers the switch dialog whenever it renders
inside the window, and a timeout with the pane mid-turn answers success:
a detached watcher keeps answering the late dialog (hint-matched Enter
only, never blind; bounded budget) and the forwarder's verbatim report
settles the picker when the switch lands. An idle-pane timeout — the
genuine swallowed case — still fails loud.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Mark a provider launch pin as the claude catalog default
Default launches on provider-configured shapes pass --model
<config.model> explicitly, so the pin — not the enumeration run's own
model — is what a Default launch actually runs. The gateway-entry shape
(one pinned alias row) went unmarked when the enumeration reported no
default, leaving the picker on a bare 'Default'. Subscription shapes
keep the enumeration-derived marker, and the appended default row only
borrows the probe's printed label when it names the same model.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Merge origin/main and green the CI suite
The merge brought main's error-pill restyle, per-frame create refactor,
and codex live UC discovery alongside this branch's model-flow work.
Fixes to keep every suite green:
- Restore the SimpleNamespace import main added to tests/host/test_connect
(the merge dropped it → ruff F821 + 2 NameErrors).
- Regenerate openapi.json for the reported_model wording (session.model
event + llm_model field descriptions).
- Update the smart-routing-create catalog tests to expect the unified
/model-options route the server now asks first (legacy alias is the
404 fallback).
- Update the runner pending-catalog test: a provider shape's launch pin
is appended as the marked default row.
- Adapt row15's e2e to main's collapsed error pill (expand to read the
detail); move the routed-modal test's seed to llm_model (routed models
arrive as the harness report now); pick codex landing options by their
decorated display name (codex options now render display names like
claude — the design's decorated-rows contract).
- Seed the in-session gear from the session's request only before any
harness report exists, so a routed session names its model.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Make row15's error-pill expand retry-safe under suite load
The single headline click could land before the disclosure handler was
wired when the suite ran the pill under load, leaving the detail
collapsed and the assertion timing out. Retry the expand until the
detail shows — same as a person clicking again.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Null-guard the native model-id fold so cursor rows don't blank the page
findNativeModelOption's fold fallback (added when the codex catalog fold
moved client-side) called comparableModelId on option.model/id without a
null guard. Cursor picker rows arrive as { id, displayName } with
model === null on the wire (typed model?: string), and the
option.model !== undefined check let null through — comparableModelId(null)
then threw 'Cannot read properties of null (reading trim)' during render,
blanking the whole chat page for any cursor-native session.
comparableModelId is now null-safe (empty fold never matches a real
target) and the fallback rejects null ids/models. Regression test covers
a cursor-shaped options list with null models.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Add a per-harness e2e_ui render-smoke matrix (all native pickers)
One hermetic case per native model-picker harness (claude, codex, cursor,
kiro, opencode, pi): shape a seeded session as that harness with its
realistic model_options — including rows with an explicit model: null
(cursor/kiro/opencode's real wire shape, typed model?: string) and a
hostile null-id row — then render the session, open the gear, and assert
the composer renders, the model control lists the rows, and no uncaught
null-deref fires.
The null-model harness cases carry a non-matching model_override so the
model-id fold actually runs (an exact-id match would return before it),
which is precisely the path that once blanked the page. Validated red on
the pre-fix bundle (cursor/kiro/opencode crash) and green after — the
coverage the earlier per-harness tests missed by using model-omitted
(undefined) rows instead of the null shape.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
201 lines
8.7 KiB
Python
201 lines
8.7 KiB
Python
"""Owned static model tables for Smart Routing.
|
|
|
|
Pre-launch picker listings carry no static stand-ins anymore — the live
|
|
harness probes (see ``omnigent.host.connect``) are their source of truth.
|
|
What remains here is the router's operational data: rankings, arm menus,
|
|
and probed exclusions that no discovery API can provide.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from omnigent.onboarding.provider_config import SUBSCRIPTION_KIND
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StaticModelFallback:
|
|
"""A release-curated model list with auditable ownership and provenance."""
|
|
|
|
model_ids: tuple[str, ...]
|
|
owner: str
|
|
provenance: str
|
|
discovery_gap: str
|
|
|
|
|
|
#: Curated preference ORDER for codex's current arms — a ranking hint only
|
|
#: (preferred first), consumed by the Databricks live-discovery ranker to
|
|
#: sort servable ids. It never invents picker rows: ids absent from the live
|
|
#: listing are simply not ranked by it.
|
|
_CODEX_ARM_PREFERENCE = StaticModelFallback(
|
|
model_ids=("gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.5"),
|
|
owner="Databricks model discovery (omnigent.databricks_model_discovery)",
|
|
provenance="Omnigent's release-curated Codex arm ordering",
|
|
discovery_gap="a workspace listing ranks models by neither recency nor capability",
|
|
)
|
|
|
|
_STATIC_MODEL_FALLBACKS: dict[tuple[str, str], StaticModelFallback] = {
|
|
(SUBSCRIPTION_KIND, "codex"): _CODEX_ARM_PREFERENCE,
|
|
}
|
|
|
|
|
|
def static_model_fallback(provider_kind: str, cli: str) -> StaticModelFallback | None:
|
|
"""Return the owned fallback table for a provider kind and CLI, if registered."""
|
|
return _STATIC_MODEL_FALLBACKS.get((provider_kind, cli))
|
|
|
|
|
|
#: Codex's launch default when nothing else names a model. The bundled
|
|
#: OpenAI catalog's newest row is a bare family alias (``gpt-5.6``) that
|
|
#: codex rejects, so a codex launch defaults to a concrete variant from
|
|
#: codex's own catalog — dotted spelling, since the Databricks hyphenated
|
|
#: form 400s against codex's own backend.
|
|
_CODEX_LAUNCH_DEFAULT = StaticModelFallback(
|
|
model_ids=("gpt-5.6-sol",),
|
|
owner="Codex native launch (omnigent.inner.codex_executor)",
|
|
provenance="codex's own catalog slug for the cheapest current arm",
|
|
discovery_gap=(
|
|
"the launch default is resolved before any app-server probe can "
|
|
"answer, and codex rejects the bundled catalog's newest row (a bare "
|
|
"family alias)"
|
|
),
|
|
)
|
|
|
|
CODEX_DEFAULT_MODEL = _CODEX_LAUNCH_DEFAULT.model_ids[0]
|
|
|
|
|
|
# ── Smart Routing ───────────────────────────────────────────────────────────
|
|
#
|
|
# The router's static tables. A live per-session catalog wins wherever one is
|
|
# in reach (``omnigent.server.smart_routing.fetch_runner_models``) and a
|
|
# deployment's ``routing.*`` settings override each table wholesale; these are
|
|
# what a router that can reach neither falls back to.
|
|
|
|
_SMART_ROUTING_FALLBACKS: dict[str, StaticModelFallback] = {
|
|
"claude_ladder": StaticModelFallback(
|
|
model_ids=(
|
|
"databricks-claude-haiku-4-5",
|
|
"databricks-claude-sonnet-4-6",
|
|
"databricks-claude-sonnet-5",
|
|
"databricks-claude-opus-4-8",
|
|
),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="AI Gateway Claude serving endpoints, cheapest → most powerful",
|
|
discovery_gap=(
|
|
"the router picks before a session's live model catalog is reachable, "
|
|
"and a gateway listing ranks models by neither cost nor capability"
|
|
),
|
|
),
|
|
"gpt_ladder": StaticModelFallback(
|
|
model_ids=(
|
|
"databricks-gpt-5-4-nano",
|
|
"databricks-gpt-5-4-mini",
|
|
"databricks-gpt-5-4",
|
|
"databricks-gpt-5-5",
|
|
),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="AI Gateway GPT serving endpoints, cheapest → most powerful",
|
|
discovery_gap=(
|
|
"the router picks before a session's live model catalog is reachable, "
|
|
"and a gateway listing ranks models by neither cost nor capability"
|
|
),
|
|
),
|
|
"pi_ladder": StaticModelFallback(
|
|
model_ids=(
|
|
"databricks-gpt-5-4-nano",
|
|
"databricks-claude-haiku-4-5",
|
|
"databricks-gpt-5-4-mini",
|
|
"databricks-claude-sonnet-4-6",
|
|
"databricks-claude-sonnet-5",
|
|
"databricks-gpt-5-4",
|
|
"databricks-gpt-5-5",
|
|
"databricks-claude-opus-4-8",
|
|
),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="the Claude and GPT ladders interleaved by cost, for multi-model pi",
|
|
discovery_gap=(
|
|
"the router picks before a session's live model catalog is reachable, "
|
|
"and a gateway listing ranks models by neither cost nor capability"
|
|
),
|
|
),
|
|
"current_generation_gpt": StaticModelFallback(
|
|
model_ids=(
|
|
"databricks-glm-5-2",
|
|
"databricks-gpt-5-6-luna",
|
|
"databricks-gpt-5-6-sol",
|
|
),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="the external router's own current arms, offered so a pick keeps its endpoint",
|
|
discovery_gap=(
|
|
"the router picks before a session's live model catalog is reachable, "
|
|
"and a gateway listing ranks models by neither cost nor capability"
|
|
),
|
|
),
|
|
"task_v1_claude_arms": StaticModelFallback(
|
|
model_ids=("claude-opus-4-8", "claude-sonnet-5"),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="the task_v1 router's Claude arm menu, which it requires in full",
|
|
discovery_gap="the router's arm menu is part of its request contract, not a catalog",
|
|
),
|
|
"task_v1_codex_arms": StaticModelFallback(
|
|
model_ids=("glm-5-2", "gpt-5-6-sol", "gpt-5-6-luna"),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="the task_v1 router's codex arm menu, which it requires in full",
|
|
discovery_gap="the router's arm menu is part of its request contract, not a catalog",
|
|
),
|
|
"family_fallbacks": StaticModelFallback(
|
|
model_ids=("claude-sonnet-5", "gpt-5-6-luna"),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="one arm per family (claude, gpt), both frozen members of the task_v1 menus",
|
|
discovery_gap="a workspace that serves no endpoint for the picked arm needs a pinned one",
|
|
),
|
|
"pi_excluded": StaticModelFallback(
|
|
model_ids=(
|
|
"databricks-claude-haiku-4-5",
|
|
"databricks-gpt-5-5",
|
|
"databricks-gpt-5-5-pro",
|
|
"databricks-gpt-5-6-luna",
|
|
"databricks-gpt-5-6-terra",
|
|
"databricks-gpt-5-6-sol",
|
|
),
|
|
owner="Smart Routing (omnigent.server.smart_routing)",
|
|
provenance="probed: pi's own gateway 400s on each of these",
|
|
discovery_gap="a gateway listing advertises these without pi's request-shape limits",
|
|
),
|
|
"codex_catalog_clone_source": StaticModelFallback(
|
|
model_ids=("gpt-5.6-luna",),
|
|
owner="Codex extended catalog (omnigent.inner.codex_executor)",
|
|
provenance="codex's own bundled catalog slug for the cheapest current arm",
|
|
discovery_gap="codex's bundled catalog carries no entry for a gateway-only arm to clone",
|
|
),
|
|
}
|
|
|
|
#: Claude serving endpoints the router ranks, cheapest → most powerful.
|
|
SMART_ROUTING_CLAUDE_LADDER = _SMART_ROUTING_FALLBACKS["claude_ladder"].model_ids
|
|
|
|
#: GPT serving endpoints the router ranks, cheapest → most powerful.
|
|
SMART_ROUTING_GPT_LADDER = _SMART_ROUTING_FALLBACKS["gpt_ladder"].model_ids
|
|
|
|
#: Both ladders interleaved by cost, for the multi-model pi harness.
|
|
SMART_ROUTING_PI_LADDER = _SMART_ROUTING_FALLBACKS["pi_ladder"].model_ids
|
|
|
|
#: The router's own current gpt-family arms (GLM included), offered as
|
|
#: candidates so a routed arm resolves to its own endpoint.
|
|
SMART_ROUTING_CURRENT_GENERATION_GPT = _SMART_ROUTING_FALLBACKS["current_generation_gpt"].model_ids
|
|
|
|
#: The ``task_v1`` router's Claude arm menu, most powerful first.
|
|
SMART_ROUTING_TASK_V1_CLAUDE_ARMS = _SMART_ROUTING_FALLBACKS["task_v1_claude_arms"].model_ids
|
|
|
|
#: The ``task_v1`` router's codex arm menu.
|
|
SMART_ROUTING_TASK_V1_CODEX_ARMS = _SMART_ROUTING_FALLBACKS["task_v1_codex_arms"].model_ids
|
|
|
|
#: One fixed fallback arm per family, ordered ``(claude, gpt)``.
|
|
SMART_ROUTING_FAMILY_FALLBACKS = _SMART_ROUTING_FALLBACKS["family_fallbacks"].model_ids
|
|
|
|
#: Models pi's own gateway rejects, so the router may not pick them under pi.
|
|
SMART_ROUTING_PI_EXCLUDED = _SMART_ROUTING_FALLBACKS["pi_excluded"].model_ids
|
|
|
|
#: The codex catalog entry a gateway-only arm is cloned from.
|
|
CODEX_CATALOG_CLONE_SOURCE_SLUG = _SMART_ROUTING_FALLBACKS["codex_catalog_clone_source"].model_ids[
|
|
0
|
|
]
|