Skip to content

Commit 3312a2d

Browse files
committed
fix: declare long-context Claude and GPT metadata
(cherry picked from commit a6359d3)
1 parent f625796 commit 3312a2d

6 files changed

Lines changed: 244 additions & 3 deletions

File tree

src/ucode/agents/claude.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,12 @@ def _maybe_add_1m_suffix(model: str) -> str:
251251
family, major_raw, minor_raw, _ = match.groups()
252252
major = int(major_raw)
253253
minor = int(minor_raw)
254+
# 1M-token context window support: Opus from 4.6, Sonnet from 4.5 (Sonnet
255+
# 4.5 gained the 1M beta window before Opus did). Without the `[1m]`
256+
# suffix the gateway serves these models with the default 200k context, so
257+
# gate each family at its own floor rather than a shared version.
254258
should_suffix = (family == "opus" and (major, minor) >= (4, 6)) or (
255-
family == "sonnet" and (major, minor) >= (4, 6)
259+
family == "sonnet" and (major, minor) >= (4, 5)
256260
)
257261
return f"{model}[1m]" if should_suffix else model
258262

src/ucode/agents/pi.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from __future__ import annotations
3939

4040
import os
41+
import re
4142
import signal
4243
import subprocess
4344
import threading
@@ -55,7 +56,9 @@
5556
from ucode.databricks import (
5657
TOKEN_REFRESH_INTERVAL_SECONDS,
5758
build_pi_base_urls,
59+
claude_model_token_limits,
5860
get_databricks_token,
61+
gpt_model_token_limits,
5962
model_is_reasoning,
6063
model_token_limits,
6164
)
@@ -117,6 +120,26 @@ def _resolve_model_selector(
117120
return model
118121

119122

123+
def _pi_claude_model_entry(model_id: str) -> dict:
124+
"""Build a Claude entry with explicit limits.
125+
126+
Databricks model ids do not match Pi's built-in Anthropic ids, so a bare
127+
custom entry silently gets Pi's 128k context / 4k output defaults.
128+
"""
129+
limits = claude_model_token_limits(model_id)
130+
entry: dict = {
131+
"id": model_id,
132+
"reasoning": True,
133+
"input": ["text", "image"],
134+
"contextWindow": limits["context"],
135+
"maxTokens": limits["output"],
136+
}
137+
normalized = model_id.lower().replace(".", "-")
138+
if re.search(r"claude-(?:opus|sonnet)-4-(?:[6-9]|[1-9]\d)", normalized):
139+
entry["compat"] = {"forceAdaptiveThinking": True}
140+
return entry
141+
142+
120143
def _pi_oss_model_entry(model_id: str) -> dict:
121144
"""Build a Pi mlflow model entry enriched from the shared limits/reasoning
122145
tables: `reasoning:true` for reasoning models (Pi renders their streamed
@@ -135,6 +158,23 @@ def _pi_oss_model_entry(model_id: str) -> dict:
135158
return entry
136159

137160

161+
def _pi_gpt_model_entry(model_id: str) -> dict:
162+
"""Build a Pi openai (codex) model entry with `contextWindow`/`maxTokens`
163+
from `databricks.gpt_model_token_limits`. GPT ids aren't in Pi's built-in
164+
catalog, so without an explicit window Pi falls back to a small default and
165+
truncates long sessions."""
166+
limits = gpt_model_token_limits(model_id)
167+
entry: dict = {
168+
"id": model_id,
169+
"contextWindow": limits["context"],
170+
"maxTokens": limits["output"],
171+
}
172+
if "gpt-5" in model_id.lower().replace(".", "-"):
173+
entry["reasoning"] = True
174+
entry["input"] = ["text", "image"]
175+
return entry
176+
177+
138178
def render_overlay(
139179
model: str,
140180
token: str,
@@ -163,7 +203,7 @@ def render_overlay(
163203
# the legacy beta header instead when this is false.
164204
"compat": {"supportsEagerToolInputStreaming": False},
165205
"headers": ua_headers,
166-
"models": [{"id": m} for m in claude_ids],
206+
"models": [_pi_claude_model_entry(m) for m in claude_ids],
167207
}
168208
keys.append(["providers", "databricks-claude"])
169209
if codex_models:
@@ -173,7 +213,7 @@ def render_overlay(
173213
"apiKey": token,
174214
"authHeader": True,
175215
"headers": ua_headers,
176-
"models": [{"id": m} for m in codex_models],
216+
"models": [_pi_gpt_model_entry(m) for m in codex_models],
177217
}
178218
keys.append(["providers", "databricks-openai"])
179219
if gemini_models:

src/ucode/databricks.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,76 @@ def model_token_limits(model_id: str) -> dict[str, int] | None:
12261226
return None
12271227

12281228

1229+
# Pi treats every custom model without explicit metadata as 128k context / 4k
1230+
# output. Gateway ids are custom ids (not Pi's built-ins), so preserve the
1231+
# upstream windows explicitly. Entries are ordered most-specific first after
1232+
# normalizing dotted OpenAI ids and hyphenated Databricks ids to one form.
1233+
# GPT-5.6 Sol/Terra/Luna support the opt-in 1.05M window; 272k is merely Pi's
1234+
# built-in short-context pricing default, not the model's hard context limit.
1235+
_GPT_TOKEN_LIMITS: tuple[tuple[str, dict[str, int]], ...] = (
1236+
("gpt-5-6-sol", {"context": 1_050_000, "output": 128_000}),
1237+
("gpt-5-6-terra", {"context": 1_050_000, "output": 128_000}),
1238+
("gpt-5-6-luna", {"context": 1_050_000, "output": 128_000}),
1239+
("gpt-5-5-pro", {"context": 1_050_000, "output": 128_000}),
1240+
("gpt-5-4-pro", {"context": 1_050_000, "output": 128_000}),
1241+
("gpt-5-5", {"context": 272_000, "output": 128_000}),
1242+
("gpt-5-4-mini", {"context": 400_000, "output": 128_000}),
1243+
("gpt-5-4-nano", {"context": 400_000, "output": 128_000}),
1244+
("gpt-5-4", {"context": 272_000, "output": 128_000}),
1245+
("gpt-5", {"context": 400_000, "output": 128_000}),
1246+
("gpt-4-1", {"context": 1_047_576, "output": 32_768}),
1247+
("gpt-4o", {"context": 128_000, "output": 16_384}),
1248+
("gpt-4-turbo", {"context": 128_000, "output": 4_096}),
1249+
("gpt-4", {"context": 8_192, "output": 8_192}),
1250+
)
1251+
_GPT_FALLBACK_LIMITS = {"context": 128_000, "output": 16_384}
1252+
1253+
1254+
def _normalized_foundation_model_id(model_id: str) -> str:
1255+
"""Strip route prefixes and normalize dotted versions to hyphens."""
1256+
tail = model_id.split("/")[-1]
1257+
if tail.startswith("system.ai."):
1258+
tail = tail[len("system.ai.") :]
1259+
if tail.startswith("databricks-"):
1260+
tail = tail[len("databricks-") :]
1261+
return tail.lower().replace(".", "-")
1262+
1263+
1264+
def gpt_model_token_limits(model_id: str) -> dict[str, int]:
1265+
"""Return Pi metadata limits for a GPT (codex/openai) gateway model."""
1266+
tail = _normalized_foundation_model_id(model_id)
1267+
for family, limits in _GPT_TOKEN_LIMITS:
1268+
if tail.startswith(family):
1269+
return dict(limits)
1270+
return dict(_GPT_FALLBACK_LIMITS)
1271+
1272+
1273+
_CLAUDE_FALLBACK_LIMITS = {"context": 200_000, "output": 64_000}
1274+
1275+
1276+
def claude_model_token_limits(model_id: str) -> dict[str, int]:
1277+
"""Return Pi metadata limits for a Claude gateway model.
1278+
1279+
Claude gateway ids are custom to Pi, so omitting these fields silently
1280+
applies Pi's 128k/4k custom-model defaults. Current 1M families mirror
1281+
Anthropic's model metadata: Opus >=4.6 and Sonnet >=4.5. Sonnet 4.5 has a
1282+
64k output cap; later 1M models have 128k output.
1283+
"""
1284+
tail = _normalized_foundation_model_id(model_id)
1285+
match = re.match(r"claude-(opus|sonnet|haiku)-(\d+)-(\d+)", tail)
1286+
if not match:
1287+
return dict(_CLAUDE_FALLBACK_LIMITS)
1288+
family, major_raw, minor_raw = match.groups()
1289+
version = (int(major_raw), int(minor_raw))
1290+
if family == "opus" and version >= (4, 6):
1291+
return {"context": 1_000_000, "output": 128_000}
1292+
if family == "sonnet" and version >= (4, 6):
1293+
return {"context": 1_000_000, "output": 128_000}
1294+
if family == "sonnet" and version >= (4, 5):
1295+
return {"context": 1_000_000, "output": 64_000}
1296+
return dict(_CLAUDE_FALLBACK_LIMITS)
1297+
1298+
12291299
def _model_service_id(service: dict) -> str | None:
12301300
"""Extract the `system.ai.<model-name>` id from one model-service entry.
12311301

tests/test_agent_claude.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,30 @@ def test_adds_1m_suffix_for_sonnet_4_6_and_later(self):
4848
overlay["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"] == "databricks-claude-sonnet-4-7[1m]"
4949
)
5050

51+
def test_adds_1m_suffix_for_sonnet_4_5(self):
52+
# Sonnet 4.5 supports the 1M context window (its 1M beta shipped before
53+
# Opus's), so it must get the [1m] suffix even though it predates the
54+
# Opus 4.6 floor.
55+
overlay, _ = claude.render_overlay(
56+
WS, "s4", claude_models={"sonnet": "databricks-claude-sonnet-4-5"}
57+
)
58+
assert (
59+
overlay["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"] == "databricks-claude-sonnet-4-5[1m]"
60+
)
61+
62+
def test_does_not_add_1m_suffix_for_sonnet_4_4(self):
63+
overlay, _ = claude.render_overlay(
64+
WS, "s4", claude_models={"sonnet": "databricks-claude-sonnet-4-4"}
65+
)
66+
assert overlay["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"] == "databricks-claude-sonnet-4-4"
67+
68+
def test_does_not_add_1m_suffix_for_opus_4_5(self):
69+
# Opus's 1M window starts at 4.6, so 4.5 stays on the default context.
70+
overlay, _ = claude.render_overlay(
71+
WS, "s4", claude_models={"opus": "databricks-claude-opus-4-5"}
72+
)
73+
assert overlay["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"] == "databricks-claude-opus-4-5"
74+
5175
def test_does_not_add_1m_suffix_for_haiku(self):
5276
overlay, _ = claude.render_overlay(
5377
WS, "s4", claude_models={"haiku": "databricks-claude-haiku-4-6"}

tests/test_agent_pi.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,55 @@ def test_openai_provider_uses_openai_responses(self):
7878
assert provider["api"] == "openai-responses"
7979
assert provider["baseUrl"] == f"{WS}/ai-gateway/codex/v1"
8080

81+
def test_gpt56_sol_model_entry_pins_1m_context(self):
82+
# Gateway ids are custom to Pi, so explicit metadata is required to
83+
# avoid its 128k custom-model default.
84+
overlay, _ = _overlay("gpt-5-6-sol", codex_models=["gpt-5-6-sol"])
85+
entry = overlay["providers"]["databricks-openai"]["models"][0]
86+
assert entry["id"] == "gpt-5-6-sol"
87+
assert entry["contextWindow"] == 1_050_000
88+
assert entry["maxTokens"] == 128_000
89+
assert entry["reasoning"] is True
90+
assert entry["input"] == ["text", "image"]
91+
92+
def test_gpt_model_entries_use_model_specific_windows(self):
93+
overlay, _ = _overlay(
94+
"system.ai.gpt-5-2",
95+
codex_models=[
96+
"system.ai.gpt-5-2",
97+
"databricks-gpt-5-4-nano",
98+
"databricks-gpt-5-6-sol",
99+
],
100+
)
101+
windows = {
102+
m["id"]: m["contextWindow"] for m in overlay["providers"]["databricks-openai"]["models"]
103+
}
104+
assert windows == {
105+
"system.ai.gpt-5-2": 400_000,
106+
"databricks-gpt-5-4-nano": 400_000,
107+
"databricks-gpt-5-6-sol": 1_050_000,
108+
}
109+
110+
def test_claude_entries_pin_limits_and_capabilities(self):
111+
overlay, _ = _overlay(
112+
"databricks-claude-opus-4-8",
113+
claude_models={
114+
"opus": "databricks-claude-opus-4-8",
115+
"sonnet": "system.ai.claude-sonnet-4-5",
116+
"haiku": "databricks-claude-haiku-4-5",
117+
},
118+
)
119+
entries = {m["id"]: m for m in overlay["providers"]["databricks-claude"]["models"]}
120+
opus = entries["databricks-claude-opus-4-8"]
121+
assert opus["contextWindow"] == 1_000_000
122+
assert opus["maxTokens"] == 128_000
123+
assert opus["reasoning"] is True
124+
assert opus["input"] == ["text", "image"]
125+
assert opus["compat"] == {"forceAdaptiveThinking": True}
126+
assert entries["system.ai.claude-sonnet-4-5"]["contextWindow"] == 1_000_000
127+
assert entries["system.ai.claude-sonnet-4-5"]["maxTokens"] == 64_000
128+
assert entries["databricks-claude-haiku-4-5"]["contextWindow"] == 200_000
129+
81130
def test_gemini_provider_uses_google_generative_ai(self):
82131
overlay, _ = _overlay("gemini-2", gemini_models=["gemini-2"])
83132
provider = overlay["providers"]["databricks-gemini"]

tests/test_databricks.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,60 @@ def test_embedding_model_returns_none_not_fallback(self):
202202
assert db_mod.model_token_limits("system.ai.qwen3-embedding-0-6b") is None
203203

204204

205+
class TestGptModelTokenLimits:
206+
def test_gpt56_sol_serves_1m_context_across_id_forms(self):
207+
for model_id in (
208+
"gpt-5.6-sol",
209+
"system.ai.gpt-5-6-sol",
210+
"databricks-gpt-5-6-sol",
211+
"databricks-openai/gpt-5.6-sol",
212+
):
213+
assert db_mod.gpt_model_token_limits(model_id) == {
214+
"context": 1_050_000,
215+
"output": 128_000,
216+
}
217+
218+
def test_gpt5_windows_are_model_specific(self):
219+
assert db_mod.gpt_model_token_limits("system.ai.gpt-5-2")["context"] == 400_000
220+
assert db_mod.gpt_model_token_limits("databricks-gpt-5-4")["context"] == 272_000
221+
assert db_mod.gpt_model_token_limits("databricks-gpt-5-4-nano")["context"] == 400_000
222+
assert db_mod.gpt_model_token_limits("gpt-5.5-pro")["context"] == 1_050_000
223+
224+
def test_gpt41_window(self):
225+
assert db_mod.gpt_model_token_limits("databricks-gpt-4-1") == {
226+
"context": 1_047_576,
227+
"output": 32_768,
228+
}
229+
230+
def test_unknown_gpt_uses_conservative_floor(self):
231+
assert db_mod.gpt_model_token_limits("gpt-6-turbo") == {
232+
"context": 128_000,
233+
"output": 16_384,
234+
}
235+
236+
237+
class TestClaudeModelTokenLimits:
238+
def test_1m_models_and_output_caps(self):
239+
assert db_mod.claude_model_token_limits("databricks-claude-opus-4-8") == {
240+
"context": 1_000_000,
241+
"output": 128_000,
242+
}
243+
assert db_mod.claude_model_token_limits("system.ai.claude-sonnet-4-5") == {
244+
"context": 1_000_000,
245+
"output": 64_000,
246+
}
247+
assert db_mod.claude_model_token_limits("claude-sonnet-4-6[1m]") == {
248+
"context": 1_000_000,
249+
"output": 128_000,
250+
}
251+
252+
def test_older_and_unknown_claude_use_200k_floor(self):
253+
expected = {"context": 200_000, "output": 64_000}
254+
assert db_mod.claude_model_token_limits("claude-opus-4-5") == expected
255+
assert db_mod.claude_model_token_limits("claude-haiku-4-5") == expected
256+
assert db_mod.claude_model_token_limits("claude-future") == expected
257+
258+
205259
class TestModelIsReasoning:
206260
def test_reasoning_families(self):
207261
assert db_mod.model_is_reasoning("system.ai.glm-5-2") is True

0 commit comments

Comments
 (0)