alysis-code 0.13.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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,1420 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+ from urllib.parse import urlsplit
6
+
7
+ from .branding import env_get
8
+ from .chatgpt_codex_static_provider import (
9
+ CHATGPT_CODEX_SUBSCRIPTION_CATALOG_SOURCE,
10
+ resolve_chatgpt_codex_static_model,
11
+ )
12
+ from .config import AppConfig
13
+ from .litellm_static_provider import (
14
+ BUNDLED_MODEL_CATALOG_SOURCE,
15
+ resolve_litellm_static_metadata,
16
+ )
17
+ from .model_metadata_utils import (
18
+ model_name_variants,
19
+ normalize_base_url,
20
+ parse_bool,
21
+ parse_non_negative_float,
22
+ parse_positive_int,
23
+ )
24
+ from .provider_url import known_provider_key_from_base_url
25
+
26
+ _INT_FIELDS = ("context_window_tokens", "max_output_tokens")
27
+ _FLOAT_FIELDS = (
28
+ "input_cost_per_token",
29
+ "output_cost_per_token",
30
+ "cache_read_input_cost_per_token",
31
+ "cache_creation_input_cost_per_token",
32
+ "cache_creation_5m_input_cost_per_token",
33
+ "cache_creation_1h_input_cost_per_token",
34
+ "reasoning_output_cost_per_token",
35
+ )
36
+ _BOOL_FIELDS = ("supports_vision", "supports_reasoning")
37
+ _TRACKED_FIELDS = (
38
+ "context_window_tokens",
39
+ "max_output_tokens",
40
+ "supports_vision",
41
+ "supports_reasoning",
42
+ "input_cost_per_token",
43
+ "output_cost_per_token",
44
+ "cache_read_input_cost_per_token",
45
+ "cache_creation_input_cost_per_token",
46
+ "cache_creation_5m_input_cost_per_token",
47
+ "cache_creation_1h_input_cost_per_token",
48
+ "reasoning_output_cost_per_token",
49
+ )
50
+ DEFAULT_UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS = 128_000
51
+ DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS = 8_192
52
+
53
+ _FALLBACKS: dict[str, Any] = {
54
+ "context_window_tokens": DEFAULT_UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS,
55
+ "max_output_tokens": DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS,
56
+ "supports_vision": False,
57
+ "supports_reasoning": None,
58
+ "input_cost_per_token": None,
59
+ "output_cost_per_token": None,
60
+ "cache_read_input_cost_per_token": None,
61
+ "cache_creation_input_cost_per_token": None,
62
+ "cache_creation_5m_input_cost_per_token": None,
63
+ "cache_creation_1h_input_cost_per_token": None,
64
+ "reasoning_output_cost_per_token": None,
65
+ }
66
+ _ENV_FIELD_MAP: dict[str, str] = {
67
+ "context_window_tokens": "ALYSIS_CONTEXT_WINDOW",
68
+ "max_output_tokens": "ALYSIS_MAX_OUTPUT_TOKENS",
69
+ "supports_vision": "ALYSIS_SUPPORTS_VISION",
70
+ "supports_reasoning": "ALYSIS_SUPPORTS_REASONING",
71
+ "input_cost_per_token": "ALYSIS_INPUT_COST_PER_TOKEN",
72
+ "output_cost_per_token": "ALYSIS_OUTPUT_COST_PER_TOKEN",
73
+ "cache_read_input_cost_per_token": "ALYSIS_CACHE_READ_INPUT_COST_PER_TOKEN",
74
+ "cache_creation_input_cost_per_token": "ALYSIS_CACHE_CREATION_INPUT_COST_PER_TOKEN",
75
+ "cache_creation_5m_input_cost_per_token": ("ALYSIS_CACHE_CREATION_5M_INPUT_COST_PER_TOKEN"),
76
+ "cache_creation_1h_input_cost_per_token": ("ALYSIS_CACHE_CREATION_1H_INPUT_COST_PER_TOKEN"),
77
+ "reasoning_output_cost_per_token": "ALYSIS_REASONING_OUTPUT_COST_PER_TOKEN",
78
+ }
79
+ _DEPRECATED_MODEL_CAPABILITIES_WARNING = (
80
+ "Config key `model_capabilities` is deprecated and ignored; use `model_metadata_overrides`."
81
+ )
82
+ _FALLBACK_WARNING = (
83
+ "Using fallback context/max_output; set model_metadata_overrides for best performance."
84
+ )
85
+ OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE = "official_provider_model_catalog"
86
+ CANONICAL_MODEL_CATALOG_SOURCE = "canonical_model_catalog"
87
+ _CANONICAL_MODEL_SOURCES: dict[str, tuple[str, ...]] = {
88
+ "qwen3.8-max": ("https://help.aliyun.com/en/model-studio/qwen3-8-max",),
89
+ "deepseek-v4-pro": ("https://api-docs.deepseek.com/quick_start/pricing/",),
90
+ "deepseek-v4-flash": ("https://api-docs.deepseek.com/quick_start/pricing/",),
91
+ "deepseek-v4-flash-vision-exp": ("https://api-docs.deepseek.com/quick_start/pricing/",),
92
+ }
93
+ _CANONICAL_MODEL_METADATA: dict[str, dict[str, Any]] = {
94
+ "qwen3.8-max": {
95
+ "context_window_tokens": 1_000_000,
96
+ "max_output_tokens": 131_072,
97
+ "supports_vision": True,
98
+ "supports_reasoning": True,
99
+ },
100
+ "deepseek-v4-pro": {
101
+ "context_window_tokens": 1_000_000,
102
+ "max_output_tokens": 384_000,
103
+ "supports_vision": False,
104
+ "supports_reasoning": True,
105
+ },
106
+ "deepseek-v4-flash": {
107
+ "context_window_tokens": 1_000_000,
108
+ "max_output_tokens": 384_000,
109
+ "supports_vision": False,
110
+ "supports_reasoning": True,
111
+ },
112
+ "deepseek-v4-flash-vision-exp": {
113
+ "context_window_tokens": 1_000_000,
114
+ "max_output_tokens": 384_000,
115
+ "supports_vision": True,
116
+ "supports_reasoning": True,
117
+ },
118
+ }
119
+ # Provider model ids are routing details, not separate capability records. This
120
+ # declarative alias table lets each route inherit the canonical model's limits
121
+ # while retaining provider-specific pricing, availability, and stricter caps.
122
+ _PROVIDER_CANONICAL_MODEL_IDS: dict[str, dict[str, str]] = {
123
+ "qwen": {
124
+ "qwen3.8-max": "qwen3.8-max",
125
+ },
126
+ "deepseek": {
127
+ "deepseek-v4-pro": "deepseek-v4-pro",
128
+ "deepseek-v4-flash": "deepseek-v4-flash",
129
+ "deepseek-v4-flash-vision-exp": "deepseek-v4-flash-vision-exp",
130
+ },
131
+ "openrouter": {
132
+ "qwen/qwen3.8-max": "qwen3.8-max",
133
+ "deepseek/deepseek-v4-pro-0813": "deepseek-v4-pro",
134
+ "deepseek/deepseek-v4-flash-0731": "deepseek-v4-flash",
135
+ "deepseek/deepseek-v4-flash-vision-exp": "deepseek-v4-flash-vision-exp",
136
+ },
137
+ "together": {
138
+ "deepseek-ai/DeepSeek-V4-Pro-0813": "deepseek-v4-pro",
139
+ "deepseek-ai/DeepSeek-V4-Flash-0731": "deepseek-v4-flash",
140
+ },
141
+ "fireworks": {
142
+ "accounts/fireworks/models/deepseek-v4-pro-0813": "deepseek-v4-pro",
143
+ "accounts/fireworks/models/deepseek-v4-flash-0731": "deepseek-v4-flash",
144
+ },
145
+ "nvidia": {
146
+ "deepseek-ai/deepseek-v4-pro": "deepseek-v4-pro",
147
+ "deepseek-ai/deepseek-v4-flash": "deepseek-v4-flash",
148
+ },
149
+ }
150
+ _OFFICIAL_PROVIDER_MODEL_SOURCES: dict[str, dict[str, tuple[str, ...]]] = {
151
+ "zai_coding_plan": {
152
+ "glm-5.3": ("https://docs.z.ai/guides/llm/glm-5.3",),
153
+ "glm-5-turbo": ("https://docs.z.ai/guides/llm/glm-5-turbo",),
154
+ "glm-4.7": ("https://docs.z.ai/guides/llm/glm-4.7",),
155
+ },
156
+ "xai": {
157
+ "grok-4.6": (
158
+ "https://docs.x.ai/developers/grok-4-6",
159
+ "https://docs.x.ai/developers/pricing",
160
+ ),
161
+ },
162
+ "gemini": {
163
+ model: (model_url, "https://ai.google.dev/gemini-api/docs/pricing")
164
+ for model, model_url in {
165
+ "gemini-3.7-flash": ("https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash"),
166
+ "gemini-3.6-flash": ("https://ai.google.dev/gemini-api/docs/models/gemini-3.6-flash"),
167
+ "gemini-3.5-flash-lite": (
168
+ "https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite"
169
+ ),
170
+ }.items()
171
+ },
172
+ "qwen": {
173
+ "qwen3.8-max": (
174
+ "https://help.aliyun.com/en/model-studio/qwen3-8-max",
175
+ "https://help.aliyun.com/en/model-studio/qwen-api-via-openai-chat-completions",
176
+ ),
177
+ },
178
+ "deepseek": {
179
+ model: (
180
+ "https://api-docs.deepseek.com/quick_start/pricing/",
181
+ "https://api-docs.deepseek.com/guides/thinking_mode/",
182
+ )
183
+ for model in (
184
+ "deepseek-v4-pro",
185
+ "deepseek-v4-flash",
186
+ "deepseek-v4-flash-vision-exp",
187
+ )
188
+ },
189
+ "openrouter": {
190
+ model: ("https://openrouter.ai/api/v1/models",)
191
+ for model in (
192
+ "qwen/qwen3.8-max",
193
+ "deepseek/deepseek-v4-pro-0813",
194
+ "deepseek/deepseek-v4-flash-0731",
195
+ "deepseek/deepseek-v4-flash-vision-exp",
196
+ )
197
+ },
198
+ "together": {
199
+ model: ("https://docs.together.ai/docs/serverless/models",)
200
+ for model in (
201
+ "deepseek-ai/DeepSeek-V4-Pro-0813",
202
+ "deepseek-ai/DeepSeek-V4-Flash-0731",
203
+ )
204
+ },
205
+ "fireworks": {
206
+ "accounts/fireworks/models/deepseek-v4-pro-0813": (
207
+ "https://fireworks.ai/models/deepseek-ai/deepseek-v4-pro-0813",
208
+ ),
209
+ "accounts/fireworks/models/deepseek-v4-flash-0731": (
210
+ "https://fireworks.ai/models/deepseek-ai/deepseek-v4-flash-0731",
211
+ ),
212
+ },
213
+ "nvidia": {
214
+ "nvidia/nemotron-3-super-120b-a12b": (
215
+ "https://build.nvidia.com/nvidia/nemotron-3-super-120b-a12b/build",
216
+ ),
217
+ "nvidia/nemotron-3-ultra-550b-a55b": (
218
+ "https://build.nvidia.com/nvidia/nemotron-3-ultra-550b-a55b",
219
+ ),
220
+ "nvidia/nemotron-3-nano-30b-a3b": (
221
+ "https://build.nvidia.com/nvidia/nemotron-3-nano-30b-a3b",
222
+ ),
223
+ "deepseek-ai/deepseek-v4-pro": ("https://build.nvidia.com/deepseek-ai/deepseek-v4-pro",),
224
+ "deepseek-ai/deepseek-v4-flash": (
225
+ "https://build.nvidia.com/deepseek-ai/deepseek-v4-flash",
226
+ ),
227
+ },
228
+ "moonshot": {
229
+ model: ("https://platform.moonshot.ai/docs/pricing/chat",)
230
+ for model in (
231
+ "kimi-k3",
232
+ "kimi-k2.7-code",
233
+ "kimi-k2.7-code-highspeed",
234
+ "kimi-k2.6",
235
+ )
236
+ },
237
+ "anthropic": {
238
+ "claude-opus-5": (
239
+ "https://docs.anthropic.com/en/docs/about-claude/models/overview",
240
+ "https://docs.anthropic.com/en/docs/about-claude/pricing",
241
+ ),
242
+ },
243
+ }
244
+ _OFFICIAL_PROVIDER_MODEL_METADATA: dict[str, dict[str, dict[str, Any]]] = {
245
+ "zai_coding_plan": {
246
+ # GLM-5.3 is currently exclusive to the subscription Coding Plan API;
247
+ # the general pay-per-token API is still marked "coming soon". Plan
248
+ # credits are not token prices, so this route intentionally carries no
249
+ # monetary cost fields.
250
+ "glm-5.3": {
251
+ "context_window_tokens": 1_000_000,
252
+ "max_output_tokens": 131_072,
253
+ "supports_vision": False,
254
+ "supports_reasoning": True,
255
+ },
256
+ "glm-5-turbo": {
257
+ "context_window_tokens": 200_000,
258
+ "max_output_tokens": 131_072,
259
+ "supports_vision": False,
260
+ "supports_reasoning": True,
261
+ },
262
+ "glm-4.7": {
263
+ "context_window_tokens": 200_000,
264
+ "max_output_tokens": 131_072,
265
+ "supports_vision": False,
266
+ "supports_reasoning": True,
267
+ },
268
+ },
269
+ "xai": {
270
+ # Grok 4.6 postdates the provenance-pinned LiteLLM snapshot. xAI
271
+ # publishes a 500K shared context with no separate text-output limit,
272
+ # so use the full window here and let the registry's generic
273
+ # shared-window policy reserve a practical output allowance. These are
274
+ # the base rates for prompts up to 200K tokens; xAI doubles all three
275
+ # rates above that threshold, which this flat-price schema cannot
276
+ # represent yet.
277
+ "grok-4.6": {
278
+ "context_window_tokens": 500_000,
279
+ "max_output_tokens": 500_000,
280
+ "supports_vision": True,
281
+ "supports_reasoning": True,
282
+ "input_cost_per_token": 0.000002,
283
+ "output_cost_per_token": 0.000006,
284
+ "cache_read_input_cost_per_token": 0.0000005,
285
+ "reasoning_output_cost_per_token": 0.000006,
286
+ },
287
+ },
288
+ "gemini": {
289
+ # These GA models postdate the provenance-pinned LiteLLM snapshot.
290
+ # Capacity and current standard-tier pricing are from Google's model
291
+ # and pricing pages. The 3.7/3.6 introductory rates expire 2026-12-31.
292
+ "gemini-3.7-flash": {
293
+ "context_window_tokens": 1_048_576,
294
+ "max_output_tokens": 65_536,
295
+ "supports_vision": True,
296
+ "supports_reasoning": True,
297
+ "input_cost_per_token": 0.00000075,
298
+ "output_cost_per_token": 0.00000375,
299
+ "cache_read_input_cost_per_token": 0.000000075,
300
+ "reasoning_output_cost_per_token": 0.00000375,
301
+ },
302
+ "gemini-3.6-flash": {
303
+ "context_window_tokens": 1_048_576,
304
+ "max_output_tokens": 65_536,
305
+ "supports_vision": True,
306
+ "supports_reasoning": True,
307
+ "input_cost_per_token": 0.00000075,
308
+ "output_cost_per_token": 0.00000375,
309
+ "cache_read_input_cost_per_token": 0.000000075,
310
+ "reasoning_output_cost_per_token": 0.00000375,
311
+ },
312
+ "gemini-3.5-flash-lite": {
313
+ "context_window_tokens": 1_048_576,
314
+ "max_output_tokens": 65_536,
315
+ "supports_vision": True,
316
+ "supports_reasoning": True,
317
+ "input_cost_per_token": 0.0000003,
318
+ "output_cost_per_token": 0.0000025,
319
+ "cache_read_input_cost_per_token": 0.00000003,
320
+ "reasoning_output_cost_per_token": 0.0000025,
321
+ },
322
+ },
323
+ "qwen": {
324
+ # Qwen3.8-Max is available from the China, Singapore, Frankfurt, US,
325
+ # and Tokyo surfaces under the same bare model id. Prices vary by
326
+ # region/currency, which this provider-level flat schema cannot encode,
327
+ # so costs remain unknown instead of applying one region globally.
328
+ "qwen3.8-max": {},
329
+ },
330
+ "deepseek": {
331
+ # DeepSeek charges half price outside two daily peak windows. The
332
+ # registry cannot express time-dependent rates, so use peak pricing as
333
+ # a conservative upper-bound estimate rather than under-reporting cost.
334
+ "deepseek-v4-pro": {
335
+ "input_cost_per_token": 0.00000132,
336
+ "output_cost_per_token": 0.00000396,
337
+ "cache_read_input_cost_per_token": 0.000000044,
338
+ "reasoning_output_cost_per_token": 0.00000396,
339
+ },
340
+ "deepseek-v4-flash": {
341
+ "input_cost_per_token": 0.00000044,
342
+ "output_cost_per_token": 0.00000132,
343
+ "cache_read_input_cost_per_token": 0.000000014,
344
+ "reasoning_output_cost_per_token": 0.00000132,
345
+ },
346
+ "deepseek-v4-flash-vision-exp": {
347
+ "input_cost_per_token": 0.00000044,
348
+ "output_cost_per_token": 0.00000132,
349
+ "cache_read_input_cost_per_token": 0.000000014,
350
+ "reasoning_output_cost_per_token": 0.00000132,
351
+ },
352
+ },
353
+ "openrouter": {
354
+ "qwen/qwen3.8-max": {
355
+ "input_cost_per_token": 0.000002,
356
+ "output_cost_per_token": 0.000006,
357
+ "cache_read_input_cost_per_token": 0.00000025,
358
+ "cache_creation_input_cost_per_token": 0.0000025,
359
+ "reasoning_output_cost_per_token": 0.000006,
360
+ },
361
+ "deepseek/deepseek-v4-pro-0813": {
362
+ "context_window_tokens": 1_048_576,
363
+ "input_cost_per_token": 0.000001188,
364
+ "output_cost_per_token": 0.000003564,
365
+ "cache_read_input_cost_per_token": 0.0000000396,
366
+ "reasoning_output_cost_per_token": 0.000003564,
367
+ },
368
+ "deepseek/deepseek-v4-flash-0731": {
369
+ # OpenRouter's public model catalog advertises a 1.31M routing
370
+ # window for this snapshot even though its current top provider is
371
+ # capped at 1,048,576 tokens. Preserve the gateway-level contract;
372
+ # route-specific limits remain OpenRouter's responsibility.
373
+ "context_window_tokens": 1_310_720,
374
+ "input_cost_per_token": 0.00000008,
375
+ "output_cost_per_token": 0.00000018,
376
+ "cache_read_input_cost_per_token": 0.000000016,
377
+ "reasoning_output_cost_per_token": 0.00000018,
378
+ },
379
+ "deepseek/deepseek-v4-flash-vision-exp": {
380
+ "context_window_tokens": 1_048_576,
381
+ # OpenRouter exposes DeepSeek's time-window overrides. Use the
382
+ # highest published rate so a flat estimate does not undercount.
383
+ "input_cost_per_token": 0.00000044,
384
+ "output_cost_per_token": 0.00000132,
385
+ "cache_read_input_cost_per_token": 0.000000014,
386
+ "reasoning_output_cost_per_token": 0.00000132,
387
+ },
388
+ },
389
+ "together": {
390
+ "deepseek-ai/DeepSeek-V4-Pro-0813": {
391
+ "context_window_tokens": 1_048_576,
392
+ "input_cost_per_token": 0.00000132,
393
+ "output_cost_per_token": 0.00000396,
394
+ "cache_read_input_cost_per_token": 0.00000013,
395
+ "reasoning_output_cost_per_token": 0.00000396,
396
+ },
397
+ "deepseek-ai/DeepSeek-V4-Flash-0731": {
398
+ "input_cost_per_token": 0.00000014,
399
+ "output_cost_per_token": 0.00000028,
400
+ "cache_read_input_cost_per_token": 0.00000003,
401
+ "reasoning_output_cost_per_token": 0.00000028,
402
+ },
403
+ },
404
+ "fireworks": {
405
+ "accounts/fireworks/models/deepseek-v4-pro-0813": {
406
+ "context_window_tokens": 1_048_576,
407
+ "input_cost_per_token": 0.00000132,
408
+ "output_cost_per_token": 0.00000396,
409
+ "cache_read_input_cost_per_token": 0.000000044,
410
+ "reasoning_output_cost_per_token": 0.00000396,
411
+ },
412
+ "accounts/fireworks/models/deepseek-v4-flash-0731": {
413
+ "context_window_tokens": 1_048_576,
414
+ "input_cost_per_token": 0.00000014,
415
+ "output_cost_per_token": 0.00000028,
416
+ "cache_read_input_cost_per_token": 0.000000028,
417
+ "reasoning_output_cost_per_token": 0.00000028,
418
+ },
419
+ },
420
+ "nvidia": {
421
+ # NVIDIA's hosted NIM catalog publishes the Nemotron 3 Super and Ultra
422
+ # models with 1M context. Nano's hosted endpoint currently advertises
423
+ # 262K. The hosted Nemotron endpoints accept up to 32,768 output tokens;
424
+ # 16,384 is their default, not their ceiling. NVIDIA-hosted DeepSeek V4
425
+ # uses a separate 16,384-token ceiling. Free Endpoint access is
426
+ # rate-limited prototyping, so no durable token price is encoded here.
427
+ "nvidia/nemotron-3-super-120b-a12b": {
428
+ "context_window_tokens": 1_048_576,
429
+ "max_output_tokens": 32_768,
430
+ "supports_vision": False,
431
+ "supports_reasoning": True,
432
+ },
433
+ "nvidia/nemotron-3-ultra-550b-a55b": {
434
+ "context_window_tokens": 1_048_576,
435
+ "max_output_tokens": 32_768,
436
+ "supports_vision": False,
437
+ "supports_reasoning": True,
438
+ },
439
+ "nvidia/nemotron-3-nano-30b-a3b": {
440
+ "context_window_tokens": 262_144,
441
+ "max_output_tokens": 32_768,
442
+ "supports_vision": False,
443
+ "supports_reasoning": True,
444
+ },
445
+ "deepseek-ai/deepseek-v4-pro": {
446
+ "context_window_tokens": 1_048_576,
447
+ "max_output_tokens": 16_384,
448
+ },
449
+ "deepseek-ai/deepseek-v4-flash": {
450
+ "context_window_tokens": 1_048_576,
451
+ "max_output_tokens": 16_384,
452
+ },
453
+ },
454
+ "moonshot": {
455
+ "kimi-k3": {
456
+ "context_window_tokens": 1_048_576,
457
+ # K3 can use the full context for output, but 131,072 is the API's
458
+ # normal completion allowance and therefore the useful local reserve.
459
+ "max_output_tokens": 131_072,
460
+ "supports_vision": True,
461
+ "supports_reasoning": True,
462
+ "input_cost_per_token": 0.000003,
463
+ "output_cost_per_token": 0.000015,
464
+ "cache_read_input_cost_per_token": 0.0000003,
465
+ },
466
+ "kimi-k2.7-code": {
467
+ "context_window_tokens": 262_144,
468
+ "max_output_tokens": 32_768,
469
+ "supports_vision": True,
470
+ "supports_reasoning": True,
471
+ "input_cost_per_token": 0.00000095,
472
+ "output_cost_per_token": 0.000004,
473
+ "cache_read_input_cost_per_token": 0.00000019,
474
+ },
475
+ "kimi-k2.7-code-highspeed": {
476
+ "context_window_tokens": 262_144,
477
+ "max_output_tokens": 32_768,
478
+ "supports_vision": True,
479
+ "supports_reasoning": True,
480
+ "input_cost_per_token": 0.0000019,
481
+ "output_cost_per_token": 0.000008,
482
+ "cache_read_input_cost_per_token": 0.00000038,
483
+ },
484
+ "kimi-k2.6": {
485
+ "context_window_tokens": 262_144,
486
+ "max_output_tokens": 32_768,
487
+ "supports_vision": True,
488
+ "supports_reasoning": True,
489
+ "input_cost_per_token": 0.00000095,
490
+ "output_cost_per_token": 0.000004,
491
+ "cache_read_input_cost_per_token": 0.00000016,
492
+ },
493
+ },
494
+ "anthropic": {
495
+ # Opus 5 ships ahead of the vendored litellm mirror, and that mirror is
496
+ # refresh-policy pinned to an upstream commit — so its capacity lives
497
+ # here rather than as a hand-edit that would falsify the snapshot's
498
+ # provenance. Drop this entry once a catalog refresh carries the model.
499
+ # Same shape as its siblings: 1M input + 128K output, Opus 4.8 pricing.
500
+ "claude-opus-5": {
501
+ "context_window_tokens": 1_128_000,
502
+ "max_output_tokens": 128_000,
503
+ "supports_vision": True,
504
+ "supports_reasoning": True,
505
+ "input_cost_per_token": 0.000005,
506
+ "output_cost_per_token": 0.000025,
507
+ "cache_read_input_cost_per_token": 0.0000005,
508
+ "cache_creation_input_cost_per_token": 0.00000625,
509
+ "cache_creation_5m_input_cost_per_token": 0.00000625,
510
+ "cache_creation_1h_input_cost_per_token": 0.00001,
511
+ },
512
+ },
513
+ }
514
+ _BUILT_IN_MODEL_METADATA: dict[str, dict[str, Any]] = {
515
+ "deepseek-chat": {
516
+ "context_window_tokens": 1_000_000,
517
+ "max_output_tokens": 384_000,
518
+ "supports_vision": False,
519
+ "input_cost_per_token": 0.000000435,
520
+ "output_cost_per_token": 0.00000087,
521
+ },
522
+ "deepseek-reasoner": {
523
+ "context_window_tokens": 1_000_000,
524
+ "max_output_tokens": 384_000,
525
+ "supports_vision": False,
526
+ "input_cost_per_token": 0.000000435,
527
+ "output_cost_per_token": 0.00000087,
528
+ },
529
+ # Hosted Xiaomi MiMo trial models. The proxy serves these ids when allowlisted,
530
+ # and falls back server-side to its canonical MiMo model otherwise.
531
+ "mimo-v2.5-pro": {
532
+ "context_window_tokens": 1_000_000,
533
+ "max_output_tokens": 131_072,
534
+ "supports_vision": False,
535
+ "input_cost_per_token": 0.000001,
536
+ "output_cost_per_token": 0.000003,
537
+ },
538
+ "mimo-v2-flash": {
539
+ "context_window_tokens": 262_144,
540
+ "max_output_tokens": 65_536,
541
+ "supports_vision": False,
542
+ "input_cost_per_token": 0.0000001,
543
+ "output_cost_per_token": 0.0000003,
544
+ },
545
+ "mimo-v2.5": {
546
+ "context_window_tokens": 1_000_000,
547
+ "max_output_tokens": 131_072,
548
+ "supports_vision": True,
549
+ "input_cost_per_token": 0.0000004,
550
+ "output_cost_per_token": 0.000002,
551
+ },
552
+ # Legacy friendly id kept for sessions that logged in before model choice.
553
+ # Hosted Xiaomi MiMo trial: the CLI sends the friendly id "mimo" (the proxy
554
+ # pins it to the real upstream id server-side). Without an entry here the id
555
+ # resolves nowhere and falls back to generic unknown-model metadata, emitting
556
+ # a metadata note on every run and silently shrinking the usable context.
557
+ # Values mirror the bundled `openrouter/xiaomi/mimo-v2-flash` catalog entry
558
+ # (262144 input / 16384 output); costs use the mimo-v2.5-pro list price.
559
+ "mimo": {
560
+ "context_window_tokens": 262_144,
561
+ "max_output_tokens": 16_384,
562
+ "supports_vision": False,
563
+ "input_cost_per_token": 0.000000435,
564
+ "output_cost_per_token": 0.00000087,
565
+ },
566
+ # Kimi Code membership ids served by api.kimi.com/coding/v1 (subscription
567
+ # billing, so no per-token costs). Context windows from
568
+ # kimi.com/code/docs, July 2026.
569
+ "k3": {
570
+ "context_window_tokens": 1_048_576,
571
+ "max_output_tokens": 1_048_576,
572
+ "supports_vision": True,
573
+ "supports_reasoning": True,
574
+ },
575
+ "kimi-for-coding": {
576
+ "context_window_tokens": 262_144,
577
+ "max_output_tokens": 262_144,
578
+ "supports_vision": True,
579
+ "supports_reasoning": True,
580
+ },
581
+ "kimi-for-coding-highspeed": {
582
+ "context_window_tokens": 262_144,
583
+ "max_output_tokens": 262_144,
584
+ "supports_vision": True,
585
+ "supports_reasoning": True,
586
+ },
587
+ # Moonshot Kimi models newer than the bundled LiteLLM snapshot (which tops
588
+ # out at moonshot/kimi-k2.6). Values from platform.kimi.ai/docs/pricing,
589
+ # July 2026; remove once a snapshot refresh covers these ids.
590
+ "kimi-k3": {
591
+ "context_window_tokens": 1_048_576,
592
+ "max_output_tokens": 1_048_576,
593
+ "supports_vision": True,
594
+ "supports_reasoning": True,
595
+ "input_cost_per_token": 0.000003,
596
+ "output_cost_per_token": 0.000015,
597
+ "cache_read_input_cost_per_token": 0.0000003,
598
+ },
599
+ "kimi-k2.7-code": {
600
+ "context_window_tokens": 262_144,
601
+ "max_output_tokens": 262_144,
602
+ "supports_vision": True,
603
+ "supports_reasoning": True,
604
+ "input_cost_per_token": 0.00000095,
605
+ "output_cost_per_token": 0.000004,
606
+ "cache_read_input_cost_per_token": 0.00000019,
607
+ },
608
+ "kimi-k2.7-code-highspeed": {
609
+ "context_window_tokens": 262_144,
610
+ "max_output_tokens": 262_144,
611
+ "supports_vision": True,
612
+ "supports_reasoning": True,
613
+ "input_cost_per_token": 0.0000019,
614
+ "output_cost_per_token": 0.000008,
615
+ "cache_read_input_cost_per_token": 0.00000038,
616
+ },
617
+ }
618
+
619
+
620
+ @dataclass(frozen=True)
621
+ class ModelMeta:
622
+ model_name: str
623
+ context_window_tokens: int
624
+ max_output_tokens: int
625
+ input_cost_per_token: float | None = None
626
+ output_cost_per_token: float | None = None
627
+ cache_read_input_cost_per_token: float | None = None
628
+ cache_creation_input_cost_per_token: float | None = None
629
+ cache_creation_5m_input_cost_per_token: float | None = None
630
+ cache_creation_1h_input_cost_per_token: float | None = None
631
+ reasoning_output_cost_per_token: float | None = None
632
+ raw_metadata: dict[str, Any] = field(default_factory=dict)
633
+ source: str = "fallback"
634
+ supports_vision: bool = False
635
+ supports_reasoning: bool | None = None
636
+ field_sources: dict[str, str] = field(default_factory=dict)
637
+ warnings: tuple[str, ...] = ()
638
+ provider_key: str | None = None
639
+
640
+
641
+ @dataclass
642
+ class _LayerData:
643
+ name: str
644
+ values: dict[str, Any] = field(default_factory=dict)
645
+ field_sources: dict[str, str] = field(default_factory=dict)
646
+ warnings: list[str] = field(default_factory=list)
647
+ error: str | None = None
648
+ raw_metadata: dict[str, Any] = field(default_factory=dict)
649
+ model_name: str | None = None
650
+
651
+
652
+ def _parse_field(field: str, raw_value: Any) -> Any | None:
653
+ if field in _INT_FIELDS:
654
+ return parse_positive_int(raw_value)
655
+ if field in _FLOAT_FIELDS:
656
+ return parse_non_negative_float(raw_value)
657
+ if field in _BOOL_FIELDS:
658
+ return parse_bool(raw_value)
659
+ return None
660
+
661
+
662
+ def _dedupe_warnings(values: list[str]) -> list[str]:
663
+ deduped: list[str] = []
664
+ seen: set[str] = set()
665
+ for value in values:
666
+ clean = value.strip()
667
+ if not clean:
668
+ continue
669
+ if clean in seen:
670
+ continue
671
+ seen.add(clean)
672
+ deduped.append(clean)
673
+ return deduped
674
+
675
+
676
+ def _build_model_alias_index(mapping: dict[str, Any]) -> dict[str, str]:
677
+ index: dict[str, str] = {}
678
+ for key in mapping:
679
+ if not isinstance(key, str):
680
+ continue
681
+ for variant in model_name_variants(key):
682
+ index.setdefault(variant.casefold(), key)
683
+ return index
684
+
685
+
686
+ def _lookup_model_override(
687
+ *,
688
+ mapping: Any,
689
+ requested_model: str,
690
+ label: str,
691
+ warnings: list[str],
692
+ ) -> tuple[str, dict[str, Any]] | None:
693
+ if mapping is None:
694
+ return None
695
+ if not isinstance(mapping, dict):
696
+ warnings.append(f"Ignoring invalid {label}: expected object.")
697
+ return None
698
+
699
+ alias_index = _build_model_alias_index(mapping)
700
+ for variant in model_name_variants(requested_model):
701
+ key = alias_index.get(variant.casefold())
702
+ if key is None:
703
+ continue
704
+ raw = mapping.get(key)
705
+ if isinstance(raw, dict):
706
+ return key, raw
707
+ warnings.append(f"Ignoring invalid {label}[{key!r}]: expected object.")
708
+ return None
709
+ return None
710
+
711
+
712
+ def _lookup_endpoint_override(
713
+ *,
714
+ endpoints: Any,
715
+ base_url: str,
716
+ warnings: list[str],
717
+ ) -> tuple[str, dict[str, Any]] | None:
718
+ if endpoints is None:
719
+ return None
720
+ if not isinstance(endpoints, dict):
721
+ warnings.append("Ignoring invalid model_metadata_overrides.endpoints: expected object.")
722
+ return None
723
+
724
+ if base_url in endpoints:
725
+ exact = endpoints.get(base_url)
726
+ if isinstance(exact, dict):
727
+ return base_url, exact
728
+ warnings.append(
729
+ f"Ignoring invalid model_metadata_overrides.endpoints[{base_url!r}]: expected object."
730
+ )
731
+ return None
732
+
733
+ normalized_base_url = normalize_base_url(base_url)
734
+ if not normalized_base_url:
735
+ return None
736
+ for endpoint_key, endpoint_value in endpoints.items():
737
+ if not isinstance(endpoint_key, str):
738
+ continue
739
+ if normalize_base_url(endpoint_key) != normalized_base_url:
740
+ continue
741
+ if isinstance(endpoint_value, dict):
742
+ return endpoint_key, endpoint_value
743
+ warnings.append(
744
+ "Ignoring invalid model_metadata_overrides.endpoints"
745
+ f"[{endpoint_key!r}]: expected object."
746
+ )
747
+ return None
748
+ return None
749
+
750
+
751
+ class ModelRegistry:
752
+ def __init__(self, *, cfg: AppConfig, api_key: str | None = None) -> None:
753
+ _ = api_key
754
+ self._cfg = cfg
755
+ self.last_error: str | None = None
756
+ self.last_warnings: list[str] = []
757
+ self.last_source: str | None = None
758
+
759
+ def _provider_route_identity(self) -> tuple[str | None, str]:
760
+ """Resolve explicit preset route identity without trusting model aliases."""
761
+
762
+ try:
763
+ from .profile_presets import find_preset_for_profile
764
+ from .profiles import get_active_profile
765
+
766
+ profile = get_active_profile(self._cfg)
767
+ preset = find_preset_for_profile(profile)
768
+ except Exception: # noqa: BLE001 - metadata fallback must remain offline-safe
769
+ return None, str(self._cfg.base_url or "").strip()
770
+ provider_key = str(preset.provider_key or "").strip() if preset is not None else ""
771
+ base_url = str(profile.base_url or self._cfg.base_url or "").strip()
772
+ return provider_key or None, base_url
773
+
774
+ def _provider_hint(self) -> str | None:
775
+ provider_key, _base_url = self._provider_route_identity()
776
+ return provider_key
777
+
778
+ def _resolve_env_layer(self) -> _LayerData:
779
+ layer = _LayerData(name="env")
780
+ for field_name, env_name in _ENV_FIELD_MAP.items():
781
+ raw_value = env_get(env_name)
782
+ if raw_value is None:
783
+ continue
784
+ parsed = _parse_field(field_name, raw_value)
785
+ if parsed is None:
786
+ layer.warnings.append(f"Ignoring invalid {env_name}: {raw_value!r}")
787
+ continue
788
+ layer.values[field_name] = parsed
789
+ layer.field_sources[field_name] = f"env:{env_name}"
790
+ return layer
791
+
792
+ def _apply_user_scope(
793
+ self,
794
+ *,
795
+ layer: _LayerData,
796
+ source: str,
797
+ payload: dict[str, Any],
798
+ ) -> None:
799
+ for field_name in _TRACKED_FIELDS:
800
+ if field_name in layer.values:
801
+ continue
802
+ if field_name not in payload:
803
+ continue
804
+ raw_value = payload.get(field_name)
805
+ if raw_value is None:
806
+ continue
807
+ parsed = _parse_field(field_name, raw_value)
808
+ if parsed is None:
809
+ layer.warnings.append(f"Ignoring invalid {field_name} in {source}: {raw_value!r}")
810
+ continue
811
+ layer.values[field_name] = parsed
812
+ layer.field_sources[field_name] = source
813
+
814
+ def _resolve_user_layer(self, requested_model: str) -> _LayerData:
815
+ layer = _LayerData(name="user")
816
+ raw_overrides = self._cfg.extra_fields.get("model_metadata_overrides")
817
+ if raw_overrides is None:
818
+ return layer
819
+ if not isinstance(raw_overrides, dict):
820
+ layer.warnings.append("Ignoring invalid model_metadata_overrides: expected object.")
821
+ return layer
822
+
823
+ scoped_payloads: list[tuple[str, dict[str, Any]]] = []
824
+ endpoint_match = _lookup_endpoint_override(
825
+ endpoints=raw_overrides.get("endpoints"),
826
+ base_url=self._cfg.base_url,
827
+ warnings=layer.warnings,
828
+ )
829
+ if endpoint_match is not None:
830
+ endpoint_key, endpoint_payload = endpoint_match
831
+ endpoint_model_match = _lookup_model_override(
832
+ mapping=endpoint_payload.get("models"),
833
+ requested_model=requested_model,
834
+ label=f"model_metadata_overrides.endpoints[{endpoint_key!r}].models",
835
+ warnings=layer.warnings,
836
+ )
837
+ if endpoint_model_match is not None:
838
+ model_key, model_payload = endpoint_model_match
839
+ scoped_payloads.append(
840
+ (
841
+ f"user:endpoints[{endpoint_key!r}].models[{model_key!r}]",
842
+ model_payload,
843
+ )
844
+ )
845
+
846
+ endpoint_default = endpoint_payload.get("default")
847
+ if endpoint_default is not None:
848
+ if isinstance(endpoint_default, dict):
849
+ scoped_payloads.append(
850
+ (f"user:endpoints[{endpoint_key!r}].default", endpoint_default)
851
+ )
852
+ else:
853
+ layer.warnings.append(
854
+ "Ignoring invalid model_metadata_overrides.endpoints"
855
+ f"[{endpoint_key!r}].default: expected object."
856
+ )
857
+
858
+ model_match = _lookup_model_override(
859
+ mapping=raw_overrides.get("models"),
860
+ requested_model=requested_model,
861
+ label="model_metadata_overrides.models",
862
+ warnings=layer.warnings,
863
+ )
864
+ if model_match is not None:
865
+ model_key, model_payload = model_match
866
+ scoped_payloads.append((f"user:models[{model_key!r}]", model_payload))
867
+
868
+ default_payload = raw_overrides.get("default")
869
+ if default_payload is not None:
870
+ if isinstance(default_payload, dict):
871
+ scoped_payloads.append(("user:default", default_payload))
872
+ else:
873
+ layer.warnings.append(
874
+ "Ignoring invalid model_metadata_overrides.default: expected object."
875
+ )
876
+
877
+ for source, payload in scoped_payloads:
878
+ self._apply_user_scope(layer=layer, source=source, payload=payload)
879
+
880
+ return layer
881
+
882
+ def _resolve_bundled_model_catalog_layer(self, requested_model: str) -> _LayerData:
883
+ layer = _LayerData(name=BUNDLED_MODEL_CATALOG_SOURCE)
884
+ provider_hint, route_base_url = self._provider_route_identity()
885
+ try:
886
+ static_meta = resolve_litellm_static_metadata(
887
+ requested_model,
888
+ base_url=route_base_url,
889
+ provider_hint=provider_hint,
890
+ )
891
+ except TypeError as exc:
892
+ # Preserve compatibility with lightweight test/plugin resolvers that
893
+ # implement the older ``(model, *, base_url)`` extension surface.
894
+ if "provider_hint" not in str(exc):
895
+ raise
896
+ static_meta = resolve_litellm_static_metadata(
897
+ requested_model,
898
+ base_url=route_base_url,
899
+ )
900
+ layer.error = static_meta.error
901
+ layer.raw_metadata = static_meta.raw_metadata
902
+ layer.model_name = static_meta.model_key
903
+
904
+ values = {
905
+ "context_window_tokens": static_meta.context_window_tokens,
906
+ "max_output_tokens": static_meta.max_output_tokens,
907
+ "supports_vision": static_meta.supports_vision,
908
+ "supports_reasoning": parse_bool(static_meta.raw_metadata.get("supports_reasoning")),
909
+ "input_cost_per_token": static_meta.input_cost_per_token,
910
+ "output_cost_per_token": static_meta.output_cost_per_token,
911
+ "cache_read_input_cost_per_token": static_meta.cache_read_input_cost_per_token,
912
+ "cache_creation_input_cost_per_token": (
913
+ static_meta.cache_creation_input_cost_per_token
914
+ ),
915
+ "cache_creation_5m_input_cost_per_token": (
916
+ static_meta.cache_creation_5m_input_cost_per_token
917
+ ),
918
+ "cache_creation_1h_input_cost_per_token": (
919
+ static_meta.cache_creation_1h_input_cost_per_token
920
+ ),
921
+ "reasoning_output_cost_per_token": static_meta.reasoning_output_cost_per_token,
922
+ }
923
+ for field_name, value in values.items():
924
+ if value is None:
925
+ continue
926
+ layer.values[field_name] = value
927
+ layer.field_sources[field_name] = BUNDLED_MODEL_CATALOG_SOURCE
928
+ return layer
929
+
930
+ def _resolve_provider_auth_layer(self, requested_model: str) -> _LayerData:
931
+ """Project the active account's live model catalog into context metadata."""
932
+
933
+ layer = _LayerData(name="provider_auth")
934
+ try:
935
+ from .profiles import get_active_profile
936
+
937
+ profile = get_active_profile(self._cfg)
938
+ except Exception: # noqa: BLE001 - malformed profiles fall through to static metadata
939
+ return layer
940
+ provider_id = str(profile.auth_provider or "").strip()
941
+ if not provider_id:
942
+ return layer
943
+ try:
944
+ from .provider_auth import create_provider_auth
945
+
946
+ models = create_provider_auth(provider_id).list_models(refresh=False)
947
+ except Exception as exc: # noqa: BLE001 - offline startup must remain possible
948
+ layer.warnings.append(f"Subscription model metadata unavailable: {exc}")
949
+ models = ()
950
+
951
+ requested_variants = {
952
+ value.casefold() for value in model_name_variants(requested_model) if value
953
+ }
954
+ selected = next(
955
+ (
956
+ model
957
+ for model in models
958
+ if requested_variants.intersection(
959
+ value.casefold() for value in model_name_variants(model.id) if value
960
+ )
961
+ ),
962
+ None,
963
+ )
964
+ if selected is None and provider_id != "openai-codex":
965
+ return layer
966
+
967
+ source = f"provider_auth:{provider_id}"
968
+ static_subscription_model = (
969
+ resolve_chatgpt_codex_static_model(requested_model)
970
+ if provider_id == "openai-codex"
971
+ else None
972
+ )
973
+ if selected is not None:
974
+ layer.model_name = selected.id
975
+ elif static_subscription_model is not None:
976
+ layer.model_name = static_subscription_model.id
977
+ source = f"provider_auth:{provider_id}:{CHATGPT_CODEX_SUBSCRIPTION_CATALOG_SOURCE}"
978
+ layer.warnings.append(
979
+ "Using the bundled ChatGPT subscription capacity snapshot because live "
980
+ "metadata for the selected model is unavailable."
981
+ )
982
+ else:
983
+ layer.model_name = requested_model
984
+ source = f"provider_auth:{provider_id}:conservative-default"
985
+ layer.warnings.append(
986
+ "Using conservative ChatGPT subscription capacity because the selected "
987
+ "model is absent from both live and bundled subscription metadata."
988
+ )
989
+
990
+ context_window_tokens = (
991
+ selected.context_window_tokens if selected is not None else None
992
+ ) or (
993
+ static_subscription_model.context_window_tokens
994
+ if static_subscription_model is not None
995
+ else None
996
+ )
997
+ max_output_tokens = (selected.max_output_tokens if selected is not None else None) or (
998
+ static_subscription_model.max_output_tokens
999
+ if static_subscription_model is not None
1000
+ else None
1001
+ )
1002
+ input_modalities = (
1003
+ selected.input_modalities
1004
+ if selected is not None
1005
+ else (
1006
+ static_subscription_model.input_modalities
1007
+ if static_subscription_model is not None
1008
+ else ("text",)
1009
+ )
1010
+ )
1011
+ layer.raw_metadata = {
1012
+ "provider_auth": provider_id,
1013
+ "subscription_backed": True,
1014
+ "input_modalities": list(input_modalities),
1015
+ }
1016
+ selected_reasoning_efforts = (
1017
+ selected.reasoning_efforts
1018
+ if selected is not None
1019
+ else (
1020
+ static_subscription_model.reasoning_efforts
1021
+ if static_subscription_model is not None
1022
+ else ()
1023
+ )
1024
+ )
1025
+ selected_default_effort = (
1026
+ selected.default_reasoning_effort
1027
+ if selected is not None
1028
+ else (
1029
+ static_subscription_model.default_reasoning_effort
1030
+ if static_subscription_model is not None
1031
+ else None
1032
+ )
1033
+ )
1034
+ if selected is not None or static_subscription_model is not None:
1035
+ supports_reasoning = bool(selected_reasoning_efforts or selected_default_effort)
1036
+ layer.values["supports_reasoning"] = supports_reasoning
1037
+ layer.field_sources["supports_reasoning"] = source
1038
+ layer.raw_metadata["supports_reasoning"] = supports_reasoning
1039
+ if context_window_tokens is not None:
1040
+ layer.values["context_window_tokens"] = context_window_tokens
1041
+ layer.field_sources["context_window_tokens"] = source
1042
+ layer.raw_metadata["context_window_tokens"] = context_window_tokens
1043
+ # The Codex catalog currently omits a response-output ceiling. Keep
1044
+ # Alysis Code's existing local reserve without adding a wire-level cap.
1045
+ layer.values["max_output_tokens"] = (
1046
+ max_output_tokens or DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS
1047
+ )
1048
+ layer.field_sources["max_output_tokens"] = (
1049
+ source if max_output_tokens is not None else f"{source}:local-default"
1050
+ )
1051
+ layer.raw_metadata["max_output_tokens"] = layer.values["max_output_tokens"]
1052
+ if max_output_tokens is not None and "max_output_tokens" not in layer.values:
1053
+ layer.values["max_output_tokens"] = max_output_tokens
1054
+ layer.field_sources["max_output_tokens"] = source
1055
+ layer.raw_metadata["max_output_tokens"] = max_output_tokens
1056
+ if provider_id == "openai-codex" and "context_window_tokens" not in layer.values:
1057
+ layer.values["context_window_tokens"] = DEFAULT_UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS
1058
+ layer.field_sources["context_window_tokens"] = source
1059
+ layer.values["max_output_tokens"] = DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS
1060
+ layer.field_sources["max_output_tokens"] = f"{source}:local-default"
1061
+ layer.raw_metadata["context_window_tokens"] = layer.values["context_window_tokens"]
1062
+ layer.raw_metadata["max_output_tokens"] = layer.values["max_output_tokens"]
1063
+ layer.values["supports_vision"] = "image" in input_modalities
1064
+ layer.field_sources["supports_vision"] = source
1065
+ for field_name in _FLOAT_FIELDS:
1066
+ layer.values[field_name] = 0.0
1067
+ layer.field_sources[field_name] = f"{source}:included"
1068
+ return layer
1069
+
1070
+ def _resolve_official_provider_layer(self, requested_model: str) -> _LayerData:
1071
+ layer = _LayerData(name=OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE)
1072
+ provider_key, route_base_url = self._provider_route_identity()
1073
+ normalized_provider = str(provider_key or "").strip().lower()
1074
+ provider_models = _OFFICIAL_PROVIDER_MODEL_METADATA.get(normalized_provider)
1075
+ if provider_models is None:
1076
+ return layer
1077
+ model_match = _lookup_model_override(
1078
+ mapping=provider_models,
1079
+ requested_model=requested_model,
1080
+ label=f"{OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE}.{normalized_provider}",
1081
+ warnings=layer.warnings,
1082
+ )
1083
+ if model_match is None:
1084
+ return layer
1085
+
1086
+ model_key, payload = model_match
1087
+ payload = dict(payload)
1088
+ if normalized_provider == "moonshot":
1089
+ try:
1090
+ route_host = (urlsplit(route_base_url).hostname or "").rstrip(".").lower()
1091
+ except ValueError:
1092
+ route_host = ""
1093
+ if route_host != "api.moonshot.ai":
1094
+ # The verified prices are Moonshot's global USD rates. Capacity
1095
+ # and capabilities also apply to China, but those prices do not.
1096
+ for field_name in _FLOAT_FIELDS:
1097
+ payload.pop(field_name, None)
1098
+ source = f"{OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE}:{normalized_provider}"
1099
+ layer.model_name = model_key
1100
+ layer.raw_metadata = {
1101
+ **payload,
1102
+ "provider": normalized_provider,
1103
+ "catalog_source": OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE,
1104
+ "catalog_sources": list(
1105
+ _OFFICIAL_PROVIDER_MODEL_SOURCES.get(normalized_provider, {}).get(model_key, ())
1106
+ ),
1107
+ }
1108
+ self._apply_user_scope(layer=layer, source=source, payload=payload)
1109
+ return layer
1110
+
1111
+ def _resolve_canonical_model_layer(self, requested_model: str) -> _LayerData:
1112
+ layer = _LayerData(name=CANONICAL_MODEL_CATALOG_SOURCE)
1113
+ provider_key, _route_base_url = self._provider_route_identity()
1114
+ normalized_provider = str(provider_key or "").strip().lower()
1115
+
1116
+ canonical_id: str | None = None
1117
+ provider_aliases = _PROVIDER_CANONICAL_MODEL_IDS.get(normalized_provider, {})
1118
+ provider_alias_index = _build_model_alias_index(provider_aliases)
1119
+ for variant in model_name_variants(requested_model):
1120
+ route_id = provider_alias_index.get(variant.casefold())
1121
+ if route_id is not None:
1122
+ canonical_id = provider_aliases.get(route_id)
1123
+ break
1124
+
1125
+ if canonical_id is None:
1126
+ canonical_alias_index = _build_model_alias_index(_CANONICAL_MODEL_METADATA)
1127
+ for variant in model_name_variants(requested_model):
1128
+ canonical_id = canonical_alias_index.get(variant.casefold())
1129
+ if canonical_id is not None:
1130
+ break
1131
+ if canonical_id is None:
1132
+ return layer
1133
+
1134
+ payload = _CANONICAL_MODEL_METADATA.get(canonical_id)
1135
+ if payload is None:
1136
+ return layer
1137
+ source = f"{CANONICAL_MODEL_CATALOG_SOURCE}:{canonical_id}"
1138
+ layer.raw_metadata = {
1139
+ **payload,
1140
+ "canonical_model": canonical_id,
1141
+ "canonical_catalog_sources": list(_CANONICAL_MODEL_SOURCES.get(canonical_id, ())),
1142
+ }
1143
+ self._apply_user_scope(layer=layer, source=source, payload=payload)
1144
+ return layer
1145
+
1146
+ def _resolve_builtin_layer(self, requested_model: str) -> _LayerData:
1147
+ layer = _LayerData(name="built_in")
1148
+ model_match = _lookup_model_override(
1149
+ mapping=_BUILT_IN_MODEL_METADATA,
1150
+ requested_model=requested_model,
1151
+ label="built_in_model_catalog",
1152
+ warnings=layer.warnings,
1153
+ )
1154
+ if model_match is None:
1155
+ return layer
1156
+
1157
+ model_key, payload = model_match
1158
+ layer.model_name = model_key
1159
+ layer.raw_metadata = dict(payload)
1160
+ self._apply_user_scope(layer=layer, source="built_in", payload=payload)
1161
+ return layer
1162
+
1163
+ def _resolve_learned_layer(self, requested_model: str) -> _LayerData:
1164
+ _ = requested_model
1165
+ # Placeholder layer for deterministic precedence; learned cache is not implemented yet.
1166
+ return _LayerData(name="learned")
1167
+
1168
+ def _resolve_field_value(self, field_name: str, layers: list[_LayerData]) -> tuple[Any, str]:
1169
+ for layer in layers:
1170
+ if field_name not in layer.values:
1171
+ continue
1172
+ value = layer.values[field_name]
1173
+ if value is None:
1174
+ continue
1175
+ return value, layer.field_sources.get(field_name, layer.name)
1176
+ return _FALLBACKS[field_name], "fallback"
1177
+
1178
+ def get(self, model_name: str, *, include_provider_auth: bool = True) -> ModelMeta:
1179
+ requested = model_name.strip() or self._cfg.model.strip() or "unknown-model"
1180
+ warnings: list[str] = []
1181
+ if "model_capabilities" in self._cfg.extra_fields:
1182
+ warnings.append(_DEPRECATED_MODEL_CAPABILITIES_WARNING)
1183
+
1184
+ env_layer = self._resolve_env_layer()
1185
+ user_layer = self._resolve_user_layer(requested)
1186
+ provider_auth_layer = (
1187
+ self._resolve_provider_auth_layer(requested)
1188
+ if include_provider_auth
1189
+ else _LayerData(name="provider_auth")
1190
+ )
1191
+ official_provider_layer = self._resolve_official_provider_layer(requested)
1192
+ bundled_catalog_layer = self._resolve_bundled_model_catalog_layer(requested)
1193
+ canonical_model_layer = self._resolve_canonical_model_layer(requested)
1194
+ route_provider, route_base_url = self._provider_route_identity()
1195
+ active_provider = (
1196
+ str(route_provider or known_provider_key_from_base_url(route_base_url) or "")
1197
+ .strip()
1198
+ .lower()
1199
+ )
1200
+ bundled_provider = (
1201
+ str(bundled_catalog_layer.raw_metadata.get("litellm_provider") or "").strip().lower()
1202
+ )
1203
+ catalog_route_provider = (
1204
+ str(bundled_catalog_layer.raw_metadata.get("catalog_provider_hint") or "")
1205
+ .strip()
1206
+ .lower()
1207
+ or bundled_provider
1208
+ )
1209
+ if catalog_route_provider and catalog_route_provider != active_provider:
1210
+ # Catalog prices belong to the route selected by the catalog lookup.
1211
+ # A model-name match may still supply portable capability data, but
1212
+ # it cannot establish prices for an unrelated compatible endpoint.
1213
+ for field_name in _FLOAT_FIELDS:
1214
+ bundled_catalog_layer.values.pop(field_name, None)
1215
+ bundled_catalog_layer.field_sources.pop(field_name, None)
1216
+ bundled_catalog_layer.raw_metadata = {}
1217
+ built_in_layer = self._resolve_builtin_layer(requested)
1218
+ learned_layer = self._resolve_learned_layer(requested)
1219
+ layers = [
1220
+ env_layer,
1221
+ user_layer,
1222
+ provider_auth_layer,
1223
+ official_provider_layer,
1224
+ canonical_model_layer,
1225
+ bundled_catalog_layer,
1226
+ built_in_layer,
1227
+ learned_layer,
1228
+ ]
1229
+
1230
+ for layer in layers:
1231
+ warnings.extend(layer.warnings)
1232
+
1233
+ field_sources: dict[str, str] = {}
1234
+ resolved_fields: dict[str, Any] = {}
1235
+ for field_name in _TRACKED_FIELDS:
1236
+ value, source = self._resolve_field_value(field_name, layers)
1237
+ resolved_fields[field_name] = value
1238
+ field_sources[field_name] = source
1239
+
1240
+ context_window_tokens = (
1241
+ parse_positive_int(resolved_fields["context_window_tokens"])
1242
+ or _FALLBACKS["context_window_tokens"]
1243
+ )
1244
+ max_output_tokens = (
1245
+ parse_positive_int(resolved_fields["max_output_tokens"])
1246
+ or _FALLBACKS["max_output_tokens"]
1247
+ )
1248
+ supports_vision = (
1249
+ parse_bool(resolved_fields["supports_vision"])
1250
+ if resolved_fields["supports_vision"] is not None
1251
+ else _FALLBACKS["supports_vision"]
1252
+ )
1253
+ if supports_vision is None:
1254
+ supports_vision = _FALLBACKS["supports_vision"]
1255
+ supports_reasoning = (
1256
+ parse_bool(resolved_fields["supports_reasoning"])
1257
+ if resolved_fields["supports_reasoning"] is not None
1258
+ else None
1259
+ )
1260
+ input_cost_per_token = (
1261
+ parse_non_negative_float(resolved_fields["input_cost_per_token"])
1262
+ if resolved_fields["input_cost_per_token"] is not None
1263
+ else None
1264
+ )
1265
+ output_cost_per_token = (
1266
+ parse_non_negative_float(resolved_fields["output_cost_per_token"])
1267
+ if resolved_fields["output_cost_per_token"] is not None
1268
+ else None
1269
+ )
1270
+ cache_read_input_cost_per_token = (
1271
+ parse_non_negative_float(resolved_fields["cache_read_input_cost_per_token"])
1272
+ if resolved_fields["cache_read_input_cost_per_token"] is not None
1273
+ else None
1274
+ )
1275
+ cache_creation_input_cost_per_token = (
1276
+ parse_non_negative_float(resolved_fields["cache_creation_input_cost_per_token"])
1277
+ if resolved_fields["cache_creation_input_cost_per_token"] is not None
1278
+ else None
1279
+ )
1280
+ cache_creation_5m_input_cost_per_token = (
1281
+ parse_non_negative_float(resolved_fields["cache_creation_5m_input_cost_per_token"])
1282
+ if resolved_fields["cache_creation_5m_input_cost_per_token"] is not None
1283
+ else None
1284
+ )
1285
+ cache_creation_1h_input_cost_per_token = (
1286
+ parse_non_negative_float(resolved_fields["cache_creation_1h_input_cost_per_token"])
1287
+ if resolved_fields["cache_creation_1h_input_cost_per_token"] is not None
1288
+ else None
1289
+ )
1290
+ reasoning_output_cost_per_token = (
1291
+ parse_non_negative_float(resolved_fields["reasoning_output_cost_per_token"])
1292
+ if resolved_fields["reasoning_output_cost_per_token"] is not None
1293
+ else None
1294
+ )
1295
+
1296
+ if max_output_tokens >= context_window_tokens:
1297
+ # Shared-window metadata (e.g. the Kimi Code ids publish max_tokens
1298
+ # up to the full context). Clamping to window-1 — the old behaviour —
1299
+ # left a 1-token input budget, which surfaced as "context: 0% left"
1300
+ # on a fresh 1M-context session. Reserve a conservative response
1301
+ # allowance instead so the input budget keeps most of the window.
1302
+ clamped = max(1, min(max_output_tokens, max(4096, context_window_tokens // 8)))
1303
+ warnings.append(
1304
+ "max_output_tokens >= context_window_tokens (shared window); "
1305
+ f"reserving {clamped} tokens for output."
1306
+ )
1307
+ max_output_tokens = clamped
1308
+
1309
+ key_fallback = (
1310
+ field_sources.get("context_window_tokens") == "fallback"
1311
+ or field_sources.get("max_output_tokens") == "fallback"
1312
+ )
1313
+ if key_fallback:
1314
+ warnings.append(_FALLBACK_WARNING)
1315
+
1316
+ all_sources = {field_sources.get(field_name, "fallback") for field_name in _TRACKED_FIELDS}
1317
+ overall_source = next(iter(all_sources)) if len(all_sources) == 1 else "mixed"
1318
+
1319
+ resolved_model_name = next(
1320
+ (
1321
+ layer.model_name
1322
+ for layer in layers
1323
+ if layer.values and isinstance(layer.model_name, str) and layer.model_name.strip()
1324
+ ),
1325
+ requested,
1326
+ )
1327
+
1328
+ final_warnings = tuple(_dedupe_warnings(warnings))
1329
+ self.last_error = bundled_catalog_layer.error if key_fallback else None
1330
+ self.last_warnings = list(final_warnings)
1331
+ self.last_source = overall_source
1332
+
1333
+ raw_metadata = dict(bundled_catalog_layer.raw_metadata)
1334
+ raw_metadata.update(canonical_model_layer.raw_metadata)
1335
+ raw_metadata.update(official_provider_layer.raw_metadata)
1336
+ raw_metadata.update(provider_auth_layer.raw_metadata)
1337
+ return ModelMeta(
1338
+ model_name=resolved_model_name,
1339
+ context_window_tokens=context_window_tokens,
1340
+ max_output_tokens=max_output_tokens,
1341
+ input_cost_per_token=input_cost_per_token,
1342
+ output_cost_per_token=output_cost_per_token,
1343
+ cache_read_input_cost_per_token=cache_read_input_cost_per_token,
1344
+ cache_creation_input_cost_per_token=cache_creation_input_cost_per_token,
1345
+ cache_creation_5m_input_cost_per_token=cache_creation_5m_input_cost_per_token,
1346
+ cache_creation_1h_input_cost_per_token=cache_creation_1h_input_cost_per_token,
1347
+ reasoning_output_cost_per_token=reasoning_output_cost_per_token,
1348
+ raw_metadata=raw_metadata,
1349
+ source=overall_source,
1350
+ supports_vision=bool(supports_vision),
1351
+ supports_reasoning=supports_reasoning,
1352
+ field_sources=field_sources,
1353
+ warnings=final_warnings,
1354
+ provider_key=self._provider_hint(),
1355
+ )
1356
+
1357
+
1358
+ def resolve_model_provider_key(
1359
+ *,
1360
+ cfg: AppConfig,
1361
+ model_name: str,
1362
+ base_url: str | None = None,
1363
+ profile_name: str | None = None,
1364
+ ) -> str | None:
1365
+ requested_model = str(model_name or "").strip()
1366
+ resolved_base_url = base_url or getattr(cfg, "base_url", None)
1367
+ try:
1368
+ from .profile_presets import find_preset_for_base_url
1369
+
1370
+ preset = find_preset_for_base_url(str(resolved_base_url or ""))
1371
+ except Exception: # noqa: BLE001 - provider inference remains best effort
1372
+ preset = None
1373
+ preset_provider = str(preset.provider_key or "").strip() if preset is not None else ""
1374
+ if preset_provider:
1375
+ return preset_provider
1376
+
1377
+ url_provider = _provider_key_from_base_url(resolved_base_url)
1378
+ if url_provider == "openrouter":
1379
+ return url_provider
1380
+
1381
+ if requested_model:
1382
+ meta = ModelRegistry(cfg=cfg).get(requested_model)
1383
+ provider = str(meta.raw_metadata.get("litellm_provider") or "").strip()
1384
+ if provider:
1385
+ return provider
1386
+ resolved_model = str(meta.model_name or "").strip()
1387
+ if "/" in resolved_model:
1388
+ return resolved_model.split("/", 1)[0].strip() or None
1389
+
1390
+ if url_provider:
1391
+ return url_provider
1392
+
1393
+ profile_provider = str(profile_name or "").strip()
1394
+ if profile_provider and profile_provider.lower() != "default":
1395
+ return profile_provider
1396
+
1397
+ if "/" in requested_model:
1398
+ return requested_model.split("/", 1)[0].strip() or None
1399
+ return requested_model or None
1400
+
1401
+
1402
+ def _provider_key_from_base_url(base_url: str | None) -> str | None:
1403
+ raw = str(base_url or "").strip()
1404
+ if not raw:
1405
+ return None
1406
+ try:
1407
+ hostname = (urlsplit(raw).hostname or "").rstrip(".").lower()
1408
+ except ValueError:
1409
+ return None
1410
+ if not hostname:
1411
+ return None
1412
+ known_provider = known_provider_key_from_base_url(raw)
1413
+ if known_provider:
1414
+ return known_provider
1415
+ parts = [
1416
+ part
1417
+ for part in hostname.split(".")
1418
+ if part and part not in {"api", "ai", "www", "com", "v1", "v1beta"}
1419
+ ]
1420
+ return parts[-1] if parts else hostname