omnimancer-cli 0.2.15__tar.gz → 0.2.19__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.15/omnimancer_cli.egg-info → omnimancer_cli-0.2.19}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/tool_handler.py +12 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/file_system_manager.py +41 -4
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/models.py +23 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/azure.py +7 -3
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/mistral.py +7 -3
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/openai.py +6 -7
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/openrouter.py +11 -4
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/xai.py +7 -3
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19/omnimancer_cli.egg-info}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/pyproject.toml +1 -1
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_models.py +114 -65
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_openrouter_provider.py +97 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_tool_handler.py +15 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_xai_provider.py +5 -3
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/LICENSE +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/README.md +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/agent_loop.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/command_dispatch.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/commands.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/completion.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/display.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/headless.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/interface.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/cli/system_prompts.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/config_migration.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/engine.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/provider_capabilities.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/sandbox_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/base.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/bedrock.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/claude.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/digitalocean.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/gemini.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/perplexity.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/providers/vertex.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/SOURCES.txt +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/setup.cfg +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_bedrock_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_claude_code_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_digitalocean_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_mcp_management_commands.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_openai_provider_tools.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_perplexity_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_provider_capability_consistency.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_provider_discovery.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_system_prompts.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_vertex_provider.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_web_request_markers.py +0 -0
- {omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/tests/test_workflows_comprehensive.py +0 -0
|
@@ -150,6 +150,18 @@ class ToolHandler:
|
|
|
150
150
|
return list(CODING_AGENT_TOOLS)
|
|
151
151
|
|
|
152
152
|
async def execute_tool_call(self, tool_call: ToolCall) -> ToolResult:
|
|
153
|
+
# ToolCall normalizes JSON-string arguments on construction; anything
|
|
154
|
+
# still not a dict here is malformed — tell the model instead of
|
|
155
|
+
# crashing on args.get(...) inside a handler.
|
|
156
|
+
if not isinstance(tool_call.arguments, dict):
|
|
157
|
+
return ToolResult(
|
|
158
|
+
content="",
|
|
159
|
+
error=(
|
|
160
|
+
f"Malformed arguments for {tool_call.name}: expected a "
|
|
161
|
+
f"JSON object, got: {str(tool_call.arguments)[:200]}. "
|
|
162
|
+
"Re-issue the call with the parameters as a JSON object."
|
|
163
|
+
),
|
|
164
|
+
)
|
|
153
165
|
try:
|
|
154
166
|
# Read and Edit need post-processing / multi-step logic.
|
|
155
167
|
if tool_call.name in _EDIT_TOOLS:
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/agent/file_system_manager.py
RENAMED
|
@@ -250,14 +250,51 @@ class FileSystemManager:
|
|
|
250
250
|
"""
|
|
251
251
|
Simple boolean check for file existence.
|
|
252
252
|
|
|
253
|
+
Performs a lightweight path-validation check followed by an async
|
|
254
|
+
filesystem probe. Unlike :meth:`check_file_exists`, this method does
|
|
255
|
+
*not* invoke the full security-audit pipeline (permission logging, risk
|
|
256
|
+
assessment, approval workflow). It is intended for high-frequency
|
|
257
|
+
callers (e.g. batch operations) that need a fast boolean answer.
|
|
258
|
+
|
|
259
|
+
.. warning::
|
|
260
|
+
This method enforces path-level access controls — hard-restricted
|
|
261
|
+
system paths (``/etc``, ``~/.ssh``, cloud-credential stores, etc.)
|
|
262
|
+
and sensitive-name patterns are still blocked via
|
|
263
|
+
:meth:`~omnimancer.core.security.permission_controller.PermissionController.validate_path_access`.
|
|
264
|
+
However, it skips the per-operation audit-log entries produced by
|
|
265
|
+
:meth:`~omnimancer.core.security.SecurityManager.secure_file_access`.
|
|
266
|
+
Do not use this method where per-access audit trails are required.
|
|
267
|
+
|
|
253
268
|
Args:
|
|
254
|
-
path: Path to check for existence
|
|
269
|
+
path: Path to check for existence.
|
|
255
270
|
|
|
256
271
|
Returns:
|
|
257
|
-
True if
|
|
272
|
+
True if the path passes security validation and exists on the
|
|
273
|
+
filesystem; False if the path is security-denied or does not exist.
|
|
258
274
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
275
|
+
try:
|
|
276
|
+
resolved = Path(path).resolve()
|
|
277
|
+
except (OSError, ValueError) as e:
|
|
278
|
+
logger.warning("file_exists: invalid path %r: %s", path, e)
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# Validate path against security policy without invoking the expensive
|
|
282
|
+
# audit pipeline. This blocks hard-restricted system paths and
|
|
283
|
+
# sensitive-name patterns (e.g. /etc, ~/.ssh, *.db) the same way the
|
|
284
|
+
# full pipeline would.
|
|
285
|
+
if not self.security.permissions.validate_path_access(str(resolved), "read"):
|
|
286
|
+
logger.warning(
|
|
287
|
+
"file_exists: access denied by security policy for %s", resolved
|
|
288
|
+
)
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
exists = bool(await aiofiles.os.path.exists(resolved))
|
|
293
|
+
logger.debug("file_exists: %s → %s", resolved, exists)
|
|
294
|
+
return exists
|
|
295
|
+
except OSError as e:
|
|
296
|
+
logger.warning("file_exists: OS error checking %s: %s", resolved, e)
|
|
297
|
+
return False
|
|
261
298
|
|
|
262
299
|
async def write_file_with_confirmation(
|
|
263
300
|
self,
|
|
@@ -113,6 +113,29 @@ class ToolCall:
|
|
|
113
113
|
# Provider-assigned call id (OpenAI protocol pairs results to calls by id).
|
|
114
114
|
id: Optional[str] = None
|
|
115
115
|
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
"""Normalize arguments that arrive as a JSON-encoded string.
|
|
118
|
+
|
|
119
|
+
OpenAI-protocol servers send ``function.arguments`` as a JSON string,
|
|
120
|
+
and some models double-encode it. A value that doesn't decode to an
|
|
121
|
+
object is left as-is so the tool handler can report it to the model.
|
|
122
|
+
"""
|
|
123
|
+
import json
|
|
124
|
+
|
|
125
|
+
if self.arguments is None:
|
|
126
|
+
self.arguments = {}
|
|
127
|
+
return
|
|
128
|
+
decoded: Any = self.arguments
|
|
129
|
+
for _ in range(2):
|
|
130
|
+
if not isinstance(decoded, str):
|
|
131
|
+
break
|
|
132
|
+
try:
|
|
133
|
+
decoded = json.loads(decoded)
|
|
134
|
+
except ValueError:
|
|
135
|
+
return
|
|
136
|
+
if isinstance(decoded, dict):
|
|
137
|
+
self.arguments = decoded
|
|
138
|
+
|
|
116
139
|
|
|
117
140
|
@dataclass
|
|
118
141
|
class ToolResultRecord:
|
|
@@ -363,19 +363,23 @@ class AzureProvider(BaseProvider):
|
|
|
363
363
|
|
|
364
364
|
if choices and len(choices) > 0:
|
|
365
365
|
message = choices[0].get("message", {})
|
|
366
|
-
|
|
366
|
+
# Tool-call responses carry "content": null — coalesce to "".
|
|
367
|
+
content = message.get("content") or ""
|
|
367
368
|
usage = data.get("usage", {})
|
|
368
369
|
|
|
369
|
-
# Extract tool calls if present
|
|
370
|
+
# Extract tool calls if present. The wire protocol sends
|
|
371
|
+
# function.arguments as a JSON string; ToolCall normalizes
|
|
372
|
+
# it to a dict on construction.
|
|
370
373
|
tool_calls = []
|
|
371
374
|
if "tool_calls" in message:
|
|
372
|
-
for tool_call in message["tool_calls"]:
|
|
375
|
+
for i, tool_call in enumerate(message["tool_calls"]):
|
|
373
376
|
if tool_call.get("type") == "function":
|
|
374
377
|
function = tool_call.get("function", {})
|
|
375
378
|
tool_calls.append(
|
|
376
379
|
ToolCall(
|
|
377
380
|
name=function.get("name", ""),
|
|
378
381
|
arguments=function.get("arguments", {}),
|
|
382
|
+
id=tool_call.get("id") or f"call_{i}",
|
|
379
383
|
)
|
|
380
384
|
)
|
|
381
385
|
|
|
@@ -324,19 +324,23 @@ class MistralProvider(BaseProvider):
|
|
|
324
324
|
|
|
325
325
|
if choices and len(choices) > 0:
|
|
326
326
|
message = choices[0].get("message", {})
|
|
327
|
-
|
|
327
|
+
# Tool-call responses carry "content": null — coalesce to "".
|
|
328
|
+
content = message.get("content") or ""
|
|
328
329
|
usage = data.get("usage", {})
|
|
329
330
|
|
|
330
|
-
# Extract tool calls if present
|
|
331
|
+
# Extract tool calls if present. The wire protocol sends
|
|
332
|
+
# function.arguments as a JSON string; ToolCall normalizes
|
|
333
|
+
# it to a dict on construction.
|
|
331
334
|
tool_calls = []
|
|
332
335
|
if "tool_calls" in message:
|
|
333
|
-
for tool_call in message["tool_calls"]:
|
|
336
|
+
for i, tool_call in enumerate(message["tool_calls"]):
|
|
334
337
|
if tool_call.get("type") == "function":
|
|
335
338
|
function = tool_call.get("function", {})
|
|
336
339
|
tool_calls.append(
|
|
337
340
|
ToolCall(
|
|
338
341
|
name=function.get("name", ""),
|
|
339
342
|
arguments=function.get("arguments", {}),
|
|
343
|
+
id=tool_call.get("id") or f"call_{i}",
|
|
340
344
|
)
|
|
341
345
|
)
|
|
342
346
|
|
|
@@ -332,7 +332,8 @@ class OpenAIProvider(BaseProvider):
|
|
|
332
332
|
|
|
333
333
|
if choices and len(choices) > 0:
|
|
334
334
|
message = choices[0].get("message", {})
|
|
335
|
-
|
|
335
|
+
# Tool-call responses carry "content": null — coalesce to "".
|
|
336
|
+
content = message.get("content") or ""
|
|
336
337
|
usage = data.get("usage", {})
|
|
337
338
|
|
|
338
339
|
return ChatResponse(
|
|
@@ -462,18 +463,16 @@ class OpenAIProvider(BaseProvider):
|
|
|
462
463
|
tool_calls = None
|
|
463
464
|
raw_tool_calls = msg.get("tool_calls")
|
|
464
465
|
if raw_tool_calls:
|
|
465
|
-
import json
|
|
466
|
-
|
|
467
466
|
tool_calls = []
|
|
468
467
|
for i, tc in enumerate(raw_tool_calls):
|
|
469
468
|
func = tc.get("function", {})
|
|
470
|
-
args = func.get("arguments", "{}")
|
|
471
|
-
if isinstance(args, str):
|
|
472
|
-
args = json.loads(args)
|
|
473
469
|
tool_calls.append(
|
|
474
470
|
ToolCall(
|
|
471
|
+
# ToolCall normalizes JSON-string arguments
|
|
472
|
+
# (including double-encoded ones) on
|
|
473
|
+
# construction.
|
|
475
474
|
name=func.get("name", ""),
|
|
476
|
-
arguments=
|
|
475
|
+
arguments=func.get("arguments", {}),
|
|
477
476
|
# Some OpenAI-compatible servers omit ids;
|
|
478
477
|
# synthesize one so results can pair to calls.
|
|
479
478
|
id=tc.get("id") or f"call_{i}",
|
|
@@ -377,7 +377,9 @@ class OpenRouterProvider(BaseProvider):
|
|
|
377
377
|
|
|
378
378
|
if choices and len(choices) > 0:
|
|
379
379
|
message = choices[0].get("message", {})
|
|
380
|
-
|
|
380
|
+
# Tool-call responses carry "content": null — coalesce to ""
|
|
381
|
+
# so the fallback-warning concatenation below can't TypeError.
|
|
382
|
+
content = message.get("content") or ""
|
|
381
383
|
usage = data.get("usage", {})
|
|
382
384
|
|
|
383
385
|
# Extract model used (OpenRouter may route to different models)
|
|
@@ -446,19 +448,24 @@ class OpenRouterProvider(BaseProvider):
|
|
|
446
448
|
|
|
447
449
|
if choices and len(choices) > 0:
|
|
448
450
|
message = choices[0].get("message", {})
|
|
449
|
-
|
|
451
|
+
# Tool-call responses carry "content": null — coalesce to ""
|
|
452
|
+
# so the fallback-warning concatenation below can't TypeError.
|
|
453
|
+
content = message.get("content") or ""
|
|
450
454
|
usage = data.get("usage", {})
|
|
451
455
|
|
|
452
|
-
# Extract tool calls if present
|
|
456
|
+
# Extract tool calls if present. The wire protocol sends
|
|
457
|
+
# function.arguments as a JSON string; ToolCall normalizes
|
|
458
|
+
# it to a dict on construction.
|
|
453
459
|
tool_calls = []
|
|
454
460
|
if "tool_calls" in message:
|
|
455
|
-
for tool_call in message["tool_calls"]:
|
|
461
|
+
for i, tool_call in enumerate(message["tool_calls"]):
|
|
456
462
|
if tool_call.get("type") == "function":
|
|
457
463
|
function = tool_call.get("function", {})
|
|
458
464
|
tool_calls.append(
|
|
459
465
|
ToolCall(
|
|
460
466
|
name=function.get("name", ""),
|
|
461
467
|
arguments=function.get("arguments", {}),
|
|
468
|
+
id=tool_call.get("id") or f"call_{i}",
|
|
462
469
|
)
|
|
463
470
|
)
|
|
464
471
|
|
|
@@ -327,19 +327,23 @@ class XAIProvider(BaseProvider):
|
|
|
327
327
|
|
|
328
328
|
if choices and len(choices) > 0:
|
|
329
329
|
message = choices[0].get("message", {})
|
|
330
|
-
|
|
330
|
+
# Tool-call responses carry "content": null — coalesce to "".
|
|
331
|
+
content = message.get("content") or ""
|
|
331
332
|
usage = data.get("usage", {})
|
|
332
333
|
|
|
333
|
-
# Extract tool calls if present
|
|
334
|
+
# Extract tool calls if present. The wire protocol sends
|
|
335
|
+
# function.arguments as a JSON string; ToolCall normalizes
|
|
336
|
+
# it to a dict on construction.
|
|
334
337
|
tool_calls = []
|
|
335
338
|
if "tool_calls" in message:
|
|
336
|
-
for tool_call in message["tool_calls"]:
|
|
339
|
+
for i, tool_call in enumerate(message["tool_calls"]):
|
|
337
340
|
if tool_call.get("type") == "function":
|
|
338
341
|
function = tool_call.get("function", {})
|
|
339
342
|
tool_calls.append(
|
|
340
343
|
ToolCall(
|
|
341
344
|
name=function.get("name", ""),
|
|
342
345
|
arguments=function.get("arguments", {}),
|
|
346
|
+
id=tool_call.get("id") or f"call_{i}",
|
|
343
347
|
)
|
|
344
348
|
)
|
|
345
349
|
|
|
@@ -591,68 +591,117 @@ class TestStreamEvent:
|
|
|
591
591
|
|
|
592
592
|
event = StreamEvent(type=StreamEventType.ERROR, error="Connection lost")
|
|
593
593
|
assert event.error == "Connection lost"
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
class TestParseDescribedToolCalls:
|
|
597
|
-
"""parse_described_tool_calls recovers calls a model emitted as text.
|
|
598
|
-
|
|
599
|
-
Weak models mimic the "[Called tools: ...]" history notation instead of
|
|
600
|
-
issuing native tool calls; the parser turns that text back into ToolCall
|
|
601
|
-
objects so the agent loop can keep going.
|
|
602
|
-
"""
|
|
603
|
-
|
|
604
|
-
def test_round_trips_describe_output(self):
|
|
605
|
-
from omnimancer.core.models import (
|
|
606
|
-
ToolCall,
|
|
607
|
-
describe_tool_calls,
|
|
608
|
-
parse_described_tool_calls,
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
calls = [
|
|
612
|
-
ToolCall(name="Grep", arguments={"pattern": "auth", "glob": "*.ts"}),
|
|
613
|
-
ToolCall(name="Read", arguments={"file_path": "/src/main.py"}),
|
|
614
|
-
]
|
|
615
|
-
parsed = parse_described_tool_calls(describe_tool_calls(calls))
|
|
616
|
-
|
|
617
|
-
assert [(c.name, c.arguments) for c in parsed] == [
|
|
618
|
-
(c.name, c.arguments) for c in calls
|
|
619
|
-
]
|
|
620
|
-
|
|
621
|
-
def test_plain_text_returns_empty(self):
|
|
622
|
-
from omnimancer.core.models import parse_described_tool_calls
|
|
623
|
-
|
|
624
|
-
assert parse_described_tool_calls("The auth flow looks correct.") == []
|
|
625
|
-
assert parse_described_tool_calls("") == []
|
|
626
|
-
assert parse_described_tool_calls(None) == []
|
|
627
|
-
|
|
628
|
-
def test_nested_json_with_braces_and_commas(self):
|
|
629
|
-
from omnimancer.core.models import parse_described_tool_calls
|
|
630
|
-
|
|
631
|
-
text = (
|
|
632
|
-
'[Called tools: Grep({"glob": "*.{ts,tsx}", '
|
|
633
|
-
'"output_mode": "files_with_matches", '
|
|
634
|
-
'"pattern": "AUTH_SESSION_KEY|automarketer_auth_session"})]'
|
|
635
|
-
)
|
|
636
|
-
parsed = parse_described_tool_calls(text)
|
|
637
|
-
|
|
638
|
-
assert len(parsed) == 1
|
|
639
|
-
assert parsed[0].name == "Grep"
|
|
640
|
-
assert parsed[0].arguments["glob"] == "*.{ts,tsx}"
|
|
641
|
-
|
|
642
|
-
def test_marker_embedded_in_other_text(self):
|
|
643
|
-
from omnimancer.core.models import parse_described_tool_calls
|
|
644
|
-
|
|
645
|
-
text = (
|
|
646
|
-
"Let me search for that.\n"
|
|
647
|
-
'[Called tools: Bash({"command": "git log --oneline -5"})]'
|
|
648
|
-
)
|
|
649
|
-
parsed = parse_described_tool_calls(text)
|
|
650
|
-
|
|
651
|
-
assert len(parsed) == 1
|
|
652
|
-
assert parsed[0].name == "Bash"
|
|
653
|
-
assert parsed[0].arguments == {"command": "git log --oneline -5"}
|
|
654
|
-
|
|
655
|
-
def test_malformed_json_is_skipped(self):
|
|
656
|
-
from omnimancer.core.models import parse_described_tool_calls
|
|
657
|
-
|
|
658
|
-
assert parse_described_tool_calls("[Called tools: Grep({'bad': json})]") == []
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class TestParseDescribedToolCalls:
|
|
597
|
+
"""parse_described_tool_calls recovers calls a model emitted as text.
|
|
598
|
+
|
|
599
|
+
Weak models mimic the "[Called tools: ...]" history notation instead of
|
|
600
|
+
issuing native tool calls; the parser turns that text back into ToolCall
|
|
601
|
+
objects so the agent loop can keep going.
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
def test_round_trips_describe_output(self):
|
|
605
|
+
from omnimancer.core.models import (
|
|
606
|
+
ToolCall,
|
|
607
|
+
describe_tool_calls,
|
|
608
|
+
parse_described_tool_calls,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
calls = [
|
|
612
|
+
ToolCall(name="Grep", arguments={"pattern": "auth", "glob": "*.ts"}),
|
|
613
|
+
ToolCall(name="Read", arguments={"file_path": "/src/main.py"}),
|
|
614
|
+
]
|
|
615
|
+
parsed = parse_described_tool_calls(describe_tool_calls(calls))
|
|
616
|
+
|
|
617
|
+
assert [(c.name, c.arguments) for c in parsed] == [
|
|
618
|
+
(c.name, c.arguments) for c in calls
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
def test_plain_text_returns_empty(self):
|
|
622
|
+
from omnimancer.core.models import parse_described_tool_calls
|
|
623
|
+
|
|
624
|
+
assert parse_described_tool_calls("The auth flow looks correct.") == []
|
|
625
|
+
assert parse_described_tool_calls("") == []
|
|
626
|
+
assert parse_described_tool_calls(None) == []
|
|
627
|
+
|
|
628
|
+
def test_nested_json_with_braces_and_commas(self):
|
|
629
|
+
from omnimancer.core.models import parse_described_tool_calls
|
|
630
|
+
|
|
631
|
+
text = (
|
|
632
|
+
'[Called tools: Grep({"glob": "*.{ts,tsx}", '
|
|
633
|
+
'"output_mode": "files_with_matches", '
|
|
634
|
+
'"pattern": "AUTH_SESSION_KEY|automarketer_auth_session"})]'
|
|
635
|
+
)
|
|
636
|
+
parsed = parse_described_tool_calls(text)
|
|
637
|
+
|
|
638
|
+
assert len(parsed) == 1
|
|
639
|
+
assert parsed[0].name == "Grep"
|
|
640
|
+
assert parsed[0].arguments["glob"] == "*.{ts,tsx}"
|
|
641
|
+
|
|
642
|
+
def test_marker_embedded_in_other_text(self):
|
|
643
|
+
from omnimancer.core.models import parse_described_tool_calls
|
|
644
|
+
|
|
645
|
+
text = (
|
|
646
|
+
"Let me search for that.\n"
|
|
647
|
+
'[Called tools: Bash({"command": "git log --oneline -5"})]'
|
|
648
|
+
)
|
|
649
|
+
parsed = parse_described_tool_calls(text)
|
|
650
|
+
|
|
651
|
+
assert len(parsed) == 1
|
|
652
|
+
assert parsed[0].name == "Bash"
|
|
653
|
+
assert parsed[0].arguments == {"command": "git log --oneline -5"}
|
|
654
|
+
|
|
655
|
+
def test_malformed_json_is_skipped(self):
|
|
656
|
+
from omnimancer.core.models import parse_described_tool_calls
|
|
657
|
+
|
|
658
|
+
assert parse_described_tool_calls("[Called tools: Grep({'bad': json})]") == []
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class TestToolCallArgumentNormalization:
|
|
662
|
+
"""ToolCall normalizes arguments that arrive as JSON-encoded strings.
|
|
663
|
+
|
|
664
|
+
OpenAI-protocol servers send function.arguments as a JSON string (and
|
|
665
|
+
some models double-encode it). Providers that passed the raw value
|
|
666
|
+
through crashed the tool handler with "'str' object has no attribute
|
|
667
|
+
'get'".
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
def test_json_string_arguments_become_dict(self):
|
|
671
|
+
from omnimancer.core.models import ToolCall
|
|
672
|
+
|
|
673
|
+
tc = ToolCall(name="Glob", arguments='{"pattern": "**/*.py"}')
|
|
674
|
+
assert tc.arguments == {"pattern": "**/*.py"}
|
|
675
|
+
|
|
676
|
+
def test_double_encoded_arguments_become_dict(self):
|
|
677
|
+
import json
|
|
678
|
+
|
|
679
|
+
from omnimancer.core.models import ToolCall
|
|
680
|
+
|
|
681
|
+
double = json.dumps(json.dumps({"pattern": "TODO"}))
|
|
682
|
+
tc = ToolCall(name="Grep", arguments=double)
|
|
683
|
+
assert tc.arguments == {"pattern": "TODO"}
|
|
684
|
+
|
|
685
|
+
def test_none_arguments_become_empty_dict(self):
|
|
686
|
+
from omnimancer.core.models import ToolCall
|
|
687
|
+
|
|
688
|
+
tc = ToolCall(name="Read", arguments=None)
|
|
689
|
+
assert tc.arguments == {}
|
|
690
|
+
|
|
691
|
+
def test_dict_arguments_unchanged(self):
|
|
692
|
+
from omnimancer.core.models import ToolCall
|
|
693
|
+
|
|
694
|
+
tc = ToolCall(name="Read", arguments={"file_path": "/src/main.py"})
|
|
695
|
+
assert tc.arguments == {"file_path": "/src/main.py"}
|
|
696
|
+
|
|
697
|
+
def test_unparseable_string_left_for_handler_to_report(self):
|
|
698
|
+
from omnimancer.core.models import ToolCall
|
|
699
|
+
|
|
700
|
+
tc = ToolCall(name="Glob", arguments="not json at all")
|
|
701
|
+
assert tc.arguments == "not json at all"
|
|
702
|
+
|
|
703
|
+
def test_non_object_json_left_for_handler_to_report(self):
|
|
704
|
+
from omnimancer.core.models import ToolCall
|
|
705
|
+
|
|
706
|
+
tc = ToolCall(name="Glob", arguments="[1, 2, 3]")
|
|
707
|
+
assert tc.arguments == "[1, 2, 3]"
|
|
@@ -613,6 +613,103 @@ class TestOpenRouterProviderToolCalling:
|
|
|
613
613
|
assert tools[0]["function"]["name"] == "fetch_url"
|
|
614
614
|
|
|
615
615
|
|
|
616
|
+
class TestOpenRouterProviderToolResponseParsing:
|
|
617
|
+
"""Parsing of tool-call responses (OpenAI wire protocol)."""
|
|
618
|
+
|
|
619
|
+
def test_string_arguments_parsed_to_dict(
|
|
620
|
+
self, openrouter_provider, mock_tool_response
|
|
621
|
+
):
|
|
622
|
+
"""function.arguments arrives as a JSON string — it must reach the
|
|
623
|
+
tool handler as a dict, not crash it with 'str' has no 'get'."""
|
|
624
|
+
mock_response = MagicMock()
|
|
625
|
+
mock_response.status_code = 200
|
|
626
|
+
mock_response.json.return_value = mock_tool_response
|
|
627
|
+
|
|
628
|
+
response = openrouter_provider._handle_response_with_tools(mock_response)
|
|
629
|
+
|
|
630
|
+
assert response.tool_calls[0].arguments == {"url": "https://example.com"}
|
|
631
|
+
|
|
632
|
+
def test_tool_call_id_preserved_or_synthesized(self, openrouter_provider):
|
|
633
|
+
mock_response = MagicMock()
|
|
634
|
+
mock_response.status_code = 200
|
|
635
|
+
mock_response.json.return_value = {
|
|
636
|
+
"choices": [
|
|
637
|
+
{
|
|
638
|
+
"message": {
|
|
639
|
+
"role": "assistant",
|
|
640
|
+
"content": None,
|
|
641
|
+
"tool_calls": [
|
|
642
|
+
{
|
|
643
|
+
"id": "call_abc",
|
|
644
|
+
"type": "function",
|
|
645
|
+
"function": {"name": "Glob", "arguments": "{}"},
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
"type": "function",
|
|
649
|
+
"function": {"name": "Grep", "arguments": "{}"},
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
],
|
|
655
|
+
"model": "anthropic/claude-3.5-sonnet",
|
|
656
|
+
"usage": {"total_tokens": 10},
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
response = openrouter_provider._handle_response_with_tools(mock_response)
|
|
660
|
+
|
|
661
|
+
assert response.tool_calls[0].id == "call_abc"
|
|
662
|
+
assert response.tool_calls[1].id == "call_1"
|
|
663
|
+
|
|
664
|
+
def test_null_content_with_model_fallback_warning(self, openrouter_provider):
|
|
665
|
+
"""Tool-call responses carry content:null; combined with OpenRouter
|
|
666
|
+
routing to a different model this used to raise TypeError
|
|
667
|
+
('can only concatenate str (not "NoneType") to str')."""
|
|
668
|
+
mock_response = MagicMock()
|
|
669
|
+
mock_response.status_code = 200
|
|
670
|
+
mock_response.json.return_value = {
|
|
671
|
+
"choices": [
|
|
672
|
+
{
|
|
673
|
+
"message": {
|
|
674
|
+
"role": "assistant",
|
|
675
|
+
"content": None,
|
|
676
|
+
"tool_calls": [
|
|
677
|
+
{
|
|
678
|
+
"id": "call_1",
|
|
679
|
+
"type": "function",
|
|
680
|
+
"function": {
|
|
681
|
+
"name": "Glob",
|
|
682
|
+
"arguments": '{"pattern": "*.py"}',
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
],
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
],
|
|
689
|
+
"model": "some/other-model",
|
|
690
|
+
"usage": {"total_tokens": 12},
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
response = openrouter_provider._handle_response_with_tools(mock_response)
|
|
694
|
+
|
|
695
|
+
assert "Model Fallback Notice" in response.content
|
|
696
|
+
assert response.tool_calls[0].arguments == {"pattern": "*.py"}
|
|
697
|
+
|
|
698
|
+
def test_null_content_without_tools_fallback_warning(self, openrouter_provider):
|
|
699
|
+
mock_response = MagicMock()
|
|
700
|
+
mock_response.status_code = 200
|
|
701
|
+
mock_response.json.return_value = {
|
|
702
|
+
"choices": [{"message": {"role": "assistant", "content": None}}],
|
|
703
|
+
"model": "some/other-model",
|
|
704
|
+
"usage": {"total_tokens": 3},
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
response = openrouter_provider._handle_response(mock_response)
|
|
708
|
+
|
|
709
|
+
assert isinstance(response.content, str)
|
|
710
|
+
assert "Model Fallback Notice" in response.content
|
|
711
|
+
|
|
712
|
+
|
|
616
713
|
# ---------------------------------------------------------------------------
|
|
617
714
|
# TestOpenRouterProviderCredentialValidation
|
|
618
715
|
# ---------------------------------------------------------------------------
|
|
@@ -353,6 +353,21 @@ class TestToolHandlerExecution:
|
|
|
353
353
|
|
|
354
354
|
assert result.error == "Unknown tool: unknown"
|
|
355
355
|
|
|
356
|
+
@pytest.mark.asyncio
|
|
357
|
+
async def test_non_dict_arguments_return_corrective_error(
|
|
358
|
+
self, tool_handler, mock_agent_engine
|
|
359
|
+
):
|
|
360
|
+
"""A provider that ships unparseable arguments must yield a clear
|
|
361
|
+
error the model can act on — not "'str' object has no attribute
|
|
362
|
+
'get'" from inside the handler."""
|
|
363
|
+
tc = ToolCall(name="Glob", arguments="**/*.py")
|
|
364
|
+
result = await tool_handler.execute_tool_call(tc)
|
|
365
|
+
|
|
366
|
+
assert result.error is not None
|
|
367
|
+
assert "JSON object" in result.error
|
|
368
|
+
assert "attribute" not in result.error
|
|
369
|
+
mock_agent_engine.execute_with_approval.assert_not_called()
|
|
370
|
+
|
|
356
371
|
@pytest.mark.asyncio
|
|
357
372
|
async def test_execute_multiple_tool_calls(self, tool_handler, mock_agent_engine):
|
|
358
373
|
tool_calls = [
|
|
@@ -763,9 +763,11 @@ class TestXAIProviderResponseHandling:
|
|
|
763
763
|
assert response.tool_calls is not None
|
|
764
764
|
assert len(response.tool_calls) == 1
|
|
765
765
|
assert response.tool_calls[0].name == "web_search"
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
766
|
+
# The wire protocol sends arguments as a JSON string; the provider
|
|
767
|
+
# must deliver them as a dict.
|
|
768
|
+
assert response.tool_calls[0].arguments == {
|
|
769
|
+
"query": "latest AI developments 2024"
|
|
770
|
+
}
|
|
769
771
|
|
|
770
772
|
def test_handle_response_with_error_json(self, xai_provider):
|
|
771
773
|
"""Test handling response with JSON error message."""
|
|
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.15 → omnimancer_cli-0.2.19}/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.15 → omnimancer_cli-0.2.19}/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
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/approval_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/permission_controller.py
RENAMED
|
File without changes
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/omnimancer/core/security/permission_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/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
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/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.15 → omnimancer_cli-0.2.19}/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
|
{omnimancer_cli-0.2.15 → omnimancer_cli-0.2.19}/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
|