Skip to content

Commit 446a24a

Browse files
authored
support glm-5-2 on opencode (#186)
* Broaden OSS model bucket to catch all non-claude/gpt/gemini families discover_model_services bucketed OSS models with an allowlist ("kimi-" only), so GLM/Llama/Qwen/DeepSeek and any future OSS family were discovered from UC model-services but dropped on the floor — never reaching opencode_models, so they couldn't be listed or selected in OpenCode (e.g. system.ai.glm-5-2). Bucket OSS by exclusion instead: anything not claude/gpt/gemini, minus non-chat model services (embeddings, rerankers) that can't back a chat agent. New OSS families now surface without a code change. Co-authored-by: Isaac * Bound GLM token limits (200k context / 25k output) for OSS-serving agents The Databricks gateway caps GLM's max output (completion) tokens and rejects requests above it. These limits are a property of the model + its /ai-gateway/mlflow/v1 route, not of any one agent, so expose them as a single model_token_limits() helper (family->limits table) in databricks.py. Each agent translates the limits into its own config dialect. OpenCode's per-model schema is strict (`additionalProperties: false`) and its `limit` object requires BOTH context and output — supplying only output made the whole opencode.json invalid ("Configuration is invalid"). The table now always provides both fields, so OpenCode pins `limit: {context, output}` and clamps `max_tokens` to a gateway-accepted value. Adding a model/limit is a one-line edit shared across opencode/pi/copilot. Co-authored-by: Isaac * Restrict OSS model bucket to kimi and glm allowlist The previous catch-all bucketed any non-claude/gpt/gemini model (llama, qwen, deepseek, ...) into OSS, offering families we haven't validated. Go back to an explicit allowlist, but include both kimi and glm. _OSS_MODEL_FAMILIES = ("kimi-", "glm-") replaces the exclusion logic and its non-chat marker filter (embeddings/rerankers never match the allowlist, so no filtering is needed). Co-authored-by: Isaac * Revert test_no_matching_families_reports_sample to llama sample Under the kimi/glm allowlist, llama-4-maverick again matches no family, so the original test (asserting it's reported in the "no families" reason) holds without the embedding-service substitute. Co-authored-by: Isaac * Trim verbose comments on OSS allowlist Co-authored-by: Isaac
1 parent a0a0d1c commit 446a24a

4 files changed

Lines changed: 103 additions & 5 deletions

File tree

src/ucode/agents/opencode.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
TOKEN_REFRESH_INTERVAL_SECONDS,
2121
build_opencode_base_urls,
2222
get_databricks_token,
23+
model_token_limits,
2324
)
2425
from ucode.state import mark_tool_managed, save_state
2526
from ucode.telemetry import agent_version, ucode_version
@@ -69,6 +70,20 @@ def _resolve_model_selector(model: str, opencode_models: dict[str, list[str]]) -
6970
return model
7071

7172

73+
def _oss_model_overlay(model: str, ua_header: dict[str, str]) -> dict:
74+
"""Per-model overlay for an OSS model entry.
75+
76+
All OSS models carry the User-Agent header; models with known token limits
77+
also pin `limit` (context + output) so OpenCode clamps `max_tokens` to a
78+
value the gateway accepts. OpenCode's schema requires both fields together,
79+
so the limits table always supplies both."""
80+
overlay: dict = {"headers": ua_header}
81+
limits = model_token_limits(model)
82+
if limits is not None:
83+
overlay["limit"] = limits
84+
return overlay
85+
86+
7287
def render_overlay(
7388
model: str,
7489
token: str,
@@ -130,7 +145,7 @@ def render_overlay(
130145
"apiKey": token,
131146
"headers": auth_headers,
132147
},
133-
"models": {m: {"headers": ua_header} for m in oss_models},
148+
"models": {m: _oss_model_overlay(m, ua_header) for m in oss_models},
134149
}
135150
keys.append(["provider", "databricks-oss"])
136151

src/ucode/databricks.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,34 @@ def build_auth_shell_command(
10961096
# Databricks-managed foundation models under `system.ai`.
10971097
_MODEL_SERVICE_REQUIRED_PREFIX = "system.ai."
10981098

1099+
# Supported OSS chat families, matched by name substring. Add an entry to
1100+
# support a new family.
1101+
_OSS_MODEL_FAMILIES = ("kimi-", "glm-")
1102+
1103+
# Per-family token limits (context window + max output tokens). These are a
1104+
# property of the model + its `/ai-gateway/mlflow/v1` route (the gateway rejects
1105+
# requests whose output exceeds the cap), not of any one agent — so every agent
1106+
# that serves OSS models reads this single table and translates it into its own
1107+
# config dialect. Both fields are provided because agents like OpenCode require
1108+
# context and output together. Keyed by family substring; add an entry to bound
1109+
# a new model.
1110+
_MODEL_TOKEN_LIMITS: dict[str, dict[str, int]] = {
1111+
# GLM-4.6: 200k context, but the gateway caps output well below the model's
1112+
# native 128k — pin 25k so requests aren't rejected.
1113+
"glm": {"context": 200_000, "output": 25_000},
1114+
}
1115+
1116+
1117+
def model_token_limits(model_id: str) -> dict[str, int] | None:
1118+
"""Return ``{"context": ..., "output": ...}`` limits for ``model_id``, or None.
1119+
1120+
Matches by family substring (e.g. any ``*glm*`` id). None means the model
1121+
has no known limits and the agent should not pin any."""
1122+
for family, limits in _MODEL_TOKEN_LIMITS.items():
1123+
if family in model_id:
1124+
return dict(limits)
1125+
return None
1126+
10991127

11001128
def _model_service_id(service: dict) -> str | None:
11011129
"""Extract the `system.ai.<model-name>` id from one model-service entry.
@@ -1223,7 +1251,8 @@ def discover_model_services(
12231251

12241252
codex_models = [m for m in ids if "gpt-" in m]
12251253
gemini_models = sorted([m for m in ids if "gemini-" in m], key=model_version_sort_key)
1226-
oss_models = [m for m in ids if "kimi-" in m]
1254+
1255+
oss_models = [m for m in ids if any(family in m for family in _OSS_MODEL_FAMILIES)]
12271256

12281257
if not (claude_models or codex_models or gemini_models or oss_models):
12291258
sample = ", ".join(ids[:5])

tests/test_agent_opencode.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ def test_oss_base_url(self):
9393
options = overlay["provider"]["databricks-oss"]["options"]
9494
assert options["baseURL"] == f"{WS}/ai-gateway/mlflow/v1"
9595

96+
def test_glm_gets_token_limits(self):
97+
models = {"oss": ["system.ai.glm-5-2"]}
98+
overlay, _ = opencode.render_overlay("system.ai.glm-5-2", "tok", _base_urls(), models)
99+
glm = overlay["provider"]["databricks-oss"]["models"]["system.ai.glm-5-2"]
100+
# OpenCode's schema requires both context and output on `limit`.
101+
assert glm["limit"] == {"context": 200000, "output": 25000}
102+
103+
def test_non_glm_oss_model_has_no_output_cap(self):
104+
models = {"oss": ["system.ai.kimi-k2-7-code"]}
105+
overlay, _ = opencode.render_overlay(
106+
"system.ai.kimi-k2-7-code", "tok", _base_urls(), models
107+
)
108+
kimi = overlay["provider"]["databricks-oss"]["models"]["system.ai.kimi-k2-7-code"]
109+
assert "limit" not in kimi
110+
96111
def test_token_in_api_key(self):
97112
models = {"anthropic": ["claude-sonnet"]}
98113
overlay, _ = opencode.render_overlay("claude-sonnet", "mytoken", _base_urls(), models)

tests/test_databricks.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,23 @@ def _model_service(model_id: str) -> dict:
140140
return {"name": f"model-services/{model_id}"}
141141

142142

143+
class TestModelTokenLimits:
144+
def test_glm_is_capped(self):
145+
assert db_mod.model_token_limits("system.ai.glm-5-2") == {
146+
"context": 200_000,
147+
"output": 25_000,
148+
}
149+
150+
def test_glm_matches_any_version(self):
151+
assert db_mod.model_token_limits("system.ai.glm-4-6-flash") == {
152+
"context": 200_000,
153+
"output": 25_000,
154+
}
155+
156+
def test_uncapped_model_returns_none(self):
157+
assert db_mod.model_token_limits("system.ai.kimi-k2-7-code") is None
158+
159+
143160
class TestDiscoverModelServices:
144161
def test_buckets_families_by_name(self, monkeypatch):
145162
payload = {
@@ -151,6 +168,7 @@ def test_buckets_families_by_name(self, monkeypatch):
151168
_model_service("system.ai.gemini-2-5-flash"),
152169
_model_service("system.ai.gemini-3-5-flash"),
153170
_model_service("system.ai.kimi-k2-7-code"),
171+
_model_service("system.ai.glm-5-2"),
154172
_model_service("system.ai.llama-4-maverick"),
155173
]
156174
}
@@ -169,9 +187,30 @@ def test_buckets_families_by_name(self, monkeypatch):
169187
assert codex == ["system.ai.gpt-5"]
170188
# Gemini ordered newest-first via the shared sort key.
171189
assert gemini[0] == "system.ai.gemini-3-5-flash"
172-
assert oss == ["system.ai.kimi-k2-7-code"]
173-
# llama is not bucketed into any of the four families.
174-
assert "system.ai.llama-4-maverick" not in codex + gemini + oss
190+
# kimi and glm are the allowlisted OSS families; llama is not.
191+
assert oss == ["system.ai.glm-5-2", "system.ai.kimi-k2-7-code"]
192+
193+
def test_oss_allowlist_drops_unsupported_families(self, monkeypatch):
194+
# Only kimi/glm are allowlisted; other families are dropped.
195+
payload = {
196+
"model_services": [
197+
_model_service("system.ai.glm-5-2"),
198+
_model_service("system.ai.kimi-k2-7-code"),
199+
_model_service("system.ai.qwen-3-coder"),
200+
_model_service("system.ai.deepseek-v3"),
201+
_model_service("system.ai.gte-large-embed"),
202+
_model_service("system.ai.bge-reranker-v2"),
203+
]
204+
}
205+
monkeypatch.setattr(
206+
db_mod, "_http_get_json", lambda url, token, timeout=10: (payload, None)
207+
)
208+
209+
claude, codex, gemini, oss, reason = db_mod.discover_model_services(WS, "token")
210+
211+
assert reason is None
212+
assert (claude, codex, gemini) == ({}, [], [])
213+
assert oss == ["system.ai.glm-5-2", "system.ai.kimi-k2-7-code"]
175214

176215
def test_paginates_via_next_page_token(self, monkeypatch):
177216
pages = {

0 commit comments

Comments
 (0)