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,1834 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from urllib.parse import urlsplit
5
+
6
+ from .llm.cache_capabilities import (
7
+ CACHE_STRATEGY_ANTHROPIC_CACHE_CONTROL,
8
+ CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT,
9
+ CACHE_STRATEGY_IMPLICIT_PROVIDER,
10
+ CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY,
11
+ CACHE_STRATEGY_OPENAI_PROMPT_CACHE,
12
+ CACHE_STRATEGY_OPENROUTER_STICKY_SESSION,
13
+ CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS,
14
+ CACHE_STRATEGY_XAI_CONVERSATION_HEADER,
15
+ CACHE_USAGE_SCHEMA_ANTHROPIC,
16
+ CACHE_USAGE_SCHEMA_GEMINI,
17
+ CACHE_USAGE_SCHEMA_OPENAI,
18
+ CACHE_USAGE_SCHEMA_PROVIDER,
19
+ OPENROUTER_SESSION_ID_FIELD,
20
+ XAI_CONVERSATION_ID_HEADER_FIELD,
21
+ CacheCapabilitySpec,
22
+ )
23
+ from .llm.protocols import (
24
+ ANTHROPIC_MESSAGES_PROTOCOL,
25
+ GEMINI_GENERATE_CONTENT_PROTOCOL,
26
+ GEMINI_INTERACTIONS_PROTOCOL,
27
+ OPENAI_COMPAT_PROTOCOL,
28
+ OPENAI_RESPONSES_PROTOCOL,
29
+ )
30
+ from .profiles import ProfileSpec
31
+ from .web_search_adapters import (
32
+ ANTHROPIC_MESSAGES_ADAPTER,
33
+ AUTO_WEB_SEARCH_ADAPTER,
34
+ COHERE_WEB_SEARCH_ADAPTER,
35
+ DASHSCOPE_CHAT_ADAPTER,
36
+ GEMINI_GROUNDING_ADAPTER,
37
+ GROQ_COMPOUND_ADAPTER,
38
+ MINIMAX_CODING_PLAN_ADAPTER,
39
+ MISTRAL_CONVERSATIONS_ADAPTER,
40
+ MOONSHOT_KIMI_ADAPTER,
41
+ OPENAI_RESPONSES_ADAPTER,
42
+ OPENROUTER_WEB_ADAPTER,
43
+ PERPLEXITY_SONAR_ADAPTER,
44
+ VOLCENGINE_WEB_SEARCH_ADAPTER,
45
+ XAI_RESPONSES_ADAPTER,
46
+ ZHIPU_WEB_SEARCH_ADAPTER,
47
+ )
48
+
49
+ NATIVE_PROFILE_PROTOCOLS: frozenset[str] = frozenset(
50
+ {
51
+ OPENAI_RESPONSES_PROTOCOL,
52
+ ANTHROPIC_MESSAGES_PROTOCOL,
53
+ GEMINI_GENERATE_CONTENT_PROTOCOL,
54
+ GEMINI_INTERACTIONS_PROTOCOL,
55
+ }
56
+ )
57
+ FIRST_PARTY_NATIVE_PRESET_KEYS: tuple[str, ...] = (
58
+ "openai-responses",
59
+ "anthropic",
60
+ "gemini",
61
+ )
62
+ FIRST_CLASS_SETUP_PRESET_KEYS: tuple[str, ...] = (
63
+ # "alysis" (hosted MiMo) deliberately absent: while no campaign is
64
+ # running it stays off the primary picker entirely (advanced picker only).
65
+ *FIRST_PARTY_NATIVE_PRESET_KEYS,
66
+ )
67
+ FIRST_PARTY_COMPATIBILITY_PRESET_KEYS: tuple[str, ...] = (
68
+ "openai",
69
+ "anthropic-compat",
70
+ "gemini-compat",
71
+ )
72
+ LEGACY_NATIVE_ALIAS_PRESET_KEYS: tuple[str, ...] = ("anthropic-native", "gemini-native")
73
+ LOCAL_PROFILE_PRESET_KEYS: tuple[str, ...] = ("ollama", "lm-studio", "vllm")
74
+ _CUSTOM_PRESET_KEY = "custom"
75
+ # Account-gated hosted presets (`alysis login`, no API key). Kept off the
76
+ # primary provider picker while no hosted campaign is running.
77
+ _ACCOUNT_GATED_PRESET_KEYS: tuple[str, ...] = ("alysis",)
78
+ _CONVERSION_PRESET_BY_FAMILY: dict[str, dict[str, str]] = {
79
+ "openai": {"native": "openai-responses", "compatibility": "openai"},
80
+ "anthropic": {"native": "anthropic", "compatibility": "anthropic-compat"},
81
+ "gemini": {"native": "gemini", "compatibility": "gemini-compat"},
82
+ }
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class ProfilePreset:
87
+ key: str
88
+ label: str
89
+ protocol: str
90
+ base_url: str
91
+ api_key_env: str | None
92
+ extra_headers: dict[str, str] = field(default_factory=dict)
93
+ suggested_models: tuple[str, ...] = ()
94
+ suggested_model_descriptions: dict[str, str] = field(default_factory=dict)
95
+ model_aliases: dict[str, str] = field(default_factory=dict)
96
+ validation_model: str = ""
97
+ web_search_adapter: str = AUTO_WEB_SEARCH_ADAPTER
98
+ web_search_model: str = ""
99
+ setup_warning: str = ""
100
+ notes: str = ""
101
+ cache_capability: CacheCapabilitySpec | None = None
102
+ # Keep new optional fields at the end so extensions using the legacy
103
+ # positional constructor continue to bind the sixth argument to headers.
104
+ provider_key: str = ""
105
+
106
+
107
+ _OPENAI_PROMPT_CACHE_CAPABILITY = CacheCapabilitySpec(
108
+ strategy=CACHE_STRATEGY_OPENAI_PROMPT_CACHE,
109
+ enabled=True,
110
+ supports_prompt_cache_key=True,
111
+ supports_prompt_cache_retention=True,
112
+ reports_cache_read_tokens=True,
113
+ reports_cache_write_tokens=True,
114
+ usage_schema=CACHE_USAGE_SCHEMA_OPENAI,
115
+ min_cacheable_tokens=1024,
116
+ source="preset",
117
+ )
118
+
119
+
120
+ def _cache_minimum(tokens: int) -> CacheCapabilitySpec:
121
+ """A scoped override that only narrows the minimum cacheable prefix."""
122
+
123
+ return CacheCapabilitySpec(min_cacheable_tokens=tokens, source="preset")
124
+
125
+
126
+ # Anthropic's minimum cacheable prefix is per-model and does not move
127
+ # monotonically with the version number: the 5-generation flagships halved the
128
+ # floor to 512 while Opus 4.6/4.5 and Haiku 4.5 still need 4096. A prefix under
129
+ # the floor is silently not cached, so the write premium buys nothing — the
130
+ # floor is what makes the request-shape report say so instead of guessing.
131
+ _ANTHROPIC_CACHE_CONTROL_CAPABILITY = CacheCapabilitySpec(
132
+ strategy=CACHE_STRATEGY_ANTHROPIC_CACHE_CONTROL,
133
+ enabled=True,
134
+ supports_cache_control=True,
135
+ reports_cache_read_tokens=True,
136
+ reports_cache_write_tokens=True,
137
+ usage_schema=CACHE_USAGE_SCHEMA_ANTHROPIC,
138
+ min_cacheable_tokens=1024,
139
+ model_family_overrides=(
140
+ ("claude-opus-5", _cache_minimum(512)),
141
+ ("claude-fable-5", _cache_minimum(512)),
142
+ ("claude-mythos-5", _cache_minimum(512)),
143
+ ("claude-mythos-preview", _cache_minimum(2048)),
144
+ ("claude-opus-4-7", _cache_minimum(2048)),
145
+ ("claude-opus-4-6", _cache_minimum(4096)),
146
+ ("claude-opus-4-5", _cache_minimum(4096)),
147
+ ("claude-haiku-4-5", _cache_minimum(4096)),
148
+ ),
149
+ source="preset",
150
+ )
151
+ _GEMINI_EXPLICIT_CACHED_CONTENT_CAPABILITY = CacheCapabilitySpec(
152
+ strategy=CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT,
153
+ enabled=True,
154
+ supports_explicit_cached_content=True,
155
+ reports_cache_read_tokens=True,
156
+ usage_schema=CACHE_USAGE_SCHEMA_GEMINI,
157
+ min_cacheable_tokens=4096,
158
+ source="preset",
159
+ )
160
+ _MISTRAL_PROMPT_CACHE_CAPABILITY = CacheCapabilitySpec(
161
+ strategy=CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY,
162
+ enabled=True,
163
+ supports_prompt_cache_key=True,
164
+ reports_cache_read_tokens=True,
165
+ usage_schema=CACHE_USAGE_SCHEMA_OPENAI,
166
+ min_cacheable_tokens=1024,
167
+ emits_request_fields=True,
168
+ notes=("Emits Mistral prompt_cache_key for stable server routing and prompt-cache hits.",),
169
+ source="preset",
170
+ )
171
+ _OPENROUTER_STICKY_SESSION_CACHE_CAPABILITY = CacheCapabilitySpec(
172
+ strategy=CACHE_STRATEGY_OPENROUTER_STICKY_SESSION,
173
+ enabled=True,
174
+ reports_cache_read_tokens=True,
175
+ reports_cache_write_tokens=True,
176
+ usage_schema=CACHE_USAGE_SCHEMA_PROVIDER,
177
+ emits_request_fields=True,
178
+ request_fields=(OPENROUTER_SESSION_ID_FIELD,),
179
+ notes=(
180
+ "Emits OpenRouter session_id for sticky routing; upstream cache semantics remain "
181
+ "route-dependent.",
182
+ ),
183
+ source="preset",
184
+ )
185
+ _XAI_CONVERSATION_HEADER_CACHE_CAPABILITY = CacheCapabilitySpec(
186
+ strategy=CACHE_STRATEGY_XAI_CONVERSATION_HEADER,
187
+ enabled=True,
188
+ reports_cache_read_tokens=True,
189
+ usage_schema=CACHE_USAGE_SCHEMA_PROVIDER,
190
+ emits_request_fields=True,
191
+ request_fields=(XAI_CONVERSATION_ID_HEADER_FIELD,),
192
+ notes=("Emits x-grok-conv-id for sticky cache routing on xAI Chat Completions.",),
193
+ source="preset",
194
+ )
195
+ _QWEN_DIAGNOSTIC_CACHE_CAPABILITY = CacheCapabilitySpec(
196
+ strategy=CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS,
197
+ enabled=True,
198
+ reports_cache_read_tokens=True,
199
+ reports_cache_write_tokens=True,
200
+ usage_schema=CACHE_USAGE_SCHEMA_PROVIDER,
201
+ min_cacheable_tokens=1024,
202
+ emits_request_fields=False,
203
+ notes=(
204
+ "Diagnostic-only in auto mode; Qwen cache_control content markers mutate "
205
+ "message shape and require request-shape gating.",
206
+ ),
207
+ source="preset",
208
+ )
209
+ _MOONSHOT_AUTOMATIC_CACHE_CAPABILITY = CacheCapabilitySpec(
210
+ strategy=CACHE_STRATEGY_IMPLICIT_PROVIDER,
211
+ enabled=True,
212
+ supports_prompt_cache_key=True,
213
+ reports_cache_read_tokens=True,
214
+ usage_schema=CACHE_USAGE_SCHEMA_PROVIDER,
215
+ emits_request_fields=True,
216
+ notes=(
217
+ "Moonshot caches matching prompt prefixes automatically; prompt_cache_key keeps a "
218
+ "session on a stable cache-affinity route.",
219
+ ),
220
+ source="preset",
221
+ )
222
+ _ZAI_CODING_PLAN_CACHE_CAPABILITY = CacheCapabilitySpec(
223
+ strategy=CACHE_STRATEGY_IMPLICIT_PROVIDER,
224
+ enabled=True,
225
+ reports_cache_read_tokens=True,
226
+ usage_schema=CACHE_USAGE_SCHEMA_PROVIDER,
227
+ emits_request_fields=False,
228
+ notes=(
229
+ "Z.AI Coding Plan caches matching prompt prefixes automatically and reports "
230
+ "cached-input usage; Alysis Code emits no provider-specific cache fields.",
231
+ ),
232
+ source="preset",
233
+ )
234
+
235
+
236
+ def preset_protocol_kind(preset: ProfilePreset) -> str:
237
+ return "native" if preset.protocol in NATIVE_PROFILE_PROTOCOLS else "compatibility"
238
+
239
+
240
+ def preset_protocol_summary(preset: ProfilePreset) -> str:
241
+ if preset.protocol in NATIVE_PROFILE_PROTOCOLS:
242
+ return (
243
+ f"Native first-party protocol: {preset.protocol} (recommended for first-party API keys)"
244
+ )
245
+ return "Compatibility protocol: OpenAI-compatible chat transport"
246
+
247
+
248
+ def preset_selection_label(preset: ProfilePreset) -> str:
249
+ """Return a setup/config label that keeps protocol details out of the primary choice."""
250
+ if preset.key == "alysis":
251
+ return "Alysis Code Pro (hosted models) - requires login"
252
+ if preset.key == "openai-responses":
253
+ return "OpenAI - Native Responses"
254
+ if preset.key in {"anthropic", "anthropic-native"}:
255
+ return "Anthropic Claude - Native Messages"
256
+ if preset.key in {"gemini", "gemini-native"}:
257
+ return "Google Gemini - Native GenerateContent"
258
+ if preset.key == "openai":
259
+ return "OpenAI - Compatibility/gateway Chat Completions"
260
+ if preset.key == "anthropic-compat":
261
+ return "Anthropic Claude compatibility - legacy OpenAI-compatible"
262
+ if preset.key == "gemini-compat":
263
+ return "Google Gemini compatibility - legacy OpenAI-compatible"
264
+ if preset.key in LOCAL_PROFILE_PRESET_KEYS:
265
+ return f"{preset.label} - Local endpoint"
266
+ if preset.key == "custom":
267
+ return "Custom OpenAI-compatible endpoint"
268
+ return preset.label
269
+
270
+
271
+ def _advanced_only_preset_keys() -> frozenset[str]:
272
+ """Preset keys deliberately kept off the primary provider picker.
273
+
274
+ Everything else in :data:`PROFILE_PRESETS` is a real hosted provider — the
275
+ native first-party APIs *and* the third-party API/gateway endpoints — and is
276
+ surfaced directly so users are not limited to the big-three brands. Only the
277
+ OpenAI-compatible duplicates of the native first-party providers, local
278
+ endpoints (Ollama/LM Studio/vLLM), the manual custom-URL entry, the
279
+ one-release legacy aliases, and the account-gated hosted MiMo preset
280
+ (no hosted campaign is running, so it is not a provider choice) stay
281
+ behind the advanced picker.
282
+ """
283
+ return frozenset(
284
+ {
285
+ _CUSTOM_PRESET_KEY,
286
+ *FIRST_PARTY_COMPATIBILITY_PRESET_KEYS,
287
+ *LOCAL_PROFILE_PRESET_KEYS,
288
+ *LEGACY_NATIVE_ALIAS_PRESET_KEYS,
289
+ *_ACCOUNT_GATED_PRESET_KEYS,
290
+ }
291
+ )
292
+
293
+
294
+ def provider_selection_presets() -> list[ProfilePreset]:
295
+ """Presets shown directly on the primary provider picker.
296
+
297
+ Native first-party providers lead — the best defaults for new users —
298
+ followed by every other hosted provider in registration order.
299
+ Compatibility duplicates, local endpoints, the custom-URL entry,
300
+ one-release legacy aliases, and the account-gated hosted MiMo preset are
301
+ the only presets held back for the advanced picker, so the user sees the
302
+ full range of hosted providers up front instead of just
303
+ OpenAI/Anthropic/Gemini.
304
+ """
305
+ by_key = PRESET_BY_KEY
306
+ advanced = _advanced_only_preset_keys()
307
+ leading = [by_key[key] for key in FIRST_CLASS_SETUP_PRESET_KEYS if key in by_key]
308
+ leading_keys = {preset.key for preset in leading}
309
+ rest = [
310
+ preset
311
+ for preset in PROFILE_PRESETS
312
+ if preset.key not in advanced and preset.key not in leading_keys
313
+ ]
314
+ return [*leading, *rest]
315
+
316
+
317
+ def advanced_provider_selection_presets() -> list[ProfilePreset]:
318
+ """Return the compatibility, local, custom, legacy alias, and account-gated presets.
319
+
320
+ These are exactly the presets held off the primary provider picker: the
321
+ OpenAI-compatible duplicates of the native first-party providers, local
322
+ endpoints (Ollama/LM Studio/vLLM), the manual custom-URL entry, the
323
+ one-release legacy aliases, and the account-gated hosted MiMo preset.
324
+ """
325
+ by_key = PRESET_BY_KEY
326
+ first_party_compat = [
327
+ by_key[key] for key in FIRST_PARTY_COMPATIBILITY_PRESET_KEYS if key in by_key
328
+ ]
329
+ local = [by_key[key] for key in LOCAL_PROFILE_PRESET_KEYS if key in by_key]
330
+ custom = [by_key[_CUSTOM_PRESET_KEY]] if _CUSTOM_PRESET_KEY in by_key else []
331
+ aliases = [by_key[key] for key in LEGACY_NATIVE_ALIAS_PRESET_KEYS if key in by_key]
332
+ account_gated = [by_key[key] for key in _ACCOUNT_GATED_PRESET_KEYS if key in by_key]
333
+ return [*first_party_compat, *local, *custom, *aliases, *account_gated]
334
+
335
+
336
+ PROFILE_PRESETS: tuple[ProfilePreset, ...] = (
337
+ ProfilePreset(
338
+ key="openai",
339
+ provider_key="openai",
340
+ label="OpenAI",
341
+ protocol="openai_compat",
342
+ base_url="https://api.openai.com/v1",
343
+ api_key_env="OPENAI_API_KEY",
344
+ suggested_models=(
345
+ "gpt-5.6-terra",
346
+ "gpt-5.6-sol",
347
+ "gpt-5.6-luna",
348
+ "gpt-5.3-codex",
349
+ "gpt-5.4-mini",
350
+ "gpt-5.4-nano",
351
+ ),
352
+ suggested_model_descriptions={
353
+ "gpt-5.6-terra": "default - balanced 5.6 tier, 1.05M context",
354
+ "gpt-5.6-sol": "advanced - flagship 5.6 tier, 1.05M context",
355
+ "gpt-5.6-luna": "fast - low-cost 5.6 tier, full 1.05M context",
356
+ "gpt-5.3-codex": "coding - agentic codex model, 400K context",
357
+ "gpt-5.4-mini": "fallback - cheap tier for subagents, 400K",
358
+ "gpt-5.4-nano": "economy - cheapest live id, 400K context",
359
+ },
360
+ model_aliases={
361
+ "gpt-5.6": "gpt-5.6-sol",
362
+ "gpt-5-nano": "gpt-5.4-nano",
363
+ # 2026-07-23 shutdowns from OpenAI's deprecations page: codex and
364
+ # chat-latest ids remap to the still-callable gpt-5.5 tier.
365
+ "gpt-5-codex": "gpt-5.5",
366
+ "gpt-5.1-codex": "gpt-5.5",
367
+ "gpt-5.1-codex-max": "gpt-5.5",
368
+ "gpt-5.2-codex": "gpt-5.5",
369
+ "gpt-5.1-codex-mini": "gpt-5.4-mini",
370
+ "gpt-5-chat-latest": "gpt-5.5",
371
+ "gpt-5.1-chat-latest": "gpt-5.5",
372
+ },
373
+ validation_model="gpt-5.4-nano",
374
+ web_search_adapter=OPENAI_RESPONSES_ADAPTER,
375
+ cache_capability=_OPENAI_PROMPT_CACHE_CAPABILITY,
376
+ setup_warning=(
377
+ "gpt-5.6/5.4 reject tool calls with reasoning_effort other than "
378
+ "'none' on Chat Completions (and 5.6 defaults to 'medium') — for "
379
+ "agentic runs use the OpenAI Responses preset, or pin effort to "
380
+ "'none' here."
381
+ ),
382
+ ),
383
+ ProfilePreset(
384
+ key="openai-responses",
385
+ provider_key="openai",
386
+ label="OpenAI Responses",
387
+ protocol="openai_responses",
388
+ base_url="https://api.openai.com/v1",
389
+ api_key_env="OPENAI_API_KEY",
390
+ suggested_models=(
391
+ "gpt-5.6-terra",
392
+ "gpt-5.6-sol",
393
+ "gpt-5.6-luna",
394
+ "gpt-5.3-codex",
395
+ "gpt-5.4-mini",
396
+ "gpt-5.4-nano",
397
+ ),
398
+ suggested_model_descriptions={
399
+ "gpt-5.6-terra": "default - balanced 5.6 tier, 1.05M context",
400
+ "gpt-5.6-sol": "advanced - flagship 5.6 tier, 1.05M context",
401
+ "gpt-5.6-luna": "fast - low-cost 5.6 tier, full 1.05M context",
402
+ "gpt-5.3-codex": "coding - agentic codex model, 400K context",
403
+ "gpt-5.4-mini": "fallback - cheap tier for subagents, 400K",
404
+ "gpt-5.4-nano": "economy - cheapest live id, 400K context",
405
+ },
406
+ model_aliases={
407
+ "gpt-5.6": "gpt-5.6-sol",
408
+ "gpt-5-nano": "gpt-5.4-nano",
409
+ # 2026-07-23 shutdowns from OpenAI's deprecations page: codex and
410
+ # chat-latest ids remap to the still-callable gpt-5.5 tier.
411
+ "gpt-5-codex": "gpt-5.5",
412
+ "gpt-5.1-codex": "gpt-5.5",
413
+ "gpt-5.1-codex-max": "gpt-5.5",
414
+ "gpt-5.2-codex": "gpt-5.5",
415
+ "gpt-5.1-codex-mini": "gpt-5.4-mini",
416
+ "gpt-5-chat-latest": "gpt-5.5",
417
+ "gpt-5.1-chat-latest": "gpt-5.5",
418
+ },
419
+ validation_model="gpt-5.4-nano",
420
+ web_search_adapter=OPENAI_RESPONSES_ADAPTER,
421
+ cache_capability=_OPENAI_PROMPT_CACHE_CAPABILITY,
422
+ notes=(
423
+ "Native OpenAI Responses API chat with SSE streaming support. Use the OpenAI compat "
424
+ "preset to keep Chat Completions-compatible behavior."
425
+ ),
426
+ ),
427
+ ProfilePreset(
428
+ key="anthropic",
429
+ provider_key="anthropic",
430
+ label="Anthropic Claude",
431
+ protocol="anthropic_messages",
432
+ base_url="https://api.anthropic.com/v1",
433
+ api_key_env="ANTHROPIC_API_KEY",
434
+ suggested_models=(
435
+ "claude-sonnet-5",
436
+ "claude-opus-5",
437
+ "claude-fable-5",
438
+ "claude-haiku-4-5",
439
+ "claude-opus-4-8",
440
+ "claude-opus-4-7",
441
+ ),
442
+ suggested_model_descriptions={
443
+ "claude-sonnet-5": "default - 1M context, best speed/intelligence mix",
444
+ "claude-opus-5": "advanced - agentic coding + deep reasoning, 1M ctx",
445
+ "claude-fable-5": "reasoning - adaptive thinking always on, 1M ctx",
446
+ "claude-haiku-4-5": "fast - 200K context, lowest cost tier",
447
+ "claude-opus-4-8": "fallback - previous-generation opus, 1M context",
448
+ "claude-opus-4-7": "legacy - prior opus generation, 1M context",
449
+ },
450
+ model_aliases={
451
+ # claude-sonnet-4-6 moved to Anthropic's Legacy table; Sonnet 5 is
452
+ # newer and cheaper. Retired haiku ids remap to the 4.5 bare alias.
453
+ "claude-sonnet-4": "claude-sonnet-5",
454
+ "claude-sonnet-4-5": "claude-sonnet-5",
455
+ "claude-sonnet-4-6": "claude-sonnet-5",
456
+ "claude-4-sonnet": "claude-sonnet-5",
457
+ "claude-3-5-haiku-latest": "claude-haiku-4-5",
458
+ "claude-3-5-haiku-20241022": "claude-haiku-4-5",
459
+ "claude-opus-4.8": "claude-opus-4-8",
460
+ "claude-opus-4.7": "claude-opus-4-7",
461
+ "claude-opus-4-1": "claude-opus-4-8",
462
+ "claude-opus-4-6": "claude-opus-4-8",
463
+ },
464
+ validation_model="claude-haiku-4-5",
465
+ web_search_adapter=ANTHROPIC_MESSAGES_ADAPTER,
466
+ cache_capability=_ANTHROPIC_CACHE_CONTROL_CAPABILITY,
467
+ notes=(
468
+ "Native Anthropic Messages API chat with SSE streaming support. Compatibility mode "
469
+ "remains available as anthropic-compat for legacy OpenAI-compatible fallback."
470
+ ),
471
+ ),
472
+ ProfilePreset(
473
+ key="anthropic-compat",
474
+ provider_key="anthropic",
475
+ label="Anthropic Claude compatibility",
476
+ protocol="openai_compat",
477
+ base_url="https://api.anthropic.com/v1/",
478
+ api_key_env="ANTHROPIC_API_KEY",
479
+ suggested_models=(
480
+ "claude-sonnet-5",
481
+ "claude-opus-5",
482
+ "claude-fable-5",
483
+ "claude-haiku-4-5",
484
+ "claude-opus-4-8",
485
+ "claude-opus-4-7",
486
+ ),
487
+ suggested_model_descriptions={
488
+ "claude-sonnet-5": "default - 1M context, best speed/intelligence mix",
489
+ "claude-opus-5": "advanced - agentic coding + deep reasoning, 1M ctx",
490
+ "claude-fable-5": "reasoning - adaptive thinking always on, 1M ctx",
491
+ "claude-haiku-4-5": "fast - 200K context, lowest cost tier",
492
+ "claude-opus-4-8": "fallback - previous-generation opus, 1M context",
493
+ "claude-opus-4-7": "legacy - prior opus generation, 1M context",
494
+ },
495
+ model_aliases={
496
+ # claude-sonnet-4-6 moved to Anthropic's Legacy table; Sonnet 5 is
497
+ # newer and cheaper. Retired haiku ids remap to the 4.5 bare alias.
498
+ "claude-sonnet-4": "claude-sonnet-5",
499
+ "claude-sonnet-4-5": "claude-sonnet-5",
500
+ "claude-sonnet-4-6": "claude-sonnet-5",
501
+ "claude-4-sonnet": "claude-sonnet-5",
502
+ "claude-3-5-haiku-latest": "claude-haiku-4-5",
503
+ "claude-3-5-haiku-20241022": "claude-haiku-4-5",
504
+ "claude-opus-4.8": "claude-opus-4-8",
505
+ "claude-opus-4.7": "claude-opus-4-7",
506
+ "claude-opus-4-1": "claude-opus-4-8",
507
+ "claude-opus-4-6": "claude-opus-4-8",
508
+ },
509
+ validation_model="claude-haiku-4-5",
510
+ web_search_adapter=ANTHROPIC_MESSAGES_ADAPTER,
511
+ setup_warning=(
512
+ "Anthropic labels the OpenAI SDK compatibility layer as a test path; "
513
+ "use the anthropic preset for native Messages API behavior."
514
+ ),
515
+ notes=(
516
+ "Chat uses Anthropic OpenAI-compat at /v1; web_search uses the native "
517
+ "Anthropic Messages web_search adapter when the model/account supports it."
518
+ ),
519
+ ),
520
+ ProfilePreset(
521
+ key="anthropic-native",
522
+ provider_key="anthropic",
523
+ label="Anthropic Claude (native alias)",
524
+ protocol="anthropic_messages",
525
+ base_url="https://api.anthropic.com/v1",
526
+ api_key_env="ANTHROPIC_API_KEY",
527
+ suggested_models=(
528
+ "claude-sonnet-5",
529
+ "claude-opus-5",
530
+ "claude-fable-5",
531
+ "claude-haiku-4-5",
532
+ "claude-opus-4-8",
533
+ "claude-opus-4-7",
534
+ ),
535
+ suggested_model_descriptions={
536
+ "claude-sonnet-5": "default - 1M context, best speed/intelligence mix",
537
+ "claude-opus-5": "advanced - agentic coding + deep reasoning, 1M ctx",
538
+ "claude-fable-5": "reasoning - adaptive thinking always on, 1M ctx",
539
+ "claude-haiku-4-5": "fast - 200K context, lowest cost tier",
540
+ "claude-opus-4-8": "fallback - previous-generation opus, 1M context",
541
+ "claude-opus-4-7": "legacy - prior opus generation, 1M context",
542
+ },
543
+ model_aliases={
544
+ # claude-sonnet-4-6 moved to Anthropic's Legacy table; Sonnet 5 is
545
+ # newer and cheaper. Retired haiku ids remap to the 4.5 bare alias.
546
+ "claude-sonnet-4": "claude-sonnet-5",
547
+ "claude-sonnet-4-5": "claude-sonnet-5",
548
+ "claude-sonnet-4-6": "claude-sonnet-5",
549
+ "claude-4-sonnet": "claude-sonnet-5",
550
+ "claude-3-5-haiku-latest": "claude-haiku-4-5",
551
+ "claude-3-5-haiku-20241022": "claude-haiku-4-5",
552
+ "claude-opus-4.8": "claude-opus-4-8",
553
+ "claude-opus-4.7": "claude-opus-4-7",
554
+ "claude-opus-4-1": "claude-opus-4-8",
555
+ "claude-opus-4-6": "claude-opus-4-8",
556
+ },
557
+ validation_model="claude-haiku-4-5",
558
+ web_search_adapter=ANTHROPIC_MESSAGES_ADAPTER,
559
+ cache_capability=_ANTHROPIC_CACHE_CONTROL_CAPABILITY,
560
+ notes=(
561
+ "Legacy alias for the native anthropic preset. Prefer the anthropic preset for new "
562
+ "first-party Claude profiles."
563
+ ),
564
+ ),
565
+ ProfilePreset(
566
+ key="gemini",
567
+ provider_key="gemini",
568
+ label="Google Gemini",
569
+ protocol="gemini_generate_content",
570
+ base_url="https://generativelanguage.googleapis.com/v1beta",
571
+ api_key_env="GEMINI_API_KEY",
572
+ suggested_models=(
573
+ "gemini-3.7-flash",
574
+ "gemini-3.6-flash",
575
+ "gemini-3.5-flash-lite",
576
+ "gemini-3.1-pro-preview",
577
+ ),
578
+ suggested_model_descriptions={
579
+ "gemini-3.7-flash": "default - newest GA coding and agentic model, 1M",
580
+ "gemini-3.6-flash": "fallback - production GA flash model, 1M context",
581
+ "gemini-3.5-flash-lite": "economy - lowest-cost GA tier, 1M context",
582
+ "gemini-3.1-pro-preview": "advanced - pro reasoning preview, 1M context",
583
+ },
584
+ model_aliases={
585
+ # Only shut-down or invalid legacy ids are rewritten. Active stable
586
+ # ids and provider-managed *-latest aliases pass through unchanged.
587
+ "gemini-2.0-flash": "gemini-3.6-flash",
588
+ "gemini-2.0-flash-lite": "gemini-3.1-flash-lite",
589
+ "gemini-3.1-preview": "gemini-3.1-pro-preview",
590
+ "gemini-3-pro-preview": "gemini-3.1-pro-preview",
591
+ "gemini-3.1-flash-lite-preview": "gemini-3.1-flash-lite",
592
+ },
593
+ validation_model="gemini-3.5-flash-lite",
594
+ web_search_adapter=GEMINI_GROUNDING_ADAPTER,
595
+ cache_capability=_GEMINI_EXPLICIT_CACHED_CONTENT_CAPABILITY,
596
+ setup_warning=(
597
+ "Gemini native GenerateContent uses the Google Gemini API v1beta surface and "
598
+ "model availability can vary by account, region, and provider rollout."
599
+ ),
600
+ notes=(
601
+ "Native Gemini GenerateContent API chat with streamGenerateContent SSE support. "
602
+ "Compatibility mode remains available as gemini-compat for legacy OpenAI-compatible "
603
+ "fallback."
604
+ ),
605
+ ),
606
+ ProfilePreset(
607
+ key="gemini-compat",
608
+ provider_key="gemini",
609
+ label="Google Gemini compatibility",
610
+ protocol="openai_compat",
611
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
612
+ api_key_env="GEMINI_API_KEY",
613
+ suggested_models=(
614
+ "gemini-3.7-flash",
615
+ "gemini-3.6-flash",
616
+ "gemini-3.5-flash-lite",
617
+ "gemini-3.1-pro-preview",
618
+ ),
619
+ suggested_model_descriptions={
620
+ "gemini-3.7-flash": "default - newest GA coding and agentic model, 1M",
621
+ "gemini-3.6-flash": "fallback - production GA flash model, 1M context",
622
+ "gemini-3.5-flash-lite": "economy - lowest-cost GA tier, 1M context",
623
+ "gemini-3.1-pro-preview": "advanced - pro reasoning preview, 1M context",
624
+ },
625
+ model_aliases={
626
+ # Only shut-down or invalid legacy ids are rewritten. Active stable
627
+ # ids and provider-managed *-latest aliases pass through unchanged.
628
+ "gemini-2.0-flash": "gemini-3.6-flash",
629
+ "gemini-2.0-flash-lite": "gemini-3.1-flash-lite",
630
+ "gemini-3.1-preview": "gemini-3.1-pro-preview",
631
+ "gemini-3-pro-preview": "gemini-3.1-pro-preview",
632
+ "gemini-3.1-flash-lite-preview": "gemini-3.1-flash-lite",
633
+ },
634
+ validation_model="gemini-3.5-flash-lite",
635
+ web_search_adapter=GEMINI_GROUNDING_ADAPTER,
636
+ setup_warning=(
637
+ "Gemini OpenAI compatibility is served from v1beta; use the gemini preset for "
638
+ "native GenerateContent behavior."
639
+ ),
640
+ ),
641
+ ProfilePreset(
642
+ key="gemini-native",
643
+ provider_key="gemini",
644
+ label="Google Gemini (native alias)",
645
+ protocol="gemini_generate_content",
646
+ base_url="https://generativelanguage.googleapis.com/v1beta",
647
+ api_key_env="GEMINI_API_KEY",
648
+ suggested_models=(
649
+ "gemini-3.7-flash",
650
+ "gemini-3.6-flash",
651
+ "gemini-3.5-flash-lite",
652
+ "gemini-3.1-pro-preview",
653
+ ),
654
+ suggested_model_descriptions={
655
+ "gemini-3.7-flash": "default - newest GA coding and agentic model, 1M",
656
+ "gemini-3.6-flash": "fallback - production GA flash model, 1M context",
657
+ "gemini-3.5-flash-lite": "economy - lowest-cost GA tier, 1M context",
658
+ "gemini-3.1-pro-preview": "advanced - pro reasoning preview, 1M context",
659
+ },
660
+ model_aliases={
661
+ # Only shut-down or invalid legacy ids are rewritten. Active stable
662
+ # ids and provider-managed *-latest aliases pass through unchanged.
663
+ "gemini-2.0-flash": "gemini-3.6-flash",
664
+ "gemini-2.0-flash-lite": "gemini-3.1-flash-lite",
665
+ "gemini-3.1-preview": "gemini-3.1-pro-preview",
666
+ "gemini-3-pro-preview": "gemini-3.1-pro-preview",
667
+ "gemini-3.1-flash-lite-preview": "gemini-3.1-flash-lite",
668
+ },
669
+ validation_model="gemini-3.5-flash-lite",
670
+ web_search_adapter=GEMINI_GROUNDING_ADAPTER,
671
+ cache_capability=_GEMINI_EXPLICIT_CACHED_CONTENT_CAPABILITY,
672
+ setup_warning=(
673
+ "Gemini native GenerateContent uses the Google Gemini API v1beta surface and "
674
+ "model availability can vary by account, region, and provider rollout."
675
+ ),
676
+ notes=(
677
+ "Legacy alias for the native gemini preset. Prefer the gemini preset for new "
678
+ "first-party Gemini profiles."
679
+ ),
680
+ ),
681
+ ProfilePreset(
682
+ key="deepseek",
683
+ provider_key="deepseek",
684
+ label="DeepSeek",
685
+ protocol="openai_compat",
686
+ base_url="https://api.deepseek.com",
687
+ api_key_env="DEEPSEEK_API_KEY",
688
+ suggested_models=(
689
+ "deepseek-v4-pro",
690
+ "deepseek-v4-flash",
691
+ "deepseek-v4-flash-vision-exp",
692
+ ),
693
+ suggested_model_descriptions={
694
+ "deepseek-v4-pro": "default - flagship coding model, 1M context",
695
+ "deepseek-v4-flash": "fast - cheap high-volume work, 1M context",
696
+ "deepseek-v4-flash-vision-exp": (
697
+ "vision preview - image understanding and tools, 1M context"
698
+ ),
699
+ },
700
+ model_aliases={
701
+ # deepseek-chat / deepseek-reasoner are discontinued 2026-07-24;
702
+ # saved configs pinning them keep working via these remaps.
703
+ "deepseek-chat": "deepseek-v4-flash",
704
+ "deepseek-reasoner": "deepseek-v4-flash",
705
+ },
706
+ validation_model="deepseek-v4-flash",
707
+ setup_warning=(
708
+ "Do not use retired legacy aliases deepseek-chat or deepseek-reasoner "
709
+ "for production defaults; use the V4 model IDs. The vision model is "
710
+ "experimental and may change without a stable-release deprecation window."
711
+ ),
712
+ ),
713
+ ProfilePreset(
714
+ key="nvidia",
715
+ provider_key="nvidia",
716
+ label="NVIDIA NIM (Hosted)",
717
+ protocol="openai_compat",
718
+ base_url="https://integrate.api.nvidia.com/v1",
719
+ api_key_env="NVIDIA_API_KEY",
720
+ suggested_models=(
721
+ "nvidia/nemotron-3-super-120b-a12b",
722
+ "nvidia/nemotron-3-ultra-550b-a55b",
723
+ "nvidia/nemotron-3-nano-30b-a3b",
724
+ "deepseek-ai/deepseek-v4-pro",
725
+ "deepseek-ai/deepseek-v4-flash",
726
+ ),
727
+ suggested_model_descriptions={
728
+ "nvidia/nemotron-3-super-120b-a12b": (
729
+ "default - balanced agentic reasoning, 1M context"
730
+ ),
731
+ "nvidia/nemotron-3-ultra-550b-a55b": (
732
+ "advanced - frontier agentic reasoning, 1M context"
733
+ ),
734
+ "nvidia/nemotron-3-nano-30b-a3b": (
735
+ "fast - efficient reasoning and tool use, 262K hosted context"
736
+ ),
737
+ "deepseek-ai/deepseek-v4-pro": (
738
+ "third-party model hosted by NVIDIA - advanced agentic reasoning"
739
+ ),
740
+ "deepseek-ai/deepseek-v4-flash": (
741
+ "third-party model hosted by NVIDIA - fast agentic reasoning"
742
+ ),
743
+ },
744
+ validation_model="nvidia/nemotron-3-nano-30b-a3b",
745
+ setup_warning=(
746
+ "NVIDIA hosted Free Endpoints are rate-limited development endpoints for "
747
+ "prototyping; availability is not a production SLA and may vary by account."
748
+ ),
749
+ notes=(
750
+ "Hosted NVIDIA NIM OpenAI-compatible API. The live catalog includes models "
751
+ "from NVIDIA and third parties; reasoning controls are model-specific."
752
+ ),
753
+ ),
754
+ ProfilePreset(
755
+ key="qwen-intl",
756
+ provider_key="qwen",
757
+ label="Alibaba Qwen / DashScope (Intl)",
758
+ protocol="openai_compat",
759
+ base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
760
+ api_key_env="DASHSCOPE_API_KEY",
761
+ suggested_models=(
762
+ "qwen3.7-plus",
763
+ "qwen3.8-max",
764
+ "qwen3.7-max",
765
+ "qwen3-coder-plus",
766
+ "qwen3-coder-next",
767
+ "qwen3.6-flash",
768
+ "qwen-flash",
769
+ ),
770
+ suggested_model_descriptions={
771
+ "qwen3.7-plus": "default - 1M context, balanced cost",
772
+ "qwen3.8-max": "advanced - newest multimodal flagship, 1M context",
773
+ "qwen3.7-max": "fallback - previous flagship, 1M context",
774
+ "qwen3-coder-plus": "coding - 1M context, long-repo work",
775
+ "qwen3-coder-next": "agentic - newest coder, 256K context",
776
+ "qwen3.6-flash": "fast - lower-latency, 1M context",
777
+ "qwen-flash": "economy - cheapest 1M-context option",
778
+ },
779
+ validation_model="qwen-flash",
780
+ web_search_adapter=DASHSCOPE_CHAT_ADAPTER,
781
+ web_search_model="qwen3.7-plus",
782
+ cache_capability=_QWEN_DIAGNOSTIC_CACHE_CAPABILITY,
783
+ setup_warning=(
784
+ "DashScope API keys are region-specific; use a key from the Singapore region."
785
+ ),
786
+ ),
787
+ ProfilePreset(
788
+ key="qwen-us",
789
+ provider_key="qwen",
790
+ label="Alibaba Qwen / DashScope (US)",
791
+ protocol="openai_compat",
792
+ base_url="https://dashscope-us.aliyuncs.com/compatible-mode/v1",
793
+ api_key_env="DASHSCOPE_API_KEY",
794
+ suggested_models=(
795
+ "qwen3.7-plus",
796
+ "qwen3.8-max",
797
+ "qwen3.7-max",
798
+ "qwen3.6-flash",
799
+ "qwen-flash",
800
+ ),
801
+ suggested_model_descriptions={
802
+ "qwen3.7-plus": "default - 1M context, balanced cost",
803
+ "qwen3.8-max": "advanced - newest multimodal flagship, 1M context",
804
+ "qwen3.7-max": "fallback - previous flagship, 1M context",
805
+ "qwen3.6-flash": "fast - lower-latency, 1M context",
806
+ "qwen-flash": "economy - cheapest 1M-context option",
807
+ },
808
+ validation_model="qwen-flash",
809
+ web_search_adapter=DASHSCOPE_CHAT_ADAPTER,
810
+ web_search_model="qwen3.7-plus",
811
+ cache_capability=_QWEN_DIAGNOSTIC_CACHE_CAPABILITY,
812
+ setup_warning=(
813
+ "DashScope API keys are region-specific; use a key from the US region. "
814
+ "Qwen coder models are not served from US (Virginia) — use qwen3.7-plus "
815
+ "for code work."
816
+ ),
817
+ ),
818
+ ProfilePreset(
819
+ key="qwen-cn",
820
+ provider_key="qwen",
821
+ label="Alibaba Qwen / DashScope (China)",
822
+ protocol="openai_compat",
823
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
824
+ api_key_env="DASHSCOPE_API_KEY",
825
+ suggested_models=(
826
+ "qwen3.7-plus",
827
+ "qwen3.8-max",
828
+ "qwen3.7-max",
829
+ "qwen3-coder-plus",
830
+ "qwen3-coder-next",
831
+ "qwen3.6-flash",
832
+ "qwen-flash",
833
+ ),
834
+ suggested_model_descriptions={
835
+ "qwen3.7-plus": "default - 1M context, balanced cost",
836
+ "qwen3.8-max": "advanced - newest multimodal flagship, 1M context",
837
+ "qwen3.7-max": "fallback - previous flagship, 1M context",
838
+ "qwen3-coder-plus": "coding - 1M context, long-repo work",
839
+ "qwen3-coder-next": "agentic - newest coder, 256K context",
840
+ "qwen3.6-flash": "fast - lower-latency, 1M context",
841
+ "qwen-flash": "economy - cheapest 1M-context option",
842
+ },
843
+ validation_model="qwen-flash",
844
+ web_search_adapter=DASHSCOPE_CHAT_ADAPTER,
845
+ web_search_model="qwen3.7-plus",
846
+ cache_capability=_QWEN_DIAGNOSTIC_CACHE_CAPABILITY,
847
+ setup_warning="DashScope API keys are region-specific; use a key from the China region.",
848
+ ),
849
+ ProfilePreset(
850
+ key="zhipu",
851
+ provider_key="zhipu",
852
+ label="Zhipu / GLM",
853
+ protocol="openai_compat",
854
+ base_url="https://open.bigmodel.cn/api/paas/v4/",
855
+ api_key_env="ZHIPUAI_API_KEY",
856
+ suggested_models=(
857
+ "glm-5.2",
858
+ "glm-5.1",
859
+ "glm-5-turbo",
860
+ "glm-4.7",
861
+ "glm-4.7-flashx",
862
+ "glm-4.7-flash",
863
+ ),
864
+ suggested_model_descriptions={
865
+ "glm-5.2": "default - 1M context, agentic coding",
866
+ "glm-5.1": "advanced - previous flagship, 200K context",
867
+ "glm-5-turbo": "coding - 200K context, cheaper than glm-5.1",
868
+ "glm-4.7": "fallback - cheap 200K context",
869
+ "glm-4.7-flashx": "fast - 200K context, no free-tier rate caps",
870
+ "glm-4.7-flash": "economy - free tier, 200K context, rate limited",
871
+ },
872
+ # No aliases on purpose: glm-5, glm-4.6 etc. remain individually priced
873
+ # and callable — remapping would silently change what users are billed.
874
+ validation_model="glm-4.7-flash",
875
+ web_search_adapter=ZHIPU_WEB_SEARCH_ADAPTER,
876
+ web_search_model="glm-5.1",
877
+ ),
878
+ ProfilePreset(
879
+ key="zai-coding-plan",
880
+ provider_key="zai_coding_plan",
881
+ label="Z.AI GLM Coding Plan",
882
+ protocol="openai_compat",
883
+ base_url="https://api.z.ai/api/coding/paas/v4",
884
+ api_key_env="ZAI_API_KEY",
885
+ suggested_models=(
886
+ "glm-5.3",
887
+ "glm-5-turbo",
888
+ "glm-4.7",
889
+ ),
890
+ suggested_model_descriptions={
891
+ "glm-5.3": "default - latest agentic coding model, 1M context",
892
+ "glm-5-turbo": "fast - lower-credit agent model, 200K context",
893
+ "glm-4.7": "fallback - lowest-credit plan model, 200K context",
894
+ },
895
+ # GLM-4.7 consumes fewer plan credits than GLM-5.3 and is available on
896
+ # every Coding Plan tier, so use it for the initial credential probe.
897
+ validation_model="glm-4.7",
898
+ cache_capability=_ZAI_CODING_PLAN_CACHE_CAPABILITY,
899
+ setup_warning=(
900
+ "Requires a Z.AI GLM Coding Plan key; general pay-as-you-go and "
901
+ "open.bigmodel.cn keys use different endpoints. Z.AI limits plan benefits "
902
+ "to supported coding tools, so verify Alysis Code eligibility for your account."
903
+ ),
904
+ notes=(
905
+ "Subscription Coding Plan endpoint, not the general Z.AI or China Zhipu API. "
906
+ "All plan tiers currently offer GLM-5.3, GLM-5-Turbo, and GLM-4.7; "
907
+ "GLM-5.2/5.1 requests are routed by the server to GLM-5.3."
908
+ ),
909
+ ),
910
+ ProfilePreset(
911
+ key="moonshot",
912
+ provider_key="moonshot",
913
+ label="Kimi",
914
+ protocol="openai_compat",
915
+ base_url="https://api.moonshot.ai/v1",
916
+ api_key_env="MOONSHOT_API_KEY",
917
+ suggested_models=(
918
+ "kimi-k2.7-code",
919
+ "kimi-k3",
920
+ "kimi-k2.7-code-highspeed",
921
+ "kimi-k2.6",
922
+ ),
923
+ suggested_model_descriptions={
924
+ # k2.7-code is the deliberate default: k3 is always-thinking at
925
+ # pinned max effort and ~3x the input price — escalate to it when a
926
+ # task needs the 1M window, don't route routine turns through it.
927
+ "kimi-k2.7-code": "default - 256K context, long-horizon agentic coding",
928
+ "kimi-k3": "advanced - 1M context, always-thinking at max effort",
929
+ "kimi-k2.7-code-highspeed": "fast - ~180 tok/s coding variant, 256K context",
930
+ "kimi-k2.6": "fallback - 256K context, thinking toggleable",
931
+ },
932
+ model_aliases={
933
+ # Only kimi-k2.6 accepts a thinking-off flag; K2.7/K3 error on it.
934
+ # kimi-k2.5 and the moonshot-v1-* family end 2026-08-31.
935
+ "kimi-k2": "kimi-k2.6",
936
+ "kimi-k2.5": "kimi-k2.6",
937
+ "kimi-k2-thinking": "kimi-k2.6",
938
+ "kimi-k2-thinking-turbo": "kimi-k2.6",
939
+ "kimi-k2-0905-preview": "kimi-k2.6",
940
+ "kimi-k2-0711-preview": "kimi-k2.6",
941
+ "kimi-k2-turbo-preview": "kimi-k2.7-code-highspeed",
942
+ "kimi-latest": "kimi-k2.6",
943
+ "kimi-thinking-preview": "kimi-k2.6",
944
+ "moonshot-v1-8k": "kimi-k2.6",
945
+ "moonshot-v1-32k": "kimi-k2.6",
946
+ "moonshot-v1-128k": "kimi-k2.6",
947
+ "moonshot-v1-auto": "kimi-k2.6",
948
+ },
949
+ validation_model="kimi-k2.6",
950
+ web_search_adapter=MOONSHOT_KIMI_ADAPTER,
951
+ # kimi-k3 cannot disable thinking, which Kimi's $web_search tool requires,
952
+ # so provider-hosted search stays pinned to kimi-k2.6.
953
+ web_search_model="kimi-k2.6",
954
+ cache_capability=_MOONSHOT_AUTOMATIC_CACHE_CAPABILITY,
955
+ setup_warning=(
956
+ "Moonshot API keys are region-scoped; use a key from the international "
957
+ "platform (platform.kimi.ai) with this endpoint."
958
+ ),
959
+ ),
960
+ ProfilePreset(
961
+ key="kimi-code",
962
+ provider_key="moonshot",
963
+ label="Kimi Code",
964
+ protocol="openai_compat",
965
+ base_url="https://api.kimi.com/coding/v1",
966
+ api_key_env="KIMI_API_KEY",
967
+ suggested_models=("k3", "kimi-for-coding", "kimi-for-coding-highspeed"),
968
+ suggested_model_descriptions={
969
+ # Tier gating: kimi-for-coding = all members; k3 = Moderato+ (256K)
970
+ # and 1M only on Allegretto+; -highspeed = Allegretto+ only.
971
+ # Disabling thinking on this endpoint silently routes to K2.6.
972
+ "k3": "default - 256K context, 1M on Allegretto+",
973
+ "kimi-for-coding": "coding - 256K context, all membership tiers",
974
+ "kimi-for-coding-highspeed": "fast - 256K context, Allegretto tier or above",
975
+ },
976
+ model_aliases={
977
+ # Cross-endpoint remaps: these are live, DIFFERENT ids on
978
+ # platform.moonshot.ai — legal only inside this preset's alias table.
979
+ "kimi-k3": "k3",
980
+ "kimi-k2.7-code": "kimi-for-coding",
981
+ "kimi-k2.7-code-highspeed": "kimi-for-coding-highspeed",
982
+ },
983
+ # Validation is a billed call against metered membership quota — no
984
+ # /models endpoint exists on this surface.
985
+ validation_model="kimi-for-coding",
986
+ setup_warning=(
987
+ "Requires a Kimi membership key from the kimi.com console; "
988
+ "platform.kimi.ai pay-as-you-go keys are not valid here. "
989
+ "Turning reasoning off routes requests to K2.6 (a different model)."
990
+ ),
991
+ ),
992
+ ProfilePreset(
993
+ key="moonshot-cn",
994
+ provider_key="moonshot",
995
+ label="Kimi (China)",
996
+ protocol="openai_compat",
997
+ base_url="https://api.moonshot.cn/v1",
998
+ api_key_env="MOONSHOT_API_KEY",
999
+ suggested_models=(
1000
+ "kimi-k2.7-code",
1001
+ "kimi-k3",
1002
+ "kimi-k2.7-code-highspeed",
1003
+ "kimi-k2.6",
1004
+ ),
1005
+ suggested_model_descriptions={
1006
+ # k2.7-code is the deliberate default: k3 is always-thinking at
1007
+ # pinned max effort and ~3x the input price — escalate to it when a
1008
+ # task needs the 1M window, don't route routine turns through it.
1009
+ "kimi-k2.7-code": "default - 256K context, long-horizon agentic coding",
1010
+ "kimi-k3": "advanced - 1M context, always-thinking at max effort",
1011
+ "kimi-k2.7-code-highspeed": "fast - ~180 tok/s coding variant, 256K context",
1012
+ "kimi-k2.6": "fallback - 256K context, thinking toggleable",
1013
+ },
1014
+ model_aliases={
1015
+ # Only kimi-k2.6 accepts a thinking-off flag; K2.7/K3 error on it.
1016
+ # kimi-k2.5 and the moonshot-v1-* family end 2026-08-31.
1017
+ "kimi-k2": "kimi-k2.6",
1018
+ "kimi-k2.5": "kimi-k2.6",
1019
+ "kimi-k2-thinking": "kimi-k2.6",
1020
+ "kimi-k2-thinking-turbo": "kimi-k2.6",
1021
+ "kimi-k2-0905-preview": "kimi-k2.6",
1022
+ "kimi-k2-0711-preview": "kimi-k2.6",
1023
+ "kimi-k2-turbo-preview": "kimi-k2.7-code-highspeed",
1024
+ "kimi-latest": "kimi-k2.6",
1025
+ "kimi-thinking-preview": "kimi-k2.6",
1026
+ "moonshot-v1-8k": "kimi-k2.6",
1027
+ "moonshot-v1-32k": "kimi-k2.6",
1028
+ "moonshot-v1-128k": "kimi-k2.6",
1029
+ "moonshot-v1-auto": "kimi-k2.6",
1030
+ },
1031
+ validation_model="kimi-k2.6",
1032
+ web_search_adapter=MOONSHOT_KIMI_ADAPTER,
1033
+ # kimi-k3 cannot disable thinking, which Kimi's $web_search tool requires,
1034
+ # so provider-hosted search stays pinned to kimi-k2.6.
1035
+ web_search_model="kimi-k2.6",
1036
+ cache_capability=_MOONSHOT_AUTOMATIC_CACHE_CAPABILITY,
1037
+ setup_warning=(
1038
+ "Moonshot API keys are region-scoped; use a key from the mainland-China "
1039
+ "platform (platform.moonshot.cn) with this endpoint."
1040
+ ),
1041
+ ),
1042
+ ProfilePreset(
1043
+ key="minimax",
1044
+ provider_key="minimax",
1045
+ label="MiniMax",
1046
+ protocol="openai_compat",
1047
+ base_url="https://api.minimax.io/v1",
1048
+ api_key_env="MINIMAX_API_KEY",
1049
+ suggested_models=(
1050
+ "MiniMax-M3",
1051
+ "MiniMax-M2.7",
1052
+ "MiniMax-M2.7-highspeed",
1053
+ "MiniMax-M2.5",
1054
+ ),
1055
+ suggested_model_descriptions={
1056
+ # No thinking/reasoning toggle is documented for any MiniMax model —
1057
+ # send no reasoning-control parameter on this preset. M3 input above
1058
+ # 512K bills at a higher long-context rate.
1059
+ "MiniMax-M3": "default - 1M context, multimodal agentic coding",
1060
+ "MiniMax-M2.7": "coding - 200K context, prior flagship",
1061
+ "MiniMax-M2.7-highspeed": "fast - same weights as M2.7, latency-tuned",
1062
+ "MiniMax-M2.5": "fallback - stable prior generation",
1063
+ },
1064
+ model_aliases={
1065
+ "MiniMax-M2": "MiniMax-M2.7",
1066
+ },
1067
+ validation_model="MiniMax-M2.5",
1068
+ web_search_adapter=MINIMAX_CODING_PLAN_ADAPTER,
1069
+ setup_warning=(
1070
+ "MiniMax hosted web search requires a Token Plan key; pay-as-you-go model keys "
1071
+ "cannot call the Token Plan search endpoint."
1072
+ ),
1073
+ notes=(
1074
+ "Chat uses the OpenAI-compatible MiniMax API. Web search uses MiniMax's Token Plan "
1075
+ "search endpoint when the configured key has Token Plan access."
1076
+ ),
1077
+ ),
1078
+ ProfilePreset(
1079
+ key="xiaomi-mimo",
1080
+ provider_key="xiaomi",
1081
+ label="Xiaomi MiMo",
1082
+ protocol="openai_compat",
1083
+ base_url="https://api.xiaomimimo.com/v1",
1084
+ api_key_env="XIAOMI_API_KEY",
1085
+ suggested_models=("mimo-v2.5-pro", "mimo-v2-flash", "mimo-v2.5"),
1086
+ suggested_model_descriptions={
1087
+ "mimo-v2.5-pro": "default - flagship reasoning, coding & agents (1M context)",
1088
+ "mimo-v2-flash": "faster & lighter (256K context)",
1089
+ "mimo-v2.5": "omni - text + image understanding (1M context)",
1090
+ },
1091
+ validation_model="mimo-v2.5-pro",
1092
+ # Migrate the legacy bare "mimo" placeholder up to the flagship model.
1093
+ model_aliases={"mimo": "mimo-v2.5-pro"},
1094
+ ),
1095
+ ProfilePreset(
1096
+ key="bytedance",
1097
+ provider_key="bytedance",
1098
+ label="ByteDance Doubao",
1099
+ protocol="openai_compat",
1100
+ base_url="https://ark.cn-beijing.volces.com/api/v3",
1101
+ api_key_env="ARK_API_KEY",
1102
+ suggested_models=(
1103
+ "doubao-seed-2-0-pro-260215",
1104
+ "doubao-seed-2-0-code-preview-260215",
1105
+ "doubao-seed-2-0-lite-260215",
1106
+ "doubao-seed-2-0-mini-260215",
1107
+ ),
1108
+ suggested_model_descriptions={
1109
+ "doubao-seed-2-0-pro-260215": "default - flagship seed 2.0, agentic tasks",
1110
+ "doubao-seed-2-0-code-preview-260215": "coding - 256K context, preview snapshot",
1111
+ "doubao-seed-2-0-lite-260215": "fast - balanced quality and latency",
1112
+ "doubao-seed-2-0-mini-260215": "economy - cheapest seed 2.0, high concurrency",
1113
+ },
1114
+ validation_model="doubao-seed-2-0-mini-260215",
1115
+ web_search_adapter=VOLCENGINE_WEB_SEARCH_ADAPTER,
1116
+ setup_warning=(
1117
+ "Model ids rest on registry evidence only (Ark docs are not "
1118
+ "machine-readable) — verify with a live Ark key; Ark may require "
1119
+ "endpoint ids (ep-...) instead of bare model names."
1120
+ ),
1121
+ ),
1122
+ ProfilePreset(
1123
+ key="groq",
1124
+ provider_key="groq",
1125
+ label="Groq",
1126
+ protocol="openai_compat",
1127
+ base_url="https://api.groq.com/openai/v1",
1128
+ api_key_env="GROQ_API_KEY",
1129
+ suggested_models=(
1130
+ "openai/gpt-oss-120b",
1131
+ "qwen/qwen3.6-27b",
1132
+ "openai/gpt-oss-20b",
1133
+ "groq/compound",
1134
+ ),
1135
+ suggested_model_descriptions={
1136
+ # groq/compound runs server-side built-in tools and does NOT accept
1137
+ # client tool_call — never route normal agent tool loops to it.
1138
+ "openai/gpt-oss-120b": "default - 131K context, adjustable reasoning",
1139
+ "qwen/qwen3.6-27b": "coding - thinking modes and vision, preview tier",
1140
+ "openai/gpt-oss-20b": "fast - cheapest non-deprecated production id",
1141
+ "groq/compound": "agentic - server-side web search and code exec",
1142
+ },
1143
+ model_aliases={
1144
+ # Both llama ids shut down 2026-08-16 (Groq deprecations table);
1145
+ # the other retired ids remap per the same table.
1146
+ "llama-3.3-70b-versatile": "openai/gpt-oss-120b",
1147
+ "llama-3.1-8b-instant": "openai/gpt-oss-20b",
1148
+ "qwen/qwen3-32b": "openai/gpt-oss-120b",
1149
+ "meta-llama/llama-4-scout-17b-16e-instruct": "openai/gpt-oss-120b",
1150
+ "meta-llama/llama-4-maverick-17b-128e-instruct": "openai/gpt-oss-120b",
1151
+ "moonshotai/kimi-k2-instruct": "openai/gpt-oss-120b",
1152
+ "moonshotai/kimi-k2-instruct-0905": "openai/gpt-oss-120b",
1153
+ },
1154
+ validation_model="openai/gpt-oss-20b",
1155
+ web_search_adapter=GROQ_COMPOUND_ADAPTER,
1156
+ web_search_model="groq/compound-mini",
1157
+ setup_warning=(
1158
+ "Groq is mostly OpenAI-compatible; avoid preview-only models as production "
1159
+ "defaults (qwen/qwen3.6-27b is preview and may be pulled without notice)."
1160
+ ),
1161
+ ),
1162
+ ProfilePreset(
1163
+ key="cerebras",
1164
+ provider_key="cerebras",
1165
+ label="Cerebras",
1166
+ protocol="openai_compat",
1167
+ base_url="https://api.cerebras.ai/v1",
1168
+ api_key_env="CEREBRAS_API_KEY",
1169
+ suggested_models=(
1170
+ "gpt-oss-120b",
1171
+ "zai-glm-4.7",
1172
+ "gemma-4-31b",
1173
+ ),
1174
+ suggested_model_descriptions={
1175
+ # Context values are the free-tier floor (65K); paid keys get 131K.
1176
+ # gpt-oss-120b cannot disable reasoning (effort low|medium|high).
1177
+ "gpt-oss-120b": "default - only GA public model, ~3000 tok/s",
1178
+ "zai-glm-4.7": "coding - strongest here, deprecates 2026-08-17",
1179
+ "gemma-4-31b": "fallback - only image-input model, preview tier",
1180
+ },
1181
+ model_aliases={
1182
+ # The llama family left Cerebras public endpoints 2026-02-16 (and
1183
+ # "llama3.3-70b" was never a valid spelling of the id).
1184
+ "llama3.3-70b": "gpt-oss-120b",
1185
+ "llama-3.3-70b": "gpt-oss-120b",
1186
+ "llama3.1-70b": "gpt-oss-120b",
1187
+ "llama3.1-8b": "gpt-oss-120b",
1188
+ "qwen-3-32b": "gpt-oss-120b",
1189
+ "qwen-3-coder-480b": "zai-glm-4.7",
1190
+ "zai-glm-4.6": "zai-glm-4.7",
1191
+ "deepseek-r1-distill-llama-70b": "gpt-oss-120b",
1192
+ },
1193
+ validation_model="gpt-oss-120b",
1194
+ ),
1195
+ ProfilePreset(
1196
+ key="mistral",
1197
+ provider_key="mistral",
1198
+ label="Mistral AI",
1199
+ protocol="openai_compat",
1200
+ base_url="https://api.mistral.ai/v1",
1201
+ api_key_env="MISTRAL_API_KEY",
1202
+ suggested_models=(
1203
+ "mistral-medium-3-5",
1204
+ "mistral-large-2512",
1205
+ "mistral-small-2603",
1206
+ "codestral-2508",
1207
+ "ministral-8b-2512",
1208
+ ),
1209
+ suggested_model_descriptions={
1210
+ # codestral is FIM/completion-oriented with ~4K max output — routers
1211
+ # should prefer the default for multi-file agentic patch turns.
1212
+ "mistral-medium-3-5": "default - agentic and coding flagship, 256K",
1213
+ "mistral-large-2512": "advanced - mistral large 3, 675B MoE, 256K",
1214
+ "mistral-small-2603": "fast - mistral small 4, low latency",
1215
+ "codestral-2508": "coding - FIM and completion, 4K max output",
1216
+ "ministral-8b-2512": "economy - small tool-capable model",
1217
+ },
1218
+ model_aliases={
1219
+ # Mistral documents mistral-medium-3-5 as the primary API id. Keep
1220
+ # the former Alysis Code default as a compatibility alias, while the
1221
+ # provider-managed -latest alias passes through unchanged.
1222
+ "mistral-medium-2604": "mistral-medium-3-5",
1223
+ "mistral-medium-3": "mistral-medium-3-5",
1224
+ "mistral-medium-2508": "mistral-medium-3-5",
1225
+ "mistral-medium-2505": "mistral-medium-3-5",
1226
+ "mistral-small-latest": "mistral-small-2603",
1227
+ "mistral-small-2506": "mistral-small-2603",
1228
+ "mistral-large-latest": "mistral-large-2512",
1229
+ "mistral-large-2411": "mistral-medium-3-5",
1230
+ "mistral-large-2407": "mistral-large-2512",
1231
+ "codestral-latest": "codestral-2508",
1232
+ "devstral-2512": "mistral-medium-3-5",
1233
+ "devstral-latest": "mistral-medium-3-5",
1234
+ "devstral-medium-latest": "mistral-medium-3-5",
1235
+ "devstral-medium-2507": "mistral-medium-3-5",
1236
+ "devstral-small-2507": "mistral-small-2603",
1237
+ "labs-devstral-small-2512": "mistral-medium-3-5",
1238
+ "magistral-medium-latest": "mistral-medium-3-5",
1239
+ "magistral-small-latest": "mistral-small-2603",
1240
+ "ministral-8b-latest": "ministral-8b-2512",
1241
+ "open-mistral-nemo-2407": "ministral-8b-2512",
1242
+ },
1243
+ validation_model="ministral-3b-2512",
1244
+ web_search_adapter=MISTRAL_CONVERSATIONS_ADAPTER,
1245
+ web_search_model="mistral-medium-latest",
1246
+ cache_capability=_MISTRAL_PROMPT_CACHE_CAPABILITY,
1247
+ ),
1248
+ ProfilePreset(
1249
+ key="xai",
1250
+ provider_key="xai",
1251
+ label="xAI Grok",
1252
+ protocol="openai_compat",
1253
+ base_url="https://api.x.ai/v1",
1254
+ api_key_env="XAI_API_KEY",
1255
+ suggested_models=(
1256
+ "grok-4.6",
1257
+ "grok-4.5",
1258
+ "grok-build-0.1",
1259
+ "grok-4.3",
1260
+ "grok-4.20-0309-reasoning",
1261
+ "grok-4.20-0309-non-reasoning",
1262
+ ),
1263
+ suggested_model_descriptions={
1264
+ # grok-build-0.1 is served from us-east-1/us-west-2 only. Max output
1265
+ # is unpublished for the 4.20 family — clamp conservatively.
1266
+ "grok-4.6": "default - newest flagship for coding and agents, 500K",
1267
+ "grok-4.5": "fallback - previous flagship for coding and agents",
1268
+ "grok-build-0.1": "coding - agentic engineering model, 256K",
1269
+ "grok-4.3": "advanced - 1M context window",
1270
+ "grok-4.20-0309-reasoning": "reasoning - dedicated snapshot, 1M context",
1271
+ "grok-4.20-0309-non-reasoning": "fast - no-reasoning snapshot, 1M context",
1272
+ },
1273
+ model_aliases={
1274
+ # Retired 2026-05-15, full shutdown 2026-08-15. The *-non-reasoning
1275
+ # slugs deliberately map to the non-reasoning snapshot (xAI's own
1276
+ # redirect lands them on grok-4.3 with effort=none, which the alias
1277
+ # table cannot express).
1278
+ "grok-code-fast-1": "grok-build-0.1",
1279
+ "grok-4": "grok-4.3",
1280
+ "grok-4-0709": "grok-4.3",
1281
+ "grok-4-fast": "grok-4.3",
1282
+ "grok-4.3-latest": "grok-4.3",
1283
+ "grok-4-fast-reasoning": "grok-4.3",
1284
+ "grok-4-1-fast-reasoning": "grok-4.3",
1285
+ "grok-4-fast-non-reasoning": "grok-4.20-0309-non-reasoning",
1286
+ "grok-4-1-fast-non-reasoning": "grok-4.20-0309-non-reasoning",
1287
+ "grok-3": "grok-4.3",
1288
+ },
1289
+ validation_model="grok-4.20-0309-non-reasoning",
1290
+ web_search_adapter=XAI_RESPONSES_ADAPTER,
1291
+ cache_capability=_XAI_CONVERSATION_HEADER_CACHE_CAPABILITY,
1292
+ setup_warning=(
1293
+ "Retired slugs (grok-4, grok-4-fast, grok-3, grok-code-fast-1) shut down "
1294
+ "fully 2026-08-15 and are billed at grok-4.3 rates until then; migrate "
1295
+ "pinned configs explicitly. Ids use dots, not dashes (grok-4.6)."
1296
+ ),
1297
+ ),
1298
+ ProfilePreset(
1299
+ key="cohere",
1300
+ provider_key="cohere",
1301
+ label="Cohere (compat)",
1302
+ protocol="openai_compat",
1303
+ base_url="https://api.cohere.ai/compatibility/v1",
1304
+ api_key_env="COHERE_API_KEY",
1305
+ suggested_models=(
1306
+ "command-a-plus-05-2026",
1307
+ "command-a-reasoning-08-2025",
1308
+ "command-a-03-2025",
1309
+ "command-r7b-12-2024",
1310
+ ),
1311
+ suggested_model_descriptions={
1312
+ # Reasoning toggle (thinking=disabled) is a native Chat V2 param and
1313
+ # may not pass through /compatibility/v1 — treat as thinking-on.
1314
+ "command-a-plus-05-2026": "default - newest command a+, 128K context",
1315
+ "command-a-reasoning-08-2025": "reasoning - 256K context, thinking is a toggle",
1316
+ "command-a-03-2025": "advanced - 256K context, prior flagship",
1317
+ "command-r7b-12-2024": "economy - cheapest live chat model, 128K",
1318
+ },
1319
+ model_aliases={
1320
+ "command": "command-a-03-2025",
1321
+ "command-light": "command-r-08-2024",
1322
+ "command-r": "command-r-08-2024",
1323
+ "command-r-plus": "command-r-plus-08-2024",
1324
+ },
1325
+ validation_model="command-r7b-12-2024",
1326
+ web_search_adapter=COHERE_WEB_SEARCH_ADAPTER,
1327
+ setup_warning=(
1328
+ "Cohere shut down the v1 hosted web-search connector on 2025-09-15; "
1329
+ "hosted web search on this preset needs migration to an external "
1330
+ "search adapter."
1331
+ ),
1332
+ notes=(
1333
+ "Chat uses Cohere's OpenAI compatibility API (api.cohere.ai/compatibility/v1 — "
1334
+ "documented and correct; do not migrate to v2/chat). The v1 hosted web-search "
1335
+ "connector this preset's adapter targeted was shut down 2025-09-15."
1336
+ ),
1337
+ ),
1338
+ ProfilePreset(
1339
+ key="openrouter",
1340
+ provider_key="openrouter",
1341
+ label="OpenRouter (gateway)",
1342
+ protocol="openai_compat",
1343
+ base_url="https://openrouter.ai/api/v1",
1344
+ api_key_env="OPENROUTER_API_KEY",
1345
+ suggested_models=(
1346
+ "anthropic/claude-sonnet-5",
1347
+ "anthropic/claude-opus-4.8",
1348
+ "openai/gpt-5.6-terra",
1349
+ "openai/gpt-5.6-luna",
1350
+ "z-ai/glm-5.2",
1351
+ "deepseek/deepseek-v4-pro-0813",
1352
+ "deepseek/deepseek-v4-flash-0731",
1353
+ "deepseek/deepseek-v4-flash-vision-exp",
1354
+ "qwen/qwen3.8-max",
1355
+ ),
1356
+ suggested_model_descriptions={
1357
+ # Vendor prefixes are exact: z-ai/ (not zai/), x-ai/, moonshotai/.
1358
+ # Avoid '-latest' floating aliases and rate-limited :free variants
1359
+ # for agent loops.
1360
+ "anthropic/claude-sonnet-5": "default - coding and agents, 1M context",
1361
+ "anthropic/claude-opus-4.8": "advanced - long-horizon autonomous work",
1362
+ "openai/gpt-5.6-terra": "coding - balanced gpt-5.6 tier, 1.05M context",
1363
+ "openai/gpt-5.6-luna": "fast - cost-efficient gpt-5.6 tier",
1364
+ "z-ai/glm-5.2": "economy - cheap 1M-context tool caller",
1365
+ "deepseek/deepseek-v4-pro-0813": "agentic - current reasoning MoE, 1M context",
1366
+ "deepseek/deepseek-v4-flash-0731": "fast - current low-cost 1M release",
1367
+ "deepseek/deepseek-v4-flash-vision-exp": (
1368
+ "vision preview - image understanding and tools, 1M context"
1369
+ ),
1370
+ "qwen/qwen3.8-max": "multimodal - flagship Qwen agent model, 1M context",
1371
+ },
1372
+ validation_model="deepseek/deepseek-v4-flash-0731",
1373
+ web_search_adapter=OPENROUTER_WEB_ADAPTER,
1374
+ cache_capability=_OPENROUTER_STICKY_SESSION_CACHE_CAPABILITY,
1375
+ setup_warning=(
1376
+ "OpenRouter routes through upstream providers; availability, pricing, privacy, "
1377
+ "and parameter support can vary by route."
1378
+ ),
1379
+ notes="Single API to many providers' models.",
1380
+ ),
1381
+ ProfilePreset(
1382
+ key="perplexity",
1383
+ provider_key="perplexity",
1384
+ label="Perplexity Sonar",
1385
+ protocol="openai_compat",
1386
+ base_url="https://api.perplexity.ai",
1387
+ api_key_env="PERPLEXITY_API_KEY",
1388
+ suggested_models=("sonar-pro", "sonar"),
1389
+ web_search_adapter=PERPLEXITY_SONAR_ADAPTER,
1390
+ web_search_model="sonar",
1391
+ setup_warning=(
1392
+ "Search-only: sonar models reject tool definitions (HTTP 400), so this "
1393
+ "preset cannot run agentic tool loops. Perplexity's coding models live "
1394
+ "on the Agent API (/v1/agent), which needs a Responses-style client "
1395
+ "Alysis Code does not ship yet."
1396
+ ),
1397
+ notes="Sonar models include web-grounded answers and citations.",
1398
+ ),
1399
+ ProfilePreset(
1400
+ key="together",
1401
+ provider_key="together",
1402
+ label="Together AI",
1403
+ protocol="openai_compat",
1404
+ base_url="https://api.together.ai/v1",
1405
+ api_key_env="TOGETHER_API_KEY",
1406
+ suggested_models=(
1407
+ "zai-org/GLM-5.2",
1408
+ "moonshotai/Kimi-K2.7-Code",
1409
+ "deepseek-ai/DeepSeek-V4-Pro-0813",
1410
+ "deepseek-ai/DeepSeek-V4-Flash-0731",
1411
+ "MiniMaxAI/MiniMax-M3",
1412
+ "openai/gpt-oss-120b",
1413
+ "openai/gpt-oss-20b",
1414
+ ),
1415
+ suggested_model_descriptions={
1416
+ # Ids are case-sensitive and vendor-prefixed. Kimi-K2.7-Code and
1417
+ # MiniMax-M3 reason unconditionally — never emit a reasoning-off or
1418
+ # effort param for them.
1419
+ "zai-org/GLM-5.2": "default - general coding, 256K context",
1420
+ "moonshotai/Kimi-K2.7-Code": "coding - code specialist, 256K context",
1421
+ "deepseek-ai/DeepSeek-V4-Pro-0813": "reasoning - current flagship, 1M context",
1422
+ "deepseek-ai/DeepSeek-V4-Flash-0731": "fast - current 1M-context release",
1423
+ "MiniMaxAI/MiniMax-M3": "economy - cheapest 512K-context option",
1424
+ "openai/gpt-oss-120b": "open - larger tool-capable GPT-OSS model, 128K context",
1425
+ "openai/gpt-oss-20b": "fallback - cheapest tool-capable id",
1426
+ },
1427
+ model_aliases={
1428
+ # Fallback policy, NOT vendor renames: Together retires serverless
1429
+ # models with a blank successor column. Two are cross-vendor
1430
+ # substitutions — surface the swap to the user at resolution time.
1431
+ "zai-org/GLM-5.1": "zai-org/GLM-5.2",
1432
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": "moonshotai/Kimi-K2.7-Code",
1433
+ "Qwen/Qwen3-Coder-Next-FP8": "moonshotai/Kimi-K2.7-Code",
1434
+ },
1435
+ validation_model="openai/gpt-oss-20b",
1436
+ setup_warning=(
1437
+ "Together retires serverless models on a published schedule with no "
1438
+ "successor mapping — expect id churn; verify access with Together's "
1439
+ "Models API."
1440
+ ),
1441
+ ),
1442
+ ProfilePreset(
1443
+ key="fireworks",
1444
+ provider_key="fireworks",
1445
+ label="Fireworks AI",
1446
+ protocol="openai_compat",
1447
+ base_url="https://api.fireworks.ai/inference/v1",
1448
+ api_key_env="FIREWORKS_API_KEY",
1449
+ suggested_models=(
1450
+ "accounts/fireworks/models/glm-5p2",
1451
+ "accounts/fireworks/models/kimi-k2p7-code",
1452
+ "accounts/fireworks/models/deepseek-v4-pro-0813",
1453
+ "accounts/fireworks/models/deepseek-v4-flash-0731",
1454
+ "accounts/fireworks/models/minimax-m3",
1455
+ "accounts/fireworks/models/qwen3p7-plus",
1456
+ ),
1457
+ suggested_model_descriptions={
1458
+ # 'p' is the decimal convention (5p2 = 5.2). Catalog membership does
1459
+ # NOT imply serverless availability on Fireworks — every id here is
1460
+ # confirmed serverless-capable.
1461
+ "accounts/fireworks/models/glm-5p2": "default - general agentic coding, 1M context",
1462
+ "accounts/fireworks/models/kimi-k2p7-code": "coding - 262K context, tool calling",
1463
+ "accounts/fireworks/models/deepseek-v4-pro-0813": (
1464
+ "reasoning - current 1M-context production release"
1465
+ ),
1466
+ "accounts/fireworks/models/deepseek-v4-flash-0731": (
1467
+ "fast - current lowest-cost 1M-context release"
1468
+ ),
1469
+ "accounts/fireworks/models/minimax-m3": "economy - 512K context, effort control",
1470
+ "accounts/fireworks/models/qwen3p7-plus": "fallback - 262K context, standard tier only",
1471
+ },
1472
+ model_aliases={
1473
+ # qwen2p5-coder is not serverless-capable at all (on-demand GPU
1474
+ # only); the other two are superseded snapshots.
1475
+ "accounts/fireworks/models/qwen2p5-coder-32b-instruct": (
1476
+ "accounts/fireworks/models/kimi-k2p7-code"
1477
+ ),
1478
+ "accounts/fireworks/models/kimi-k2p6": "accounts/fireworks/models/kimi-k2p7-code",
1479
+ "accounts/fireworks/models/glm-5p1": "accounts/fireworks/models/glm-5p2",
1480
+ },
1481
+ validation_model="accounts/fireworks/models/deepseek-v4-flash-0731",
1482
+ ),
1483
+ # Account-gated hosted preset — registered after the hosted third-party
1484
+ # vendors so listings that read PROFILE_PRESETS order do not headline it.
1485
+ ProfilePreset(
1486
+ key="alysis",
1487
+ provider_key="alysis",
1488
+ label="Alysis Code Pro",
1489
+ protocol="openai_compat",
1490
+ # The Alysis Code hosted proxy (`llm` Supabase Edge Function). It
1491
+ # authenticates the user's slk_ key, meters the free daily allowance /
1492
+ # Pro credits server-side, and forwards to DeepSeek. The login flow
1493
+ # overrides this from alysis_cloud at runtime (env-configurable), so
1494
+ # this literal is just the default.
1495
+ base_url="https://vzigujbcjjmpntxhmyvr.supabase.co/functions/v1/llm/v1",
1496
+ api_key_env=None,
1497
+ # The models the subscription offers. Live availability is discovered
1498
+ # from the gateway's /v1/models at runtime; this static list is the
1499
+ # offline fallback and the menu shown before a model is chosen.
1500
+ suggested_models=("deepseek-v4-flash", "deepseek-v4-pro"),
1501
+ suggested_model_descriptions={
1502
+ "deepseek-v4-flash": "default - fast high-volume coding (1M context, free daily allowance)",
1503
+ "deepseek-v4-pro": "flagship - deeper reasoning (1M context, requires Alysis Code Pro)",
1504
+ },
1505
+ validation_model="deepseek-v4-flash",
1506
+ # Migrate ids from the retired Xiaomi MiMo trial to the Pro default so
1507
+ # old sessions keep working after upgrade.
1508
+ model_aliases={
1509
+ "mimo": "deepseek-v4-flash",
1510
+ "mimo-v2.5-pro": "deepseek-v4-flash",
1511
+ "mimo-v2-flash": "deepseek-v4-flash",
1512
+ "mimo-v2.5": "deepseek-v4-flash",
1513
+ },
1514
+ setup_warning=("Requires an Alysis Code Pro subscription — run `alysis login` to connect."),
1515
+ notes="Hosted models via your Alysis Code Pro subscription. Authenticate with `alysis login`.",
1516
+ ),
1517
+ ProfilePreset(
1518
+ key="ollama",
1519
+ provider_key="ollama",
1520
+ label="Ollama (local)",
1521
+ protocol="openai_compat",
1522
+ base_url="http://localhost:11434/v1",
1523
+ api_key_env=None,
1524
+ suggested_models=("llama3.3",),
1525
+ notes="Local Ollama server. No API key required.",
1526
+ ),
1527
+ ProfilePreset(
1528
+ key="lm-studio",
1529
+ provider_key="lm-studio",
1530
+ label="LM Studio (local)",
1531
+ protocol="openai_compat",
1532
+ base_url="http://localhost:1234/v1",
1533
+ api_key_env=None,
1534
+ suggested_models=("local-model",),
1535
+ notes="Local LM Studio server. No API key required.",
1536
+ ),
1537
+ ProfilePreset(
1538
+ key="vllm",
1539
+ provider_key="vllm",
1540
+ label="vLLM (self-hosted)",
1541
+ protocol="openai_compat",
1542
+ base_url="http://localhost:8000/v1",
1543
+ api_key_env=None,
1544
+ suggested_models=("local-model",),
1545
+ ),
1546
+ ProfilePreset(
1547
+ key="custom",
1548
+ label="Custom (specify URL manually)",
1549
+ protocol="openai_compat",
1550
+ base_url="",
1551
+ api_key_env=None,
1552
+ suggested_models=(),
1553
+ notes="Use for unlisted endpoints. Type the URL during setup.",
1554
+ ),
1555
+ )
1556
+
1557
+ PRESET_BY_KEY: dict[str, ProfilePreset] = {preset.key: preset for preset in PROFILE_PRESETS}
1558
+
1559
+
1560
+ def get_preset(key: str) -> ProfilePreset | None:
1561
+ return PRESET_BY_KEY.get(str(key or "").strip().lower())
1562
+
1563
+
1564
+ def model_options_for_preset(preset: ProfilePreset) -> tuple[tuple[str, str, str], ...]:
1565
+ """Return picker rows for the models this preset intentionally supports."""
1566
+ rows: list[tuple[str, str, str]] = []
1567
+ seen: set[str] = set()
1568
+ for model in preset.suggested_models:
1569
+ model_id = str(model or "").strip()
1570
+ if not model_id or model_id in seen:
1571
+ continue
1572
+ seen.add(model_id)
1573
+ description = str(preset.suggested_model_descriptions.get(model_id) or "").strip()
1574
+ rows.append((model_id, model_id, description or "suggested by provider preset"))
1575
+ return tuple(rows)
1576
+
1577
+
1578
+ def canonical_model_alias_for_preset(preset: ProfilePreset, model: str) -> str:
1579
+ """Map explicit stale provider aliases to the preset's current model ID."""
1580
+ raw = str(model or "").strip()
1581
+ if not raw:
1582
+ return raw
1583
+ for alias, canonical in preset.model_aliases.items():
1584
+ if str(alias or "").strip().casefold() == raw.casefold():
1585
+ normalized = str(canonical or "").strip()
1586
+ return normalized or raw
1587
+ return raw
1588
+
1589
+
1590
+ def find_preset_for_profile(profile: ProfileSpec) -> ProfilePreset | None:
1591
+ """Best-effort mapping from a persisted profile back to a known provider preset."""
1592
+ name_match = get_preset(profile.name)
1593
+ if name_match is not None and _profile_matches_preset(profile, name_match):
1594
+ return name_match
1595
+
1596
+ for preset in PROFILE_PRESETS:
1597
+ if preset.key == "custom":
1598
+ continue
1599
+ if _profile_matches_preset(profile, preset):
1600
+ return preset
1601
+
1602
+ if profile.protocol != OPENAI_COMPAT_PROTOCOL:
1603
+ return None
1604
+ return find_preset_for_base_url(profile.base_url)
1605
+
1606
+
1607
+ def find_preset_for_base_url(base_url: str) -> ProfilePreset | None:
1608
+ normalized = _normalized_base_url(base_url)
1609
+ if not normalized:
1610
+ return None
1611
+ matches: list[ProfilePreset] = []
1612
+ for preset in PROFILE_PRESETS:
1613
+ if preset.key == "custom":
1614
+ continue
1615
+ if _normalized_base_url(preset.base_url) == normalized:
1616
+ matches.append(preset)
1617
+ if not matches:
1618
+ return None
1619
+ compatibility = next(
1620
+ (preset for preset in matches if preset.protocol == OPENAI_COMPAT_PROTOCOL),
1621
+ None,
1622
+ )
1623
+ return compatibility or matches[0]
1624
+
1625
+
1626
+ def _profile_matches_preset(profile: ProfileSpec, preset: ProfilePreset) -> bool:
1627
+ if str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip() != preset.protocol:
1628
+ return False
1629
+ profile_url = _normalized_base_url(profile.base_url)
1630
+ preset_url = _normalized_base_url(preset.base_url)
1631
+ if not profile_url or not preset_url or profile_url != preset_url:
1632
+ return False
1633
+ return True
1634
+
1635
+
1636
+ def profile_provider_family(profile: ProfileSpec) -> str | None:
1637
+ """Resolve a profile to a first-party family for protocol conversion and diagnostics."""
1638
+ protocol = str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip()
1639
+ if protocol == OPENAI_RESPONSES_PROTOCOL:
1640
+ return "openai"
1641
+ if protocol == ANTHROPIC_MESSAGES_PROTOCOL:
1642
+ return "anthropic"
1643
+ if protocol == GEMINI_GENERATE_CONTENT_PROTOCOL:
1644
+ return "gemini"
1645
+ if protocol == GEMINI_INTERACTIONS_PROTOCOL:
1646
+ return "gemini"
1647
+
1648
+ preset = get_preset(profile.name)
1649
+ if preset is not None:
1650
+ if preset.key in {"openai", "openai-responses"}:
1651
+ return "openai"
1652
+ if preset.key in {"anthropic", "anthropic-compat", "anthropic-native"}:
1653
+ return "anthropic"
1654
+ if preset.key in {"gemini", "gemini-compat", "gemini-native"}:
1655
+ return "gemini"
1656
+
1657
+ normalized_name = str(profile.name or "").strip().lower()
1658
+ if "openai" in normalized_name:
1659
+ return "openai"
1660
+ if "anthropic" in normalized_name or "claude" in normalized_name:
1661
+ return "anthropic"
1662
+ if "gemini" in normalized_name or "google" in normalized_name:
1663
+ return "gemini"
1664
+
1665
+ parsed = _split_base_url(profile.base_url)
1666
+ if parsed[0] == "api.openai.com":
1667
+ return "openai"
1668
+ if parsed[0] == "api.anthropic.com":
1669
+ return "anthropic"
1670
+ if parsed[0] == "generativelanguage.googleapis.com":
1671
+ return "gemini"
1672
+ return None
1673
+
1674
+
1675
+ def target_preset_for_profile_conversion(
1676
+ profile: ProfileSpec,
1677
+ *,
1678
+ target: str,
1679
+ ) -> ProfilePreset | None:
1680
+ normalized_target = normalize_conversion_target(target)
1681
+ family = profile_provider_family(profile)
1682
+ if family is None:
1683
+ return None
1684
+ preset_key = _CONVERSION_PRESET_BY_FAMILY.get(family, {}).get(normalized_target)
1685
+ if preset_key is None:
1686
+ return None
1687
+ return get_preset(preset_key)
1688
+
1689
+
1690
+ def normalize_conversion_target(value: str) -> str:
1691
+ target = str(value or "").strip().lower().replace("_", "-")
1692
+ if target in {"native", "first-party", "firstparty"}:
1693
+ return "native"
1694
+ if target in {"compat", "compatibility", "openai-compatible", "gateway"}:
1695
+ return "compatibility"
1696
+ raise ValueError("conversion target must be 'native' or 'compatibility'")
1697
+
1698
+
1699
+ def convert_profile_to_preset(profile: ProfileSpec, preset: ProfilePreset) -> ProfileSpec:
1700
+ current_model = str(profile.default_model or "").strip()
1701
+ default_model = canonical_model_alias_for_preset(preset, current_model)
1702
+ target_family = _preset_provider_family(preset)
1703
+ if not default_model or _model_known_incompatible_with_family(default_model, target_family):
1704
+ default_model = preset.suggested_models[0] if preset.suggested_models else ""
1705
+ notes = _converted_profile_notes(profile, preset)
1706
+
1707
+ return ProfileSpec(
1708
+ name=profile.name,
1709
+ protocol=preset.protocol,
1710
+ base_url=preset.base_url,
1711
+ api_key_env=profile.api_key_env or preset.api_key_env,
1712
+ extra_headers=dict(profile.extra_headers),
1713
+ default_model=default_model,
1714
+ reasoning_effort=profile.reasoning_effort,
1715
+ web_search_adapter=preset.web_search_adapter,
1716
+ web_search_model=preset.web_search_model,
1717
+ notes=notes,
1718
+ )
1719
+
1720
+
1721
+ def _converted_profile_notes(profile: ProfileSpec, preset: ProfilePreset) -> str:
1722
+ notes = str(profile.notes or "").strip()
1723
+ if not notes:
1724
+ return preset.notes
1725
+ source_preset = find_preset_for_profile(profile)
1726
+ if source_preset is not None and notes == str(source_preset.notes or "").strip():
1727
+ return preset.notes
1728
+
1729
+ lowered = notes.lower()
1730
+ target_is_native = preset.protocol in NATIVE_PROFILE_PROTOCOLS
1731
+ if target_is_native and (
1732
+ "openai-compat" in lowered
1733
+ or "openai compatible" in lowered
1734
+ or "openai-compatible" in lowered
1735
+ or "compatibility mode" in lowered
1736
+ ):
1737
+ return preset.notes
1738
+ if not target_is_native and "native" in lowered:
1739
+ return preset.notes
1740
+ return notes
1741
+
1742
+
1743
+ def _split_base_url(value: str | None) -> tuple[str, str]:
1744
+ try:
1745
+ parsed = urlsplit(str(value or "").strip())
1746
+ except ValueError:
1747
+ return "", ""
1748
+ path = parsed.path.rstrip("/").lower()
1749
+ return (parsed.hostname or "").rstrip(".").lower(), path
1750
+
1751
+
1752
+ def _preset_provider_family(preset: ProfilePreset) -> str | None:
1753
+ for family, targets in _CONVERSION_PRESET_BY_FAMILY.items():
1754
+ if preset.key in targets.values():
1755
+ return family
1756
+ return None
1757
+
1758
+
1759
+ def _model_known_incompatible_with_family(model: str, family: str | None) -> bool:
1760
+ if family is None:
1761
+ return False
1762
+ normalized = model.strip().lower()
1763
+ model_family = _known_model_family(normalized)
1764
+ if model_family is None:
1765
+ return False
1766
+ if model_family != family:
1767
+ return True
1768
+ return _has_known_provider_namespace(normalized)
1769
+
1770
+
1771
+ def known_model_family(model: str) -> str | None:
1772
+ """Best-effort model-family classifier for static diagnostics.
1773
+
1774
+ This is intentionally conservative. Unknown custom gateway models return None so doctor
1775
+ diagnostics do not over-warn on valid provider-specific names Alysis Code cannot know offline.
1776
+ """
1777
+ return _known_model_family(str(model or "").strip().lower())
1778
+
1779
+
1780
+ def model_known_incompatible_with_family(model: str, family: str | None) -> bool:
1781
+ """Public wrapper used by diagnostics and tests."""
1782
+ return _model_known_incompatible_with_family(model, family)
1783
+
1784
+
1785
+ def _known_model_family(model: str) -> str | None:
1786
+ known_prefixes: dict[str, tuple[str, ...]] = {
1787
+ "openai": ("gpt-", "chatgpt-", "o1", "o3", "o4", "o5"),
1788
+ "anthropic": ("claude-",),
1789
+ "gemini": ("gemini-",),
1790
+ }
1791
+ known_namespaces: dict[str, tuple[str, ...]] = {
1792
+ "openai": ("openai",),
1793
+ "anthropic": ("anthropic", "anthropic-ai"),
1794
+ "gemini": ("google", "gemini"),
1795
+ }
1796
+ parts = [part for part in model.split("/") if part]
1797
+ for known_family, namespaces in known_namespaces.items():
1798
+ if parts and parts[0] in namespaces:
1799
+ return known_family
1800
+ model_id = parts[-1] if parts else model
1801
+ for known_family, prefixes in known_prefixes.items():
1802
+ if model_id.startswith(prefixes):
1803
+ return known_family
1804
+ return None
1805
+
1806
+
1807
+ def _has_known_provider_namespace(model: str) -> bool:
1808
+ if "/" not in model:
1809
+ return False
1810
+ namespace = model.split("/", 1)[0]
1811
+ return namespace in {"openai", "anthropic", "anthropic-ai", "google", "gemini"}
1812
+
1813
+
1814
+ def _normalized_base_url(value: str | None) -> str:
1815
+ return str(value or "").strip().rstrip("/")
1816
+
1817
+
1818
+ def make_profile_from_preset(
1819
+ preset: ProfilePreset,
1820
+ *,
1821
+ name: str | None = None,
1822
+ ) -> ProfileSpec:
1823
+ profile_name = str(name or preset.key).strip().lower()
1824
+ return ProfileSpec(
1825
+ name=profile_name,
1826
+ protocol=preset.protocol,
1827
+ base_url=preset.base_url,
1828
+ api_key_env=preset.api_key_env,
1829
+ extra_headers=dict(preset.extra_headers),
1830
+ default_model=preset.suggested_models[0] if preset.suggested_models else "",
1831
+ web_search_adapter=preset.web_search_adapter,
1832
+ web_search_model=preset.web_search_model,
1833
+ notes=preset.notes,
1834
+ )