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,685 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import re
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Any, Literal
9
+ from urllib.parse import urlsplit
10
+
11
+ import httpx
12
+
13
+ from .llm.protocols import (
14
+ ANTHROPIC_MESSAGES_PROTOCOL,
15
+ GEMINI_GENERATE_CONTENT_PROTOCOL,
16
+ GEMINI_INTERACTIONS_PROTOCOL,
17
+ OPENAI_COMPAT_PROTOCOL,
18
+ )
19
+ from .profiles import ProfileSpec
20
+
21
+ ProviderModelCatalogStrategy = Literal["gemini", "anthropic", "openai"]
22
+ ModelChatCompatibility = Literal["chat", "unknown"]
23
+
24
+ _DEFAULT_TIMEOUT_S = 6.0
25
+ _MAX_RESPONSE_BYTES = 5 * 1024 * 1024
26
+ _MAX_PAGES = 25
27
+ _MAX_MODELS = 5_000
28
+ _MAX_MODEL_ID_LENGTH = 512
29
+ _STREAM_CHUNK_SIZE = 64 * 1024
30
+ _USER_AGENT = "alysis-code"
31
+ _ANTHROPIC_VERSION = "2023-06-01"
32
+
33
+ _NON_CHAT_MODES = frozenset(
34
+ {
35
+ "audio_speech",
36
+ "audio_transcription",
37
+ "completion",
38
+ "completions",
39
+ "embedding",
40
+ "embeddings",
41
+ "image",
42
+ "image_generation",
43
+ "moderation",
44
+ "rerank",
45
+ "reranking",
46
+ "speech",
47
+ "transcription",
48
+ "text_completion",
49
+ "text_completions",
50
+ "video",
51
+ "video_generation",
52
+ }
53
+ )
54
+ _CHAT_CAPABILITY_NAMES = frozenset(
55
+ {
56
+ "chat",
57
+ "chat_completion",
58
+ "chat_completions",
59
+ "completion_chat",
60
+ "generate_content",
61
+ "generatecontent",
62
+ "messages",
63
+ "responses",
64
+ "text_generation",
65
+ }
66
+ )
67
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|[@-_])")
68
+ _TERMINAL_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class ProviderModelOption:
73
+ """A model advertised by the active API-key provider."""
74
+
75
+ id: str
76
+ label: str
77
+ description: str = ""
78
+ chat_compatibility: ModelChatCompatibility = "unknown"
79
+
80
+
81
+ class ProviderModelCatalogError(RuntimeError):
82
+ """A provider catalog could not be loaded safely.
83
+
84
+ Messages intentionally contain neither response bodies nor request exception
85
+ details because both can echo credentials supplied by a user-controlled endpoint.
86
+ """
87
+
88
+
89
+ def provider_model_catalog_strategy(profile: ProfileSpec) -> ProviderModelCatalogStrategy:
90
+ """Return the list-model wire format used by ``profile``.
91
+
92
+ First-party compatibility profiles still use their provider-native catalog:
93
+ Gemini's native endpoint exposes ``generateContent`` support and Anthropic's
94
+ native endpoint supplies account-aware model metadata. All other profiles use
95
+ the broadly implemented OpenAI-shaped ``/models`` surface.
96
+ """
97
+
98
+ protocol = str(profile.protocol or "").strip().lower()
99
+ if protocol in {GEMINI_GENERATE_CONTENT_PROTOCOL, GEMINI_INTERACTIONS_PROTOCOL}:
100
+ return "gemini"
101
+ if protocol == ANTHROPIC_MESSAGES_PROTOCOL:
102
+ return "anthropic"
103
+ # Compatibility profile names are user-controlled. Dispatching a custom
104
+ # gateway called "gemini-proxy" or "claude" to a first-party wire format
105
+ # would send the wrong auth headers and parse the wrong schema. Only the
106
+ # official first-party hosts opt a compatibility profile into native listing.
107
+ if protocol == OPENAI_COMPAT_PROTOCOL:
108
+ hostname = _url_hostname(profile.base_url)
109
+ if hostname == "generativelanguage.googleapis.com":
110
+ return "gemini"
111
+ if hostname == "api.anthropic.com":
112
+ return "anthropic"
113
+ return "openai"
114
+
115
+
116
+ def discover_provider_models(
117
+ *,
118
+ profile: ProfileSpec,
119
+ api_key: str | None,
120
+ timeout_s: float = _DEFAULT_TIMEOUT_S,
121
+ transport: httpx.BaseTransport | None = None,
122
+ ) -> tuple[ProviderModelOption, ...]:
123
+ """Fetch the active provider's router-compatible model catalog.
124
+
125
+ Discovery is deliberately bounded and offline-safe at the API boundary: it
126
+ never follows redirects, caps pages, model count, and response size, and
127
+ translates all provider/transport failures into a sanitized
128
+ :class:`ProviderModelCatalogError`. Callers can then retain their curated
129
+ preset rows as a fallback without exposing secrets in the TUI.
130
+ """
131
+
132
+ base_url = str(profile.base_url or "").strip().rstrip("/")
133
+ if not base_url:
134
+ raise ProviderModelCatalogError("Provider model catalog URL is not configured.")
135
+ if not _valid_timeout(timeout_s):
136
+ raise ProviderModelCatalogError("Provider model catalog timeout must be positive.")
137
+
138
+ strategy = provider_model_catalog_strategy(profile)
139
+ normalized_key = str(api_key or "").strip()
140
+ deadline = time.monotonic() + float(timeout_s)
141
+ if strategy in {"gemini", "anthropic"} and not normalized_key:
142
+ raise ProviderModelCatalogError(
143
+ "An API key is required to load this provider's model catalog."
144
+ )
145
+
146
+ try:
147
+ with httpx.Client(
148
+ timeout=float(timeout_s),
149
+ transport=transport,
150
+ follow_redirects=False,
151
+ ) as client:
152
+ if strategy == "gemini":
153
+ return _discover_gemini_models(
154
+ client=client,
155
+ profile=profile,
156
+ api_key=normalized_key,
157
+ deadline=deadline,
158
+ )
159
+ if strategy == "anthropic":
160
+ return _discover_anthropic_models(
161
+ client=client,
162
+ profile=profile,
163
+ api_key=normalized_key,
164
+ deadline=deadline,
165
+ )
166
+ return _discover_openai_models(
167
+ client=client,
168
+ profile=profile,
169
+ api_key=normalized_key,
170
+ deadline=deadline,
171
+ )
172
+ except ProviderModelCatalogError:
173
+ raise
174
+ except Exception as exc: # noqa: BLE001 - sanitize every provider boundary failure
175
+ raise ProviderModelCatalogError(
176
+ f"Provider model catalog is unavailable ({type(exc).__name__})."
177
+ ) from None
178
+
179
+
180
+ def _discover_gemini_models(
181
+ *,
182
+ client: httpx.Client,
183
+ profile: ProfileSpec,
184
+ api_key: str,
185
+ deadline: float,
186
+ ) -> tuple[ProviderModelOption, ...]:
187
+ url = f"{_gemini_catalog_base_url(profile.base_url)}/models"
188
+ headers = _merged_headers(
189
+ {
190
+ "Accept": "application/json",
191
+ "User-Agent": _USER_AGENT,
192
+ "x-goog-api-key": api_key,
193
+ },
194
+ profile.extra_headers,
195
+ )
196
+ options: list[ProviderModelOption] = []
197
+ seen_ids: set[str] = set()
198
+ seen_tokens: set[str] = set()
199
+ page_token = ""
200
+
201
+ for _page in range(_MAX_PAGES):
202
+ params: dict[str, str | int] = {"pageSize": 1000}
203
+ if page_token:
204
+ params["pageToken"] = page_token
205
+ payload = _get_json(
206
+ client,
207
+ url=url,
208
+ headers=headers,
209
+ params=params,
210
+ deadline=deadline,
211
+ )
212
+ if not isinstance(payload, dict):
213
+ raise ProviderModelCatalogError(
214
+ "Provider model catalog returned an invalid Gemini response."
215
+ )
216
+ raw_models = payload.get("models", [])
217
+ if not isinstance(raw_models, list):
218
+ raise ProviderModelCatalogError(
219
+ "Provider model catalog returned an invalid Gemini model list."
220
+ )
221
+ for raw in raw_models:
222
+ if not isinstance(raw, dict) or not _gemini_supports_generate_content(raw):
223
+ continue
224
+ model_id = _gemini_model_id(raw)
225
+ if not model_id:
226
+ continue
227
+ _append_option(
228
+ options,
229
+ seen_ids,
230
+ ProviderModelOption(
231
+ id=model_id,
232
+ label=_clean_text(raw.get("displayName")) or model_id,
233
+ description=_clean_text(raw.get("description")),
234
+ chat_compatibility="chat",
235
+ ),
236
+ )
237
+ _enforce_model_bound(options)
238
+
239
+ next_token = str(payload.get("nextPageToken") or "").strip()
240
+ if not next_token:
241
+ return tuple(options)
242
+ if next_token == page_token or next_token in seen_tokens:
243
+ raise ProviderModelCatalogError(
244
+ "Provider model catalog returned a repeated Gemini page token."
245
+ )
246
+ seen_tokens.add(next_token)
247
+ page_token = next_token
248
+
249
+ raise ProviderModelCatalogError("Provider model catalog exceeded the Gemini page limit.")
250
+
251
+
252
+ def _discover_anthropic_models(
253
+ *,
254
+ client: httpx.Client,
255
+ profile: ProfileSpec,
256
+ api_key: str,
257
+ deadline: float,
258
+ ) -> tuple[ProviderModelOption, ...]:
259
+ url = f"{str(profile.base_url).strip().rstrip('/')}/models"
260
+ headers = _merged_headers(
261
+ {
262
+ "Accept": "application/json",
263
+ "User-Agent": _USER_AGENT,
264
+ "x-api-key": api_key,
265
+ "anthropic-version": _ANTHROPIC_VERSION,
266
+ },
267
+ profile.extra_headers,
268
+ )
269
+ options: list[ProviderModelOption] = []
270
+ seen_ids: set[str] = set()
271
+ seen_cursors: set[str] = set()
272
+ after_id = ""
273
+
274
+ for _page in range(_MAX_PAGES):
275
+ params: dict[str, str | int] = {"limit": 1000}
276
+ if after_id:
277
+ params["after_id"] = after_id
278
+ payload = _get_json(
279
+ client,
280
+ url=url,
281
+ headers=headers,
282
+ params=params,
283
+ deadline=deadline,
284
+ )
285
+ if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
286
+ raise ProviderModelCatalogError(
287
+ "Provider model catalog returned an invalid Anthropic response."
288
+ )
289
+ for raw in payload["data"]:
290
+ if not isinstance(raw, dict):
291
+ continue
292
+ model_id = _clean_model_id(raw.get("id"))
293
+ if not model_id:
294
+ continue
295
+ _append_option(
296
+ options,
297
+ seen_ids,
298
+ ProviderModelOption(
299
+ id=model_id,
300
+ label=_clean_text(raw.get("display_name")) or model_id,
301
+ description=_anthropic_description(raw),
302
+ chat_compatibility="chat",
303
+ ),
304
+ )
305
+ _enforce_model_bound(options)
306
+
307
+ if not bool(payload.get("has_more")):
308
+ return tuple(options)
309
+ next_cursor = str(payload.get("last_id") or "").strip()
310
+ if not next_cursor:
311
+ raise ProviderModelCatalogError(
312
+ "Provider model catalog omitted the next Anthropic cursor."
313
+ )
314
+ if next_cursor == after_id or next_cursor in seen_cursors:
315
+ raise ProviderModelCatalogError(
316
+ "Provider model catalog returned a repeated Anthropic cursor."
317
+ )
318
+ seen_cursors.add(next_cursor)
319
+ after_id = next_cursor
320
+
321
+ raise ProviderModelCatalogError("Provider model catalog exceeded the Anthropic page limit.")
322
+
323
+
324
+ def _discover_openai_models(
325
+ *,
326
+ client: httpx.Client,
327
+ profile: ProfileSpec,
328
+ api_key: str,
329
+ deadline: float,
330
+ ) -> tuple[ProviderModelOption, ...]:
331
+ url = f"{str(profile.base_url).strip().rstrip('/')}/models"
332
+ base_headers = {
333
+ "Accept": "application/json",
334
+ "User-Agent": _USER_AGENT,
335
+ }
336
+ if api_key:
337
+ base_headers["Authorization"] = f"Bearer {api_key}"
338
+ headers = _merged_headers(base_headers, profile.extra_headers)
339
+ payload = _get_json(client, url=url, headers=headers, deadline=deadline)
340
+ raw_models: Any
341
+ if isinstance(payload, list):
342
+ raw_models = payload
343
+ elif isinstance(payload, dict):
344
+ raw_models = payload.get("data")
345
+ if raw_models is None:
346
+ raw_models = payload.get("models")
347
+ else:
348
+ raw_models = None
349
+ if not isinstance(raw_models, list):
350
+ raise ProviderModelCatalogError(
351
+ "Provider model catalog returned an invalid OpenAI-style response."
352
+ )
353
+
354
+ options: list[ProviderModelOption] = []
355
+ seen_ids: set[str] = set()
356
+ for raw in raw_models:
357
+ if isinstance(raw, str):
358
+ model_id = _clean_model_id(raw)
359
+ if model_id:
360
+ _append_option(
361
+ options,
362
+ seen_ids,
363
+ ProviderModelOption(id=model_id, label=model_id),
364
+ )
365
+ _enforce_model_bound(options)
366
+ continue
367
+ if not isinstance(raw, dict):
368
+ continue
369
+ model_id = _clean_model_id(raw.get("id") or raw.get("model") or raw.get("name"))
370
+ if not model_id or _generic_model_is_non_chat(raw):
371
+ continue
372
+ _append_option(
373
+ options,
374
+ seen_ids,
375
+ ProviderModelOption(
376
+ id=model_id,
377
+ label=_clean_text(raw.get("display_name") or raw.get("displayName")) or model_id,
378
+ description=_clean_text(raw.get("description")),
379
+ chat_compatibility=("chat" if _has_explicit_chat_capability(raw) else "unknown"),
380
+ ),
381
+ )
382
+ _enforce_model_bound(options)
383
+ return tuple(options)
384
+
385
+
386
+ def _get_json(
387
+ client: httpx.Client,
388
+ *,
389
+ url: str,
390
+ headers: dict[str, str],
391
+ params: dict[str, str | int] | None = None,
392
+ deadline: float,
393
+ ) -> Any:
394
+ remaining = deadline - time.monotonic()
395
+ if remaining <= 0:
396
+ raise ProviderModelCatalogError("Provider model catalog request timed out.")
397
+ try:
398
+ with client.stream(
399
+ "GET",
400
+ url,
401
+ headers=headers,
402
+ params=params,
403
+ timeout=remaining,
404
+ ) as response:
405
+ if not 200 <= response.status_code < 300:
406
+ raise ProviderModelCatalogError(
407
+ f"Provider model catalog request failed (HTTP {response.status_code})."
408
+ )
409
+ content_length = _positive_int(response.headers.get("content-length"))
410
+ if content_length is not None and content_length > _MAX_RESPONSE_BYTES:
411
+ raise ProviderModelCatalogError("Provider model catalog response was too large.")
412
+ body = bytearray()
413
+ for chunk in response.iter_bytes(chunk_size=_STREAM_CHUNK_SIZE):
414
+ if time.monotonic() >= deadline:
415
+ raise ProviderModelCatalogError("Provider model catalog request timed out.")
416
+ if len(body) + len(chunk) > _MAX_RESPONSE_BYTES:
417
+ raise ProviderModelCatalogError(
418
+ "Provider model catalog response was too large."
419
+ )
420
+ body.extend(chunk)
421
+ except ProviderModelCatalogError:
422
+ raise
423
+ except Exception as exc: # noqa: BLE001 - never leak request details from provider errors
424
+ raise ProviderModelCatalogError(
425
+ f"Provider model catalog request failed ({type(exc).__name__})."
426
+ ) from None
427
+ try:
428
+ return json.loads(bytes(body))
429
+ except Exception: # noqa: BLE001 - body is untrusted and must not appear in the error
430
+ raise ProviderModelCatalogError("Provider model catalog returned malformed JSON.") from None
431
+
432
+
433
+ def _gemini_catalog_base_url(base_url: str) -> str:
434
+ normalized = str(base_url or "").strip().rstrip("/")
435
+ for suffix in ("/openai/v1", "/openai"):
436
+ if normalized.casefold().endswith(suffix):
437
+ return normalized[: -len(suffix)].rstrip("/")
438
+ return normalized
439
+
440
+
441
+ def _gemini_supports_generate_content(raw: dict[str, Any]) -> bool:
442
+ methods = raw.get("supportedGenerationMethods")
443
+ return isinstance(methods, list) and any(
444
+ str(method or "").strip().casefold() == "generatecontent" for method in methods
445
+ )
446
+
447
+
448
+ def _gemini_model_id(raw: dict[str, Any]) -> str:
449
+ model_id = _clean_model_id(raw.get("baseModelId"))
450
+ if model_id:
451
+ return model_id.removeprefix("models/")
452
+ return _clean_model_id(raw.get("name")).removeprefix("models/")
453
+
454
+
455
+ def _anthropic_description(raw: dict[str, Any]) -> str:
456
+ description = _clean_text(raw.get("description"))
457
+ if description:
458
+ return description
459
+ details: list[str] = []
460
+ max_input = _positive_int(raw.get("max_input_tokens"))
461
+ max_output = _positive_int(raw.get("max_tokens"))
462
+ if max_input is not None:
463
+ details.append(f"{max_input:,} input tokens")
464
+ if max_output is not None:
465
+ details.append(f"{max_output:,} max output tokens")
466
+ return " · ".join(details)
467
+
468
+
469
+ def _generic_model_is_non_chat(raw: dict[str, Any]) -> bool:
470
+ explicit_chat = _has_explicit_chat_capability(raw)
471
+ if _has_explicit_non_chat_capability(raw, explicit_chat=explicit_chat):
472
+ return True
473
+ return False
474
+
475
+
476
+ def _has_explicit_chat_capability(raw: dict[str, Any]) -> bool:
477
+ mode = _normalized_capability_name(raw.get("mode"))
478
+ if mode in _CHAT_CAPABILITY_NAMES:
479
+ return True
480
+
481
+ endpoints = _string_list(raw.get("supported_endpoints"))
482
+ if endpoints and any(_endpoint_is_chat(endpoint) for endpoint in endpoints):
483
+ return True
484
+
485
+ capabilities = raw.get("capabilities")
486
+ if isinstance(capabilities, dict):
487
+ for name, enabled in capabilities.items():
488
+ if _normalized_capability_name(name) in _CHAT_CAPABILITY_NAMES and _is_supported(
489
+ enabled
490
+ ):
491
+ return True
492
+ elif isinstance(capabilities, list):
493
+ if any(
494
+ _normalized_capability_name(value) in _CHAT_CAPABILITY_NAMES for value in capabilities
495
+ ):
496
+ return True
497
+ return False
498
+
499
+
500
+ def _has_explicit_non_chat_capability(
501
+ raw: dict[str, Any],
502
+ *,
503
+ explicit_chat: bool,
504
+ ) -> bool:
505
+ mode = _normalized_capability_name(raw.get("mode"))
506
+ if mode in _NON_CHAT_MODES:
507
+ return True
508
+
509
+ object_type = _normalized_capability_name(raw.get("type"))
510
+ if object_type in _NON_CHAT_MODES:
511
+ return True
512
+
513
+ output_modalities = _output_modalities(raw)
514
+ if output_modalities and "text" not in output_modalities:
515
+ return True
516
+
517
+ endpoints = _string_list(raw.get("supported_endpoints"))
518
+ if endpoints and not explicit_chat and all(_endpoint_is_clearly_non_chat(v) for v in endpoints):
519
+ return True
520
+
521
+ capabilities = raw.get("capabilities")
522
+ if isinstance(capabilities, dict) and not explicit_chat:
523
+ if any(
524
+ _normalized_capability_name(name) in _NON_CHAT_MODES and _is_supported(enabled)
525
+ for name, enabled in capabilities.items()
526
+ ):
527
+ return True
528
+ recognized = [
529
+ _is_supported(enabled)
530
+ for name, enabled in capabilities.items()
531
+ if _normalized_capability_name(name) in _CHAT_CAPABILITY_NAMES
532
+ ]
533
+ if recognized and not any(recognized):
534
+ return True
535
+ elif isinstance(capabilities, list) and not explicit_chat:
536
+ if any(_normalized_capability_name(value) in _NON_CHAT_MODES for value in capabilities):
537
+ return True
538
+ return False
539
+
540
+
541
+ def _output_modalities(raw: dict[str, Any]) -> set[str]:
542
+ candidates = [raw.get("output_modalities"), raw.get("supported_output_modalities")]
543
+ architecture = raw.get("architecture")
544
+ if isinstance(architecture, dict):
545
+ candidates.append(architecture.get("output_modalities"))
546
+ modalities: set[str] = set()
547
+ for candidate in candidates:
548
+ modalities.update(value.casefold() for value in _string_list(candidate))
549
+ return modalities
550
+
551
+
552
+ def _endpoint_is_chat(endpoint: str) -> bool:
553
+ normalized = endpoint.strip().casefold().replace("_", "-")
554
+ return any(
555
+ marker in normalized
556
+ for marker in (
557
+ "chat/completions",
558
+ "generatecontent",
559
+ "/messages",
560
+ "/responses",
561
+ )
562
+ )
563
+
564
+
565
+ def _endpoint_is_clearly_non_chat(endpoint: str) -> bool:
566
+ normalized = endpoint.strip().casefold().replace("_", "-")
567
+ if normalized.endswith("/completions") and "chat/completions" not in normalized:
568
+ return True
569
+ return any(
570
+ marker in normalized
571
+ for marker in (
572
+ "/audio/",
573
+ "/embeddings",
574
+ "/images",
575
+ "/moderations",
576
+ "/rerank",
577
+ "/videos",
578
+ )
579
+ )
580
+
581
+
582
+ def _merged_headers(
583
+ defaults: dict[str, str],
584
+ extra_headers: dict[str, str] | None,
585
+ ) -> dict[str, str]:
586
+ headers = {
587
+ str(key).strip(): str(value).strip()
588
+ for key, value in defaults.items()
589
+ if str(key).strip() and str(value).strip()
590
+ }
591
+ for raw_key, raw_value in (extra_headers or {}).items():
592
+ key = str(raw_key).strip()
593
+ value = str(raw_value).strip()
594
+ if not key or not value:
595
+ continue
596
+ folded = key.casefold()
597
+ headers = {
598
+ existing: item for existing, item in headers.items() if existing.casefold() != folded
599
+ }
600
+ headers[key] = value
601
+ return headers
602
+
603
+
604
+ def _append_option(
605
+ options: list[ProviderModelOption],
606
+ seen_ids: set[str],
607
+ option: ProviderModelOption,
608
+ ) -> None:
609
+ if not option.id or option.id in seen_ids:
610
+ return
611
+ seen_ids.add(option.id)
612
+ options.append(option)
613
+
614
+
615
+ def _enforce_model_bound(options: list[ProviderModelOption]) -> None:
616
+ if len(options) > _MAX_MODELS:
617
+ raise ProviderModelCatalogError("Provider model catalog exceeded the model limit.")
618
+
619
+
620
+ def _clean_model_id(value: Any) -> str:
621
+ text = _sanitize_terminal_text(value)
622
+ return text if len(text) <= _MAX_MODEL_ID_LENGTH else ""
623
+
624
+
625
+ def _clean_text(value: Any, *, limit: int = 320) -> str:
626
+ text = _sanitize_terminal_text(value)
627
+ if len(text) > limit:
628
+ return text[: max(0, limit - 3)].rstrip() + "..."
629
+ return text
630
+
631
+
632
+ def _sanitize_terminal_text(value: Any) -> str:
633
+ text = str(value or "")
634
+ text = _ANSI_ESCAPE_RE.sub("", text)
635
+ text = _TERMINAL_CONTROL_RE.sub("", text)
636
+ return " ".join(text.split())
637
+
638
+
639
+ def _positive_int(value: Any) -> int | None:
640
+ try:
641
+ parsed = int(value)
642
+ except (TypeError, ValueError):
643
+ return None
644
+ return parsed if parsed > 0 else None
645
+
646
+
647
+ def _string_list(value: Any) -> tuple[str, ...]:
648
+ if not isinstance(value, (list, tuple)):
649
+ return ()
650
+ return tuple(text for item in value if (text := str(item or "").strip()))
651
+
652
+
653
+ def _normalized_capability_name(value: Any) -> str:
654
+ return re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().casefold()).strip("_")
655
+
656
+
657
+ def _is_supported(value: Any) -> bool:
658
+ if isinstance(value, dict):
659
+ return bool(value.get("supported"))
660
+ return bool(value)
661
+
662
+
663
+ def _valid_timeout(value: Any) -> bool:
664
+ try:
665
+ parsed = float(value)
666
+ except (TypeError, ValueError):
667
+ return False
668
+ return math.isfinite(parsed) and parsed > 0
669
+
670
+
671
+ def _url_hostname(value: str) -> str:
672
+ try:
673
+ return str(urlsplit(str(value or "").strip()).hostname or "").rstrip(".").casefold()
674
+ except ValueError:
675
+ return ""
676
+
677
+
678
+ __all__ = [
679
+ "ModelChatCompatibility",
680
+ "ProviderModelCatalogError",
681
+ "ProviderModelCatalogStrategy",
682
+ "ProviderModelOption",
683
+ "discover_provider_models",
684
+ "provider_model_catalog_strategy",
685
+ ]