gdmcode 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. src/voice/voice_loop.py +156 -0
@@ -0,0 +1,459 @@
1
+ """Model definitions — IDs, pricing, context windows for all supported providers.
2
+
3
+ All values are named constants. No magic strings or numbers anywhere else in
4
+ the codebase should reference model IDs or pricing directly.
5
+
6
+ Pricing as of April 2026 (USD per 1M tokens unless noted).
7
+ Providers:
8
+ - xAI Grok — primary, best models, native web_search tool
9
+ - Google Gemini — fallback, free tier (Flash), easy signup for everyone
10
+ - OpenAI Codex — optional, o3/o4-mini for reasoning, GPT-4.1 for cheap bulk
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from dataclasses import dataclass, replace
16
+
17
+ __all__ = [
18
+ "Provider",
19
+ "ModelTier",
20
+ "PROXY_DEFAULT_URL",
21
+ "ModelDef",
22
+ "MODELS",
23
+ "SCOUT",
24
+ "CODER",
25
+ "THINKER",
26
+ "REASONER",
27
+ "DEBATE",
28
+ "STANDARD",
29
+ "GEMINI_SCOUT",
30
+ "GEMINI_CODER",
31
+ "GEMINI_THINKER",
32
+ "GEMINI_REASONER",
33
+ "CODEX_SCOUT",
34
+ "CODEX_CODER",
35
+ "CODEX_THINKER",
36
+ "CODEX_REASONER",
37
+ "CODEX_DEBATE",
38
+ "PROVIDER_BASE_URLS",
39
+ "TOOL_CALL_COSTS",
40
+ "get_model",
41
+ "get_capability",
42
+ "models_for_provider",
43
+ "apply_model_id_overrides",
44
+ ]
45
+
46
+
47
+ class Provider:
48
+ """Supported API providers (string constants for serialisation safety)."""
49
+ GROK = "grok"
50
+ GEMINI = "gemini"
51
+ CODEX = "codex"
52
+ OLLAMA = "ollama"
53
+ VLLM = "vllm"
54
+
55
+
56
+ # Default proxy relay URL — used when no custom URL is configured.
57
+ # The proxy implements the OpenAI-compatible /v1 API and forwards requests
58
+ # to the real providers, allowing access from geo-restricted regions.
59
+ PROXY_DEFAULT_URL: str = "https://proxy.gdm.dev/v1"
60
+
61
+
62
+ class ModelTier:
63
+ """Human-readable tier names used in routing logic."""
64
+ SCOUT = "scout"
65
+ CODER = "coder"
66
+ THINKER = "thinker"
67
+ REASONER = "reasoner"
68
+ DEBATE = "debate"
69
+ STANDARD = "standard"
70
+
71
+
72
+ PROVIDER_BASE_URLS: dict[str, str] = {
73
+ Provider.GROK: "https://api.x.ai/v1",
74
+ Provider.GEMINI: "https://generativelanguage.googleapis.com/v1beta/openai/",
75
+ Provider.CODEX: "https://api.openai.com/v1",
76
+ Provider.OLLAMA: "http://localhost:11434",
77
+ Provider.VLLM: "http://localhost:8000",
78
+ }
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class ModelDef:
83
+ """Complete definition of one model variant."""
84
+
85
+ id: str
86
+ tier: str
87
+ provider: str
88
+ input_per_m: float # USD per 1M input tokens
89
+ output_per_m: float # USD per 1M output tokens
90
+ cached_per_m: float # USD per 1M cached input tokens (0 if unsupported)
91
+ context_window: int # max tokens
92
+ supports_reasoning: bool = False
93
+ supports_tools: bool = True
94
+ supports_web_search: bool = False # native web_search tool (Grok only)
95
+ notes: str = ""
96
+
97
+ def input_cost(self, tokens: int) -> float:
98
+ return (tokens / 1_000_000) * self.input_per_m
99
+
100
+ def output_cost(self, tokens: int) -> float:
101
+ return (tokens / 1_000_000) * self.output_per_m
102
+
103
+ def cached_cost(self, tokens: int) -> float:
104
+ return (tokens / 1_000_000) * self.cached_per_m
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # xAI Grok models
109
+ # ---------------------------------------------------------------------------
110
+
111
+ SCOUT = ModelDef(
112
+ id="grok-4-1-fast-non-reasoning",
113
+ tier=ModelTier.SCOUT,
114
+ provider=Provider.GROK,
115
+ input_per_m=0.20,
116
+ output_per_m=0.50,
117
+ cached_per_m=0.05,
118
+ context_window=2_000_000,
119
+ supports_web_search=True,
120
+ notes="File reads, grep, summarization, compression, Tree-sitter indexing, commit messages",
121
+ )
122
+
123
+ CODER = ModelDef(
124
+ id="grok-4-1-fast-reasoning",
125
+ tier=ModelTier.CODER,
126
+ provider=Provider.GROK,
127
+ input_per_m=0.20,
128
+ output_per_m=0.50,
129
+ cached_per_m=0.05,
130
+ context_window=2_000_000,
131
+ supports_reasoning=True,
132
+ supports_web_search=True,
133
+ notes="Quick code edits, tests, boilerplate, ensemble patching strategies",
134
+ )
135
+
136
+ THINKER = ModelDef(
137
+ id="grok-4.20-0309-non-reasoning",
138
+ tier=ModelTier.THINKER,
139
+ provider=Provider.GROK,
140
+ input_per_m=2.00,
141
+ output_per_m=6.00,
142
+ cached_per_m=0.20,
143
+ context_window=2_000_000,
144
+ supports_web_search=True,
145
+ notes="Standard coding tasks, refactoring, multi-file edits",
146
+ )
147
+
148
+ REASONER = ModelDef(
149
+ id="grok-4.20-0309-reasoning",
150
+ tier=ModelTier.REASONER,
151
+ provider=Provider.GROK,
152
+ input_per_m=2.00,
153
+ output_per_m=6.00,
154
+ cached_per_m=0.20,
155
+ context_window=2_000_000,
156
+ supports_reasoning=True,
157
+ supports_web_search=True,
158
+ notes="Architecture decisions, hard bugs, cross-file design, security reviews",
159
+ )
160
+
161
+ DEBATE = ModelDef(
162
+ id="grok-4.20-multi-agent-0309",
163
+ tier=ModelTier.DEBATE,
164
+ provider=Provider.GROK,
165
+ input_per_m=2.00,
166
+ output_per_m=6.00,
167
+ cached_per_m=0.20,
168
+ context_window=2_000_000,
169
+ supports_reasoning=True,
170
+ supports_web_search=True,
171
+ notes="4-agent internal debate. Requires SuperGrok Heavy. Falls back to REASONER.",
172
+ )
173
+
174
+ STANDARD = ModelDef(
175
+ id="grok-4-0709",
176
+ tier=ModelTier.STANDARD,
177
+ provider=Provider.GROK,
178
+ input_per_m=3.00,
179
+ output_per_m=15.00,
180
+ cached_per_m=0.20,
181
+ context_window=256_000,
182
+ supports_reasoning=True,
183
+ supports_web_search=True,
184
+ notes="Classic Grok 4. Fallback when 2M context not needed.",
185
+ )
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Google Gemini models — OpenAI-compatible endpoint, no extra SDK
189
+ # Flash-8B for Coder tier: ultra-cheap bulk edits and indexing
190
+ # ---------------------------------------------------------------------------
191
+
192
+ GEMINI_SCOUT = ModelDef(
193
+ id="gemini-2.5-flash",
194
+ tier=ModelTier.SCOUT,
195
+ provider=Provider.GEMINI,
196
+ input_per_m=0.15,
197
+ output_per_m=0.60,
198
+ cached_per_m=0.0,
199
+ context_window=1_000_000,
200
+ notes="Free tier: 1K req/day. File reads, summarization, quick answers.",
201
+ )
202
+
203
+ GEMINI_CODER = ModelDef(
204
+ id="gemini-2.5-flash-8b",
205
+ tier=ModelTier.CODER,
206
+ provider=Provider.GEMINI,
207
+ input_per_m=0.075,
208
+ output_per_m=0.30,
209
+ cached_per_m=0.0,
210
+ context_window=1_000_000,
211
+ notes="Ultra-cheap bulk: file indexing, convention extraction, boilerplate edits.",
212
+ )
213
+
214
+ GEMINI_THINKER = ModelDef(
215
+ id="gemini-2.5-pro",
216
+ tier=ModelTier.THINKER,
217
+ provider=Provider.GEMINI,
218
+ input_per_m=1.25,
219
+ output_per_m=10.00,
220
+ cached_per_m=0.0,
221
+ context_window=1_000_000,
222
+ supports_reasoning=True,
223
+ notes="≤200K input tier. Architecture, refactor, hard bugs.",
224
+ )
225
+
226
+ GEMINI_REASONER = ModelDef(
227
+ id="gemini-2.5-pro",
228
+ tier=ModelTier.REASONER,
229
+ provider=Provider.GEMINI,
230
+ input_per_m=2.50,
231
+ output_per_m=10.00,
232
+ cached_per_m=0.0,
233
+ context_window=1_000_000,
234
+ supports_reasoning=True,
235
+ notes=">200K input tier (long context premium). Also handles DEBATE fallback.",
236
+ )
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # OpenAI Codex models — o3/o4-mini for reasoning, GPT-4.1 for bulk
240
+ # o4-mini: best cost/performance reasoning model globally for algorithmic bugs
241
+ # ---------------------------------------------------------------------------
242
+
243
+ CODEX_SCOUT = ModelDef(
244
+ id="gpt-4.1-mini",
245
+ tier=ModelTier.SCOUT,
246
+ provider=Provider.CODEX,
247
+ input_per_m=0.40,
248
+ output_per_m=1.60,
249
+ cached_per_m=0.10,
250
+ context_window=1_047_576,
251
+ notes="Fast, cheap bulk. File reads, summarization, grep-class tasks.",
252
+ )
253
+
254
+ CODEX_CODER = ModelDef(
255
+ id="gpt-4.1",
256
+ tier=ModelTier.CODER,
257
+ provider=Provider.CODEX,
258
+ input_per_m=2.00,
259
+ output_per_m=8.00,
260
+ cached_per_m=0.50,
261
+ context_window=1_047_576,
262
+ supports_tools=True,
263
+ notes="Strong code generation, multi-file edits, test writing.",
264
+ )
265
+
266
+ CODEX_THINKER = ModelDef(
267
+ id="o4-mini",
268
+ tier=ModelTier.THINKER,
269
+ provider=Provider.CODEX,
270
+ input_per_m=1.10,
271
+ output_per_m=4.40,
272
+ cached_per_m=0.275,
273
+ context_window=200_000,
274
+ supports_reasoning=True,
275
+ notes="Best cost/perf reasoning. Exceptional for algorithmic bugs and hard logic.",
276
+ )
277
+
278
+ CODEX_REASONER = ModelDef(
279
+ id="o3",
280
+ tier=ModelTier.REASONER,
281
+ provider=Provider.CODEX,
282
+ input_per_m=2.00,
283
+ output_per_m=8.00,
284
+ cached_per_m=0.50,
285
+ context_window=200_000,
286
+ supports_reasoning=True,
287
+ notes="Deep multi-step reasoning. Architecture decisions, security audits.",
288
+ )
289
+
290
+ CODEX_DEBATE = ModelDef(
291
+ id="o3",
292
+ tier=ModelTier.DEBATE,
293
+ provider=Provider.CODEX,
294
+ input_per_m=2.00,
295
+ output_per_m=8.00,
296
+ cached_per_m=0.50,
297
+ context_window=200_000,
298
+ supports_reasoning=True,
299
+ notes="Simulated debate: 4 parallel o3 calls with adversarial prompts, merged by CODER.",
300
+ )
301
+
302
+ # ---------------------------------------------------------------------------
303
+ # Catalogue and routing
304
+ # ---------------------------------------------------------------------------
305
+
306
+ _GROK_MODELS: list[ModelDef] = [SCOUT, CODER, THINKER, REASONER, DEBATE, STANDARD]
307
+ _GEMINI_MODELS: list[ModelDef] = [GEMINI_SCOUT, GEMINI_CODER, GEMINI_THINKER, GEMINI_REASONER]
308
+ _CODEX_MODELS: list[ModelDef] = [CODEX_SCOUT, CODEX_CODER, CODEX_THINKER, CODEX_REASONER, CODEX_DEBATE]
309
+ MODELS: list[ModelDef] = _GROK_MODELS + _GEMINI_MODELS + _CODEX_MODELS
310
+
311
+ _GROK_BY_TIER: dict[str, ModelDef] = {m.tier: m for m in _GROK_MODELS}
312
+ _GEMINI_BY_TIER: dict[str, ModelDef] = {m.tier: m for m in _GEMINI_MODELS}
313
+ _CODEX_BY_TIER: dict[str, ModelDef] = {m.tier: m for m in _CODEX_MODELS}
314
+
315
+ _TIER_MAPS: dict[str, dict[str, ModelDef]] = {
316
+ Provider.GROK: _GROK_BY_TIER,
317
+ Provider.GEMINI: _GEMINI_BY_TIER,
318
+ Provider.CODEX: _CODEX_BY_TIER,
319
+ }
320
+
321
+ # Fallback chain when a tier is missing for a provider
322
+ _FALLBACKS: dict[tuple[str, str], str] = {
323
+ (Provider.GEMINI, ModelTier.DEBATE): ModelTier.REASONER,
324
+ (Provider.GEMINI, ModelTier.STANDARD): ModelTier.REASONER,
325
+ }
326
+
327
+
328
+ def get_model(id_or_tier: str, provider: str = Provider.GROK) -> ModelDef:
329
+ """Look up a ModelDef by (tier, provider). Falls back gracefully."""
330
+ tier_map = _TIER_MAPS.get(provider, _GROK_BY_TIER)
331
+ if id_or_tier in tier_map:
332
+ return tier_map[id_or_tier]
333
+ # Check provider-specific fallbacks
334
+ fallback_tier = _FALLBACKS.get((provider, id_or_tier))
335
+ if fallback_tier and fallback_tier in tier_map:
336
+ return tier_map[fallback_tier]
337
+ raise KeyError(f"Unknown model tier '{id_or_tier}' for provider '{provider}'.")
338
+
339
+
340
+ def models_for_provider(provider: str) -> list[ModelDef]:
341
+ """Return all models for a given provider in escalation order."""
342
+ match provider:
343
+ case Provider.GEMINI:
344
+ return list(_GEMINI_MODELS)
345
+ case Provider.CODEX:
346
+ return list(_CODEX_MODELS)
347
+ case _:
348
+ return list(_GROK_MODELS)
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Tool call costs (USD per 1K calls) — Grok-native tools only
353
+ # ---------------------------------------------------------------------------
354
+
355
+ @dataclass(frozen=True)
356
+ class ToolCallCost:
357
+ name: str
358
+ per_1k_calls: float
359
+
360
+ def cost(self, calls: int) -> float:
361
+ return (calls / 1000) * self.per_1k_calls
362
+
363
+
364
+ TOOL_CALL_COSTS: dict[str, ToolCallCost] = {
365
+ "web_search": ToolCallCost("web_search", 5.00),
366
+ "x_search": ToolCallCost("x_search", 5.00),
367
+ "document_query": ToolCallCost("document_query", 2.50),
368
+ "file_attachment_search": ToolCallCost("file_attachment_search", 10.00),
369
+ }
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Constant name → module attribute map (for apply_model_id_overrides)
373
+ # ---------------------------------------------------------------------------
374
+
375
+ _CONST_MAP: dict[str, str] = {
376
+ "grok.scout": "SCOUT",
377
+ "grok.coder": "CODER",
378
+ "grok.thinker": "THINKER",
379
+ "grok.reasoner": "REASONER",
380
+ "grok.debate": "DEBATE",
381
+ "grok.standard": "STANDARD",
382
+ "gemini.scout": "GEMINI_SCOUT",
383
+ "gemini.coder": "GEMINI_CODER",
384
+ "gemini.thinker": "GEMINI_THINKER",
385
+ "gemini.reasoner": "GEMINI_REASONER",
386
+ "codex.scout": "CODEX_SCOUT",
387
+ "codex.coder": "CODEX_CODER",
388
+ "codex.thinker": "CODEX_THINKER",
389
+ "codex.reasoner": "CODEX_REASONER",
390
+ "codex.debate": "CODEX_DEBATE",
391
+ }
392
+
393
+
394
+ def apply_model_id_overrides(overrides: dict[str, str]) -> None:
395
+ """Patch module-level ModelDef constants with configured IDs.
396
+
397
+ Resolution order: env var > config.toml > hardcoded default.
398
+ Must be called once during ``load_config()`` before any model is used.
399
+
400
+ Args:
401
+ overrides: mapping of ``"{provider}.{tier}"`` → new model ID string.
402
+ """
403
+ module = sys.modules[__name__]
404
+ patched: list[str] = []
405
+ for key, new_id in overrides.items():
406
+ const_name = _CONST_MAP.get(key.lower())
407
+ if const_name is None:
408
+ continue
409
+ old_def: ModelDef = getattr(module, const_name)
410
+ if old_def.id != new_id:
411
+ setattr(module, const_name, replace(old_def, id=new_id))
412
+ patched.append(f"{const_name}: {old_def.id!r} → {new_id!r}")
413
+ if patched:
414
+ _rebuild_lookup_dicts()
415
+
416
+
417
+ def _rebuild_lookup_dicts() -> None:
418
+ """Rebuild tier-lookup dicts after ID overrides."""
419
+ module = sys.modules[__name__]
420
+ grok_models = [
421
+ module.SCOUT, module.CODER, module.THINKER,
422
+ module.REASONER, module.DEBATE, module.STANDARD,
423
+ ]
424
+ gemini_models = [
425
+ module.GEMINI_SCOUT, module.GEMINI_CODER,
426
+ module.GEMINI_THINKER, module.GEMINI_REASONER,
427
+ ]
428
+ codex_models = [
429
+ module.CODEX_SCOUT, module.CODEX_CODER, module.CODEX_THINKER,
430
+ module.CODEX_REASONER, module.CODEX_DEBATE,
431
+ ]
432
+ module.MODELS = grok_models + gemini_models + codex_models # type: ignore[attr-defined]
433
+ module._GROK_BY_TIER = {m.tier: m for m in grok_models} # type: ignore[attr-defined]
434
+ module._GEMINI_BY_TIER = {m.tier: m for m in gemini_models} # type: ignore[attr-defined]
435
+ module._CODEX_BY_TIER = {m.tier: m for m in codex_models} # type: ignore[attr-defined]
436
+ module._TIER_MAPS = { # type: ignore[attr-defined]
437
+ Provider.GROK: module._GROK_BY_TIER,
438
+ Provider.GEMINI: module._GEMINI_BY_TIER,
439
+ Provider.CODEX: module._CODEX_BY_TIER,
440
+ }
441
+
442
+
443
+ def get_capability(tier: str, provider: str, capability: str) -> bool:
444
+ """Return a boolean capability flag for a given model tier + provider.
445
+
446
+ Args:
447
+ tier: ModelTier constant (e.g. ``ModelTier.CODER``).
448
+ provider: Provider constant (e.g. ``Provider.GROK``).
449
+ capability: Attribute name on ModelDef (e.g. ``"supports_tools"``).
450
+
451
+ Returns:
452
+ ``False`` if the tier/provider is unknown or capability is unrecognised.
453
+ """
454
+ try:
455
+ m = get_model(tier, provider)
456
+ return bool(getattr(m, capability, False))
457
+ except KeyError:
458
+ return False
459
+