Skip to content

Commit bc65755

Browse files
lukecameronclaude
authored andcommitted
fix(discovery): fall back to serving-endpoints for OSS models
OSS model discovery only queried UC model-services for `system.ai.*` ids and, unlike the claude/gemini/codex families, had no AI-Gateway fallback. On workspaces that expose OSS foundation models as regular `databricks-*` serving endpoints (rather than `system.ai.*` UC model-services), `oss_models` came back empty and pi/opencode never offered GLM, Qwen, Gemma, inkling, etc. Add `discover_oss_models`, which lists foundation-model endpoints advertising `mlflow/v1/chat/completions` and keeps the OSS chat families via the existing `_is_oss_chat_model` filter (the family filter is required because Claude/Gemini endpoints can advertise the same dialect on some workspaces). Wire it as the fallback in cli.py, mirroring the other three families. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b326a4f commit bc65755

3 files changed

Lines changed: 107 additions & 0 deletions

File tree

src/ucode/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
discover_codex_models,
3838
discover_gemini_models,
3939
discover_model_services,
40+
discover_oss_models,
4041
ensure_ai_gateway_v2,
4142
ensure_databricks_auth,
4243
ensure_pat_bearer,
@@ -402,6 +403,8 @@ def configure_shared_state(
402403
codex_models, codex_reason = discover_codex_models(workspace, token)
403404
if want_oss:
404405
oss_models, oss_reason = ms_oss, ms_reason
406+
if not oss_models:
407+
oss_models, oss_reason = discover_oss_models(workspace, token)
405408
if claude_models:
406409
opencode_models["anthropic"] = list(claude_models.values())
407410
if gemini_models:

src/ucode/databricks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,6 +2092,34 @@ def discover_codex_models(workspace: str, token: str) -> tuple[list[str], str |
20922092
return discover_endpoints_with_api_type(workspace, token, "openai/v1/responses")
20932093

20942094

2095+
def discover_oss_models(workspace: str, token: str) -> tuple[list[str], str | None]:
2096+
"""Discover OSS chat models served as AI Gateway foundation-model endpoints.
2097+
2098+
Fallback for workspaces that don't register OSS foundation models as
2099+
`system.ai.*` UC model-services (see `discover_model_services`): those
2100+
workspaces expose the same models as regular `databricks-*` serving
2101+
endpoints instead. Lists every endpoint advertising the
2102+
`mlflow/v1/chat/completions` dialect, then keeps only the OSS chat families
2103+
(`_is_oss_chat_model`) — on some workspaces the Claude/Gemini endpoints also
2104+
advertise that dialect, so the family filter is what separates the OSS
2105+
cohort from them. Mirrors the AI-Gateway fallback the other families use
2106+
when the UC model-services listing is empty.
2107+
"""
2108+
endpoints, reason = discover_endpoints_with_api_type(
2109+
workspace, token, "mlflow/v1/chat/completions"
2110+
)
2111+
if not endpoints:
2112+
return [], reason
2113+
oss = [e for e in endpoints if _is_oss_chat_model(e)]
2114+
if oss:
2115+
return oss, None
2116+
sample = ", ".join(endpoints[:5])
2117+
return [], (
2118+
"foundation-models exposing `mlflow/v1/chat/completions` matched no OSS "
2119+
f"chat family (got: {sample})"
2120+
)
2121+
2122+
20952123
def fetch_gemini_models(workspace: str, token: str) -> list[str]:
20962124
models, _ = discover_gemini_models(workspace, token)
20972125
return models

tests/test_databricks.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,82 @@ def test_codex_discovery_keeps_alphabetical_order(self, monkeypatch):
924924
assert models == ["databricks-gpt-4-1", "databricks-gpt-5-2-codex"]
925925

926926

927+
def _mlflow_chat_payload(names, *, api_type="mlflow/v1/chat/completions", v2=True):
928+
return {
929+
"endpoints": [
930+
{
931+
"name": name,
932+
"config": {
933+
"served_entities": [
934+
{
935+
"foundation_model": {
936+
"ai_gateway_v2_supported": v2,
937+
"api_types": [api_type],
938+
}
939+
}
940+
]
941+
},
942+
}
943+
for name in names
944+
]
945+
}
946+
947+
948+
class TestDiscoverOssModels:
949+
def test_finds_oss_endpoints_via_foundation_models(self, monkeypatch):
950+
# Mirrors a workspace with no system.ai UC model-services: OSS models are
951+
# plain databricks-* serving endpoints under the mlflow chat dialect.
952+
payload = _mlflow_chat_payload(
953+
[
954+
"databricks-glm-5-2",
955+
"databricks-inkling",
956+
"databricks-qwen35-122b-a10b",
957+
"databricks-gemma-3-12b",
958+
]
959+
)
960+
monkeypatch.setattr(db_mod, "_http_get_json", lambda url, token: (payload, None))
961+
962+
models, reason = db_mod.discover_oss_models(WS, "token")
963+
964+
assert reason is None
965+
assert "databricks-glm-5-2" in models
966+
assert set(models) == {
967+
"databricks-glm-5-2",
968+
"databricks-inkling",
969+
"databricks-qwen35-122b-a10b",
970+
"databricks-gemma-3-12b",
971+
}
972+
973+
def test_excludes_claude_and_gemini_sharing_the_mlflow_dialect(self, monkeypatch):
974+
# On some workspaces every foundation model advertises the mlflow chat
975+
# dialect, so the api_type filter alone is too broad — the OSS family
976+
# filter must drop Claude/Gemini and keep only the OSS cohort.
977+
payload = _mlflow_chat_payload(
978+
[
979+
"databricks-claude-opus-4-8",
980+
"databricks-gemini-2-5-pro",
981+
"databricks-glm-5-2",
982+
"databricks-qwen3-embedding-0-6b",
983+
]
984+
)
985+
monkeypatch.setattr(db_mod, "_http_get_json", lambda url, token: (payload, None))
986+
987+
models, reason = db_mod.discover_oss_models(WS, "token")
988+
989+
assert reason is None
990+
assert models == ["databricks-glm-5-2"]
991+
992+
def test_reports_reason_when_no_oss_family_matches(self, monkeypatch):
993+
payload = _mlflow_chat_payload(["databricks-claude-opus-4-8"])
994+
monkeypatch.setattr(db_mod, "_http_get_json", lambda url, token: (payload, None))
995+
996+
models, reason = db_mod.discover_oss_models(WS, "token")
997+
998+
assert models == []
999+
assert reason is not None
1000+
assert "no OSS" in reason
1001+
1002+
9271003
class TestResolvePatToken:
9281004
def test_reads_pat_profile_token_from_cfg(self, monkeypatch, tmp_path):
9291005
cfg = tmp_path / "databrickscfg"

0 commit comments

Comments
 (0)