omnimancer-cli 0.2.19__tar.gz → 0.2.20__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.20}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/base.py +30 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/openai.py +32 -12
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/openrouter.py +5 -2
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20/omnimancer_cli.egg-info}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer_cli.egg-info/SOURCES.txt +1 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/pyproject.toml +1 -1
- omnimancer_cli-0.2.20/tests/test_openai_request_timeout.py +129 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/LICENSE +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/README.md +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/agent_loop.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/command_dispatch.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/commands.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/completion.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/headless.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/interface.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/system_prompts.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/cli/tool_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/file_system_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/engine.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/models.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/provider_capabilities.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/sandbox_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/azure.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/digitalocean.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/mistral.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/perplexity.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/providers/xai.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/setup.cfg +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_bedrock_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_claude_code_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_digitalocean_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_mcp_management_commands.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_models.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_openai_provider_tools.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_openrouter_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_perplexity_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_provider_capability_consistency.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_provider_discovery.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_system_prompts.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_tool_handler.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_vertex_provider.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_web_request_markers.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_workflows_comprehensive.py +0 -0
- {omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/tests/test_xai_provider.py +0 -0
|
@@ -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
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/agent/file_system_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/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.20}/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.20}/omnimancer/core/security/approval_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/permission_controller.py
RENAMED
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/omnimancer/core/security/permission_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.19 → omnimancer_cli-0.2.20}/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.20}/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.20}/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.20}/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
|