omnimancer-cli 0.2.19__tar.gz → 0.2.22__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.2.19/omnimancer_cli.egg-info → omnimancer_cli-0.2.22}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/headless.py +77 -4
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/base.py +30 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/openai.py +32 -12
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/openrouter.py +5 -2
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22/omnimancer_cli.egg-info}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer_cli.egg-info/SOURCES.txt +1 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/pyproject.toml +1 -1
- omnimancer_cli-0.2.22/tests/test_openai_request_timeout.py +129 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/LICENSE +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/README.md +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/agent_loop.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/command_dispatch.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/commands.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/completion.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/interface.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/system_prompts.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/cli/tool_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/file_system_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/engine.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/models.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/provider_capabilities.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/sandbox_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/azure.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/digitalocean.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/mistral.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/perplexity.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/providers/xai.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/setup.cfg +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_bedrock_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_claude_code_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_digitalocean_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_mcp_management_commands.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_models.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_openai_provider_tools.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_openrouter_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_perplexity_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_provider_capability_consistency.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_provider_discovery.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_system_prompts.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_tool_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_vertex_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_web_request_markers.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_workflows_comprehensive.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_xai_provider.py +0 -0
|
@@ -5,6 +5,8 @@ results to stdout. No Rich console, no readline, no interactive UI.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
8
10
|
import sys
|
|
9
11
|
import uuid
|
|
10
12
|
from enum import Enum
|
|
@@ -24,6 +26,53 @@ from .tool_handler import (
|
|
|
24
26
|
ToolHandler,
|
|
25
27
|
)
|
|
26
28
|
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# A turn with no tool calls used to end the run immediately. Models that
|
|
32
|
+
# narrate their plan ("Let me look into X...") without attaching a tool call
|
|
33
|
+
# made headless runs "complete" with zero work done, so the runner now nudges
|
|
34
|
+
# the model to either keep working or explicitly declare completion.
|
|
35
|
+
NO_TOOL_CALL_NUDGE = (
|
|
36
|
+
"You did not call any tools. If the task is fully complete, reply with "
|
|
37
|
+
"exactly DONE. Otherwise continue working now by calling tools — do not "
|
|
38
|
+
"describe what you plan to do without doing it."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Consecutive tool-less turns tolerated before the run is ended anyway.
|
|
42
|
+
MAX_NO_TOOL_NUDGES = 2
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_done_reply(content: Optional[str]) -> bool:
|
|
46
|
+
"""True when the model explicitly declares completion per the nudge."""
|
|
47
|
+
if not content:
|
|
48
|
+
return False
|
|
49
|
+
lines = content.strip().splitlines()
|
|
50
|
+
if not lines:
|
|
51
|
+
return False
|
|
52
|
+
return lines[-1].strip().rstrip(".!").upper() == "DONE"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_max_iterations(explicit: Optional[int]) -> int:
|
|
56
|
+
"""Resolve the tool-iteration cap: explicit value wins, then the
|
|
57
|
+
OMNIMANCER_MAX_ITERATIONS environment variable, then the default.
|
|
58
|
+
|
|
59
|
+
The default (25) suits interactive use; orchestrators running real
|
|
60
|
+
coding tasks inject a higher cap — exhausting it mid-task previously
|
|
61
|
+
looked like a clean "completed" run with no changes.
|
|
62
|
+
"""
|
|
63
|
+
if explicit:
|
|
64
|
+
return explicit
|
|
65
|
+
raw = os.environ.get("OMNIMANCER_MAX_ITERATIONS")
|
|
66
|
+
if raw:
|
|
67
|
+
try:
|
|
68
|
+
value = int(raw)
|
|
69
|
+
if value > 0:
|
|
70
|
+
return value
|
|
71
|
+
logger.warning("Ignoring non-positive OMNIMANCER_MAX_ITERATIONS %r", raw)
|
|
72
|
+
except ValueError:
|
|
73
|
+
logger.warning("Ignoring invalid OMNIMANCER_MAX_ITERATIONS %r", raw)
|
|
74
|
+
return MAX_TOOL_ITERATIONS
|
|
75
|
+
|
|
27
76
|
|
|
28
77
|
class OutputFormat(Enum):
|
|
29
78
|
TEXT = "text"
|
|
@@ -229,7 +278,7 @@ class HeadlessRunner:
|
|
|
229
278
|
self._verbose = verbose
|
|
230
279
|
# Headless runs are unattended, so a cap (not a check-in prompt) is
|
|
231
280
|
# the safety net; --max-iterations sizes it to the job.
|
|
232
|
-
self._max_iterations = max_iterations
|
|
281
|
+
self._max_iterations = _resolve_max_iterations(max_iterations)
|
|
233
282
|
session_id = str(uuid.uuid4())
|
|
234
283
|
self._emitter = HeadlessOutputEmitter(output_format, session_id, verbose)
|
|
235
284
|
self._tokens = TokenAccumulator()
|
|
@@ -296,6 +345,7 @@ class HeadlessRunner:
|
|
|
296
345
|
|
|
297
346
|
current_message = f"{agent_prompt}\n\nUser: {prompt}"
|
|
298
347
|
last_content = ""
|
|
348
|
+
prev_content = ""
|
|
299
349
|
last_model = model
|
|
300
350
|
last_stop_reason = "end_turn"
|
|
301
351
|
# Record of the actions the agent took, surfaced in the JSON result.
|
|
@@ -304,6 +354,7 @@ class HeadlessRunner:
|
|
|
304
354
|
# Identical repeated calls get skipped with a corrective nudge; the
|
|
305
355
|
# run is aborted only if the model keeps repeating despite nudges.
|
|
306
356
|
repeat_tracker = RepeatedCallTracker()
|
|
357
|
+
no_tool_nudges = 0
|
|
307
358
|
|
|
308
359
|
for iteration in range(self._max_iterations):
|
|
309
360
|
turns = iteration + 1
|
|
@@ -322,6 +373,7 @@ class HeadlessRunner:
|
|
|
322
373
|
last_stop_reason = response.stop_reason or "end_turn"
|
|
323
374
|
|
|
324
375
|
if response.content:
|
|
376
|
+
prev_content = last_content
|
|
325
377
|
last_content = response.content
|
|
326
378
|
self._emitter.emit_assistant(
|
|
327
379
|
response.content,
|
|
@@ -334,9 +386,30 @@ class HeadlessRunner:
|
|
|
334
386
|
# those so the run doesn't end with the work undone.
|
|
335
387
|
if not response.tool_calls:
|
|
336
388
|
recovered = parse_described_tool_calls(response.content)
|
|
337
|
-
if
|
|
338
|
-
|
|
339
|
-
|
|
389
|
+
if recovered:
|
|
390
|
+
response.tool_calls = recovered
|
|
391
|
+
else:
|
|
392
|
+
# No tool call: either the final answer or narration from
|
|
393
|
+
# a model that stopped without acting. End the run only on
|
|
394
|
+
# an explicit DONE or after repeated nudges — breaking
|
|
395
|
+
# immediately made agents "complete" with zero changes.
|
|
396
|
+
if (
|
|
397
|
+
_is_done_reply(response.content)
|
|
398
|
+
or no_tool_nudges >= MAX_NO_TOOL_NUDGES
|
|
399
|
+
):
|
|
400
|
+
# A bare "DONE" acknowledgment must not replace the
|
|
401
|
+
# model's actual final summary in the result payload.
|
|
402
|
+
if (
|
|
403
|
+
response.content
|
|
404
|
+
and response.content.strip().rstrip(".!").upper() == "DONE"
|
|
405
|
+
and prev_content
|
|
406
|
+
):
|
|
407
|
+
last_content = prev_content
|
|
408
|
+
break
|
|
409
|
+
no_tool_nudges += 1
|
|
410
|
+
current_message = NO_TOOL_CALL_NUDGE
|
|
411
|
+
continue
|
|
412
|
+
no_tool_nudges = 0
|
|
340
413
|
|
|
341
414
|
repeat_tracker.record(response.tool_calls)
|
|
342
415
|
offender = repeat_tracker.abort_offender(response.tool_calls)
|
|
@@ -6,6 +6,8 @@ to ensure consistent behavior across different AI services.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
9
11
|
from abc import ABC, abstractmethod
|
|
10
12
|
from typing import Any, AsyncIterator, Dict, List, Union
|
|
11
13
|
|
|
@@ -19,6 +21,8 @@ from ..core.models import (
|
|
|
19
21
|
ToolDefinition,
|
|
20
22
|
)
|
|
21
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class BaseProvider(ABC):
|
|
24
28
|
"""
|
|
@@ -28,6 +32,32 @@ class BaseProvider(ABC):
|
|
|
28
32
|
the required abstract methods to ensure consistent behavior.
|
|
29
33
|
"""
|
|
30
34
|
|
|
35
|
+
# Default chat-completion timeout. Large models on serverless backends
|
|
36
|
+
# (e.g. 300B+ models on DigitalOcean inference) can take well over a
|
|
37
|
+
# minute to first byte with tool-heavy agent prompts — hardcoded 30s/60s
|
|
38
|
+
# timeouts made those models unusable in headless runs.
|
|
39
|
+
DEFAULT_REQUEST_TIMEOUT = 120.0
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _resolve_request_timeout(cls, configured: Any) -> float:
|
|
43
|
+
"""
|
|
44
|
+
Resolve the chat-completion timeout: explicit config wins, then the
|
|
45
|
+
OMNIMANCER_REQUEST_TIMEOUT environment variable, then the default.
|
|
46
|
+
Invalid or non-positive values fall back to the default.
|
|
47
|
+
"""
|
|
48
|
+
for raw in (configured, os.environ.get("OMNIMANCER_REQUEST_TIMEOUT")):
|
|
49
|
+
if raw is None or raw == "":
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
value = float(raw)
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
logger.warning("Ignoring invalid request timeout %r", raw)
|
|
55
|
+
continue
|
|
56
|
+
if value > 0:
|
|
57
|
+
return value
|
|
58
|
+
logger.warning("Ignoring non-positive request timeout %r", raw)
|
|
59
|
+
return cls.DEFAULT_REQUEST_TIMEOUT
|
|
60
|
+
|
|
31
61
|
def __init__(self, api_key: str, model: str, **kwargs: Any) -> None:
|
|
32
62
|
"""
|
|
33
63
|
Initialize the provider.
|
|
@@ -57,13 +57,18 @@ class OpenAIProvider(BaseProvider):
|
|
|
57
57
|
Args:
|
|
58
58
|
api_key: OpenAI API key
|
|
59
59
|
model: OpenAI model to use (e.g., 'gpt-4', 'gpt-4o', 'gpt-3.5-turbo')
|
|
60
|
-
**kwargs: Additional configuration
|
|
60
|
+
**kwargs: Additional configuration. Supports ``request_timeout``
|
|
61
|
+
(seconds) for chat completions; the ``OMNIMANCER_REQUEST_TIMEOUT``
|
|
62
|
+
environment variable is used when the kwarg is not given.
|
|
61
63
|
"""
|
|
62
64
|
super().__init__(api_key, model or "gpt-4", **kwargs)
|
|
63
65
|
# Allow overriding the API endpoint for OpenAI-compatible services
|
|
64
66
|
self.base_url = (kwargs.get("base_url") or self.BASE_URL).rstrip("/")
|
|
65
67
|
self.max_tokens = kwargs.get("max_tokens", 4096)
|
|
66
68
|
self.temperature = kwargs.get("temperature", 0.7)
|
|
69
|
+
self.request_timeout = self._resolve_request_timeout(
|
|
70
|
+
kwargs.get("request_timeout")
|
|
71
|
+
)
|
|
67
72
|
|
|
68
73
|
async def send_message(self, message: str, context: ChatContext) -> ChatResponse:
|
|
69
74
|
"""
|
|
@@ -88,7 +93,7 @@ class OpenAIProvider(BaseProvider):
|
|
|
88
93
|
"max_tokens": self.max_tokens,
|
|
89
94
|
"temperature": self.temperature,
|
|
90
95
|
},
|
|
91
|
-
timeout=
|
|
96
|
+
timeout=self.request_timeout,
|
|
92
97
|
)
|
|
93
98
|
|
|
94
99
|
# Handle response
|
|
@@ -249,6 +254,19 @@ class OpenAIProvider(BaseProvider):
|
|
|
249
254
|
provider=provider,
|
|
250
255
|
)
|
|
251
256
|
|
|
257
|
+
async def _post_once(self, body: Dict[str, Any], timeout: float) -> httpx.Response:
|
|
258
|
+
"""Single POST to the chat completions endpoint."""
|
|
259
|
+
async with httpx.AsyncClient() as client:
|
|
260
|
+
return await client.post(
|
|
261
|
+
f"{self.base_url}/chat/completions",
|
|
262
|
+
headers={
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
265
|
+
},
|
|
266
|
+
json=body,
|
|
267
|
+
timeout=timeout,
|
|
268
|
+
)
|
|
269
|
+
|
|
252
270
|
async def _post_chat(
|
|
253
271
|
self, request_body: Dict[str, Any], timeout: float
|
|
254
272
|
) -> httpx.Response:
|
|
@@ -264,16 +282,18 @@ class OpenAIProvider(BaseProvider):
|
|
|
264
282
|
self._require_api_key()
|
|
265
283
|
|
|
266
284
|
async def _do_post(body: Dict[str, Any]) -> httpx.Response:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
# Serverless backends (notably DigitalOcean inference) sporadically
|
|
286
|
+
# time out on an otherwise-fine request; one retry absorbs those
|
|
287
|
+
# transient stalls instead of failing the whole agent run.
|
|
288
|
+
try:
|
|
289
|
+
return await self._post_once(body, timeout)
|
|
290
|
+
except httpx.TimeoutException:
|
|
291
|
+
logger.warning(
|
|
292
|
+
"%s chat completion timed out after %.0fs — retrying once",
|
|
293
|
+
self.PROVIDER_LABEL,
|
|
294
|
+
timeout,
|
|
276
295
|
)
|
|
296
|
+
return await self._post_once(body, timeout)
|
|
277
297
|
|
|
278
298
|
body = request_body
|
|
279
299
|
response = await _do_post(body)
|
|
@@ -414,7 +434,7 @@ class OpenAIProvider(BaseProvider):
|
|
|
414
434
|
if tools:
|
|
415
435
|
request_body["tools"] = tools
|
|
416
436
|
|
|
417
|
-
response = await self._post_chat(request_body, timeout=
|
|
437
|
+
response = await self._post_chat(request_body, timeout=self.request_timeout)
|
|
418
438
|
|
|
419
439
|
return self._handle_response_with_tools(response)
|
|
420
440
|
|
|
@@ -67,6 +67,9 @@ class OpenRouterProvider(BaseProvider):
|
|
|
67
67
|
self.top_p = kwargs.get("top_p", 1.0)
|
|
68
68
|
self.frequency_penalty = kwargs.get("frequency_penalty", 0.0)
|
|
69
69
|
self.presence_penalty = kwargs.get("presence_penalty", 0.0)
|
|
70
|
+
self.request_timeout = self._resolve_request_timeout(
|
|
71
|
+
kwargs.get("request_timeout")
|
|
72
|
+
)
|
|
70
73
|
|
|
71
74
|
# Cost optimization settings
|
|
72
75
|
self.enable_fallback = kwargs.get("enable_fallback", True)
|
|
@@ -115,7 +118,7 @@ class OpenRouterProvider(BaseProvider):
|
|
|
115
118
|
f"{self.base_url}/chat/completions",
|
|
116
119
|
headers=self._get_headers(),
|
|
117
120
|
json=payload,
|
|
118
|
-
timeout=
|
|
121
|
+
timeout=self.request_timeout,
|
|
119
122
|
)
|
|
120
123
|
|
|
121
124
|
# If we get here, the request succeeded
|
|
@@ -201,7 +204,7 @@ class OpenRouterProvider(BaseProvider):
|
|
|
201
204
|
f"{self.base_url}/chat/completions",
|
|
202
205
|
headers=self._get_headers(),
|
|
203
206
|
json=payload,
|
|
204
|
-
timeout=
|
|
207
|
+
timeout=self.request_timeout,
|
|
205
208
|
)
|
|
206
209
|
|
|
207
210
|
# If we get here, the request succeeded
|
|
@@ -132,6 +132,7 @@ tests/test_models.py
|
|
|
132
132
|
tests/test_ollama_provider.py
|
|
133
133
|
tests/test_openai_context_fit.py
|
|
134
134
|
tests/test_openai_provider_tools.py
|
|
135
|
+
tests/test_openai_request_timeout.py
|
|
135
136
|
tests/test_openrouter_provider.py
|
|
136
137
|
tests/test_permission_rules.py
|
|
137
138
|
tests/test_perplexity_provider.py
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Tests for chat-completion request timeouts and timeout retry.
|
|
2
|
+
|
|
3
|
+
Covers the OpenAI provider and, by inheritance, the DigitalOcean and other
|
|
4
|
+
OpenAI-compatible providers. Large models on serverless backends (e.g.
|
|
5
|
+
DigitalOcean inference) exceed the old hardcoded 30s/60s timeouts, and the
|
|
6
|
+
backend also stalls sporadically — headless agent runs died with
|
|
7
|
+
"Request to OpenAI API timed out" and no output.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from omnimancer.core.models import ChatContext
|
|
16
|
+
from omnimancer.providers.digitalocean import DigitalOceanProvider
|
|
17
|
+
from omnimancer.providers.openai import OpenAIProvider
|
|
18
|
+
from omnimancer.providers.openrouter import OpenRouterProvider
|
|
19
|
+
from omnimancer.utils.errors import NetworkError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ok_response(content="done"):
|
|
23
|
+
resp = MagicMock()
|
|
24
|
+
resp.status_code = 200
|
|
25
|
+
resp.json = MagicMock(
|
|
26
|
+
return_value={
|
|
27
|
+
"choices": [{"message": {"content": content}, "finish_reason": "stop"}],
|
|
28
|
+
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
return resp
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def context():
|
|
36
|
+
return ChatContext(messages=[], current_model="m", session_id="s")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestTimeoutResolution:
|
|
40
|
+
def test_default(self, monkeypatch):
|
|
41
|
+
monkeypatch.delenv("OMNIMANCER_REQUEST_TIMEOUT", raising=False)
|
|
42
|
+
provider = OpenAIProvider(api_key="k")
|
|
43
|
+
assert provider.request_timeout == OpenAIProvider.DEFAULT_REQUEST_TIMEOUT
|
|
44
|
+
|
|
45
|
+
def test_kwarg_wins_over_env(self, monkeypatch):
|
|
46
|
+
monkeypatch.setenv("OMNIMANCER_REQUEST_TIMEOUT", "300")
|
|
47
|
+
provider = OpenAIProvider(api_key="k", request_timeout=45)
|
|
48
|
+
assert provider.request_timeout == 45.0
|
|
49
|
+
|
|
50
|
+
def test_env_var(self, monkeypatch):
|
|
51
|
+
monkeypatch.setenv("OMNIMANCER_REQUEST_TIMEOUT", "300")
|
|
52
|
+
provider = DigitalOceanProvider(api_key="k", model="qwen-x")
|
|
53
|
+
assert provider.request_timeout == 300.0
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("bad", ["not-a-number", "-5", "0"])
|
|
56
|
+
def test_invalid_values_fall_back(self, monkeypatch, bad):
|
|
57
|
+
monkeypatch.setenv("OMNIMANCER_REQUEST_TIMEOUT", bad)
|
|
58
|
+
provider = OpenAIProvider(api_key="k")
|
|
59
|
+
assert provider.request_timeout == OpenAIProvider.DEFAULT_REQUEST_TIMEOUT
|
|
60
|
+
|
|
61
|
+
def test_openrouter_env_var(self, monkeypatch):
|
|
62
|
+
monkeypatch.setenv("OMNIMANCER_REQUEST_TIMEOUT", "300")
|
|
63
|
+
provider = OpenRouterProvider(api_key="k")
|
|
64
|
+
assert provider.request_timeout == 300.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestOpenRouterTimeoutUsed:
|
|
68
|
+
async def test_send_message_uses_configured_timeout(self, context):
|
|
69
|
+
provider = OpenRouterProvider(api_key="k", request_timeout=250)
|
|
70
|
+
seen = []
|
|
71
|
+
|
|
72
|
+
async def fake_post(url, **kwargs):
|
|
73
|
+
seen.append(kwargs["timeout"])
|
|
74
|
+
return _ok_response()
|
|
75
|
+
|
|
76
|
+
with patch("httpx.AsyncClient") as client:
|
|
77
|
+
inst = client.return_value.__aenter__.return_value
|
|
78
|
+
inst.post = AsyncMock(side_effect=fake_post)
|
|
79
|
+
await provider.send_message("hi", context)
|
|
80
|
+
|
|
81
|
+
assert seen == [250]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestTimeoutUsedOnRequests:
|
|
85
|
+
async def test_send_message_uses_configured_timeout(self, context):
|
|
86
|
+
provider = OpenAIProvider(api_key="k", request_timeout=222)
|
|
87
|
+
seen = []
|
|
88
|
+
|
|
89
|
+
async def fake_post(url, **kwargs):
|
|
90
|
+
seen.append(kwargs["timeout"])
|
|
91
|
+
return _ok_response()
|
|
92
|
+
|
|
93
|
+
with patch("httpx.AsyncClient") as client:
|
|
94
|
+
inst = client.return_value.__aenter__.return_value
|
|
95
|
+
inst.post = AsyncMock(side_effect=fake_post)
|
|
96
|
+
await provider.send_message("hi", context)
|
|
97
|
+
|
|
98
|
+
assert seen == [222]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestTimeoutRetry:
|
|
102
|
+
async def test_retries_once_on_timeout_then_succeeds(self, context):
|
|
103
|
+
provider = DigitalOceanProvider(api_key="k", model="qwen-x")
|
|
104
|
+
calls = []
|
|
105
|
+
|
|
106
|
+
async def fake_post(url, **kwargs):
|
|
107
|
+
calls.append(url)
|
|
108
|
+
if len(calls) == 1:
|
|
109
|
+
raise httpx.ReadTimeout("stalled")
|
|
110
|
+
return _ok_response("recovered")
|
|
111
|
+
|
|
112
|
+
with patch("httpx.AsyncClient") as client:
|
|
113
|
+
inst = client.return_value.__aenter__.return_value
|
|
114
|
+
inst.post = AsyncMock(side_effect=fake_post)
|
|
115
|
+
result = await provider.send_message("hi", context)
|
|
116
|
+
|
|
117
|
+
assert result.content == "recovered"
|
|
118
|
+
assert len(calls) == 2
|
|
119
|
+
|
|
120
|
+
async def test_second_timeout_raises_network_error(self, context):
|
|
121
|
+
provider = DigitalOceanProvider(api_key="k", model="qwen-x")
|
|
122
|
+
|
|
123
|
+
with patch("httpx.AsyncClient") as client:
|
|
124
|
+
inst = client.return_value.__aenter__.return_value
|
|
125
|
+
inst.post = AsyncMock(side_effect=httpx.ReadTimeout("stalled"))
|
|
126
|
+
with pytest.raises(NetworkError):
|
|
127
|
+
await provider.send_message("hi", context)
|
|
128
|
+
|
|
129
|
+
assert inst.post.await_count == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/file_system_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/read_before_write_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/agent/workflow_orchestrator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/approval_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/permission_controller.py
RENAMED
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/permission_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer/core/security/security_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/omnimancer_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_approval_integration_compatibility.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.22}/tests/test_provider_capability_consistency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|