novacode-cli 1.2.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 (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,695 @@
1
+ """Model provider management for Nova CLI.
2
+
3
+ Handles switching between different LLM providers (OpenAI, Anthropic, Ollama,
4
+ Google, OpenRouter) during interactive sessions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ from string import Template
12
+ from typing import TYPE_CHECKING, Any
13
+ from urllib.parse import urlsplit
14
+
15
+ from novacode_cli.config.config import Settings, console
16
+ from novacode_cli.config.nova_config import NovaConfig
17
+ from novacode_cli.config.provider_registry import CATALOG_PROVIDERS
18
+
19
+ if TYPE_CHECKING:
20
+ from langchain_core.language_models import BaseChatModel
21
+
22
+ from novacode_cli.config.provider_auth import ProviderAuthStatus
23
+
24
+ # OpenRouter is OpenAI-API-compatible; routed through ChatOpenAI with this base URL.
25
+ OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
26
+
27
+ # OpenCode Go is OpenCode's subscription model gateway, OpenAI-API-compatible.
28
+ # Routed through ChatOpenAI with this base URL (same pattern as OpenRouter).
29
+ OPENCODE_BASE_URL = "https://opencode.ai/zen/go/v1"
30
+ OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1"
31
+ # Command Code's Provider API (https://commandcode.ai/docs/provider).
32
+ COMMANDCODE_BASE_URL = "https://api.commandcode.ai/provider/v1"
33
+
34
+ # Type for supported providers
35
+ ProviderType = str
36
+
37
+ #: Providers that speak OpenAI Chat Completions at a fixed URL (``base_url`` in
38
+ #: their preset, overridable through ``base_url_env``). One client covers them all.
39
+ OPENAI_COMPATIBLE_PROVIDERS: frozenset[str] = frozenset(
40
+ {"mistral", "groq", "cerebras", "xai", "huggingface", "kimi_coding"}
41
+ )
42
+
43
+ #: Providers that accept a custom OpenAI-compatible endpoint. OpenRouter and
44
+ #: OpenCode are also OpenAI-compatible but pin their own base URL, so offering
45
+ #: an endpoint there would only let the user break the gateway; Anthropic,
46
+ #: Google and Ollama take none at all.
47
+ #:
48
+ #: Shared by `/auth` (which owns the field) and `/model` (which applies the
49
+ #: stored value), so the two can never disagree about who accepts one.
50
+ ENDPOINT_PROVIDERS: frozenset[str] = frozenset({"openai", "azure"})
51
+
52
+
53
+ # Model provider presets
54
+ #: The model Nova falls back to when nothing else is chosen: no saved /model
55
+ #: pick, or the saved provider has no API key.
56
+ DEFAULT_OLLAMA_MODEL = "deepseek-v4.1-flash:cloud"
57
+
58
+ MODEL_PRESETS: dict[str, dict[str, Any]] = {
59
+ "openai": {
60
+ "name": "OpenAI",
61
+ "description": "OpenAI GPT models (gpt-4, gpt-4-turbo, gpt-5-mini, etc.)",
62
+ "default_model": "gpt-5-mini",
63
+ "env_var": "OPENAI_MODEL",
64
+ "api_key_var": "OPENAI_API_KEY",
65
+ "requires_api_key": True,
66
+ "models": [
67
+ "gpt-5-mini",
68
+ "gpt-4-turbo",
69
+ "gpt-4",
70
+ "gpt-4o",
71
+ "gpt-4o-mini",
72
+ "gpt-3.5-turbo",
73
+ ],
74
+ },
75
+ "anthropic": {
76
+ "name": "Anthropic",
77
+ "description": "Claude models (claude-sonnet, claude-opus, etc.)",
78
+ "default_model": "claude-sonnet-4-5-20250929",
79
+ "env_var": "ANTHROPIC_MODEL",
80
+ "api_key_var": "ANTHROPIC_API_KEY",
81
+ "requires_api_key": True,
82
+ "models": [
83
+ "claude-sonnet-4-5-20250929",
84
+ "claude-opus-4-5-20251101",
85
+ "claude-3-5-sonnet-20241022",
86
+ "claude-3-opus-20240229",
87
+ ],
88
+ },
89
+ "ollama": {
90
+ "name": "Ollama",
91
+ "description": "Local Ollama models (qwen, llama, mistral, etc.)",
92
+ "default_model": DEFAULT_OLLAMA_MODEL,
93
+ "env_var": "OLLAMA_MODEL",
94
+ "api_key_var": None,
95
+ "requires_api_key": False,
96
+ "models": [
97
+ DEFAULT_OLLAMA_MODEL,
98
+ "qwen3-coder:480b-cloud",
99
+ "qwen2.5:72b",
100
+ "llama3.3:70b",
101
+ "deepseek-r1:70b",
102
+ "mistral",
103
+ "codestral",
104
+ ],
105
+ },
106
+ "google": {
107
+ "name": "Google",
108
+ "description": "Google Gemini models",
109
+ "default_model": "gemini-3-pro-preview",
110
+ "env_var": "GOOGLE_MODEL",
111
+ "api_key_var": "GOOGLE_API_KEY",
112
+ "requires_api_key": True,
113
+ "models": [
114
+ "gemini-3-pro-preview",
115
+ "gemini-2.0-flash-exp",
116
+ "gemini-1.5-pro",
117
+ "gemini-1.5-flash",
118
+ ],
119
+ },
120
+ "openrouter": {
121
+ "name": "OpenRouter",
122
+ "description": "Unified access to many models (Anthropic, OpenAI, Llama, etc.)",
123
+ "default_model": "anthropic/claude-3.5-sonnet",
124
+ "env_var": "OPENROUTER_MODEL",
125
+ "api_key_var": "OPENROUTER_API_KEY",
126
+ "requires_api_key": True,
127
+ "base_url": OPENROUTER_BASE_URL,
128
+ "models": [
129
+ "anthropic/claude-3.5-sonnet",
130
+ "openai/gpt-4o",
131
+ "openai/gpt-4o-mini",
132
+ "google/gemini-2.0-flash-exp",
133
+ "meta-llama/llama-3.3-70b-instruct",
134
+ "deepseek/deepseek-chat",
135
+ ],
136
+ },
137
+ "opencode": {
138
+ "name": "OpenCode Go",
139
+ "description": "OpenCode's subscription gateway (OpenAI-compatible chat models: GLM, Kimi, DeepSeek, MiMo, Hy3, Ox)",
140
+ "default_model": "glm-5.3",
141
+ "env_var": "OPENCODE_MODEL",
142
+ "api_key_var": "OPENCODE_API_KEY",
143
+ "requires_api_key": True,
144
+ "base_url": OPENCODE_BASE_URL,
145
+ # Only the models served via /chat/completions work through ChatOpenAI.
146
+ # MiniMax/Qwen need the Anthropic Messages API and Grok/GPT-5.6/Muse
147
+ # need the Responses API — those are NOT OpenAI-chat-compatible and are
148
+ # intentionally excluded here (they'd fail with ChatOpenAI).
149
+ "models": [
150
+ "glm-5.3",
151
+ "glm-5.2",
152
+ "glm-5.1",
153
+ "glm-5",
154
+ "kimi-k3",
155
+ "kimi-k2.7-code",
156
+ "kimi-k2.6",
157
+ "kimi-k2.5",
158
+ "deepseek-v4-pro",
159
+ "deepseek-v4-flash",
160
+ "deepseek-v4-flash-vision-exp",
161
+ "mimo-v2.5-pro",
162
+ "mimo-v2.5",
163
+ "mimo-v2-pro",
164
+ "mimo-v2-omni",
165
+ "hy3",
166
+ "hy3-preview",
167
+ "ox-alpha-free",
168
+ ],
169
+ },
170
+ "opencode_zen": {
171
+ "name": "OpenCode Zen",
172
+ "description": "OpenCode's pay-as-you-go gateway",
173
+ "default_model": "big-pickle",
174
+ "env_var": "OPENCODE_ZEN_MODEL",
175
+ "api_key_var": "OPENCODE_ZEN_API_KEY",
176
+ "requires_api_key": True,
177
+ "base_url": OPENCODE_ZEN_BASE_URL,
178
+ "models": ["big-pickle", "deepseek-v4.1-flash", "glm-5.3", "kimi-k3", "claude-sonnet-4-6", "gpt-5.4"],
179
+ },
180
+ "commandcode": {
181
+ "name": "Command Code",
182
+ "description": "Command Code's Provider API (Claude, GPT, DeepSeek, GLM, Kimi, Qwen...)",
183
+ "default_model": "deepseek/deepseek-v4-flash",
184
+ "env_var": "COMMANDCODE_MODEL",
185
+ "api_key_var": "COMMANDCODE_API_KEY",
186
+ "requires_api_key": True,
187
+ "base_url": COMMANDCODE_BASE_URL,
188
+ # Offline fallback only; the picker reads the live /models list.
189
+ "models": [
190
+ "deepseek/deepseek-v4-flash",
191
+ "deepseek/deepseek-v4-pro",
192
+ "zai-org/GLM-5.3",
193
+ "moonshotai/Kimi-K3",
194
+ "claude-sonnet-5-5",
195
+ "gpt-5.5",
196
+ ],
197
+ },
198
+ "nvidia": {
199
+ "name": "NVIDIA NIM",
200
+ "description": "NVIDIA-hosted models (DeepSeek, Nemotron, Llama) via build.nvidia.com",
201
+ "default_model": "deepseek-ai/deepseek-v4-pro-0813",
202
+ "env_var": "NVIDIA_MODEL",
203
+ "api_key_var": "NVIDIA_API_KEY",
204
+ "requires_api_key": True,
205
+ # Verified callable against a live NVIDIA account. Two catalog
206
+ # entries (llama-3.1-nemotron-70b-instruct, codestral-22b) were
207
+ # dropped: they are listed by /v1/models but return 404 'Function
208
+ # not found for account', so offering them only produces a
209
+ # confusing failure at first use.
210
+ #
211
+ # The deepseek reasoning models are slow — a single structured
212
+ # /council turn measured ~280s — so nemotron-3-super is the one
213
+ # to pick for anything latency-sensitive.
214
+ "models": [
215
+ "deepseek-ai/deepseek-v4-pro-0813",
216
+ "deepseek-ai/deepseek-v4-flash-0731",
217
+ "nvidia/nemotron-3-super-120b-a12b",
218
+ "openai/gpt-oss-120b",
219
+ ],
220
+ },
221
+ # The providers below list their tool-capable models from models.dev
222
+ # ("models_dev" is its provider id), newest first, after these curated ids.
223
+ # "key_aliases" are other variable names the same credential is read from.
224
+ "azure": {
225
+ "name": "Azure OpenAI",
226
+ "description": "Your Azure OpenAI / AI Foundry deployments (v1 API)",
227
+ "default_model": "gpt-6.1-sol",
228
+ "env_var": "AZURE_OPENAI_MODEL",
229
+ "api_key_var": "AZURE_OPENAI_API_KEY",
230
+ "key_aliases": ("AZURE_API_KEY",),
231
+ "requires_api_key": True,
232
+ "models_dev": "azure",
233
+ # Claude on Azure speaks the Anthropic API, not this provider's OpenAI v1 route.
234
+ "models_dev_skip": ("claude-",),
235
+ # Deployment names, which default to the model name in Azure.
236
+ "models": ["gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol"],
237
+ },
238
+ "bedrock": {
239
+ "name": "Amazon Bedrock",
240
+ "description": "Claude, GPT and open models on AWS (Bedrock API key or AWS credentials)",
241
+ "default_model": "global.anthropic.claude-sonnet-5-5",
242
+ "env_var": "BEDROCK_MODEL",
243
+ "api_key_var": "AWS_BEARER_TOKEN_BEDROCK",
244
+ # Any standard AWS credential works too; boto3 finds it itself.
245
+ "key_aliases": ("AWS_ACCESS_KEY_ID", "AWS_PROFILE"),
246
+ "requires_api_key": True,
247
+ "models_dev": "amazon-bedrock",
248
+ "models": [
249
+ "global.anthropic.claude-sonnet-5-5",
250
+ "us.anthropic.claude-opus-5-5",
251
+ "global.anthropic.claude-haiku-5-5",
252
+ "global.openai.gpt-6.1-sol",
253
+ ],
254
+ },
255
+ "mistral": {
256
+ "name": "Mistral",
257
+ "description": "Mistral AI models (Devstral, Mistral Large/Medium/Small)",
258
+ "default_model": "devstral-latest",
259
+ "env_var": "MISTRAL_MODEL",
260
+ "api_key_var": "MISTRAL_API_KEY",
261
+ "requires_api_key": True,
262
+ "base_url": "https://api.mistral.ai/v1",
263
+ "models_dev": "mistral",
264
+ "models": [
265
+ "devstral-latest",
266
+ "mistral-large-4",
267
+ "mistral-medium-latest",
268
+ "mistral-small-latest",
269
+ ],
270
+ },
271
+ "groq": {
272
+ "name": "Groq",
273
+ "description": "Fast inference on Groq LPUs (GPT-OSS, Qwen, Llama)",
274
+ "default_model": "openai/gpt-oss-120b",
275
+ "env_var": "GROQ_MODEL",
276
+ "api_key_var": "GROQ_API_KEY",
277
+ "requires_api_key": True,
278
+ "base_url": "https://api.groq.com/openai/v1",
279
+ "models_dev": "groq",
280
+ "models": ["openai/gpt-oss-120b", "qwen/qwen3.8-27b", "llama-3.3-70b-versatile"],
281
+ },
282
+ "cerebras": {
283
+ "name": "Cerebras",
284
+ "description": "Fast inference on Cerebras (GPT-OSS, Qwen)",
285
+ "default_model": "gpt-oss-120b",
286
+ "env_var": "CEREBRAS_MODEL",
287
+ "api_key_var": "CEREBRAS_API_KEY",
288
+ "requires_api_key": True,
289
+ "base_url": "https://api.cerebras.ai/v1",
290
+ "models_dev": "cerebras",
291
+ "models": ["gpt-oss-120b", "qwen-3.8-27b"],
292
+ },
293
+ "xai": {
294
+ "name": "xAI",
295
+ "description": "xAI Grok models",
296
+ "default_model": "grok-4.7",
297
+ "env_var": "XAI_MODEL",
298
+ "api_key_var": "XAI_API_KEY",
299
+ "requires_api_key": True,
300
+ "base_url": "https://api.x.ai/v1",
301
+ "models_dev": "xai",
302
+ "models": ["grok-4.7", "grok-4.6", "grok-build-0.1", "grok-4.3"],
303
+ },
304
+ "huggingface": {
305
+ "name": "Hugging Face",
306
+ "description": "Hugging Face Inference Providers (DeepSeek, GLM, Kimi, Qwen...)",
307
+ "default_model": "deepseek-ai/DeepSeek-V4.1-Flash",
308
+ "env_var": "HF_MODEL",
309
+ "api_key_var": "HF_TOKEN",
310
+ "requires_api_key": True,
311
+ "base_url": "https://router.huggingface.co/v1",
312
+ "models_dev": "huggingface",
313
+ "models": [
314
+ "deepseek-ai/DeepSeek-V4.1-Flash",
315
+ "zai-org/GLM-5.3",
316
+ "moonshotai/Kimi-K3",
317
+ "Qwen/Qwen3.8-27B",
318
+ ],
319
+ },
320
+ "kimi_coding": {
321
+ "name": "Kimi For Coding (kimi.com)",
322
+ # kimi.ai accounts: KIMI_BASE_URL=https://api.kimi.ai/coding/v1
323
+ "description": "Kimi Code plan subscription (kimi.com / kimi.ai)",
324
+ "default_model": "kimi-for-coding",
325
+ "env_var": "KIMI_MODEL",
326
+ "api_key_var": "KIMI_API_KEY",
327
+ "requires_api_key": True,
328
+ "base_url": "https://api.kimi.com/coding/v1",
329
+ "base_url_env": "KIMI_BASE_URL",
330
+ "models_dev": "kimi-code-plan-cn",
331
+ "models": ["kimi-for-coding", "kimi-for-coding-highspeed", "k3"],
332
+ },
333
+ "minimax": {
334
+ "name": "MiniMax (minimax.io)",
335
+ "description": "MiniMax models, pay-as-you-go or Token Plan (Anthropic-compatible API)",
336
+ "default_model": "MiniMax-M3",
337
+ "env_var": "MINIMAX_MODEL",
338
+ "api_key_var": "MINIMAX_API_KEY",
339
+ "requires_api_key": True,
340
+ # The Anthropic SDK appends /v1/messages. MINIMAX_BASE_URL switches region
341
+ # (https://api.minimax.cn/anthropic for minimax.cn accounts).
342
+ "base_url": "https://api.minimax.io/anthropic",
343
+ "base_url_env": "MINIMAX_BASE_URL",
344
+ "models_dev": "minimax-coding-plan",
345
+ "models": ["MiniMax-M3", "MiniMax-M3.1-Flash", "MiniMax-M2.7"],
346
+ },
347
+ }
348
+
349
+ MODEL_PRESETS.update(CATALOG_PROVIDERS)
350
+ OPENAI_COMPATIBLE_PROVIDERS |= frozenset(
351
+ provider for provider, preset in CATALOG_PROVIDERS.items()
352
+ if preset["protocol"] in {"chat", "responses"}
353
+ )
354
+
355
+
356
+ def provider_base_url(provider: str, model_name: str | None = None) -> str:
357
+ """The provider's endpoint: its ``base_url_env`` override, else the preset's."""
358
+ preset = MODEL_PRESETS[provider]
359
+ override = os.environ.get(preset.get("base_url_env") or "", "").strip()
360
+ model_url = preset.get("model_overrides", {}).get(model_name, {}).get("base_url")
361
+ template = override or model_url or preset["base_url"]
362
+ values = os.environ.copy()
363
+ if provider == "databricks" and values.get("DATABRICKS_HOST", "").startswith("https://"):
364
+ values["DATABRICKS_HOST"] = values["DATABRICKS_HOST"].removeprefix("https://").rstrip("/")
365
+ try:
366
+ endpoint = Template(template).substitute(values).rstrip("/")
367
+ except KeyError as exc:
368
+ message = f"{preset['name']} requires endpoint setting {exc.args[0]}"
369
+ raise ValueError(message) from None
370
+ parts = urlsplit(endpoint)
371
+ if not parts.hostname or parts.scheme not in {"http", "https"} or parts.username is not None:
372
+ message = f"{preset['name']} requires a valid HTTP(S) endpoint without credentials"
373
+ raise ValueError(message)
374
+ return endpoint
375
+
376
+
377
+ def provider_api_key(provider: str) -> str | None:
378
+ """The provider's key: keychain, then environment, over every name it is read as."""
379
+ from novacode_cli.config.credentials import get_stored_key
380
+
381
+ preset = MODEL_PRESETS[provider]
382
+ for var in (preset["api_key_var"], *preset.get("key_aliases", ())):
383
+ if not var:
384
+ continue
385
+ if key := get_stored_key(var) or os.environ.get(var):
386
+ return key
387
+ return None
388
+
389
+
390
+ def get_ollama_models() -> list[str]:
391
+ """Get list of available Ollama models by running 'ollama list'.
392
+
393
+ Returns:
394
+ List of model names, or fallback list if command fails
395
+ """
396
+ try:
397
+ result = subprocess.run(
398
+ ["ollama", "list"],
399
+ capture_output=True,
400
+ text=True,
401
+ encoding="utf-8",
402
+ errors="replace",
403
+ timeout=5,
404
+ check=False,
405
+ )
406
+
407
+ if result.returncode == 0:
408
+ lines = result.stdout.strip().split("\n")
409
+ models = []
410
+
411
+ # Skip header line and parse model names
412
+ for line in lines[1:]:
413
+ if line.strip():
414
+ # Model name is the first column
415
+ model_name = line.split()[0]
416
+ models.append(model_name)
417
+
418
+ if models:
419
+ return models
420
+
421
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
422
+ # If ollama command fails, fall back to preset list
423
+ pass
424
+
425
+ # Fallback to preset models if command fails
426
+ return MODEL_PRESETS["ollama"]["models"]
427
+
428
+
429
+ def get_opencode_models(provider: str = "opencode") -> list[str]:
430
+ """Get the live list of conversational models from Go or Zen.
431
+
432
+ Mirrors :func:`get_ollama_models`: ask the provider what it actually serves
433
+ rather than trusting the hand-maintained preset list, which drifts (six of
434
+ the preset's eighteen ids are already ``deprecated`` and 400 on first use).
435
+
436
+ The model list can be public; retrieving it does not validate a key or
437
+ confirm the account's entitlement to a particular model.
438
+
439
+ Returns:
440
+ Model ids sorted for a stable display, or the preset list if the request
441
+ fails (offline, gateway down) — same best-effort contract as Ollama.
442
+ """
443
+ # Resolve keyring-or-env: a key saved to the system keychain is not in
444
+ # os.environ on a fresh session, so reading the env alone would 401.
445
+ from novacode_cli.config.opencode_gateway import headers as gateway_headers, protocol
446
+ from novacode_cli.config.model_create import opencode_session_id
447
+
448
+ preset = MODEL_PRESETS[provider]
449
+ api_key = getattr(Settings.from_environment(), f"{provider}_api_key", None) or os.environ.get(preset["api_key_var"])
450
+
451
+ models: list[str] = []
452
+ try:
453
+ import httpx
454
+
455
+ headers = {"Accept": "application/json"}
456
+ if api_key:
457
+ headers["Authorization"] = f"Bearer {api_key}"
458
+ # OpenCode asks third-party coding agents to identify themselves.
459
+ if provider != "commandcode":
460
+ headers.update(gateway_headers(opencode_session_id()))
461
+
462
+ response = httpx.get(f"{preset['base_url']}/models", headers=headers, timeout=10)
463
+ response.raise_for_status()
464
+ payload = response.json()
465
+
466
+ models = [
467
+ entry["id"]
468
+ for entry in payload.get("data", [])
469
+ if isinstance(entry, dict) and isinstance(entry.get("id"), str) and entry["id"].strip()
470
+ and (provider == "commandcode" or protocol(provider, entry["id"]) != "unsupported")
471
+ ]
472
+ except Exception: # noqa: BLE001 — network/parse failure falls back, as Ollama does
473
+ models = []
474
+
475
+ # Fallback to preset models if the request returned nothing usable
476
+ return sorted(set(models)) if models else preset["models"]
477
+
478
+
479
+ class ModelManager:
480
+ """Manages model provider selection and switching."""
481
+
482
+ def __init__(self):
483
+ """Initialize model manager with current settings."""
484
+ self.settings = Settings.from_environment()
485
+ self.nova_config = NovaConfig()
486
+ self.current_provider: ProviderType | None = None
487
+ self.current_model: str | None = None
488
+
489
+ def get_available_providers(self) -> list[tuple[str, dict[str, Any]]]:
490
+ """Get list of providers that can actually be called right now.
491
+
492
+ Readiness comes from
493
+ :func:`novacode_cli.config.provider_auth.get_provider_auth_status`, which
494
+ checks the credential store before the environment. Checking
495
+ ``os.environ`` alone was wrong: a key saved through `/auth` lives in the
496
+ OS keychain and only reaches the environment during startup hydration, so
497
+ a keychain-only install reported every provider as unavailable.
498
+
499
+ Returns:
500
+ List of (provider_id, preset) tuples for available providers
501
+ """
502
+ from novacode_cli.config.provider_auth import get_all_auth_statuses
503
+
504
+ statuses = get_all_auth_statuses(include_providers_without_keys=True)
505
+ available = []
506
+ for provider_id, preset in MODEL_PRESETS.items():
507
+ status = statuses.get(provider_id)
508
+ if status is not None and status.is_usable:
509
+ available.append((provider_id, preset))
510
+ return available
511
+
512
+ def get_provider_statuses(self) -> dict[str, ProviderAuthStatus]:
513
+ """Credential readiness for every provider, keyed by provider id.
514
+
515
+ Built from one credential-store pass, so a caller rendering a screen
516
+ does not probe the keychain once per provider.
517
+
518
+ Returns:
519
+ Mapping of provider id to its readiness status.
520
+ """
521
+ from novacode_cli.config.provider_auth import get_all_auth_statuses
522
+
523
+ return get_all_auth_statuses(include_providers_without_keys=True)
524
+
525
+ def get_current_provider(self) -> tuple[str, str] | None:
526
+ """Get currently active provider and model.
527
+
528
+ Checks saved config first, then falls back to environment variables.
529
+
530
+ Returns:
531
+ Tuple of (provider_name, model_name) or None
532
+ """
533
+ # Check saved configuration first
534
+ saved_config = self.nova_config.get_model_config()
535
+ if saved_config:
536
+ provider_id = saved_config["provider"]
537
+ model_name = saved_config["model"]
538
+ preset = MODEL_PRESETS.get(provider_id)
539
+ if preset:
540
+ return (preset["name"], model_name)
541
+
542
+ # Fall back to environment variables - check API keys in order
543
+ if self.settings.has_openai:
544
+ model = os.environ.get("OPENAI_MODEL", "gpt-5-mini")
545
+ return ("OpenAI", model)
546
+ try:
547
+ from novacode_cli.config.openai_chatgpt_auth import has_credentials
548
+
549
+ if has_credentials():
550
+ return ("OpenAI", os.environ.get("OPENAI_MODEL", "gpt-5-mini"))
551
+ except Exception: # noqa: BLE001 — status display is best-effort
552
+ pass
553
+ if self.settings.has_anthropic:
554
+ model = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-5-20250929")
555
+ return ("Anthropic", model)
556
+ from novacode_cli.config.provider_auth import get_provider_auth_status
557
+ if get_provider_auth_status("google").is_usable:
558
+ model = os.environ.get("GOOGLE_MODEL", "gemini-3-pro-preview")
559
+ return ("Google", model)
560
+ if self.settings.has_openrouter:
561
+ model = os.environ.get("OPENROUTER_MODEL", "anthropic/claude-3.5-sonnet")
562
+ return ("OpenRouter", model)
563
+ if self.settings.has_opencode:
564
+ model = os.environ.get("OPENCODE_MODEL", "glm-5.3")
565
+ return ("OpenCode Go", model)
566
+ if self.settings.has_opencode_zen:
567
+ return ("OpenCode Zen", os.environ.get("OPENCODE_ZEN_MODEL", "big-pickle"))
568
+ if self.settings.has_commandcode:
569
+ model = os.environ.get("COMMANDCODE_MODEL", "deepseek/deepseek-v4-flash")
570
+ return ("Command Code", model)
571
+ if self.settings.has_nvidia:
572
+ model = os.environ.get("NVIDIA_MODEL", "deepseek-ai/deepseek-v4-pro-0813")
573
+ return ("NVIDIA NIM", model)
574
+ # Default to Ollama (always available, no API key needed)
575
+ model = os.environ.get("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL)
576
+ return ("Ollama", model)
577
+
578
+ def get_current_provider_id(self) -> str | None:
579
+ """Preset key (e.g. 'openai') for the currently active provider.
580
+
581
+ Reverse-maps the display name from get_current_provider(); the id is
582
+ what set_provider and the UI pickers speak.
583
+ """
584
+ current = self.get_current_provider()
585
+ if not current:
586
+ return None
587
+ for provider_id, preset in MODEL_PRESETS.items():
588
+ if preset["name"] == current[0]:
589
+ return provider_id
590
+ return None
591
+
592
+ def resolve_api_key(self, provider: str) -> str | None:
593
+ """Existing API key for a provider, from the OS keychain or environment.
594
+
595
+ Exports the key into os.environ when found so model construction can
596
+ read it. Key Policy: what to do when this returns None (prompt, warn,
597
+ or fail) belongs to the caller, never here.
598
+ """
599
+ preset = MODEL_PRESETS.get(provider)
600
+ api_key_var = preset.get("api_key_var") if preset else None
601
+ if not api_key_var:
602
+ return None
603
+
604
+ # Read through the credential store, not SecretManager directly: the
605
+ # store is the one place that knows how a credential is filed, and a
606
+ # second reader here is how the keychain and the TUI drifted apart.
607
+ key = provider_api_key(provider)
608
+ if key:
609
+ os.environ[api_key_var] = key
610
+ return key
611
+
612
+ def create_model_for_provider(
613
+ self, provider: ProviderType, model_name: str | None = None
614
+ ) -> BaseChatModel:
615
+ """Create a model instance for the specified provider.
616
+
617
+ Args:
618
+ provider: Provider identifier (openai, anthropic, ollama, google)
619
+ model_name: Specific model name (optional, uses default if not provided)
620
+
621
+ Returns:
622
+ BaseChatModel instance
623
+
624
+ Raises:
625
+ ValueError: If provider is invalid or API key is missing
626
+ """
627
+ preset = MODEL_PRESETS.get(provider)
628
+ if not preset:
629
+ raise ValueError(f"Unknown provider: {provider}")
630
+
631
+ # Use provided model name or default
632
+ if model_name is None:
633
+ model_name = preset["default_model"]
634
+ if not model_name:
635
+ message = f"Choose a current model ID for {preset['name']}"
636
+ raise ValueError(message)
637
+
638
+ # Check API key requirement. `resolve_api_key` is keychain-aware and
639
+ # exports what it finds, so a key saved through `/auth` works here too —
640
+ # an env-only check rejected it until the process was restarted.
641
+ if preset["requires_api_key"] and not self.resolve_api_key(provider):
642
+ if provider != "openai":
643
+ api_key_var = preset["api_key_var"]
644
+ raise ValueError(f"{preset['name']} requires {api_key_var} environment variable")
645
+ from novacode_cli.config.model_create import _openai_auth_available
646
+
647
+ if not _openai_auth_available():
648
+ api_key_var = preset["api_key_var"]
649
+ raise ValueError(
650
+ f"OpenAI requires {api_key_var} or a ChatGPT sign-in configured in /auth"
651
+ )
652
+
653
+ # Construction lives in ONE place (build_chat_model). This path used to
654
+ # keep its own drifted copies — mid-session model switches silently
655
+ # lost reasoning_effort / max_retries / thinking budgets vs. boot.
656
+ from novacode_cli.config.model_create import build_chat_model
657
+
658
+ return build_chat_model(provider, model_name) # type: ignore[arg-type]
659
+
660
+ def set_provider(
661
+ self,
662
+ provider: ProviderType,
663
+ model_name: str | None = None,
664
+ base_url: str | None = None,
665
+ ) -> None:
666
+ """Set the current provider and model.
667
+
668
+ Saves configuration to Nova.config.json for persistence across sessions.
669
+
670
+ Args:
671
+ provider: Provider to use
672
+ model_name: Model name (optional)
673
+ base_url: Optional OpenAI-compatible endpoint override, for pointing
674
+ the ``openai`` provider at Azure, LM Studio, vLLM or a proxy.
675
+ Blank clears any previously saved override.
676
+ """
677
+ preset = MODEL_PRESETS.get(provider)
678
+ if not preset:
679
+ raise ValueError(f"Unknown provider: {provider}")
680
+
681
+ # Use default model if not specified
682
+ if model_name is None:
683
+ model_name = preset["default_model"]
684
+
685
+ # Save to persistent configuration
686
+ self.nova_config.set_model_config(provider, model_name, base_url)
687
+
688
+ # Also set environment variables for immediate effect in current session
689
+ if preset["env_var"]:
690
+ os.environ[preset["env_var"]] = model_name
691
+
692
+ self.current_provider = provider
693
+ self.current_model = model_name
694
+
695
+ console.print(f"[dim]Switched to {preset['name']}: {self.current_model}[/dim]")