omnimancer-cli 0.3.4__tar.gz → 0.3.5__tar.gz
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.
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/PKG-INFO +1 -1
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/h2l_headless.py +17 -1
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/subagent.py +73 -22
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/engine.py +16 -16
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/env_loader.py +36 -3
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/prompt_enhancer.py +12 -11
- omnimancer_cli-0.3.5/omnimancer/core/provider_resolver.py +226 -0
- omnimancer_cli-0.3.5/omnimancer/h2l/refs.py +86 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer_cli.egg-info/PKG-INFO +1 -1
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer_cli.egg-info/SOURCES.txt +1 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/pyproject.toml +1 -1
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_env_loader.py +38 -1
- omnimancer_cli-0.3.4/omnimancer/h2l/refs.py +0 -135
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/LICENSE +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/README.md +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omn_fleet_hook.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/agent_loop.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/command_dispatch.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/commands.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/completion.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/file_mentions.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/h2l_command.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/headless.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/headless_checkpoint.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/interface.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/prompt_input.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/pt_completion.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/session.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/system_prompts.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/tool_handler.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/turn_notify.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/cli/usage.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/file_system_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/models.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/provider_capabilities.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/sandbox_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/events/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/events/emitter.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/events/jsonl_writer.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/events/schema.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/judge.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/loop.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/models.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/orchestrator.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/planner.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/prompts.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/render.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/h2l/worker.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/azure.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/base.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/cache_tokens.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/digitalocean.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/mistral.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/openai.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/openai_compatible.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/openrouter.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/perplexity.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/providers/xai.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/fleet/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/fleet/app.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/fleet/cli.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/fleet/models.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/fleet/sources.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/tui/fleet/widgets.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/ui/turn_activity.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/setup.cfg +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_bedrock_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_claude_code_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_claude_prompt_cache.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_config_migration_passthrough.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_digitalocean_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_mcp_management_commands.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_models.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_openai_compatible_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_openai_provider_tools.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_openai_request_timeout.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_openai_streaming.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_openrouter_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_perplexity_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_aliases.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_cache_tokens.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_capability_consistency.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_discovery.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_system_prompts.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_tool_handler.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_vertex_provider.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_web_request_markers.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_workflows_comprehensive.py +0 -0
- {omnimancer_cli-0.3.4 → omnimancer_cli-0.3.5}/tests/test_xai_provider.py +0 -0
|
@@ -13,6 +13,7 @@ from dataclasses import dataclass, field
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any, Dict, List, Optional
|
|
15
15
|
|
|
16
|
+
from ..core.env_loader import apply_env_overrides
|
|
16
17
|
from ..core.models import ChatResponse, Config
|
|
17
18
|
from ..h2l.models import (
|
|
18
19
|
H2LConfig,
|
|
@@ -48,13 +49,28 @@ class H2LOptions:
|
|
|
48
49
|
plan_iterations: Optional[int] = None
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
def _effective_default_provider(config: Config) -> str:
|
|
53
|
+
"""The default provider a bare ref should use.
|
|
54
|
+
|
|
55
|
+
Environment beats stored config everywhere else, so a bare ``--high
|
|
56
|
+
kimi-k2`` under OMNIMANCER_DEFAULT_PROVIDER=bedrock must resolve to
|
|
57
|
+
bedrock rather than to the stale entry on disk.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
effective = apply_env_overrides(config)
|
|
61
|
+
except Exception as exc: # overrides must never block a run
|
|
62
|
+
logger.debug("H2L: env overrides unavailable: %s", exc)
|
|
63
|
+
effective = config
|
|
64
|
+
return getattr(effective, "default_provider", "") or ""
|
|
65
|
+
|
|
66
|
+
|
|
51
67
|
def resolve_h2l_config(config: Config, options: H2LOptions) -> H2LConfig:
|
|
52
68
|
"""Flags override the config block field by field; never persisted."""
|
|
53
69
|
base: Dict[str, Any] = {}
|
|
54
70
|
block = getattr(config, "h2l", None)
|
|
55
71
|
if block is not None:
|
|
56
72
|
base = block.model_dump()
|
|
57
|
-
default_provider =
|
|
73
|
+
default_provider = _effective_default_provider(config)
|
|
58
74
|
|
|
59
75
|
if options.high:
|
|
60
76
|
base["high"] = parse_model_ref(options.high, default_provider)
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A subagent (see :class:`~omnimancer.core.models.SubAgentDefinition`) handles a
|
|
4
4
|
focused task with its own system prompt, a restricted tool allowlist, and an
|
|
5
|
-
optional model override. It runs an isolated loop
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
optional model override. It runs an isolated loop using its *own* conversation
|
|
6
|
+
context, so the parent conversation is never touched. A model override also
|
|
7
|
+
gets its own provider instance from
|
|
8
|
+
:mod:`omnimancer.core.provider_resolver` rather than reassigning the shared
|
|
9
|
+
session provider's model. This is the lightweight counterpart to Claude
|
|
10
|
+
Code's AgentTool.
|
|
8
11
|
"""
|
|
9
12
|
|
|
10
13
|
import logging
|
|
@@ -13,7 +16,12 @@ from dataclasses import dataclass, field
|
|
|
13
16
|
from typing import Any, List, Optional
|
|
14
17
|
|
|
15
18
|
from ..core.agent.status_core import EventType
|
|
16
|
-
from ..core.models import ChatContext, SubAgentDefinition
|
|
19
|
+
from ..core.models import ChatContext, ProviderConfig, SubAgentDefinition
|
|
20
|
+
from ..core.provider_resolver import (
|
|
21
|
+
ProviderResolutionError,
|
|
22
|
+
resolve_entry,
|
|
23
|
+
resolve_provider,
|
|
24
|
+
)
|
|
17
25
|
from ..events import emitter as fleet_events
|
|
18
26
|
from .tool_handler import ToolHandler
|
|
19
27
|
|
|
@@ -48,6 +56,50 @@ class SubAgentRunner:
|
|
|
48
56
|
allowed = set(definition.tools)
|
|
49
57
|
return [t for t in all_tools if t.name in allowed]
|
|
50
58
|
|
|
59
|
+
def _provider_name(self, provider: Any) -> str:
|
|
60
|
+
"""The config-entry name for the running provider, if we can tell."""
|
|
61
|
+
providers = getattr(self.engine, "providers", None)
|
|
62
|
+
if isinstance(providers, dict):
|
|
63
|
+
for name, instance in providers.items():
|
|
64
|
+
if instance is provider:
|
|
65
|
+
return str(name)
|
|
66
|
+
getter = getattr(provider, "get_provider_name", None)
|
|
67
|
+
if callable(getter):
|
|
68
|
+
raw_name = getter()
|
|
69
|
+
if isinstance(raw_name, str):
|
|
70
|
+
return str(raw_name)
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
def _isolated_provider(self, provider_name: str, model: str) -> Optional[Any]:
|
|
74
|
+
"""A provider instance of our own for a model override.
|
|
75
|
+
|
|
76
|
+
Returns None when the entry cannot be resolved, so the caller falls
|
|
77
|
+
back to the legacy mutate-and-restore behaviour rather than failing
|
|
78
|
+
a subagent over a provider-resolution detail.
|
|
79
|
+
"""
|
|
80
|
+
config_manager = getattr(self.engine, "config_manager", None)
|
|
81
|
+
if not provider_name or config_manager is None:
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
# Only a real entry is safe to build from: anything else means we
|
|
85
|
+
# cannot tell what we would be constructing.
|
|
86
|
+
entry = resolve_entry(provider_name, config_manager, self.engine, model)
|
|
87
|
+
if not isinstance(entry, ProviderConfig):
|
|
88
|
+
return None
|
|
89
|
+
return resolve_provider(provider_name, config_manager, model, self.engine)
|
|
90
|
+
except ProviderResolutionError as exc:
|
|
91
|
+
logger.debug(
|
|
92
|
+
"Subagent model override falling back to the shared provider: %s",
|
|
93
|
+
exc,
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.debug(
|
|
98
|
+
"Subagent model override falling back to the shared provider: %s",
|
|
99
|
+
exc,
|
|
100
|
+
)
|
|
101
|
+
return None
|
|
102
|
+
|
|
51
103
|
async def run(self, definition: SubAgentDefinition, task: str) -> SubAgentResult:
|
|
52
104
|
"""Execute the subagent's isolated tool-calling loop for ``task``."""
|
|
53
105
|
provider = getattr(self.engine, "current_provider", None)
|
|
@@ -62,26 +114,24 @@ class SubAgentRunner:
|
|
|
62
114
|
tool_handler = ToolHandler(agent_engine)
|
|
63
115
|
tools = self._scoped_tools(tool_handler, definition)
|
|
64
116
|
|
|
117
|
+
# Identity is read before any override so the entry name is the
|
|
118
|
+
# session's, not something derived from a swapped-in instance.
|
|
119
|
+
provider_name = self._provider_name(provider)
|
|
120
|
+
|
|
121
|
+
shared_provider = provider
|
|
65
122
|
original_model = getattr(provider, "model", None)
|
|
123
|
+
restore_model = False
|
|
66
124
|
if definition.model:
|
|
67
|
-
|
|
125
|
+
isolated = self._isolated_provider(provider_name, definition.model)
|
|
126
|
+
if isolated is not None:
|
|
127
|
+
provider = isolated
|
|
128
|
+
else:
|
|
129
|
+
# Last resort: the shared object, restored in the finally.
|
|
130
|
+
provider.model = definition.model
|
|
131
|
+
restore_model = True
|
|
68
132
|
|
|
69
|
-
# Compute the provider/model identity for fleet lifecycle events.
|
|
70
133
|
raw_model = getattr(provider, "model", "")
|
|
71
134
|
model_used: str = raw_model if isinstance(raw_model, str) else ""
|
|
72
|
-
provider_name = ""
|
|
73
|
-
providers = getattr(self.engine, "providers", None)
|
|
74
|
-
if isinstance(providers, dict):
|
|
75
|
-
for name, instance in providers.items():
|
|
76
|
-
if instance is provider:
|
|
77
|
-
provider_name = name
|
|
78
|
-
break
|
|
79
|
-
if not provider_name:
|
|
80
|
-
getter = getattr(provider, "get_provider_name", None)
|
|
81
|
-
if callable(getter):
|
|
82
|
-
raw_name = getter()
|
|
83
|
-
if isinstance(raw_name, str):
|
|
84
|
-
provider_name = raw_name
|
|
85
135
|
|
|
86
136
|
# Isolated context — the parent conversation is never modified. The
|
|
87
137
|
# run id is unique per invocation so two runs of the same subagent
|
|
@@ -164,9 +214,10 @@ class SubAgentRunner:
|
|
|
164
214
|
finally:
|
|
165
215
|
# Restore the shared provider BEFORE any await: a cancellation
|
|
166
216
|
# during the emit must never leave the parent conversation
|
|
167
|
-
# running on the subagent's model.
|
|
168
|
-
|
|
169
|
-
|
|
217
|
+
# running on the subagent's model. Only the fallback path
|
|
218
|
+
# touched it; a resolved instance leaves it alone entirely.
|
|
219
|
+
if restore_model and original_model is not None:
|
|
220
|
+
shared_provider.model = original_model
|
|
170
221
|
try:
|
|
171
222
|
await fleet_events.emit_event(
|
|
172
223
|
EventType.SESSION_END,
|
|
@@ -136,17 +136,13 @@ class CoreEngine:
|
|
|
136
136
|
async def initialize_providers(self) -> None:
|
|
137
137
|
"""Initialize all configured providers."""
|
|
138
138
|
try:
|
|
139
|
-
config
|
|
140
|
-
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
try:
|
|
145
|
-
from .env_loader import apply_env_overrides
|
|
139
|
+
# The effective configuration: stored config plus ephemeral
|
|
140
|
+
# environment overrides (API keys, base_url, model, default
|
|
141
|
+
# provider), never persisted. Every other surface resolves
|
|
142
|
+
# against this same view via core.provider_resolver.
|
|
143
|
+
from .provider_resolver import effective_config
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
except Exception as e:
|
|
149
|
-
logger.warning(f"Skipping environment overrides: {e}")
|
|
145
|
+
config = effective_config(self.config_manager)
|
|
150
146
|
|
|
151
147
|
# Use the optimized provider initializer with
|
|
152
148
|
# config_manager for API key decryption
|
|
@@ -232,11 +228,13 @@ class CoreEngine:
|
|
|
232
228
|
break
|
|
233
229
|
if not provider_name or not model:
|
|
234
230
|
try:
|
|
235
|
-
|
|
231
|
+
from .provider_resolver import effective_config
|
|
232
|
+
|
|
233
|
+
config = effective_config(self.config_manager)
|
|
236
234
|
if not provider_name:
|
|
237
235
|
provider_name = config.default_provider or "unknown"
|
|
238
236
|
if not model:
|
|
239
|
-
entry = config.providers.get(config.default_provider)
|
|
237
|
+
entry = (config.providers or {}).get(config.default_provider)
|
|
240
238
|
raw = getattr(entry, "model", "")
|
|
241
239
|
model = raw if isinstance(raw, str) else ""
|
|
242
240
|
except Exception as e: # identity is best-effort diagnostics
|
|
@@ -945,11 +943,13 @@ class CoreEngine:
|
|
|
945
943
|
Dictionary with health status information
|
|
946
944
|
"""
|
|
947
945
|
try:
|
|
948
|
-
|
|
946
|
+
from .provider_resolver import effective_providers
|
|
947
|
+
|
|
948
|
+
providers = effective_providers(self.config_manager)
|
|
949
949
|
|
|
950
950
|
if provider_name:
|
|
951
951
|
# Check specific provider
|
|
952
|
-
if provider_name not in
|
|
952
|
+
if provider_name not in providers:
|
|
953
953
|
return {
|
|
954
954
|
provider_name: {
|
|
955
955
|
"status": "error",
|
|
@@ -959,7 +959,7 @@ class CoreEngine:
|
|
|
959
959
|
}
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
-
provider_config =
|
|
962
|
+
provider_config = providers[provider_name]
|
|
963
963
|
status = await self.health_monitor.check_provider_health(
|
|
964
964
|
provider_name, provider_config, force=force
|
|
965
965
|
)
|
|
@@ -967,7 +967,7 @@ class CoreEngine:
|
|
|
967
967
|
else:
|
|
968
968
|
# Check all providers
|
|
969
969
|
return await self.health_monitor.check_all_providers_health(
|
|
970
|
-
|
|
970
|
+
providers, force=force
|
|
971
971
|
)
|
|
972
972
|
|
|
973
973
|
except Exception as e:
|
|
@@ -7,13 +7,16 @@ and inject them into provider configurations.
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
-
from typing import TYPE_CHECKING, Dict, Optional
|
|
10
|
+
from typing import TYPE_CHECKING, Dict, Optional, Set
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from .models import Config
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
|
+
# Namespaced env-var suffixes recognized for every provider.
|
|
18
|
+
ENV_VAR_SUFFIXES = ("API_KEY", "BASE_URL", "MODEL")
|
|
19
|
+
|
|
17
20
|
# Mapping of provider names to (conventional) environment variable names for API keys.
|
|
18
21
|
ENV_VAR_MAPPING = {
|
|
19
22
|
"claude": "ANTHROPIC_API_KEY",
|
|
@@ -37,6 +40,31 @@ def _omnimancer_env_prefix(provider_name: str) -> str:
|
|
|
37
40
|
return "OMNIMANCER_" + provider_name.upper().replace("-", "_")
|
|
38
41
|
|
|
39
42
|
|
|
43
|
+
def env_configured_provider_names() -> Set[str]:
|
|
44
|
+
"""Registered provider names that have OMNIMANCER_<PROVIDER>_* vars set.
|
|
45
|
+
|
|
46
|
+
``ENV_VAR_MAPPING`` only lists providers that have a conventional
|
|
47
|
+
single-key variable, so without this a namespaced key for anything else
|
|
48
|
+
(bedrock, vertex, openai-compatible) was never even considered as a
|
|
49
|
+
candidate and was silently ignored. The registry is the source of names so
|
|
50
|
+
the underscore-to-hyphen mapping stays unambiguous.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
from ..providers.factory import ProviderFactory
|
|
54
|
+
|
|
55
|
+
registered = set(ProviderFactory.get_available_providers())
|
|
56
|
+
except Exception as exc: # discovery must never block config loading
|
|
57
|
+
logger.debug(f"Provider registry unavailable for env discovery: {exc}")
|
|
58
|
+
return set()
|
|
59
|
+
|
|
60
|
+
names: Set[str] = set()
|
|
61
|
+
for name in registered:
|
|
62
|
+
prefix = _omnimancer_env_prefix(name)
|
|
63
|
+
if any(os.environ.get(f"{prefix}_{suffix}") for suffix in ENV_VAR_SUFFIXES):
|
|
64
|
+
names.add(name)
|
|
65
|
+
return names
|
|
66
|
+
|
|
67
|
+
|
|
40
68
|
def load_provider_env_overrides(provider_name: str) -> Dict[str, str]:
|
|
41
69
|
"""
|
|
42
70
|
Collect per-provider overrides from environment variables.
|
|
@@ -176,8 +204,13 @@ def apply_env_overrides(config: "Config") -> "Config":
|
|
|
176
204
|
effective = config.model_copy(deep=True)
|
|
177
205
|
|
|
178
206
|
# Consider providers already configured, those with a recognized API-key
|
|
179
|
-
# env var, plus any provider
|
|
180
|
-
|
|
207
|
+
# env var or a namespaced OMNIMANCER_<PROVIDER>_* var, plus any provider
|
|
208
|
+
# named by OMNIMANCER_DEFAULT_PROVIDER.
|
|
209
|
+
candidates = (
|
|
210
|
+
set(effective.providers.keys())
|
|
211
|
+
| set(ENV_VAR_MAPPING.keys())
|
|
212
|
+
| env_configured_provider_names()
|
|
213
|
+
)
|
|
181
214
|
default_override = os.environ.get(DEFAULT_PROVIDER_ENV_VAR)
|
|
182
215
|
if default_override:
|
|
183
216
|
candidates.add(default_override)
|
|
@@ -264,23 +264,24 @@ async def enhance(
|
|
|
264
264
|
if fallback_model is not None and fallback_model not in candidates:
|
|
265
265
|
candidates.append(fallback_model)
|
|
266
266
|
|
|
267
|
-
from
|
|
267
|
+
from .provider_resolver import ProviderResolutionError, resolve_provider
|
|
268
268
|
|
|
269
269
|
for provider_name, model in candidates:
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
try:
|
|
271
|
+
# The shared resolver so an entry materialized from
|
|
272
|
+
# OMNIMANCER_<PROVIDER>_* is as usable here as in the engine.
|
|
273
|
+
provider = resolve_provider(
|
|
274
|
+
provider_name,
|
|
275
|
+
config_manager,
|
|
276
|
+
model,
|
|
277
|
+
overrides={"temperature": temperature},
|
|
278
|
+
)
|
|
279
|
+
except ProviderResolutionError as exc:
|
|
272
280
|
logger.warning(
|
|
273
|
-
"Enhancement provider %r
|
|
281
|
+
"Enhancement provider %r unusable: %s", provider_name, exc
|
|
274
282
|
)
|
|
275
283
|
continue
|
|
276
284
|
try:
|
|
277
|
-
effective = provider_config.model_copy(
|
|
278
|
-
update={"model": model, "temperature": temperature}
|
|
279
|
-
)
|
|
280
|
-
provider = ProviderFactory.create_provider(
|
|
281
|
-
provider_name, effective, config_manager
|
|
282
|
-
)
|
|
283
|
-
|
|
284
285
|
# Isolated context: the session conversation never leaks
|
|
285
286
|
# into the rewrite, and the rewrite never lands in session
|
|
286
287
|
# history.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""The single answer to "which provider entry is in force, and give me one".
|
|
2
|
+
|
|
3
|
+
Every surface needs the same two things: the *effective* configuration
|
|
4
|
+
(stored config with environment overrides applied, in that precedence) and an
|
|
5
|
+
isolated provider instance built from one entry of it. Before this module each
|
|
6
|
+
caller re-derived that independently: ``CoreEngine.initialize_providers``
|
|
7
|
+
applied the overrides to a local variable and threw it away, H2L re-applied
|
|
8
|
+
them per model ref, the prompt enhancer read the stored config and so never
|
|
9
|
+
saw them, and subagents skipped resolution entirely and mutated the shared
|
|
10
|
+
session provider's model. A provider configured purely through
|
|
11
|
+
``OMNIMANCER_<PROVIDER>_API_KEY`` therefore existed for the engine and for
|
|
12
|
+
nobody else.
|
|
13
|
+
|
|
14
|
+
Two rules hold for everything here:
|
|
15
|
+
|
|
16
|
+
* **Nothing is ever persisted.** :func:`effective_config` returns a deep copy
|
|
17
|
+
carrying ephemeral environment values; saving it would write env secrets
|
|
18
|
+
into ``~/.omnimancer/config.json``. Writers keep using
|
|
19
|
+
``ConfigManager.get_config()``.
|
|
20
|
+
* **Never hand out an object from ``engine.providers``.** Each resolution
|
|
21
|
+
builds its own instance from a ``model_copy`` of the entry, so parallel
|
|
22
|
+
work (H2L tiers, subagents) cannot interleave through a shared provider.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Dict, Optional, Union
|
|
29
|
+
|
|
30
|
+
from ..providers.factory import ProviderFactory
|
|
31
|
+
from .env_loader import apply_env_overrides
|
|
32
|
+
from .models import Config, ProviderConfig
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Either a ConfigManager or an already-loaded Config is accepted, because the
|
|
37
|
+
# callers have one or the other at hand and converting is this module's job.
|
|
38
|
+
ConfigSource = Union[Config, Any]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProviderResolutionError(ValueError):
|
|
42
|
+
"""Base class: a provider entry could not be turned into an instance."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EntryNotConfigured(ProviderResolutionError):
|
|
46
|
+
"""No config entry, no environment entry, and no live instance."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, provider: str) -> None:
|
|
49
|
+
self.provider = provider
|
|
50
|
+
self.env_var = "OMNIMANCER_" + provider.upper().replace("-", "_") + "_API_KEY"
|
|
51
|
+
super().__init__(
|
|
52
|
+
f"provider entry '{provider}' is not configured "
|
|
53
|
+
f"(see /providers, or set {self.env_var})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ProviderInitFailed(ProviderResolutionError):
|
|
58
|
+
"""The factory could not build the provider (bad credentials, bad model)."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, provider: str, model: str, cause: Exception) -> None:
|
|
61
|
+
self.provider = provider
|
|
62
|
+
self.model = model
|
|
63
|
+
self.cause = cause
|
|
64
|
+
super().__init__(f"could not initialize {provider}:{model}: {cause}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ToolsUnsupported(ProviderResolutionError):
|
|
68
|
+
"""The caller required native tool calling and this provider has none."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, provider: str) -> None:
|
|
71
|
+
self.provider = provider
|
|
72
|
+
super().__init__(f"provider '{provider}' does not support tool calling")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _as_config(source: ConfigSource) -> Config:
|
|
76
|
+
"""Accept a ``Config``, a ``ConfigManager``, or any config-like object.
|
|
77
|
+
|
|
78
|
+
Config-like (anything exposing ``providers``) is allowed because callers
|
|
79
|
+
and tests pass lightweight stand-ins; environment overrides need a real
|
|
80
|
+
pydantic ``Config`` and degrade to a no-op for the rest.
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(source, Config):
|
|
83
|
+
return source
|
|
84
|
+
getter = getattr(source, "get_config", None)
|
|
85
|
+
if callable(getter):
|
|
86
|
+
config = getter()
|
|
87
|
+
if config is not None:
|
|
88
|
+
return config # type: ignore[no-any-return,return-value]
|
|
89
|
+
if hasattr(source, "providers"):
|
|
90
|
+
return source # type: ignore[no-any-return,return-value]
|
|
91
|
+
raise ProviderResolutionError(
|
|
92
|
+
"provider resolution needs a Config or a ConfigManager"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def effective_config(source: ConfigSource) -> Config:
|
|
97
|
+
"""Stored configuration with environment overrides applied.
|
|
98
|
+
|
|
99
|
+
This is the configuration the engine actually runs with. Never save it:
|
|
100
|
+
it carries ephemeral environment values, including API keys.
|
|
101
|
+
"""
|
|
102
|
+
config = _as_config(source)
|
|
103
|
+
try:
|
|
104
|
+
return apply_env_overrides(config)
|
|
105
|
+
except Exception as exc: # overrides must never block a run
|
|
106
|
+
logger.debug(f"Environment overrides unavailable: {exc}")
|
|
107
|
+
return config
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def effective_providers(source: ConfigSource) -> Dict[str, ProviderConfig]:
|
|
111
|
+
"""The provider entries in force, including env-materialized ones."""
|
|
112
|
+
return effective_config(source).providers or {}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _entry_from_live_instance(
|
|
116
|
+
provider_name: str, engine: Optional[Any], model: str
|
|
117
|
+
) -> Optional[ProviderConfig]:
|
|
118
|
+
"""Rebuild an entry from a running instance (session flags, earlier init)."""
|
|
119
|
+
live = (getattr(engine, "providers", None) or {}) if engine is not None else {}
|
|
120
|
+
instance = live.get(provider_name) if isinstance(live, dict) else None
|
|
121
|
+
if instance is None:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
api_key = getattr(instance, "api_key", "") or ""
|
|
125
|
+
kwargs = {
|
|
126
|
+
k: v
|
|
127
|
+
for k, v in (getattr(instance, "config", None) or {}).items()
|
|
128
|
+
if v is not None and k not in ("api_key", "model")
|
|
129
|
+
}
|
|
130
|
+
for candidate in (kwargs, {}):
|
|
131
|
+
try:
|
|
132
|
+
return ProviderConfig(api_key=api_key, model=model, **candidate)
|
|
133
|
+
except Exception:
|
|
134
|
+
continue
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_entry(
|
|
139
|
+
provider_name: str,
|
|
140
|
+
source: ConfigSource,
|
|
141
|
+
engine: Optional[Any] = None,
|
|
142
|
+
model: str = "",
|
|
143
|
+
) -> ProviderConfig:
|
|
144
|
+
"""The entry for ``provider_name``, from the first source that has it.
|
|
145
|
+
|
|
146
|
+
Environment-effective config first (what the engine initialized from),
|
|
147
|
+
then the stored config, then a live instance in ``engine.providers``.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
EntryNotConfigured: when no source knows this name.
|
|
151
|
+
"""
|
|
152
|
+
config = _as_config(source)
|
|
153
|
+
stored = getattr(config, "providers", None) or {}
|
|
154
|
+
entry = effective_providers(config).get(provider_name) or stored.get(provider_name)
|
|
155
|
+
if entry is not None:
|
|
156
|
+
return entry
|
|
157
|
+
|
|
158
|
+
from_live = _entry_from_live_instance(provider_name, engine, model)
|
|
159
|
+
if from_live is not None:
|
|
160
|
+
return from_live
|
|
161
|
+
|
|
162
|
+
raise EntryNotConfigured(provider_name)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_provider(
|
|
166
|
+
provider_name: str,
|
|
167
|
+
source: ConfigSource,
|
|
168
|
+
model: Optional[str] = None,
|
|
169
|
+
engine: Optional[Any] = None,
|
|
170
|
+
*,
|
|
171
|
+
min_output_tokens: Optional[int] = None,
|
|
172
|
+
require_tools: bool = False,
|
|
173
|
+
overrides: Optional[Dict[str, Any]] = None,
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""Build a fresh provider instance for one entry.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
provider_name: Config-entry name (may be an alias with provider_type).
|
|
179
|
+
source: ConfigManager or Config.
|
|
180
|
+
model: Model id to run; the entry's own model when omitted.
|
|
181
|
+
engine: Live engine, for the last-resort instance lookup.
|
|
182
|
+
min_output_tokens: Floor on ``max_tokens``; a larger configured value
|
|
183
|
+
wins. Providers default to 4096, which truncates long output.
|
|
184
|
+
require_tools: Reject a provider without native tool calling.
|
|
185
|
+
overrides: Extra entry fields to set (e.g. the enhancer's temperature).
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
EntryNotConfigured, ProviderInitFailed, ToolsUnsupported
|
|
189
|
+
"""
|
|
190
|
+
entry = resolve_entry(provider_name, source, engine, model or "")
|
|
191
|
+
|
|
192
|
+
update: Dict[str, Any] = dict(overrides or {})
|
|
193
|
+
if model:
|
|
194
|
+
update["model"] = model
|
|
195
|
+
if min_output_tokens is not None:
|
|
196
|
+
current = getattr(entry, "max_tokens", None) or 0
|
|
197
|
+
update["max_tokens"] = max(int(current), int(min_output_tokens))
|
|
198
|
+
effective = entry.model_copy(update=update) if update else entry
|
|
199
|
+
|
|
200
|
+
config_manager = None if isinstance(source, Config) else source
|
|
201
|
+
try:
|
|
202
|
+
provider = ProviderFactory.create_provider(
|
|
203
|
+
provider_name, effective, config_manager
|
|
204
|
+
)
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
raise ProviderInitFailed(provider_name, effective.model, exc) from exc
|
|
207
|
+
|
|
208
|
+
if require_tools:
|
|
209
|
+
try:
|
|
210
|
+
supports_tools = bool(provider.supports_tools())
|
|
211
|
+
except Exception: # a provider that cannot even answer is not usable
|
|
212
|
+
supports_tools = False
|
|
213
|
+
if not supports_tools:
|
|
214
|
+
raise ToolsUnsupported(provider_name)
|
|
215
|
+
|
|
216
|
+
# Belt and braces: the factory caches on (name, model), so an unchanged
|
|
217
|
+
# model can legitimately return the session's own object. Callers that
|
|
218
|
+
# need isolation pass a distinct model; warn when they do not get it.
|
|
219
|
+
shared = getattr(engine, "providers", None) if engine is not None else None
|
|
220
|
+
if isinstance(shared, dict) and any(provider is p for p in shared.values()):
|
|
221
|
+
logger.debug(
|
|
222
|
+
"Resolved %s to the session's own provider instance; work may "
|
|
223
|
+
"interleave with the main conversation",
|
|
224
|
+
provider_name,
|
|
225
|
+
)
|
|
226
|
+
return provider
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Resolve H2L model refs into isolated provider instances (PRD §1).
|
|
2
|
+
|
|
3
|
+
Entry lookup and instance building live in
|
|
4
|
+
:mod:`omnimancer.core.provider_resolver`, which every surface shares; this
|
|
5
|
+
module is the H2L-flavoured wrapper that turns a resolution failure into an
|
|
6
|
+
:class:`H2LConfigError` naming the tier. The rule that makes parallel tiers
|
|
7
|
+
safe is the resolver's: never hand out an object from ``engine.providers``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, List, Optional
|
|
15
|
+
|
|
16
|
+
from ..core.provider_resolver import (
|
|
17
|
+
EntryNotConfigured,
|
|
18
|
+
ProviderInitFailed,
|
|
19
|
+
ProviderResolutionError,
|
|
20
|
+
ToolsUnsupported,
|
|
21
|
+
resolve_provider,
|
|
22
|
+
)
|
|
23
|
+
from .models import H2LConfig, H2LModelRef
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class H2LConfigError(ValueError):
|
|
29
|
+
"""A tier cannot run; the message says which one and why."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ResolvedTiers:
|
|
34
|
+
high: Any
|
|
35
|
+
low: List[Any] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve(
|
|
39
|
+
ref: H2LModelRef,
|
|
40
|
+
config_manager: Any,
|
|
41
|
+
engine: Optional[Any],
|
|
42
|
+
min_output_tokens: Optional[int] = None,
|
|
43
|
+
) -> Any:
|
|
44
|
+
"""Build a fresh, tool-capable provider for ``ref``.
|
|
45
|
+
|
|
46
|
+
``min_output_tokens`` is a floor on the instance's ``max_tokens``: the
|
|
47
|
+
providers default to 4096, which truncates plans and verdict feedback (the
|
|
48
|
+
first live runs lost whole plans to it). A larger configured value wins.
|
|
49
|
+
|
|
50
|
+
Raises :class:`H2LConfigError` when the entry is not configured, the
|
|
51
|
+
factory cannot build it, or the provider reports no tool support.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
return resolve_provider(
|
|
55
|
+
ref.provider,
|
|
56
|
+
config_manager,
|
|
57
|
+
ref.model,
|
|
58
|
+
engine,
|
|
59
|
+
min_output_tokens=min_output_tokens,
|
|
60
|
+
require_tools=True,
|
|
61
|
+
)
|
|
62
|
+
except EntryNotConfigured as exc:
|
|
63
|
+
raise H2LConfigError(f"H2L: {exc}") from exc
|
|
64
|
+
except ToolsUnsupported as exc:
|
|
65
|
+
raise H2LConfigError(
|
|
66
|
+
f"H2L: {exc}; use an openai-compatible alias pointed at its "
|
|
67
|
+
"OpenAI endpoint (see docs/provider-setup.md)"
|
|
68
|
+
) from exc
|
|
69
|
+
except ProviderInitFailed as exc:
|
|
70
|
+
raise H2LConfigError(
|
|
71
|
+
f"H2L: could not initialize {ref.label}: {exc.cause}"
|
|
72
|
+
) from exc
|
|
73
|
+
except ProviderResolutionError as exc: # future resolver failure modes
|
|
74
|
+
raise H2LConfigError(f"H2L: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validate_tiers(
|
|
78
|
+
config: H2LConfig, config_manager: Any, engine: Optional[Any]
|
|
79
|
+
) -> ResolvedTiers:
|
|
80
|
+
"""Resolve the high ref and every low ref before any model call (PRD §1)."""
|
|
81
|
+
high = resolve(config.high, config_manager, engine, config.high_max_tokens)
|
|
82
|
+
low = [
|
|
83
|
+
resolve(ref, config_manager, engine, config.low_max_tokens)
|
|
84
|
+
for ref in config.low
|
|
85
|
+
]
|
|
86
|
+
return ResolvedTiers(high=high, low=low)
|