Skip to content

fix(opencode): list every model the gateway serves, not three - #394

Open
larsmoan wants to merge 5 commits into
databricks:mainfrom
larsmoan:fix/opencode-capability-model-discovery
Open

fix(opencode): list every model the gateway serves, not three#394
larsmoan wants to merge 5 commits into
databricks:mainfrom
larsmoan:fix/opencode-capability-model-discovery

Conversation

@larsmoan

@larsmoan larsmoan commented Aug 25, 2026

Copy link
Copy Markdown

Problem

ucode opencode offered 3 Anthropic models on a workspace whose Unity AI Gateway serves 17 chat models.

Discovery guessed each model's capability from its name, on a premise stated in discover_model_services:

Family bucketing is by name substring because the model-services API does not expose per-model API dialects.

That is false. Every /api/2.1/unity-catalog/model-services entry carries supported_api_types. Name guessing then failed three ways at once:

Cause Effect on a 19-model workspace
Claude collapsed to the newest id per family (candidates[0]) — a shape that exists for Claude Code's ANTHROPIC_DEFAULT_<FAMILY>_MODEL env vars 10 Claude → 3. Sonnet 4, 4.5, 4.6 hidden
OSS bucket matched a hardcoded ("kimi-", "glm-", "deepseek-") allowlist llama, qwen, gemma dropped
GPT models discovered into codex_models, then never routed to any OpenCode provider gpt-oss-120b/20b dropped

The third is pure waste: databricks-oss is already @ai-sdk/openai against /ai-gateway/mlflow/v1, the dialect those models speak.

Change

Route on the dialect each model advertises, and keep every model. OpenCode picks from a list, so sonnet 4, 4.5, 4.6 and 5 all belong in it — which one to use is the user's call.

Benefits beyond the count: a newly-added family (Grok, a new Llama, a new Qwen) appears with no code change, and embedding-only models are excluded because they advertise no chat dialect rather than because their names happen to miss an allowlist.

Each bucket keys on the dialect its provider really sends

Bucket npm package sends gate
databricks-anthropic @ai-sdk/anthropic /v1/messages anthropic/v1/messages
databricks-google @ai-sdk/google :generateContent gemini/v1/generateContent
databricks-oss @ai-sdk/openai /responses mlflow/v1/responses

oss gates on responses, not the more widely advertised chat/completions, because that is what OpenCode actually sends: it builds the provider with createOpenAI, and @ai-sdk/openai's default model instance has used the Responses API since AI SDK 5. The gateway enforces supported_api_types exactly — verified live:

Model supported_api_types POST /responses POST /chat/completions
system.ai.gpt-oss-120b chat + responses 200 200
system.ai.qwen35-122b-a10b chat + responses 200 200
system.ai.meta-llama-3-3-70b-instruct chat only 400 BAD_REQUEST 200

Gating on chat/completions would list the two chat-only Llama 3.x models and let them fail on first use — the same "listed but broken" bug this PR removes. Losing them is the deliberate trade.

No ranking

The lists carry no ordering opinion: OpenCode receives a JSON object per provider and orders its own picker, so a sort here never reached the UI. It only fed default_model, which needs one answer to start a session — that now reads the Claude family pin ucode already computes for Claude Code.

Result on a real workspace

19 system.ai models → 3 before, 15 after:

  • anthropic (10): every claude-opus-4-5 … opus-5, sonnet-4 … sonnet-5, haiku-4-5
  • oss (5): gpt-oss-120b, gpt-oss-20b, llama-4-maverick, qwen35-122b-a10b, gemma-3-12b
  • excluded: 2 embedding models, 2 chat-only Llama 3.x
  • boots on system.ai.claude-opus-5, unchanged

No Gemini and no Grok appear because this workspace serves neither. They will appear automatically once it does.

Notes

  • list_model_services keeps its signature; it is now the names-only view of the new list_model_service_api_types. The per-process cache holds the capability map, so a ucode setup run still pages the listing once — asserted by a test.
  • The new bucket build is guarded on the warm cache, so a workspace without UC model-services does not pay the listing's retry budget twice; it falls back to the previous AI-Gateway-derived shape.
  • Scope is the OpenCode discovery path. claude, codex, gemini, pi and copilot are untouched, as is the admin-manifest path in managed_resolve._bucket_by_provider, which still buckets by name because a manifest carries names only.

Testing

uv run pytest — 1953 passed, 41 skipped. uv run ruff check . clean.

New coverage in TestDiscoverOpencodeModels: dialect routing, every Claude version kept, OSS models outside the old allowlist, chat-only models excluded, embedding-only models excluded, Claude preferring its native dialect, fable opt-in, HTTP failure reason, no-routable-dialect reason, single listing walk, no cache entry on failure. Plus default_model boot-model tests in TestOpencodeDefaultModel.

The e2e fixture had to be fixed first

tests/conftest.py::e2e_state built opencode_models from the AI-Gateway per-family listings — the path this PR replaces — although its docstring claims it mirrors configure_shared_state. On the test workspace those routes list nothing, so the fixture produced {} and the one e2e test that covers this change skipped silently:

SKIPPED tests/test_e2e.py:784: No OpenCode models available on this workspace

The opencode binary was installed. The skip came from the empty fixture, not from a missing binary.

Source Result on the test workspace
fetch_ai_gateway_claude_models {}
fetch_gemini_models []
fetch_codex_models []
→ old fixture opencode_models {}test skips
discover_opencode_models (this PR) 15 models

e2e_state now mirrors the production order: dialect routing first, per-family shape as the fallback.

E2E on a real workspace

UCODE_TEST_WORKSPACE=… uv run pytest tests/test_e2e.py17 passed, 12 skipped, 1 pre-existing failure (16 passed before the fixture fix).

TestOpencodeLaunch::test_launch_opencode_per_model now runs and passes. It launches every discovered model against the live gateway:

[opencode-per-model] 15 models to test
  oss/system.ai.gemma-3-12b              OK (10.2s)
  oss/system.ai.gpt-oss-120b             OK (9.4s)
  oss/system.ai.gpt-oss-20b              OK (10.4s)
  oss/system.ai.llama-4-maverick         OK (10.3s)
  oss/system.ai.qwen35-122b-a10b         OK (10.1s)
  anthropic/system.ai.claude-haiku-4-5   OK (9.8s)
  anthropic/system.ai.claude-opus-4-5    OK (10.6s)
  anthropic/system.ai.claude-opus-4-6    OK (9.8s)
  anthropic/system.ai.claude-opus-4-7    OK (9.9s)
  anthropic/system.ai.claude-opus-4-8    OK (10.1s)
  anthropic/system.ai.claude-opus-5      OK (9.9s)
  anthropic/system.ai.claude-sonnet-4    OK (10.3s)
  anthropic/system.ai.claude-sonnet-4-5  OK (9.5s)
  anthropic/system.ai.claude-sonnet-4-6  OK (10.8s)
  anthropic/system.ai.claude-sonnet-5    OK (13.6s)
PASSED in 158s

That covers the models the old code dropped: both gpt-oss models it discovered into codex_models and never routed, and the llama-4-maverick, qwen35-122b-a10b and gemma-3-12b models the hardcoded kimi-/glm-/deepseek- allowlist excluded. The responses-over-chat/completions gate holds live.

Pre-existing failures, unrelated to this PR

  • tests/test_e2e.py::TestModelDiscovery::test_fetch_claude_models_returns_dict — the workspace answers GET /ai-gateway/anthropic/v1/models with HTTP 200 and {'data': []}. Fails identically on origin/main, verified in a clean worktree. This is also the cause of most remaining e2e skips.
  • tests/test_e2e_user_agent.py::TestOpencodeUserAgent::test_user_agent_arrives_at_gateway — fails identically on origin/main, verified in a clean worktree.

Both are worth a separate issue. On this workspace the AI-Gateway per-family fallback at cli.py:653 is effectively dead: if the UC model-services listing ever failed here, OpenCode would get zero models, because the fallback has nothing to fall back to.

OpenCode showed only 3 Claude models on a workspace serving 15 usable
ones. Discovery guessed each model's capability from its name, on the
premise -- stated in `discover_model_services` -- that "the
model-services API does not expose per-model API dialects". It does:
every entry carries `supported_api_types`. Name guessing then failed
three ways at once:

- Claude collapsed to the newest id per family, because that shape
  exists for Claude Code's `ANTHROPIC_DEFAULT_<FAMILY>_MODEL` env vars.
  OpenCode picks from a list, so sonnet 4, 4.5 and 4.6 were hidden for
  no reason.
- OSS models were matched against a hardcoded `kimi-/glm-/deepseek-`
  allowlist, which drops llama, qwen and gemma.
- GPT models were discovered into `codex_models` and then never routed,
  although `databricks-oss` is already `@ai-sdk/openai` against
  `/ai-gateway/mlflow/v1` -- the dialect they speak.

Route on the advertised dialect instead, and keep every model. A new
family (Grok, a new Llama) now appears with no code change, and
embedding-only models are excluded because they advertise no chat
dialect rather than because their names miss an allowlist.

Each bucket keys on the dialect its provider's npm package really
sends, so a listed model always works. That makes `oss` key on
`mlflow/v1/responses`, not the more widely advertised
`mlflow/v1/chat/completions`: `@ai-sdk/openai` has sent Responses from
its default model instance since AI SDK 5, and the gateway answers
`/responses` with HTTP 400 for a chat-only model such as
`system.ai.meta-llama-3-3-70b-instruct`.

The model lists carry no ranking -- OpenCode receives a JSON object per
provider and orders its own picker. Only the starting model needs one
answer, so `default_model` reads the Claude family pin ucode already
computes rather than ranking the lists.
@larsmoan
larsmoan marked this pull request as ready for review August 25, 2026 23:08
Copilot AI lite review requested due to automatic review settings August 25, 2026 23:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates ucode opencode model discovery to stop name-guessing capabilities and instead surface all workspace-served chat models by routing them to OpenCode providers using each model-service’s advertised supported_api_types. This aligns OpenCode’s picker with what the Unity AI Gateway can actually serve and avoids listing models that would fail at runtime due to dialect mismatch.

Changes:

  • Introduces capability-aware model-service listing (list_model_service_api_types) and a new discover_opencode_models that buckets models by advertised API dialect (Anthropic Messages, Gemini generateContent, OpenAI Responses).
  • Updates CLI shared-state configuration to prefer dialect-based OpenCode discovery when UC model-services listing is available/cached, with fallback to the prior family/name-based discovery shape.
  • Adjusts OpenCode default boot model selection to prefer the existing Claude family pin over incidental list ordering, and adds focused tests covering the new discovery and boot behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/ucode/databricks.py Adds API-dialect-based model-service parsing and OpenCode discovery; expands model-services cache to include supported_api_types.
src/ucode/cli.py Uses cached UC model-services listing to build OpenCode buckets from dialects, with fallback to existing per-family discovery outputs.
src/ucode/agents/opencode.py Changes default OpenCode boot model selection to prefer Claude family pins before falling back to list order.
tests/test_databricks.py Adds unit tests for dialect routing, inclusion/exclusion rules, caching behavior, and new discovery failure reasons.
tests/test_agent_opencode.py Adds tests ensuring OpenCode boots on the Claude pin (when present and routable) rather than list ordering.
Suppressed comments (1)

src/ucode/databricks.py:1835

  • In discover_opencode_models()’s docstring, the oss bullet says “the rest”, but the implementation only includes models advertising mlflow/v1/responses and intentionally excludes chat-only models (and any other non-routable dialects). Wording this as “the rest” is misleading for callers reading the contract.
    - ``anthropic`` — models advertising ``anthropic/v1/messages`` (`@ai-sdk/anthropic`).
    - ``gemini`` — models advertising ``gemini/v1/generateContent`` (`@ai-sdk/google`).
    - ``oss`` — the rest, advertising ``mlflow/v1/responses`` (`@ai-sdk/openai`).


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/ucode/databricks.py
`e2e_state` built `opencode_models` from the AI-Gateway per-family
listings, which is the path the previous commit replaced. The fixture
docstring claims it mirrors `configure_shared_state`. That stopped being
true.

The cost is a silent skip. On a workspace whose AI-Gateway per-family
routes list nothing, while UC model-services lists every model, the
fixture produced an empty dict. `test_launch_opencode_per_model` then
skipped with "No OpenCode models available on this workspace". The one
e2e test that covers this change never ran.

Mirror the production order instead. Route by advertised dialect first,
and keep the per-family shape as the fallback. The launch test now
exercises every model the workspace serves.
Review feedback on two docstrings that overstate what the code does.

`has_cached_model_services` said "only successes are cached". That reads
as if an HTTP 200 with zero models were cached. It is not.
`list_model_service_api_types` reports an empty listing as a failure, so
the predicate means "the listing returned models".

`discover_opencode_models` called `oss` "the rest", but the bucket gates
on `mlflow/v1/responses`. A model that advertises only `chat/completions`
is dropped, so "the rest" contradicted the gate in the same sentence.
Copilot AI review requested due to automatic review settings August 25, 2026 23:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 26, 2026 09:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/ucode/cli.py
Comment on lines +653 to +656
if not opencode_models:
# No UC model-services on this workspace: build the buckets from whatever the per-family
# AI Gateway listings returned.
if claude_models:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants