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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- 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]")
|