|
22 | 22 | from concurrent.futures import ( |
23 | 23 | TimeoutError as FutureTimeoutError, |
24 | 24 | ) |
| 25 | +from dataclasses import dataclass |
25 | 26 | from pathlib import Path |
26 | 27 | from typing import Literal, cast, overload |
27 | 28 | from urllib import error as urllib_error |
@@ -1232,6 +1233,106 @@ def model_token_limits(model_id: str) -> dict[str, int] | None: |
1232 | 1233 | return None |
1233 | 1234 |
|
1234 | 1235 |
|
| 1236 | +# Pi treats every custom model without explicit metadata as 128k context / 4k |
| 1237 | +# output. Gateway ids are custom ids (not Pi's built-ins), so preserve the |
| 1238 | +# upstream windows explicitly. Entries are ordered most-specific first after |
| 1239 | +# normalizing dotted OpenAI ids and hyphenated Databricks ids to one form. |
| 1240 | +# GPT-5.6 Sol/Terra/Luna support the opt-in 1.05M window; 272k is merely Pi's |
| 1241 | +# built-in short-context pricing default, not the model's hard context limit. |
| 1242 | +_GPT_TOKEN_LIMITS: tuple[tuple[str, dict[str, int]], ...] = ( |
| 1243 | + ("gpt-5-6-sol", {"context": 1_050_000, "output": 128_000}), |
| 1244 | + ("gpt-5-6-terra", {"context": 1_050_000, "output": 128_000}), |
| 1245 | + ("gpt-5-6-luna", {"context": 1_050_000, "output": 128_000}), |
| 1246 | + ("gpt-5-5-pro", {"context": 1_050_000, "output": 128_000}), |
| 1247 | + ("gpt-5-4-pro", {"context": 1_050_000, "output": 128_000}), |
| 1248 | + ("gpt-5-5", {"context": 272_000, "output": 128_000}), |
| 1249 | + ("gpt-5-4-mini", {"context": 400_000, "output": 128_000}), |
| 1250 | + ("gpt-5-4-nano", {"context": 400_000, "output": 128_000}), |
| 1251 | + ("gpt-5-4", {"context": 272_000, "output": 128_000}), |
| 1252 | + ("gpt-5", {"context": 400_000, "output": 128_000}), |
| 1253 | + ("gpt-4-1", {"context": 1_047_576, "output": 32_768}), |
| 1254 | + ("gpt-4o", {"context": 128_000, "output": 16_384}), |
| 1255 | + ("gpt-4-turbo", {"context": 128_000, "output": 4_096}), |
| 1256 | + ("gpt-4", {"context": 8_192, "output": 8_192}), |
| 1257 | +) |
| 1258 | +_GPT_FALLBACK_LIMITS = {"context": 128_000, "output": 16_384} |
| 1259 | + |
| 1260 | + |
| 1261 | +def _normalized_foundation_model_id(model_id: str) -> str: |
| 1262 | + """Strip route prefixes and normalize dotted versions to hyphens.""" |
| 1263 | + tail = model_id.split("/")[-1] |
| 1264 | + if tail.startswith("system.ai."): |
| 1265 | + tail = tail[len("system.ai.") :] |
| 1266 | + if tail.startswith("databricks-"): |
| 1267 | + tail = tail[len("databricks-") :] |
| 1268 | + return tail.lower().replace(".", "-") |
| 1269 | + |
| 1270 | + |
| 1271 | +def gpt_model_token_limits(model_id: str) -> dict[str, int]: |
| 1272 | + """Return Pi metadata limits for a GPT (codex/openai) gateway model.""" |
| 1273 | + tail = _normalized_foundation_model_id(model_id) |
| 1274 | + for family, limits in _GPT_TOKEN_LIMITS: |
| 1275 | + if tail.startswith(family): |
| 1276 | + return dict(limits) |
| 1277 | + return dict(_GPT_FALLBACK_LIMITS) |
| 1278 | + |
| 1279 | + |
| 1280 | +@dataclass(frozen=True) |
| 1281 | +class ClaudeModelCapabilities: |
| 1282 | + context: int |
| 1283 | + output: int |
| 1284 | + supports_1m: bool = False |
| 1285 | + force_adaptive_thinking: bool = False |
| 1286 | + |
| 1287 | + |
| 1288 | +_CLAUDE_FALLBACK_CAPABILITIES = ClaudeModelCapabilities(context=200_000, output=64_000) |
| 1289 | +_CLAUDE_MODEL_RE = re.compile(r"^claude-(fable|opus|sonnet|haiku)-(\d+)(?:-(\d+))?") |
| 1290 | + |
| 1291 | + |
| 1292 | +def claude_model_capabilities(model_id: str) -> ClaudeModelCapabilities: |
| 1293 | + """Return the shared Claude capability policy for every agent. |
| 1294 | +
|
| 1295 | + Opus gained the opt-in 1M window in 4.6; Sonnet gained it in 4.5. |
| 1296 | + Sonnet 4.5 retains a 64k output cap, while later 1M tiers use 128k. |
| 1297 | + Opus 4.5, Haiku, Fable, and unrecognized ids intentionally use the |
| 1298 | + conservative 200k fallback until separately verified. |
| 1299 | + """ |
| 1300 | + tail = _normalized_foundation_model_id(model_id) |
| 1301 | + match = _CLAUDE_MODEL_RE.match(tail) |
| 1302 | + if not match: |
| 1303 | + return _CLAUDE_FALLBACK_CAPABILITIES |
| 1304 | + family, major_raw, minor_raw = match.groups() |
| 1305 | + version = (int(major_raw), int(minor_raw or 0)) |
| 1306 | + if family == "opus" and version >= (4, 6): |
| 1307 | + return ClaudeModelCapabilities( |
| 1308 | + context=1_000_000, |
| 1309 | + output=128_000, |
| 1310 | + supports_1m=True, |
| 1311 | + force_adaptive_thinking=True, |
| 1312 | + ) |
| 1313 | + if family == "sonnet" and version >= (4, 6): |
| 1314 | + return ClaudeModelCapabilities( |
| 1315 | + context=1_000_000, |
| 1316 | + output=128_000, |
| 1317 | + supports_1m=True, |
| 1318 | + force_adaptive_thinking=True, |
| 1319 | + ) |
| 1320 | + if family == "sonnet" and version >= (4, 5): |
| 1321 | + return ClaudeModelCapabilities(context=1_000_000, output=64_000, supports_1m=True) |
| 1322 | + return _CLAUDE_FALLBACK_CAPABILITIES |
| 1323 | + |
| 1324 | + |
| 1325 | +def claude_model_supports_1m(model_id: str) -> bool: |
| 1326 | + """Whether Claude Code should request the model's opt-in ``[1m]`` tier.""" |
| 1327 | + return claude_model_capabilities(model_id).supports_1m |
| 1328 | + |
| 1329 | + |
| 1330 | +def claude_model_token_limits(model_id: str) -> dict[str, int]: |
| 1331 | + """Return Pi metadata limits from the shared Claude capability policy.""" |
| 1332 | + capabilities = claude_model_capabilities(model_id) |
| 1333 | + return {"context": capabilities.context, "output": capabilities.output} |
| 1334 | + |
| 1335 | + |
1235 | 1336 | def _model_service_id(service: dict) -> str | None: |
1236 | 1337 | """Extract the `system.ai.<model-name>` id from one model-service entry. |
1237 | 1338 |
|
|
0 commit comments