fix(opencode): list every model the gateway serves, not three - #394
Open
larsmoan wants to merge 5 commits into
Open
fix(opencode): list every model the gateway serves, not three#394larsmoan wants to merge 5 commits into
larsmoan wants to merge 5 commits into
Conversation
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
marked this pull request as ready for review
August 25, 2026 23:08
There was a problem hiding this comment.
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 newdiscover_opencode_modelsthat 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, theossbullet says “the rest”, but the implementation only includes models advertisingmlflow/v1/responsesand 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.
`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.
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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ucode opencodeoffered 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:That is false. Every
/api/2.1/unity-catalog/model-servicesentry carriessupported_api_types. Name guessing then failed three ways at once:candidates[0]) — a shape that exists for Claude Code'sANTHROPIC_DEFAULT_<FAMILY>_MODELenv vars("kimi-", "glm-", "deepseek-")allowlistcodex_models, then never routed to any OpenCode providerThe third is pure waste:
databricks-ossis already@ai-sdk/openaiagainst/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
databricks-anthropic@ai-sdk/anthropic/v1/messagesanthropic/v1/messagesdatabricks-google@ai-sdk/google:generateContentgemini/v1/generateContentdatabricks-oss@ai-sdk/openai/responsesmlflow/v1/responsesossgates onresponses, not the more widely advertisedchat/completions, because that is what OpenCode actually sends: it builds the provider withcreateOpenAI, and@ai-sdk/openai's default model instance has used the Responses API since AI SDK 5. The gateway enforcessupported_api_typesexactly — verified live:supported_api_types/responses/chat/completionssystem.ai.gpt-oss-120bsystem.ai.qwen35-122b-a10bsystem.ai.meta-llama-3-3-70b-instructGating on
chat/completionswould 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.aimodels → 3 before, 15 after:anthropic(10): everyclaude-opus-4-5 … opus-5,sonnet-4 … sonnet-5,haiku-4-5oss(5):gpt-oss-120b,gpt-oss-20b,llama-4-maverick,qwen35-122b-a10b,gemma-3-12bsystem.ai.claude-opus-5, unchangedNo Gemini and no Grok appear because this workspace serves neither. They will appear automatically once it does.
Notes
list_model_serviceskeeps its signature; it is now the names-only view of the newlist_model_service_api_types. The per-process cache holds the capability map, so aucode setuprun still pages the listing once — asserted by a test.claude,codex,gemini,piandcopilotare untouched, as is the admin-manifest path inmanaged_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. Plusdefault_modelboot-model tests inTestOpencodeDefaultModel.The e2e fixture had to be fixed first
tests/conftest.py::e2e_statebuiltopencode_modelsfrom the AI-Gateway per-family listings — the path this PR replaces — although its docstring claims it mirrorsconfigure_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:The
opencodebinary was installed. The skip came from the empty fixture, not from a missing binary.fetch_ai_gateway_claude_models{}fetch_gemini_models[]fetch_codex_models[]opencode_models{}→ test skipsdiscover_opencode_models(this PR)e2e_statenow 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.py— 17 passed, 12 skipped, 1 pre-existing failure (16 passed before the fixture fix).TestOpencodeLaunch::test_launch_opencode_per_modelnow runs and passes. It launches every discovered model against the live gateway:That covers the models the old code dropped: both
gpt-ossmodels it discovered intocodex_modelsand never routed, and thellama-4-maverick,qwen35-122b-a10bandgemma-3-12bmodels the hardcodedkimi-/glm-/deepseek-allowlist excluded. Theresponses-over-chat/completionsgate holds live.Pre-existing failures, unrelated to this PR
tests/test_e2e.py::TestModelDiscovery::test_fetch_claude_models_returns_dict— the workspace answersGET /ai-gateway/anthropic/v1/modelswith HTTP 200 and{'data': []}. Fails identically onorigin/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 onorigin/main, verified in a clean worktree.Both are worth a separate issue. On this workspace the AI-Gateway per-family fallback at
cli.py:653is 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.