From ce19d93b4e65ced719cc6497c1b5235f8356032f Mon Sep 17 00:00:00 2001 From: Taimoor Date: Sat, 14 Feb 2026 13:46:19 -0500 Subject: [PATCH 1/2] feat: OpenAI-compatible tool_calls passthrough and model updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ToolCall/FunctionCall models for OpenAI-format tool_calls responses - Detect passthrough mode when callers send tools in requests - Enable Claude's built-in tools (Read, Bash, WebSearch, etc.) in passthrough mode - Translate Claude ToolUseBlock responses to OpenAI streaming tool_calls format - Handle `tool` role messages in message_adapter for multi-turn tool conversations - Fix system_prompt format to use SDK-expected structure (prevents crash) - Update default model to claude-opus-4-6 - Add openclaw_bridge.py for future MCP-based tool bridging - All 438 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .hypothesis/constants/085e16c86601bbe6 | 4 + .hypothesis/constants/090841bf7b1e878c | 4 + .hypothesis/constants/176fc1cccfb67bf2 | 4 + .hypothesis/constants/1b5c5c0364f2d5af | 4 + .hypothesis/constants/1c3d3f4a26f40abd | 4 + .hypothesis/constants/483b79bd85591992 | 4 + .hypothesis/constants/51e66ffb8306a103 | 4 + .hypothesis/constants/5ecb8d27c15539fb | 4 + .hypothesis/constants/61575172bb6be502 | 4 + .hypothesis/constants/67b0a8ccf18bf5d2 | 4 + .hypothesis/constants/7e8b22c9c355ef48 | 4 + .hypothesis/constants/92b66ce282ffc534 | 4 + .hypothesis/constants/9adb793441356481 | 4 + .hypothesis/constants/b557a9a709d4c7cf | 4 + .hypothesis/constants/d14c45ee4f738a0e | 4 + .hypothesis/constants/eeb6e6047832521f | 4 + .hypothesis/constants/f102fa85cdaff8e2 | 4 + .../unicode_data/15.0.0/charmap.json.gz | Bin 0 -> 21726 bytes .../unicode_data/15.0.0/codec-utf-8.json.gz | Bin 0 -> 60 bytes package-lock.json | 6 + poetry.lock | 54 +++- pyproject.toml | 1 + src/claude_cli.py | 16 +- src/constants.py | 32 +- src/main.py | 277 +++++++++++++++--- src/message_adapter.py | 38 ++- src/models.py | 63 +++- src/openclaw_bridge.py | 270 +++++++++++++++++ tests/test_claude_cli_unit.py | 2 +- tests/test_sdk_migration.py | 2 +- 30 files changed, 749 insertions(+), 80 deletions(-) create mode 100644 .hypothesis/constants/085e16c86601bbe6 create mode 100644 .hypothesis/constants/090841bf7b1e878c create mode 100644 .hypothesis/constants/176fc1cccfb67bf2 create mode 100644 .hypothesis/constants/1b5c5c0364f2d5af create mode 100644 .hypothesis/constants/1c3d3f4a26f40abd create mode 100644 .hypothesis/constants/483b79bd85591992 create mode 100644 .hypothesis/constants/51e66ffb8306a103 create mode 100644 .hypothesis/constants/5ecb8d27c15539fb create mode 100644 .hypothesis/constants/61575172bb6be502 create mode 100644 .hypothesis/constants/67b0a8ccf18bf5d2 create mode 100644 .hypothesis/constants/7e8b22c9c355ef48 create mode 100644 .hypothesis/constants/92b66ce282ffc534 create mode 100644 .hypothesis/constants/9adb793441356481 create mode 100644 .hypothesis/constants/b557a9a709d4c7cf create mode 100644 .hypothesis/constants/d14c45ee4f738a0e create mode 100644 .hypothesis/constants/eeb6e6047832521f create mode 100644 .hypothesis/constants/f102fa85cdaff8e2 create mode 100644 .hypothesis/unicode_data/15.0.0/charmap.json.gz create mode 100644 .hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz create mode 100644 package-lock.json create mode 100644 src/openclaw_bridge.py diff --git a/.hypothesis/constants/085e16c86601bbe6 b/.hypothesis/constants/085e16c86601bbe6 new file mode 100644 index 0000000..9213fb4 --- /dev/null +++ b/.hypothesis/constants/085e16c86601bbe6 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/__init__.py +# hypothesis_version: 6.148.8 + +['2.2.0'] \ No newline at end of file diff --git a/.hypothesis/constants/090841bf7b1e878c b/.hypothesis/constants/090841bf7b1e878c new file mode 100644 index 0000000..4939327 --- /dev/null +++ b/.hypothesis/constants/090841bf7b1e878c @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/message_adapter.py +# hypothesis_version: 6.148.8 + +[30000, '.*?', '.*?', 'Invoicing', '\\bBilling\\b', '\\bbilling\\b', '\\bpayment required\\b', '\\n\\s*\\n\\s*\\n', 'assistant', 'content', 'finish_reason', 'insufficient balance', 'invoicing', 'model', 'payment needed', 'role', 'stop', 'system', 'user'] \ No newline at end of file diff --git a/.hypothesis/constants/176fc1cccfb67bf2 b/.hypothesis/constants/176fc1cccfb67bf2 new file mode 100644 index 0000000..977b385 --- /dev/null +++ b/.hypothesis/constants/176fc1cccfb67bf2 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/main.py +# hypothesis_version: 6.148.8 + +[30.0, 400, 404, 413, 422, 429, 500, 503, 1000, 1024, 8000, 100000, ' Example usage:', ' -> ', '#22c55e', '#ef4444', '*', '-_', '/', '/health', '/v1/', '/v1/auth/status', '/v1/chat/completions', '/v1/compatibility', '/v1/debug/request', '/v1/mcp/connect', '/v1/mcp/disconnect', '/v1/mcp/servers', '/v1/mcp/stats', '/v1/messages', '/v1/models', '/v1/sessions', '/v1/sessions/stats', '/v1/tools', '/v1/tools/config', '/v1/tools/stats', '/version', '0.0.0.0', '1', '1.0.0', '127.0.0.1', '600000', '8000', '=', 'API_KEY', 'CLAUDE_CWD', 'CLAUDE_WRAPPER_HOST', 'CORS_ORIGINS', 'Cache-Control', 'Connected', 'Connection', 'DEBUG_MODE', 'Hello, world!', 'MAX_REQUEST_SIZE', 'MAX_TIMEOUT', 'Not Connected', 'PORT', 'POST', 'Session not found', 'Unknown error', 'VERBOSE', 'X-Claude-Max-Turns', 'X-Request-ID', '["*"]', '__main__', 'allowed_tools', 'anthropic', 'api_error', 'api_key_required', 'api_key_source', 'api_version', 'arguments', 'assistant', 'auth', 'bypassPermissions', 'chat', 'choices', 'claude_code_auth', 'code', 'common_issues', 'compatibility_report', 'completion_tokens', 'content', 'content-length', 'custom_headers', 'cwd', 'data', 'data: [DONE]\n\n', 'debug', 'debug_info', 'debug_mode_enabled', 'debug_tip', 'default_ttl_hours', 'details', 'disallowed_tools', 'end_turn', 'environment', 'error', 'errors', 'false', 'field', 'finish_reason', 'function', 'general', 'headers', 'health', 'healthy', 'help', 'id', 'index', 'input', 'json_parse_error', 'keep-alive', 'list', 'loc', 'max_thinking_tokens', 'max_turns', 'message', 'messages', 'method', 'model', 'msg', 'n', 'name', 'no', 'no-cache', 'none', 'object', 'on', 'owned_by', 'parsed_body', 'permission_mode', 'prompt_tokens', 'prompts', 'raw_body', 'raw_request_body', 'request_id', 'request_too_large', 'resources', 'resume', 'role', 'runtime', 'server_info', 'service', 'session_stats', 'status', 'stop', 'stream', 'streaming_error', 'supported', 'system_prompt', 'text', 'text/event-stream', 'tool_calls', 'tool_use', 'tools', 'total_tokens', 'true', 'type', 'unknown', 'url', 'usage', 'user', 'v1', 'valid', 'validated_data', 'validation_error', 'validation_result', 'version', 'y', 'yes', '🔑 API Key Generated!'] \ No newline at end of file diff --git a/.hypothesis/constants/1b5c5c0364f2d5af b/.hypothesis/constants/1b5c5c0364f2d5af new file mode 100644 index 0000000..fe78fa2 --- /dev/null +++ b/.hypothesis/constants/1b5c5c0364f2d5af @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/parameter_validator.py +# hypothesis_version: 6.148.8 + +[1.0, 100, 50000, ',', 'acceptEdits', 'allowed_tools', 'bypassPermissions', 'default', 'disallowed_tools', 'frequency_penalty', 'logit_bias', 'max_thinking_tokens', 'max_tokens', 'max_turns', 'messages', 'model', 'n', 'permission_mode', 'plan', 'presence_penalty', 'stop', 'stream', 'suggestions', 'supported_parameters', 'temperature', 'top_p', 'user (for logging)', 'warnings', 'x-claude-max-turns'] \ No newline at end of file diff --git a/.hypothesis/constants/1c3d3f4a26f40abd b/.hypothesis/constants/1c3d3f4a26f40abd new file mode 100644 index 0000000..5fd0f6e --- /dev/null +++ b/.hypothesis/constants/1c3d3f4a26f40abd @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/main.py +# hypothesis_version: 6.148.8 + +[30.0, 400, 404, 413, 422, 429, 500, 503, 1000, 1024, 8000, 100000, ' Example usage:', ' -> ', '#22c55e', '#ef4444', '*', '-_', '/', '/health', '/v1/', '/v1/auth/status', '/v1/chat/completions', '/v1/compatibility', '/v1/debug/request', '/v1/mcp/connect', '/v1/mcp/disconnect', '/v1/mcp/servers', '/v1/mcp/stats', '/v1/messages', '/v1/models', '/v1/sessions', '/v1/sessions/stats', '/v1/tools', '/v1/tools/config', '/v1/tools/stats', '/version', '0.0.0.0', '1', '1.0.0', '127.0.0.1', '600000', '8000', '=', 'API_KEY', 'CLAUDE_CWD', 'CLAUDE_WRAPPER_HOST', 'CORS_ORIGINS', 'Cache-Control', 'Connected', 'Connection', 'DEBUG_MODE', 'Hello, world!', 'MAX_REQUEST_SIZE', 'MAX_TIMEOUT', 'Not Connected', 'PORT', 'POST', 'Session not found', 'Unknown error', 'VERBOSE', 'X-Claude-Max-Turns', 'X-Request-ID', '["*"]', '__main__', 'allowed_tools', 'anthropic', 'api_error', 'api_key_required', 'api_key_source', 'api_version', 'arguments', 'assistant', 'auth', 'bypassPermissions', 'chat', 'choices', 'claude_code_auth', 'code', 'common_issues', 'compatibility_report', 'completion_tokens', 'content', 'content-length', 'custom_headers', 'cwd', 'data', 'data: [DONE]\n\n', 'debug', 'debug_info', 'debug_mode_enabled', 'debug_tip', 'default_ttl_hours', 'details', 'disallowed_tools', 'end_turn', 'environment', 'error', 'errors', 'false', 'field', 'finish_reason', 'function', 'general', 'headers', 'health', 'healthy', 'help', 'id', 'index', 'input', 'json_parse_error', 'keep-alive', 'list', 'loc', 'max_thinking_tokens', 'max_turns', 'message', 'messages', 'method', 'model', 'msg', 'n', 'name', 'no', 'no-cache', 'none', 'object', 'on', 'openclaw_tools', 'owned_by', 'parsed_body', 'permission_mode', 'prompt_tokens', 'prompts', 'raw_body', 'raw_request_body', 'request_id', 'request_too_large', 'resources', 'resume', 'role', 'runtime', 'server_info', 'service', 'session_stats', 'status', 'stop', 'stream', 'streaming_error', 'supported', 'system_prompt', 'text', 'text/event-stream', 'tool_calls', 'tool_use', 'tools', 'total_tokens', 'true', 'type', 'unknown', 'url', 'usage', 'user', 'v1', 'valid', 'validated_data', 'validation_error', 'validation_result', 'version', 'y', 'yes', '🔑 API Key Generated!'] \ No newline at end of file diff --git a/.hypothesis/constants/483b79bd85591992 b/.hypothesis/constants/483b79bd85591992 new file mode 100644 index 0000000..977b385 --- /dev/null +++ b/.hypothesis/constants/483b79bd85591992 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/main.py +# hypothesis_version: 6.148.8 + +[30.0, 400, 404, 413, 422, 429, 500, 503, 1000, 1024, 8000, 100000, ' Example usage:', ' -> ', '#22c55e', '#ef4444', '*', '-_', '/', '/health', '/v1/', '/v1/auth/status', '/v1/chat/completions', '/v1/compatibility', '/v1/debug/request', '/v1/mcp/connect', '/v1/mcp/disconnect', '/v1/mcp/servers', '/v1/mcp/stats', '/v1/messages', '/v1/models', '/v1/sessions', '/v1/sessions/stats', '/v1/tools', '/v1/tools/config', '/v1/tools/stats', '/version', '0.0.0.0', '1', '1.0.0', '127.0.0.1', '600000', '8000', '=', 'API_KEY', 'CLAUDE_CWD', 'CLAUDE_WRAPPER_HOST', 'CORS_ORIGINS', 'Cache-Control', 'Connected', 'Connection', 'DEBUG_MODE', 'Hello, world!', 'MAX_REQUEST_SIZE', 'MAX_TIMEOUT', 'Not Connected', 'PORT', 'POST', 'Session not found', 'Unknown error', 'VERBOSE', 'X-Claude-Max-Turns', 'X-Request-ID', '["*"]', '__main__', 'allowed_tools', 'anthropic', 'api_error', 'api_key_required', 'api_key_source', 'api_version', 'arguments', 'assistant', 'auth', 'bypassPermissions', 'chat', 'choices', 'claude_code_auth', 'code', 'common_issues', 'compatibility_report', 'completion_tokens', 'content', 'content-length', 'custom_headers', 'cwd', 'data', 'data: [DONE]\n\n', 'debug', 'debug_info', 'debug_mode_enabled', 'debug_tip', 'default_ttl_hours', 'details', 'disallowed_tools', 'end_turn', 'environment', 'error', 'errors', 'false', 'field', 'finish_reason', 'function', 'general', 'headers', 'health', 'healthy', 'help', 'id', 'index', 'input', 'json_parse_error', 'keep-alive', 'list', 'loc', 'max_thinking_tokens', 'max_turns', 'message', 'messages', 'method', 'model', 'msg', 'n', 'name', 'no', 'no-cache', 'none', 'object', 'on', 'owned_by', 'parsed_body', 'permission_mode', 'prompt_tokens', 'prompts', 'raw_body', 'raw_request_body', 'request_id', 'request_too_large', 'resources', 'resume', 'role', 'runtime', 'server_info', 'service', 'session_stats', 'status', 'stop', 'stream', 'streaming_error', 'supported', 'system_prompt', 'text', 'text/event-stream', 'tool_calls', 'tool_use', 'tools', 'total_tokens', 'true', 'type', 'unknown', 'url', 'usage', 'user', 'v1', 'valid', 'validated_data', 'validation_error', 'validation_result', 'version', 'y', 'yes', '🔑 API Key Generated!'] \ No newline at end of file diff --git a/.hypothesis/constants/51e66ffb8306a103 b/.hypothesis/constants/51e66ffb8306a103 new file mode 100644 index 0000000..819ebd7 --- /dev/null +++ b/.hypothesis/constants/51e66ffb8306a103 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/models.py +# hypothesis_version: 6.148.8 + +[0.3, 0.5, 0.7, 0.9, 1.0, 1.5, 100, 200, 500, 4096, '-_.', 'System prompt', 'after', 'allow', 'assistant', 'chat.completion', 'command', 'content_filter', 'developer', 'end_turn', 'function', 'ignore', 'length', 'max_thinking_tokens', 'max_tokens', 'message', 'model', 'n', 'name', 'null', 'server_name', 'stop', 'stop_sequence', 'system', 'text', 'tool', 'tool_calls', 'tool_name', 'type', 'user'] \ No newline at end of file diff --git a/.hypothesis/constants/5ecb8d27c15539fb b/.hypothesis/constants/5ecb8d27c15539fb new file mode 100644 index 0000000..3072723 --- /dev/null +++ b/.hypothesis/constants/5ecb8d27c15539fb @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/rate_limiter.py +# hypothesis_version: 6.148.8 + +[429, '/', '1', '10/minute', '15/minute', '2/minute', '30', '30/minute', 'RATE_LIMIT_ENABLED', 'Retry-After', 'auth', 'chat', 'code', 'debug', 'error', 'general', 'health', 'message', 'on', 'rate_limit_exceeded', 'retry_after', 'session', 'too_many_requests', 'true', 'type', 'yes'] \ No newline at end of file diff --git a/.hypothesis/constants/61575172bb6be502 b/.hypothesis/constants/61575172bb6be502 new file mode 100644 index 0000000..ef7ff63 --- /dev/null +++ b/.hypothesis/constants/61575172bb6be502 @@ -0,0 +1,4 @@ +# file: /home/tekron/.cache/pypoetry/virtualenvs/claude-code-openai-wrapper-kCeJR7gw-py3.12/bin/pytest +# hypothesis_version: 6.148.8 + +['__main__'] \ No newline at end of file diff --git a/.hypothesis/constants/67b0a8ccf18bf5d2 b/.hypothesis/constants/67b0a8ccf18bf5d2 new file mode 100644 index 0000000..924b9cb --- /dev/null +++ b/.hypothesis/constants/67b0a8ccf18bf5d2 @@ -0,0 +1,4 @@ +# file: /usr/lib/python3.12/sitecustomize.py +# hypothesis_version: 6.148.8 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/7e8b22c9c355ef48 b/.hypothesis/constants/7e8b22c9c355ef48 new file mode 100644 index 0000000..b761116 --- /dev/null +++ b/.hypothesis/constants/7e8b22c9c355ef48 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/claude_cli.py +# hypothesis_version: 6.148.8 + +[0.0, 1000, 600000, 'Hello', '_', '__dict__', 'assistant', 'claude_code', 'completion_tokens', 'content', 'data', 'duration_ms', 'error_message', 'init', 'is_error', 'message', 'model', 'num_turns', 'preset', 'prompt_tokens', 'result', 'session_id', 'subtype', 'success', 'system', 'text', 'total_cost_usd', 'total_tokens', 'type'] \ No newline at end of file diff --git a/.hypothesis/constants/92b66ce282ffc534 b/.hypothesis/constants/92b66ce282ffc534 new file mode 100644 index 0000000..44ab1ed --- /dev/null +++ b/.hypothesis/constants/92b66ce282ffc534 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/openclaw_bridge.py +# hypothesis_version: 6.148.8 + +['1.0.0', 'arguments', 'assistant', 'choices', 'content', 'delta', 'description', 'finish_reason', 'function', 'id', 'index', 'input', 'message', 'model', 'name', 'object', 'openclaw_tools', 'parameters', 'properties', 'result', 'subtype', 'success', 'text', 'tool_calls', 'tool_use', 'type'] \ No newline at end of file diff --git a/.hypothesis/constants/9adb793441356481 b/.hypothesis/constants/9adb793441356481 new file mode 100644 index 0000000..b8d9e81 --- /dev/null +++ b/.hypothesis/constants/9adb793441356481 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/tool_manager.py +# hypothesis_version: 6.148.8 + +['Bash', 'BashOutput', 'Create a new file', 'Delete notebook cell', 'Edit', 'Execute git status', 'Find TODO comments', 'Glob', 'Grep', 'ID of cell to edit', 'KillShell', 'New cell content', 'NotebookEdit', 'Path to .ipynb file', 'Read', 'Read blog post', 'Read entire file', 'Read images and PDFs', 'Rename a variable', 'Replacement text', 'Run npm install', 'Search query', 'Skill', 'SlashCommand', 'Task', 'Text to replace', 'TodoWrite', 'WebFetch', 'WebSearch', 'Write', 'agent', 'allowed_domains', 'bash_id', 'blocked_domains', 'cell_id', 'cell_type', 'code or markdown', 'command', 'content', 'description', 'edit_mode', 'file', 'file_path', 'filter', 'glob', 'global_allowed', 'global_disallowed', 'limit', 'new_source', 'new_string', 'notebook_path', 'offset', 'old_string', 'output_mode', 'path', 'pattern', 'productivity', 'prompt', 'query', 'replace_all', 'run_in_background', 'session_configs', 'shell_id', 'subagent_type', 'system', 'timeout', 'todos', 'tool_categories', 'total_tools', 'url', 'web'] \ No newline at end of file diff --git a/.hypothesis/constants/b557a9a709d4c7cf b/.hypothesis/constants/b557a9a709d4c7cf new file mode 100644 index 0000000..6ab5624 --- /dev/null +++ b/.hypothesis/constants/b557a9a709d4c7cf @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/auth.py +# hypothesis_version: 6.148.8 + +[401, '1', 'ANTHROPIC_API_KEY', 'API_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_DEFAULT_REGION', 'AWS_REGION', 'Bearer', 'CLAUDE_AUTH_METHOD', 'CLOUD_ML_REGION', 'Invalid API key', 'Missing API key', 'WWW-Authenticate', 'anthropic', 'api_key', 'api_key_length', 'api_key_present', 'aws_region', 'bedrock', 'claude_cli', 'cli', 'config', 'errors', 'method', 'note', 'project_id', 'region', 'runtime_api_key', 'status', 'valid', 'vertex'] \ No newline at end of file diff --git a/.hypothesis/constants/d14c45ee4f738a0e b/.hypothesis/constants/d14c45ee4f738a0e new file mode 100644 index 0000000..f611206 --- /dev/null +++ b/.hypothesis/constants/d14c45ee4f738a0e @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/session_manager.py +# hypothesis_version: 6.148.8 + +['active_sessions', 'expired_sessions', 'total_messages'] \ No newline at end of file diff --git a/.hypothesis/constants/eeb6e6047832521f b/.hypothesis/constants/eeb6e6047832521f new file mode 100644 index 0000000..178cd77 --- /dev/null +++ b/.hypothesis/constants/eeb6e6047832521f @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/constants.py +# hypothesis_version: 6.148.8 + +[100, 200, 8000, 600000, 'Bash', 'BashOutput', 'DEFAULT_MODEL', 'Edit', 'Glob', 'Grep', 'KillShell', 'NotebookEdit', 'Read', 'Skill', 'SlashCommand', 'Task', 'TodoWrite', 'WebFetch', 'WebSearch', 'Write', 'claude-opus-4-6', 'claude_code', 'preset', 'text'] \ No newline at end of file diff --git a/.hypothesis/constants/f102fa85cdaff8e2 b/.hypothesis/constants/f102fa85cdaff8e2 new file mode 100644 index 0000000..c2cb86f --- /dev/null +++ b/.hypothesis/constants/f102fa85cdaff8e2 @@ -0,0 +1,4 @@ +# file: /home/tekron/claude-code-openai-wrapper/src/mcp_client.py +# hypothesis_version: 6.148.8 + +['arguments', 'connected', 'connected_servers', 'description', 'enabled', 'inputSchema', 'input_schema', 'mcp_available', 'mimeType', 'name', 'prompts', 'registered_servers', 'resources', 'servers', 'tools', 'total_prompts', 'total_resources', 'total_tools', 'uri'] \ No newline at end of file diff --git a/.hypothesis/unicode_data/15.0.0/charmap.json.gz b/.hypothesis/unicode_data/15.0.0/charmap.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..b8229995c10399be329c5155621cfaa20e021e81 GIT binary patch literal 21726 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 literal 0 HcmV?d00001 diff --git a/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz b/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c5f5cfef3f9eba3535b03226d0b5cf1b92199a30 GIT binary patch literal 60 zcmb2|=HOstU|?YSUy@r;S(uk;W>R3x(A{TtA<)3obdyQ%)xZujQ*$%Ji-86vCMJxJ PZbUHDMd`1Q1L^?)HsKQ9 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0dcf105 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "claude-code-openai-wrapper", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/poetry.lock b/poetry.lock index 03d8e92..66d585e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -12,6 +12,33 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "anthropic" +version = "0.79.0" +description = "The official Python library for the anthropic API" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anthropic-0.79.0-py3-none-any.whl", hash = "sha256:04cbd473b6bbda4ca2e41dd670fe2f829a911530f01697d0a1e37321eb75f3cf"}, + {file = "anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +docstring-parser = ">=0.15,<1" +httpx = ">=0.25.0,<1" +jiter = ">=0.4.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +typing-extensions = ">=4.10,<5" + +[package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +vertex = ["google-auth[requests] (>=2,<3)"] + [[package]] name = "anyio" version = "4.9.0" @@ -665,12 +692,29 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708"}, + {file = "docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912"}, +] + +[package.extras] +dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"] +docs = ["pydoctor (>=25.4.0)"] +test = ["pytest"] + [[package]] name = "dparse" version = "0.6.4" @@ -958,7 +1002,7 @@ version = "0.10.0" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303"}, {file = "jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e"}, @@ -1065,7 +1109,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -3053,4 +3097,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "995cbb6b6bfbf14612eff7e0690ca47fc7b0c01fd2ef3351dea01d6940be0ed6" +content-hash = "a8afd4e405e3cc48eb3448318558896957c3719239b8839606eb855788e97d11" diff --git a/pyproject.toml b/pyproject.toml index e0cc381..49877ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ sse-starlette = "^2.1.3" python-multipart = "^0.0.18" claude-agent-sdk = "^0.1.18" slowapi = "^0.1.9" +anthropic = "^0.79.0" [tool.poetry.group.dev.dependencies] black = "^24.0.0" diff --git a/src/claude_cli.py b/src/claude_cli.py index d87057e..0d63331 100644 --- a/src/claude_cli.py +++ b/src/claude_cli.py @@ -104,6 +104,7 @@ async def run_completion( session_id: Optional[str] = None, continue_session: bool = False, permission_mode: Optional[str] = None, + mcp_servers: Optional[Dict[str, Any]] = None, ) -> AsyncGenerator[Dict[str, Any], None]: """Run Claude Agent using the Python SDK and yield response chunks.""" @@ -123,13 +124,14 @@ async def run_completion( if model: options.model = model - # Set system prompt - CLAUDE AGENT SDK STRUCTURED FORMAT - # Use structured format as per SDK documentation + # Set system prompt + # SDK's _build_command only handles: None, str, or {"type": "preset", "append": "..."} + # Passing a plain string ensures --system-prompt flag is added to CLI command if system_prompt: - options.system_prompt = {"type": "text", "text": system_prompt} + options.system_prompt = system_prompt else: - # Use Claude Code preset to maintain expected behavior - options.system_prompt = {"type": "preset", "preset": "claude_code"} + # No custom prompt - let Claude Code use its default behavior + options.system_prompt = None # Set tool restrictions if allowed_tools: @@ -141,6 +143,10 @@ async def run_completion( if permission_mode: options.permission_mode = permission_mode + # Set MCP servers (for OpenClaw tool bridge) + if mcp_servers: + options.mcp_servers = mcp_servers + # Handle session continuity if continue_session: options.continue_session = True diff --git a/src/constants.py b/src/constants.py index 5fb452b..b2018f4 100644 --- a/src/constants.py +++ b/src/constants.py @@ -58,6 +58,21 @@ async def chat_endpoint(): ... "Edit", ] +# Tools to allow in passthrough mode (agent frameworks like OpenClaw) +# Broader set including network tools needed for agent functionality +PASSTHROUGH_ALLOWED_TOOLS = [ + "Read", + "Glob", + "Grep", + "Bash", + "Write", + "Edit", + "WebFetch", + "WebSearch", + "NotebookEdit", + "Task", +] + # Tools to disallow by default (potentially dangerous or slow) DEFAULT_DISALLOWED_TOOLS = [ "Task", # Can spawn sub-agents @@ -69,26 +84,21 @@ async def chat_endpoint(): ... # Models supported by Claude Agent SDK (as of November 2025) # NOTE: Claude Agent SDK only supports Claude 4+ models, not Claude 3.x CLAUDE_MODELS = [ - # Claude 4.5 Family (Latest - Fall 2025) - RECOMMENDED - "claude-opus-4-5-20250929", # Latest Opus 4.5 - Most capable - "claude-sonnet-4-5-20250929", # Recommended - best coding model + # Claude 4.6 (Latest - February 2026) + "claude-opus-4-6", # Latest Opus 4.6 - Most capable + # Claude 4.5 Family (Fall 2025) + "claude-sonnet-4-5-20250929", # Best coding model "claude-haiku-4-5-20251001", # Fast & cheap # Claude 4.1 - "claude-opus-4-1-20250805", # Upgraded Opus 4 + "claude-opus-4-1-20250805", # Claude 4.0 Family (Original - May 2025) "claude-opus-4-20250514", "claude-sonnet-4-20250514", - # Claude 3.x Family - NOT SUPPORTED by Claude Agent SDK - # These models work with Anthropic API but NOT with Claude Code - # Uncomment only if using direct Anthropic API (not Claude Agent SDK) - # "claude-3-7-sonnet-20250219", - # "claude-3-5-sonnet-20241022", - # "claude-3-5-haiku-20241022", ] # Default model (recommended for most use cases) # Can be overridden via DEFAULT_MODEL environment variable -DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "claude-sonnet-4-5-20250929") +DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "claude-opus-4-6") # Fast model (for speed/cost optimization) FAST_MODEL = "claude-haiku-4-5-20251001" diff --git a/src/main.py b/src/main.py index 4a74aa4..2e5d937 100644 --- a/src/main.py +++ b/src/main.py @@ -24,6 +24,8 @@ Message, Usage, StreamChoice, + FunctionCall, + ToolCall, SessionListResponse, ToolListResponse, ToolMetadataResponse, @@ -46,12 +48,14 @@ from src.session_manager import session_manager from src.tool_manager import tool_manager from src.mcp_client import mcp_client, MCPServerConfig +# OpenClaw bridge available for future MCP-based tool passthrough +# from src.openclaw_bridge import openai_tools_to_mcp_server from src.rate_limiter import ( limiter, rate_limit_exceeded_handler, rate_limit_endpoint, ) -from src.constants import CLAUDE_MODELS, CLAUDE_TOOLS, DEFAULT_ALLOWED_TOOLS +from src.constants import CLAUDE_MODELS, CLAUDE_TOOLS, DEFAULT_ALLOWED_TOOLS, PASSTHROUGH_ALLOWED_TOOLS # Load environment variables load_dotenv() @@ -410,10 +414,14 @@ async def generate_streaming_response( system_prompt = sampling_instructions logger.debug(f"Added sampling instructions: {sampling_instructions}") - # Filter content for unsupported features - prompt = MessageAdapter.filter_content(prompt) - if system_prompt: - system_prompt = MessageAdapter.filter_content(system_prompt) + # Detect passthrough mode: when caller sends tools (e.g. OpenClaw agent framework) + passthrough_mode = bool(request.tools) + + # Filter content for unsupported features (skip in passthrough mode) + if not passthrough_mode: + prompt = MessageAdapter.filter_content(prompt) + if system_prompt: + system_prompt = MessageAdapter.filter_content(system_prompt) # Get Claude Agent SDK options from request claude_options = request.to_claude_options() @@ -426,23 +434,39 @@ async def generate_streaming_response( if claude_options.get("model"): ParameterValidator.validate_model(claude_options["model"]) - # Handle tools - disabled by default for OpenAI compatibility - if not request.enable_tools: - # Disable all tools by using CLAUDE_TOOLS constant - claude_options["disallowed_tools"] = CLAUDE_TOOLS - claude_options["max_turns"] = 1 # Single turn for Q&A - logger.info("Tools disabled (default behavior for OpenAI compatibility)") - else: + # Handle tools based on mode + if passthrough_mode: + # Passthrough mode: caller sent tools (e.g. OpenClaw agent framework). + # Enable Claude's built-in tools so it can fulfill requests using Read, Bash, etc. + # The caller's tool definitions are embedded in the system prompt text by the + # agent framework, so Claude sees them and outputs text-based tool invocations + # that the framework parses on its side. + claude_options["allowed_tools"] = PASSTHROUGH_ALLOWED_TOOLS + claude_options["permission_mode"] = "bypassPermissions" + # Allow many turns so the model can complete complex multi-step tasks + claude_options["max_turns"] = 50 + logger.info( + f"Passthrough mode: {len(PASSTHROUGH_ALLOWED_TOOLS)} built-in tools enabled, " + f"{len(request.tools)} caller tools detected (handled via system prompt)" + ) + elif request.enable_tools: # Enable tools - use default safe subset (Read, Glob, Grep, Bash, Write, Edit) claude_options["allowed_tools"] = DEFAULT_ALLOWED_TOOLS # Set permission mode to bypass prompts (required for API/headless usage) claude_options["permission_mode"] = "bypassPermissions" logger.info(f"Tools enabled by user request: {DEFAULT_ALLOWED_TOOLS}") + else: + # Disable all tools by using CLAUDE_TOOLS constant + claude_options["disallowed_tools"] = CLAUDE_TOOLS + claude_options["max_turns"] = 1 # Single turn for Q&A + logger.info("Tools disabled (default behavior for OpenAI compatibility)") # Run Claude Code chunks_buffer = [] role_sent = False # Track if we've sent the initial role chunk content_sent = False # Track if we've sent any content + tool_calls_collected = [] # Collect tool_use blocks for passthrough + tool_call_index = 0 # Track tool call index for streaming deltas async for chunk in claude_cli.run_completion( prompt=prompt, @@ -488,6 +512,75 @@ async def generate_streaming_response( # Handle content blocks if isinstance(content, list): for block in content: + # === TOOL CALL PASSTHROUGH === + # Detect ToolUseBlock from Claude Agent SDK and convert to + # OpenAI tool_calls format for agent framework passthrough + block_type = getattr(block, "type", None) or ( + block.get("type") if isinstance(block, dict) else None + ) + + if block_type == "tool_use" and passthrough_mode: + # Extract tool_use data from either object or dict format + if hasattr(block, "id"): + tc_id = block.id + tc_name = block.name + tc_input = block.input + else: + tc_id = block.get("id", f"call_{uuid.uuid4().hex[:24]}") + tc_name = block.get("name", "") + tc_input = block.get("input", {}) + + # Strip MCP namespace prefix if present + # Claude returns "mcp__openclaw_tools__cron" but caller expects "cron" + mcp_prefix = "mcp__openclaw_tools__" + if tc_name.startswith(mcp_prefix): + tc_name = tc_name[len(mcp_prefix):] + + # Only passthrough tool calls for external (caller) tools + # Skip Claude's built-in tool calls (Read, Bash, etc.) + if external_tool_names and tc_name not in external_tool_names: + logger.debug(f"Skipping built-in tool call: {tc_name}") + continue + + tc_args = json.dumps(tc_input) if isinstance(tc_input, dict) else str(tc_input) + + # Collect for finish_reason decision + tool_calls_collected.append({ + "id": tc_id, + "name": tc_name, + "arguments": tc_args, + }) + + # Emit OpenAI-format tool_calls delta + # First chunk: includes function name and id + tc_delta = { + "tool_calls": [{ + "index": tool_call_index, + "id": tc_id, + "type": "function", + "function": { + "name": tc_name, + "arguments": tc_args, + }, + }] + } + tc_chunk = ChatCompletionStreamResponse( + id=request_id, + model=request.model, + choices=[ + StreamChoice( + index=0, + delta=tc_delta, + finish_reason=None, + ) + ], + ) + yield f"data: {tc_chunk.model_dump_json()}\n\n" + tool_call_index += 1 + content_sent = True + logger.info(f"Passthrough tool_call emitted: {tc_name}({tc_args[:100]}...)") + continue + # Handle TextBlock objects from Claude Agent SDK if hasattr(block, "text"): raw_text = block.text @@ -497,8 +590,11 @@ async def generate_streaming_response( else: continue - # Filter out tool usage and thinking blocks - filtered_text = MessageAdapter.filter_content(raw_text) + # Filter out tool usage and thinking blocks (skip in passthrough) + if passthrough_mode: + filtered_text = raw_text + else: + filtered_text = MessageAdapter.filter_content(raw_text) if filtered_text and not filtered_text.isspace(): # Create streaming chunk @@ -518,8 +614,11 @@ async def generate_streaming_response( content_sent = True elif isinstance(content, str): - # Filter out tool usage and thinking blocks - filtered_content = MessageAdapter.filter_content(content) + # Filter out tool usage and thinking blocks (skip in passthrough) + if passthrough_mode: + filtered_content = content + else: + filtered_content = MessageAdapter.filter_content(content) if filtered_content and not filtered_content.isspace(): # Create streaming chunk @@ -551,20 +650,31 @@ async def generate_streaming_response( yield f"data: {initial_chunk.model_dump_json()}\n\n" role_sent = True - # If we sent role but no content, send a minimal response + # If we sent role but no content (and no tool calls), try to extract from parse_claude_message + # This can happen when the SDK returns only tool_use blocks with no text if role_sent and not content_sent: + # Try to extract any text from the collected chunks + fallback_text = None + if chunks_buffer: + fallback_text = claude_cli.parse_claude_message(chunks_buffer) + + if not fallback_text: + fallback_text = "I completed my internal processing but didn't produce a text response. Please try rephrasing your request or asking me to explain what I found." + logger.warning("Fallback response triggered: SDK returned no text content blocks") + fallback_chunk = ChatCompletionStreamResponse( id=request_id, model=request.model, choices=[ StreamChoice( index=0, - delta={"content": "I'm unable to provide a response at the moment."}, + delta={"content": fallback_text}, finish_reason=None, ) ], ) yield f"data: {fallback_chunk.model_dump_json()}\n\n" + content_sent = True # Extract assistant response from all chunks assistant_content = None @@ -589,11 +699,14 @@ async def generate_streaming_response( ) logger.debug(f"Estimated usage: {usage_data}") + # Determine finish_reason: "tool_calls" if we emitted tool calls, "stop" otherwise + finish_reason = "tool_calls" if tool_calls_collected else "stop" + # Send final chunk with finish reason and optionally usage data final_chunk = ChatCompletionStreamResponse( id=request_id, model=request.model, - choices=[StreamChoice(index=0, delta={}, finish_reason="stop")], + choices=[StreamChoice(index=0, delta={}, finish_reason=finish_reason)], usage=usage_data, ) yield f"data: {final_chunk.model_dump_json()}\n\n" @@ -672,10 +785,14 @@ async def chat_completions( system_prompt = sampling_instructions logger.debug(f"Added sampling instructions: {sampling_instructions}") - # Filter content - prompt = MessageAdapter.filter_content(prompt) - if system_prompt: - system_prompt = MessageAdapter.filter_content(system_prompt) + # Detect passthrough mode + passthrough_mode = bool(request_body.tools) + + # Filter content (skip in passthrough mode) + if not passthrough_mode: + prompt = MessageAdapter.filter_content(prompt) + if system_prompt: + system_prompt = MessageAdapter.filter_content(system_prompt) # Get Claude Agent SDK options from request claude_options = request_body.to_claude_options() @@ -688,18 +805,29 @@ async def chat_completions( if claude_options.get("model"): ParameterValidator.validate_model(claude_options["model"]) - # Handle tools - disabled by default for OpenAI compatibility - if not request_body.enable_tools: - # Disable all tools by using CLAUDE_TOOLS constant - claude_options["disallowed_tools"] = CLAUDE_TOOLS - claude_options["max_turns"] = 1 # Single turn for Q&A - logger.info("Tools disabled (default behavior for OpenAI compatibility)") - else: + # Handle tools based on mode + if passthrough_mode: + # Passthrough mode: caller sent tools (e.g. OpenClaw agent framework). + # Enable Claude's built-in tools. Caller's tool definitions are in the + # system prompt text, so Claude outputs text-based invocations for them. + claude_options["allowed_tools"] = PASSTHROUGH_ALLOWED_TOOLS + claude_options["permission_mode"] = "bypassPermissions" + claude_options["max_turns"] = 50 + logger.info( + f"Passthrough mode (non-streaming): {len(PASSTHROUGH_ALLOWED_TOOLS)} built-in tools enabled, " + f"{len(request_body.tools)} caller tools detected (handled via system prompt)" + ) + elif request_body.enable_tools: # Enable tools - use default safe subset (Read, Glob, Grep, Bash, Write, Edit) claude_options["allowed_tools"] = DEFAULT_ALLOWED_TOOLS # Set permission mode to bypass prompts (required for API/headless usage) claude_options["permission_mode"] = "bypassPermissions" logger.info(f"Tools enabled by user request: {DEFAULT_ALLOWED_TOOLS}") + else: + # Disable all tools by using CLAUDE_TOOLS constant + claude_options["disallowed_tools"] = CLAUDE_TOOLS + claude_options["max_turns"] = 1 # Single turn for Q&A + logger.info("Tools disabled (default behavior for OpenAI compatibility)") # Collect all chunks chunks = [] @@ -715,14 +843,55 @@ async def chat_completions( ): chunks.append(chunk) - # Extract assistant message + # Extract assistant message text raw_assistant_content = claude_cli.parse_claude_message(chunks) - if not raw_assistant_content: + # Extract tool_use blocks from chunks for passthrough mode + # Note: In the current architecture, Claude's built-in tool calls are + # handled internally by the SDK. This extraction catches any ToolUseBlock + # that surfaces in the response (future MCP bridge support). + tool_calls_list = [] + if passthrough_mode: + for chunk in chunks: + content = None + if chunk.get("type") == "assistant" and "message" in chunk: + message = chunk["message"] + if isinstance(message, dict) and "content" in message: + content = message["content"] + elif "content" in chunk and isinstance(chunk["content"], list): + content = chunk["content"] + + if content and isinstance(content, list): + for block in content: + block_type = getattr(block, "type", None) or ( + block.get("type") if isinstance(block, dict) else None + ) + if block_type == "tool_use": + if hasattr(block, "id"): + tc_id = block.id + tc_name = block.name + tc_input = block.input + else: + tc_id = block.get("id", f"call_{uuid.uuid4().hex[:24]}") + tc_name = block.get("name", "") + tc_input = block.get("input", {}) + + tc_args = json.dumps(tc_input) if isinstance(tc_input, dict) else str(tc_input) + tool_calls_list.append( + ToolCall( + id=tc_id, + function=FunctionCall(name=tc_name, arguments=tc_args), + ) + ) + + if not raw_assistant_content and not tool_calls_list: raise HTTPException(status_code=500, detail="No response from Claude Code") - # Filter out tool usage and thinking blocks - assistant_content = MessageAdapter.filter_content(raw_assistant_content) + # Filter out tool usage and thinking blocks (skip in passthrough) + if passthrough_mode: + assistant_content = raw_assistant_content or "" + else: + assistant_content = MessageAdapter.filter_content(raw_assistant_content or "") # Add assistant response to session if using session mode if actual_session_id: @@ -733,23 +902,37 @@ async def chat_completions( prompt_tokens = MessageAdapter.estimate_tokens(prompt) completion_tokens = MessageAdapter.estimate_tokens(assistant_content) - # Create response - response = ChatCompletionResponse( - id=request_id, - model=request_body.model, - choices=[ - Choice( - index=0, - message=Message(role="assistant", content=assistant_content), - finish_reason="stop", - ) - ], - usage=Usage( + # Determine finish_reason + finish_reason = "tool_calls" if tool_calls_list else "stop" + + # Build response message + response_message = Message(role="assistant", content=assistant_content or None) + + # Create response - include tool_calls in the choice if present + choice_data = { + "index": 0, + "message": response_message, + "finish_reason": finish_reason, + } + response_dict = { + "id": request_id, + "model": request_body.model, + "choices": [Choice(**choice_data)], + "usage": Usage( prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, total_tokens=prompt_tokens + completion_tokens, ), - ) + } + response = ChatCompletionResponse(**response_dict) + + # Inject tool_calls into the serialized response (bypasses Pydantic model) + if tool_calls_list: + resp_json = response.model_dump() + resp_json["choices"][0]["message"]["tool_calls"] = [ + tc.model_dump() for tc in tool_calls_list + ] + return JSONResponse(content=resp_json) return response diff --git a/src/message_adapter.py b/src/message_adapter.py index 1c9d732..90a548d 100644 --- a/src/message_adapter.py +++ b/src/message_adapter.py @@ -6,11 +6,18 @@ class MessageAdapter: """Converts between OpenAI message format and Claude Code prompts.""" + # Max prompt size in characters. Keep small to avoid slow CLI responses. + # ~30K chars ≈ ~7.5K tokens — enough context without multi-minute waits. + MAX_PROMPT_CHARS = 30_000 + @staticmethod def messages_to_prompt(messages: List[Message]) -> tuple[str, Optional[str]]: """ Convert OpenAI messages to Claude Code prompt format. Returns (prompt, system_prompt) + + Truncates older conversation history if the prompt would exceed + the OS command-line argument size limit (ARG_MAX). """ system_prompt = None conversation_parts = [] @@ -24,12 +31,25 @@ def messages_to_prompt(messages: List[Message]) -> tuple[str, Optional[str]]: elif message.role == "assistant": conversation_parts.append(f"Assistant: {message.content}") - # Join conversation parts - prompt = "\n\n".join(conversation_parts) - - # If the last message wasn't from the user, add a prompt for assistant + # If the last message wasn't from the user, add a continuation prompt if messages and messages[-1].role != "user": - prompt += "\n\nHuman: Please continue." + conversation_parts.append("Human: Please continue.") + + # Truncate from the front (oldest messages) if prompt is too large + prompt = "\n\n".join(conversation_parts) + if len(prompt) > MessageAdapter.MAX_PROMPT_CHARS and len(conversation_parts) > 1: + # Always keep the last message; drop oldest until it fits + while len(conversation_parts) > 1: + conversation_parts.pop(0) + candidate = "[Earlier conversation truncated for length]\n\n" + "\n\n".join( + conversation_parts + ) + if len(candidate) <= MessageAdapter.MAX_PROMPT_CHARS: + prompt = candidate + break + else: + # Even a single message is too long — hard-truncate it + prompt = conversation_parts[0][: MessageAdapter.MAX_PROMPT_CHARS] return prompt, system_prompt @@ -96,6 +116,14 @@ def replace_image(match): if not content or content.isspace(): return "I understand you're testing the system. How can I help you today?" + # Avoid false-positive billing error detection by downstream platforms. + # Some platforms rewrite responses containing "billing" + "credits"/"plans" + # to a billing error message. Replace with safe synonyms. + content = re.sub(r'\bbilling\b', 'invoicing', content, flags=re.IGNORECASE) + content = re.sub(r'\bBilling\b', 'Invoicing', content) + content = re.sub(r'\binsufficient credits\b', 'insufficient balance', content, flags=re.IGNORECASE) + content = re.sub(r'\bpayment required\b', 'payment needed', content, flags=re.IGNORECASE) + return content @staticmethod diff --git a/src/models.py b/src/models.py index 82e85f4..331f843 100644 --- a/src/models.py +++ b/src/models.py @@ -1,5 +1,5 @@ from typing import List, Optional, Dict, Any, Union, Literal -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from datetime import datetime import uuid import logging @@ -17,19 +17,49 @@ def get_default_model(): class ContentPart(BaseModel): """Content part for multimodal messages (OpenAI format).""" + model_config = ConfigDict(extra="ignore") type: Literal["text"] text: str class Message(BaseModel): - role: Literal["system", "user", "assistant"] - content: Union[str, List[ContentPart]] + model_config = ConfigDict(extra="ignore") + + role: Literal["system", "user", "assistant", "developer", "tool"] + content: Optional[Union[str, List[ContentPart]]] = None name: Optional[str] = None + tool_calls: Optional[List[Any]] = Field( + default=None, + description="Tool calls made by the assistant (OpenAI format)", + ) + tool_call_id: Optional[str] = Field( + default=None, + description="Tool call ID this message is responding to (for role=tool)", + ) @model_validator(mode="after") def normalize_content(self): """Convert array content to string for Claude Code compatibility.""" + # Treat 'developer' role as 'system' for Claude compatibility + if self.role == "developer": + self.role = "system" + # Treat 'tool' role as 'user' for Claude compatibility + # Include tool_call_id context so Claude knows which tool result this is + if self.role == "tool": + tool_result_prefix = "" + if self.tool_call_id: + tool_result_prefix = f"[Tool result for call {self.tool_call_id}]\n" + if self.name: + tool_result_prefix += f"[Tool: {self.name}]\n" + self.role = "user" + if self.content is None: + self.content = tool_result_prefix or "" + else: + self.content = f"{tool_result_prefix}{self.content}" if tool_result_prefix else self.content + # Handle null content (e.g. assistant messages with tool_calls) + if self.content is None: + self.content = "" if isinstance(self.content, list): # Extract text from content parts and concatenate text_parts = [] @@ -54,6 +84,8 @@ class StreamOptions(BaseModel): class ChatCompletionRequest(BaseModel): + model_config = ConfigDict(extra="allow") + model: str = Field(default_factory=get_default_model) messages: List[Message] temperature: Optional[float] = Field(default=1.0, ge=0, le=2) @@ -76,6 +108,14 @@ class ChatCompletionRequest(BaseModel): default=False, description="Enable Claude Code tools (Read, Write, Bash, etc.) - disabled by default for OpenAI compatibility", ) + tools: Optional[List[Dict[str, Any]]] = Field( + default=None, + description="OpenAI-format tool definitions. When present, enables passthrough mode with full tool access.", + ) + tool_choice: Optional[Any] = Field( + default=None, + description="Tool choice preference (auto, none, or specific tool)", + ) stream_options: Optional[StreamOptions] = Field( default=None, description="Options for streaming responses" ) @@ -199,10 +239,23 @@ def to_claude_options(self) -> Dict[str, Any]: return options +class FunctionCall(BaseModel): + """OpenAI function call format within a tool call.""" + name: str + arguments: str # JSON-encoded arguments + + +class ToolCall(BaseModel): + """OpenAI tool call format.""" + id: str + type: Literal["function"] = "function" + function: FunctionCall + + class Choice(BaseModel): index: int message: Message - finish_reason: Optional[Literal["stop", "length", "content_filter", "null"]] = None + finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls", "null"]] = None class Usage(BaseModel): @@ -224,7 +277,7 @@ class ChatCompletionResponse(BaseModel): class StreamChoice(BaseModel): index: int delta: Dict[str, Any] - finish_reason: Optional[Literal["stop", "length", "content_filter", "null"]] = None + finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls", "null"]] = None class ChatCompletionStreamResponse(BaseModel): diff --git a/src/openclaw_bridge.py b/src/openclaw_bridge.py new file mode 100644 index 0000000..16b09c2 --- /dev/null +++ b/src/openclaw_bridge.py @@ -0,0 +1,270 @@ +""" +OpenClaw Bridge - Converts OpenAI-format tool definitions to SDK MCP tools +and translates Claude tool_use blocks back to OpenAI tool_calls format. + +This module enables agent frameworks like OpenClaw to use their native tools +through the Claude Agent SDK by: +1. Converting OpenAI function definitions to in-process MCP tools +2. Extracting ToolUseBlock content from Claude's response +3. Formatting tool calls as OpenAI-compatible SSE streaming events +""" + +import json +import logging +import uuid +from typing import Any, Dict, List, Optional, Tuple + +from claude_agent_sdk import create_sdk_mcp_server, tool, SdkMcpTool + +logger = logging.getLogger(__name__) + +# Sentinel prefix for placeholder tool results +TOOL_CALL_PLACEHOLDER = "[OPENCLAW_TOOL_CALL_FORWARDED]" + + +def openai_tools_to_mcp_server( + tools: List[Dict[str, Any]], + server_name: str = "openclaw_tools", +) -> Tuple[Any, List[str]]: + """Convert OpenAI-format tool definitions to an SDK MCP server. + + Args: + tools: List of OpenAI tool definitions with format: + [{"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}] + server_name: Name for the MCP server + + Returns: + Tuple of (McpSdkServerConfig, list of tool names) + """ + sdk_tools: List[SdkMcpTool] = [] + tool_names: List[str] = [] + + for tool_def in tools: + if tool_def.get("type") != "function": + continue + + func_def = tool_def.get("function", {}) + name = func_def.get("name", "") + description = func_def.get("description", "") + parameters = func_def.get("parameters", {"type": "object", "properties": {}}) + + if not name: + continue + + tool_names.append(name) + + # Create MCP tool with a placeholder handler + # In single-turn mode (max_turns=1), this handler is never called + # because the SDK returns the assistant message without executing tools. + # If it IS called (shouldn't happen), return a placeholder. + sdk_tool = _create_placeholder_tool(name, description, parameters) + sdk_tools.append(sdk_tool) + + if not sdk_tools: + logger.warning("No valid tool definitions found in request") + return None, [] + + mcp_server = create_sdk_mcp_server( + name=server_name, + version="1.0.0", + tools=sdk_tools, + ) + + logger.info(f"Created MCP server '{server_name}' with {len(sdk_tools)} tools: {tool_names}") + return mcp_server, tool_names + + +def _create_placeholder_tool( + name: str, description: str, parameters: Dict[str, Any] +) -> SdkMcpTool: + """Create an MCP tool with a placeholder handler. + + The handler should never be called in single-turn mode, but if it is, + it returns a sentinel value that signals this is an external tool call. + """ + + @tool(name, description, parameters) + async def placeholder_handler(args: Any) -> Dict[str, Any]: + logger.warning( + f"Placeholder handler called for tool '{name}' - " + "this shouldn't happen in single-turn mode. " + f"Args: {args}" + ) + return { + "content": [ + { + "type": "text", + "text": f"{TOOL_CALL_PLACEHOLDER}: {name}({json.dumps(args)})", + } + ] + } + + return placeholder_handler + + +def extract_tool_calls_from_chunks( + chunks: List[Dict[str, Any]], + external_tool_names: Optional[List[str]] = None, +) -> Tuple[Optional[str], List[Dict[str, Any]]]: + """Extract text content and tool calls from Claude SDK response chunks. + + Scans assistant messages for TextBlock and ToolUseBlock content, + separating them into text content and tool_calls. + + Args: + chunks: List of SDK message dicts + external_tool_names: If provided, only extract tool calls for these tool names. + Tool calls for other tools (Claude built-in) are ignored. + + Returns: + Tuple of (text_content, tool_calls) where tool_calls is a list of + OpenAI-format tool call dicts. + """ + text_parts = [] + tool_calls = [] + tool_call_index = 0 + + for chunk in chunks: + content = None + + # Handle ResultMessage with 'result' field + if chunk.get("subtype") == "success" and "result" in chunk: + if chunk["result"]: + text_parts.append(chunk["result"]) + continue + + # Handle AssistantMessage content + if "content" in chunk and isinstance(chunk["content"], list): + content = chunk["content"] + elif chunk.get("type") == "assistant" and "message" in chunk: + message = chunk["message"] + if isinstance(message, dict) and "content" in message: + content = message["content"] + + if content is None: + continue + + if not isinstance(content, list): + if isinstance(content, str) and content: + text_parts.append(content) + continue + + for block in content: + # Handle TextBlock + if hasattr(block, "text"): + if block.text: + text_parts.append(block.text) + elif isinstance(block, dict) and block.get("type") == "text": + text = block.get("text", "") + if text: + text_parts.append(text) + + # Handle ToolUseBlock + elif hasattr(block, "name") and hasattr(block, "input") and hasattr(block, "id"): + tool_name = block.name + # Filter by external tool names if provided + if external_tool_names and tool_name not in external_tool_names: + logger.debug(f"Skipping internal tool call: {tool_name}") + continue + + tool_call = { + "index": tool_call_index, + "id": getattr(block, "id", f"call_{uuid.uuid4().hex[:24]}"), + "type": "function", + "function": { + "name": tool_name, + "arguments": json.dumps(block.input) if isinstance(block.input, dict) else str(block.input), + }, + } + tool_calls.append(tool_call) + tool_call_index += 1 + logger.info(f"Extracted tool call: {tool_name} (id={tool_call['id']})") + + elif isinstance(block, dict) and block.get("type") == "tool_use": + tool_name = block.get("name", "") + if external_tool_names and tool_name not in external_tool_names: + logger.debug(f"Skipping internal tool call: {tool_name}") + continue + + tool_input = block.get("input", {}) + tool_call = { + "index": tool_call_index, + "id": block.get("id", f"call_{uuid.uuid4().hex[:24]}"), + "type": "function", + "function": { + "name": tool_name, + "arguments": json.dumps(tool_input) if isinstance(tool_input, dict) else str(tool_input), + }, + } + tool_calls.append(tool_call) + tool_call_index += 1 + logger.info(f"Extracted tool call (dict): {tool_name}") + + text_content = "\n".join(text_parts) if text_parts else None + return text_content, tool_calls + + +def format_tool_calls_for_sse( + tool_calls: List[Dict[str, Any]], + request_id: str, + model: str, +) -> List[str]: + """Format tool calls as OpenAI SSE streaming events. + + Returns a list of SSE-formatted data strings ready to yield. + """ + events = [] + + for tc in tool_calls: + # Send tool call start (with id, type, name) + start_chunk = { + "id": request_id, + "object": "chat.completion.chunk", + "model": model, + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": tc["index"], + "id": tc["id"], + "type": "function", + "function": { + "name": tc["function"]["name"], + "arguments": "", + }, + } + ] + }, + "finish_reason": None, + } + ], + } + events.append(f"data: {json.dumps(start_chunk)}\n\n") + + # Send arguments in chunks (could split for large args, but send all at once for simplicity) + args_chunk = { + "id": request_id, + "object": "chat.completion.chunk", + "model": model, + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": tc["index"], + "function": { + "arguments": tc["function"]["arguments"], + }, + } + ] + }, + "finish_reason": None, + } + ], + } + events.append(f"data: {json.dumps(args_chunk)}\n\n") + + return events diff --git a/tests/test_claude_cli_unit.py b/tests/test_claude_cli_unit.py index c67c7fe..774e9e4 100644 --- a/tests/test_claude_cli_unit.py +++ b/tests/test_claude_cli_unit.py @@ -548,7 +548,7 @@ async def mock_query(prompt, options): assert len(captured_options) == 1 opts = captured_options[0] - assert opts.system_prompt == {"type": "text", "text": "You are helpful"} + assert opts.system_prompt == "You are helpful" @pytest.mark.asyncio async def test_run_completion_with_model(self, cli_instance): diff --git a/tests/test_sdk_migration.py b/tests/test_sdk_migration.py index 6ad2d95..d28a654 100644 --- a/tests/test_sdk_migration.py +++ b/tests/test_sdk_migration.py @@ -74,7 +74,7 @@ def test_default_model_defined(self): from src.constants import DEFAULT_MODEL, CLAUDE_MODELS assert DEFAULT_MODEL in CLAUDE_MODELS - assert DEFAULT_MODEL == "claude-sonnet-4-5-20250929" + assert DEFAULT_MODEL == "claude-opus-4-6" def test_fast_model_defined(self): """Test that FAST_MODEL is set to fastest model.""" From 46dda59a2fe3cb8b96d8d951b6714a229062cda6 Mon Sep 17 00:00:00 2001 From: Taimoor Date: Sat, 14 Feb 2026 13:52:58 -0500 Subject: [PATCH 2/2] chore: add .hypothesis to gitignore and remove test artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 2 +- .hypothesis/constants/085e16c86601bbe6 | 4 ---- .hypothesis/constants/090841bf7b1e878c | 4 ---- .hypothesis/constants/176fc1cccfb67bf2 | 4 ---- .hypothesis/constants/1b5c5c0364f2d5af | 4 ---- .hypothesis/constants/1c3d3f4a26f40abd | 4 ---- .hypothesis/constants/483b79bd85591992 | 4 ---- .hypothesis/constants/51e66ffb8306a103 | 4 ---- .hypothesis/constants/5ecb8d27c15539fb | 4 ---- .hypothesis/constants/61575172bb6be502 | 4 ---- .hypothesis/constants/67b0a8ccf18bf5d2 | 4 ---- .hypothesis/constants/7e8b22c9c355ef48 | 4 ---- .hypothesis/constants/92b66ce282ffc534 | 4 ---- .hypothesis/constants/9adb793441356481 | 4 ---- .hypothesis/constants/b557a9a709d4c7cf | 4 ---- .hypothesis/constants/d14c45ee4f738a0e | 4 ---- .hypothesis/constants/eeb6e6047832521f | 4 ---- .hypothesis/constants/f102fa85cdaff8e2 | 4 ---- .hypothesis/unicode_data/15.0.0/charmap.json.gz | Bin 21726 -> 0 bytes .../unicode_data/15.0.0/codec-utf-8.json.gz | Bin 60 -> 0 bytes 20 files changed, 1 insertion(+), 69 deletions(-) delete mode 100644 .hypothesis/constants/085e16c86601bbe6 delete mode 100644 .hypothesis/constants/090841bf7b1e878c delete mode 100644 .hypothesis/constants/176fc1cccfb67bf2 delete mode 100644 .hypothesis/constants/1b5c5c0364f2d5af delete mode 100644 .hypothesis/constants/1c3d3f4a26f40abd delete mode 100644 .hypothesis/constants/483b79bd85591992 delete mode 100644 .hypothesis/constants/51e66ffb8306a103 delete mode 100644 .hypothesis/constants/5ecb8d27c15539fb delete mode 100644 .hypothesis/constants/61575172bb6be502 delete mode 100644 .hypothesis/constants/67b0a8ccf18bf5d2 delete mode 100644 .hypothesis/constants/7e8b22c9c355ef48 delete mode 100644 .hypothesis/constants/92b66ce282ffc534 delete mode 100644 .hypothesis/constants/9adb793441356481 delete mode 100644 .hypothesis/constants/b557a9a709d4c7cf delete mode 100644 .hypothesis/constants/d14c45ee4f738a0e delete mode 100644 .hypothesis/constants/eeb6e6047832521f delete mode 100644 .hypothesis/constants/f102fa85cdaff8e2 delete mode 100644 .hypothesis/unicode_data/15.0.0/charmap.json.gz delete mode 100644 .hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz diff --git a/.gitignore b/.gitignore index a59cdee..5f5dc85 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,4 @@ test_debug_*.py test_performance_*.py test_user_*.py test_new_*.py -test_roocode_compatibility.py \ No newline at end of file +test_roocode_compatibility.py.hypothesis/ diff --git a/.hypothesis/constants/085e16c86601bbe6 b/.hypothesis/constants/085e16c86601bbe6 deleted file mode 100644 index 9213fb4..0000000 --- a/.hypothesis/constants/085e16c86601bbe6 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/__init__.py -# hypothesis_version: 6.148.8 - -['2.2.0'] \ No newline at end of file diff --git a/.hypothesis/constants/090841bf7b1e878c b/.hypothesis/constants/090841bf7b1e878c deleted file mode 100644 index 4939327..0000000 --- a/.hypothesis/constants/090841bf7b1e878c +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/message_adapter.py -# hypothesis_version: 6.148.8 - -[30000, '.*?', '.*?', 'Invoicing', '\\bBilling\\b', '\\bbilling\\b', '\\bpayment required\\b', '\\n\\s*\\n\\s*\\n', 'assistant', 'content', 'finish_reason', 'insufficient balance', 'invoicing', 'model', 'payment needed', 'role', 'stop', 'system', 'user'] \ No newline at end of file diff --git a/.hypothesis/constants/176fc1cccfb67bf2 b/.hypothesis/constants/176fc1cccfb67bf2 deleted file mode 100644 index 977b385..0000000 --- a/.hypothesis/constants/176fc1cccfb67bf2 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/main.py -# hypothesis_version: 6.148.8 - -[30.0, 400, 404, 413, 422, 429, 500, 503, 1000, 1024, 8000, 100000, ' Example usage:', ' -> ', '#22c55e', '#ef4444', '*', '-_', '/', '/health', '/v1/', '/v1/auth/status', '/v1/chat/completions', '/v1/compatibility', '/v1/debug/request', '/v1/mcp/connect', '/v1/mcp/disconnect', '/v1/mcp/servers', '/v1/mcp/stats', '/v1/messages', '/v1/models', '/v1/sessions', '/v1/sessions/stats', '/v1/tools', '/v1/tools/config', '/v1/tools/stats', '/version', '0.0.0.0', '1', '1.0.0', '127.0.0.1', '600000', '8000', '=', 'API_KEY', 'CLAUDE_CWD', 'CLAUDE_WRAPPER_HOST', 'CORS_ORIGINS', 'Cache-Control', 'Connected', 'Connection', 'DEBUG_MODE', 'Hello, world!', 'MAX_REQUEST_SIZE', 'MAX_TIMEOUT', 'Not Connected', 'PORT', 'POST', 'Session not found', 'Unknown error', 'VERBOSE', 'X-Claude-Max-Turns', 'X-Request-ID', '["*"]', '__main__', 'allowed_tools', 'anthropic', 'api_error', 'api_key_required', 'api_key_source', 'api_version', 'arguments', 'assistant', 'auth', 'bypassPermissions', 'chat', 'choices', 'claude_code_auth', 'code', 'common_issues', 'compatibility_report', 'completion_tokens', 'content', 'content-length', 'custom_headers', 'cwd', 'data', 'data: [DONE]\n\n', 'debug', 'debug_info', 'debug_mode_enabled', 'debug_tip', 'default_ttl_hours', 'details', 'disallowed_tools', 'end_turn', 'environment', 'error', 'errors', 'false', 'field', 'finish_reason', 'function', 'general', 'headers', 'health', 'healthy', 'help', 'id', 'index', 'input', 'json_parse_error', 'keep-alive', 'list', 'loc', 'max_thinking_tokens', 'max_turns', 'message', 'messages', 'method', 'model', 'msg', 'n', 'name', 'no', 'no-cache', 'none', 'object', 'on', 'owned_by', 'parsed_body', 'permission_mode', 'prompt_tokens', 'prompts', 'raw_body', 'raw_request_body', 'request_id', 'request_too_large', 'resources', 'resume', 'role', 'runtime', 'server_info', 'service', 'session_stats', 'status', 'stop', 'stream', 'streaming_error', 'supported', 'system_prompt', 'text', 'text/event-stream', 'tool_calls', 'tool_use', 'tools', 'total_tokens', 'true', 'type', 'unknown', 'url', 'usage', 'user', 'v1', 'valid', 'validated_data', 'validation_error', 'validation_result', 'version', 'y', 'yes', '🔑 API Key Generated!'] \ No newline at end of file diff --git a/.hypothesis/constants/1b5c5c0364f2d5af b/.hypothesis/constants/1b5c5c0364f2d5af deleted file mode 100644 index fe78fa2..0000000 --- a/.hypothesis/constants/1b5c5c0364f2d5af +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/parameter_validator.py -# hypothesis_version: 6.148.8 - -[1.0, 100, 50000, ',', 'acceptEdits', 'allowed_tools', 'bypassPermissions', 'default', 'disallowed_tools', 'frequency_penalty', 'logit_bias', 'max_thinking_tokens', 'max_tokens', 'max_turns', 'messages', 'model', 'n', 'permission_mode', 'plan', 'presence_penalty', 'stop', 'stream', 'suggestions', 'supported_parameters', 'temperature', 'top_p', 'user (for logging)', 'warnings', 'x-claude-max-turns'] \ No newline at end of file diff --git a/.hypothesis/constants/1c3d3f4a26f40abd b/.hypothesis/constants/1c3d3f4a26f40abd deleted file mode 100644 index 5fd0f6e..0000000 --- a/.hypothesis/constants/1c3d3f4a26f40abd +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/main.py -# hypothesis_version: 6.148.8 - -[30.0, 400, 404, 413, 422, 429, 500, 503, 1000, 1024, 8000, 100000, ' Example usage:', ' -> ', '#22c55e', '#ef4444', '*', '-_', '/', '/health', '/v1/', '/v1/auth/status', '/v1/chat/completions', '/v1/compatibility', '/v1/debug/request', '/v1/mcp/connect', '/v1/mcp/disconnect', '/v1/mcp/servers', '/v1/mcp/stats', '/v1/messages', '/v1/models', '/v1/sessions', '/v1/sessions/stats', '/v1/tools', '/v1/tools/config', '/v1/tools/stats', '/version', '0.0.0.0', '1', '1.0.0', '127.0.0.1', '600000', '8000', '=', 'API_KEY', 'CLAUDE_CWD', 'CLAUDE_WRAPPER_HOST', 'CORS_ORIGINS', 'Cache-Control', 'Connected', 'Connection', 'DEBUG_MODE', 'Hello, world!', 'MAX_REQUEST_SIZE', 'MAX_TIMEOUT', 'Not Connected', 'PORT', 'POST', 'Session not found', 'Unknown error', 'VERBOSE', 'X-Claude-Max-Turns', 'X-Request-ID', '["*"]', '__main__', 'allowed_tools', 'anthropic', 'api_error', 'api_key_required', 'api_key_source', 'api_version', 'arguments', 'assistant', 'auth', 'bypassPermissions', 'chat', 'choices', 'claude_code_auth', 'code', 'common_issues', 'compatibility_report', 'completion_tokens', 'content', 'content-length', 'custom_headers', 'cwd', 'data', 'data: [DONE]\n\n', 'debug', 'debug_info', 'debug_mode_enabled', 'debug_tip', 'default_ttl_hours', 'details', 'disallowed_tools', 'end_turn', 'environment', 'error', 'errors', 'false', 'field', 'finish_reason', 'function', 'general', 'headers', 'health', 'healthy', 'help', 'id', 'index', 'input', 'json_parse_error', 'keep-alive', 'list', 'loc', 'max_thinking_tokens', 'max_turns', 'message', 'messages', 'method', 'model', 'msg', 'n', 'name', 'no', 'no-cache', 'none', 'object', 'on', 'openclaw_tools', 'owned_by', 'parsed_body', 'permission_mode', 'prompt_tokens', 'prompts', 'raw_body', 'raw_request_body', 'request_id', 'request_too_large', 'resources', 'resume', 'role', 'runtime', 'server_info', 'service', 'session_stats', 'status', 'stop', 'stream', 'streaming_error', 'supported', 'system_prompt', 'text', 'text/event-stream', 'tool_calls', 'tool_use', 'tools', 'total_tokens', 'true', 'type', 'unknown', 'url', 'usage', 'user', 'v1', 'valid', 'validated_data', 'validation_error', 'validation_result', 'version', 'y', 'yes', '🔑 API Key Generated!'] \ No newline at end of file diff --git a/.hypothesis/constants/483b79bd85591992 b/.hypothesis/constants/483b79bd85591992 deleted file mode 100644 index 977b385..0000000 --- a/.hypothesis/constants/483b79bd85591992 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/main.py -# hypothesis_version: 6.148.8 - -[30.0, 400, 404, 413, 422, 429, 500, 503, 1000, 1024, 8000, 100000, ' Example usage:', ' -> ', '#22c55e', '#ef4444', '*', '-_', '/', '/health', '/v1/', '/v1/auth/status', '/v1/chat/completions', '/v1/compatibility', '/v1/debug/request', '/v1/mcp/connect', '/v1/mcp/disconnect', '/v1/mcp/servers', '/v1/mcp/stats', '/v1/messages', '/v1/models', '/v1/sessions', '/v1/sessions/stats', '/v1/tools', '/v1/tools/config', '/v1/tools/stats', '/version', '0.0.0.0', '1', '1.0.0', '127.0.0.1', '600000', '8000', '=', 'API_KEY', 'CLAUDE_CWD', 'CLAUDE_WRAPPER_HOST', 'CORS_ORIGINS', 'Cache-Control', 'Connected', 'Connection', 'DEBUG_MODE', 'Hello, world!', 'MAX_REQUEST_SIZE', 'MAX_TIMEOUT', 'Not Connected', 'PORT', 'POST', 'Session not found', 'Unknown error', 'VERBOSE', 'X-Claude-Max-Turns', 'X-Request-ID', '["*"]', '__main__', 'allowed_tools', 'anthropic', 'api_error', 'api_key_required', 'api_key_source', 'api_version', 'arguments', 'assistant', 'auth', 'bypassPermissions', 'chat', 'choices', 'claude_code_auth', 'code', 'common_issues', 'compatibility_report', 'completion_tokens', 'content', 'content-length', 'custom_headers', 'cwd', 'data', 'data: [DONE]\n\n', 'debug', 'debug_info', 'debug_mode_enabled', 'debug_tip', 'default_ttl_hours', 'details', 'disallowed_tools', 'end_turn', 'environment', 'error', 'errors', 'false', 'field', 'finish_reason', 'function', 'general', 'headers', 'health', 'healthy', 'help', 'id', 'index', 'input', 'json_parse_error', 'keep-alive', 'list', 'loc', 'max_thinking_tokens', 'max_turns', 'message', 'messages', 'method', 'model', 'msg', 'n', 'name', 'no', 'no-cache', 'none', 'object', 'on', 'owned_by', 'parsed_body', 'permission_mode', 'prompt_tokens', 'prompts', 'raw_body', 'raw_request_body', 'request_id', 'request_too_large', 'resources', 'resume', 'role', 'runtime', 'server_info', 'service', 'session_stats', 'status', 'stop', 'stream', 'streaming_error', 'supported', 'system_prompt', 'text', 'text/event-stream', 'tool_calls', 'tool_use', 'tools', 'total_tokens', 'true', 'type', 'unknown', 'url', 'usage', 'user', 'v1', 'valid', 'validated_data', 'validation_error', 'validation_result', 'version', 'y', 'yes', '🔑 API Key Generated!'] \ No newline at end of file diff --git a/.hypothesis/constants/51e66ffb8306a103 b/.hypothesis/constants/51e66ffb8306a103 deleted file mode 100644 index 819ebd7..0000000 --- a/.hypothesis/constants/51e66ffb8306a103 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/models.py -# hypothesis_version: 6.148.8 - -[0.3, 0.5, 0.7, 0.9, 1.0, 1.5, 100, 200, 500, 4096, '-_.', 'System prompt', 'after', 'allow', 'assistant', 'chat.completion', 'command', 'content_filter', 'developer', 'end_turn', 'function', 'ignore', 'length', 'max_thinking_tokens', 'max_tokens', 'message', 'model', 'n', 'name', 'null', 'server_name', 'stop', 'stop_sequence', 'system', 'text', 'tool', 'tool_calls', 'tool_name', 'type', 'user'] \ No newline at end of file diff --git a/.hypothesis/constants/5ecb8d27c15539fb b/.hypothesis/constants/5ecb8d27c15539fb deleted file mode 100644 index 3072723..0000000 --- a/.hypothesis/constants/5ecb8d27c15539fb +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/rate_limiter.py -# hypothesis_version: 6.148.8 - -[429, '/', '1', '10/minute', '15/minute', '2/minute', '30', '30/minute', 'RATE_LIMIT_ENABLED', 'Retry-After', 'auth', 'chat', 'code', 'debug', 'error', 'general', 'health', 'message', 'on', 'rate_limit_exceeded', 'retry_after', 'session', 'too_many_requests', 'true', 'type', 'yes'] \ No newline at end of file diff --git a/.hypothesis/constants/61575172bb6be502 b/.hypothesis/constants/61575172bb6be502 deleted file mode 100644 index ef7ff63..0000000 --- a/.hypothesis/constants/61575172bb6be502 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/.cache/pypoetry/virtualenvs/claude-code-openai-wrapper-kCeJR7gw-py3.12/bin/pytest -# hypothesis_version: 6.148.8 - -['__main__'] \ No newline at end of file diff --git a/.hypothesis/constants/67b0a8ccf18bf5d2 b/.hypothesis/constants/67b0a8ccf18bf5d2 deleted file mode 100644 index 924b9cb..0000000 --- a/.hypothesis/constants/67b0a8ccf18bf5d2 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /usr/lib/python3.12/sitecustomize.py -# hypothesis_version: 6.148.8 - -[] \ No newline at end of file diff --git a/.hypothesis/constants/7e8b22c9c355ef48 b/.hypothesis/constants/7e8b22c9c355ef48 deleted file mode 100644 index b761116..0000000 --- a/.hypothesis/constants/7e8b22c9c355ef48 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/claude_cli.py -# hypothesis_version: 6.148.8 - -[0.0, 1000, 600000, 'Hello', '_', '__dict__', 'assistant', 'claude_code', 'completion_tokens', 'content', 'data', 'duration_ms', 'error_message', 'init', 'is_error', 'message', 'model', 'num_turns', 'preset', 'prompt_tokens', 'result', 'session_id', 'subtype', 'success', 'system', 'text', 'total_cost_usd', 'total_tokens', 'type'] \ No newline at end of file diff --git a/.hypothesis/constants/92b66ce282ffc534 b/.hypothesis/constants/92b66ce282ffc534 deleted file mode 100644 index 44ab1ed..0000000 --- a/.hypothesis/constants/92b66ce282ffc534 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/openclaw_bridge.py -# hypothesis_version: 6.148.8 - -['1.0.0', 'arguments', 'assistant', 'choices', 'content', 'delta', 'description', 'finish_reason', 'function', 'id', 'index', 'input', 'message', 'model', 'name', 'object', 'openclaw_tools', 'parameters', 'properties', 'result', 'subtype', 'success', 'text', 'tool_calls', 'tool_use', 'type'] \ No newline at end of file diff --git a/.hypothesis/constants/9adb793441356481 b/.hypothesis/constants/9adb793441356481 deleted file mode 100644 index b8d9e81..0000000 --- a/.hypothesis/constants/9adb793441356481 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/tool_manager.py -# hypothesis_version: 6.148.8 - -['Bash', 'BashOutput', 'Create a new file', 'Delete notebook cell', 'Edit', 'Execute git status', 'Find TODO comments', 'Glob', 'Grep', 'ID of cell to edit', 'KillShell', 'New cell content', 'NotebookEdit', 'Path to .ipynb file', 'Read', 'Read blog post', 'Read entire file', 'Read images and PDFs', 'Rename a variable', 'Replacement text', 'Run npm install', 'Search query', 'Skill', 'SlashCommand', 'Task', 'Text to replace', 'TodoWrite', 'WebFetch', 'WebSearch', 'Write', 'agent', 'allowed_domains', 'bash_id', 'blocked_domains', 'cell_id', 'cell_type', 'code or markdown', 'command', 'content', 'description', 'edit_mode', 'file', 'file_path', 'filter', 'glob', 'global_allowed', 'global_disallowed', 'limit', 'new_source', 'new_string', 'notebook_path', 'offset', 'old_string', 'output_mode', 'path', 'pattern', 'productivity', 'prompt', 'query', 'replace_all', 'run_in_background', 'session_configs', 'shell_id', 'subagent_type', 'system', 'timeout', 'todos', 'tool_categories', 'total_tools', 'url', 'web'] \ No newline at end of file diff --git a/.hypothesis/constants/b557a9a709d4c7cf b/.hypothesis/constants/b557a9a709d4c7cf deleted file mode 100644 index 6ab5624..0000000 --- a/.hypothesis/constants/b557a9a709d4c7cf +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/auth.py -# hypothesis_version: 6.148.8 - -[401, '1', 'ANTHROPIC_API_KEY', 'API_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_DEFAULT_REGION', 'AWS_REGION', 'Bearer', 'CLAUDE_AUTH_METHOD', 'CLOUD_ML_REGION', 'Invalid API key', 'Missing API key', 'WWW-Authenticate', 'anthropic', 'api_key', 'api_key_length', 'api_key_present', 'aws_region', 'bedrock', 'claude_cli', 'cli', 'config', 'errors', 'method', 'note', 'project_id', 'region', 'runtime_api_key', 'status', 'valid', 'vertex'] \ No newline at end of file diff --git a/.hypothesis/constants/d14c45ee4f738a0e b/.hypothesis/constants/d14c45ee4f738a0e deleted file mode 100644 index f611206..0000000 --- a/.hypothesis/constants/d14c45ee4f738a0e +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/session_manager.py -# hypothesis_version: 6.148.8 - -['active_sessions', 'expired_sessions', 'total_messages'] \ No newline at end of file diff --git a/.hypothesis/constants/eeb6e6047832521f b/.hypothesis/constants/eeb6e6047832521f deleted file mode 100644 index 178cd77..0000000 --- a/.hypothesis/constants/eeb6e6047832521f +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/constants.py -# hypothesis_version: 6.148.8 - -[100, 200, 8000, 600000, 'Bash', 'BashOutput', 'DEFAULT_MODEL', 'Edit', 'Glob', 'Grep', 'KillShell', 'NotebookEdit', 'Read', 'Skill', 'SlashCommand', 'Task', 'TodoWrite', 'WebFetch', 'WebSearch', 'Write', 'claude-opus-4-6', 'claude_code', 'preset', 'text'] \ No newline at end of file diff --git a/.hypothesis/constants/f102fa85cdaff8e2 b/.hypothesis/constants/f102fa85cdaff8e2 deleted file mode 100644 index c2cb86f..0000000 --- a/.hypothesis/constants/f102fa85cdaff8e2 +++ /dev/null @@ -1,4 +0,0 @@ -# file: /home/tekron/claude-code-openai-wrapper/src/mcp_client.py -# hypothesis_version: 6.148.8 - -['arguments', 'connected', 'connected_servers', 'description', 'enabled', 'inputSchema', 'input_schema', 'mcp_available', 'mimeType', 'name', 'prompts', 'registered_servers', 'resources', 'servers', 'tools', 'total_prompts', 'total_resources', 'total_tools', 'uri'] \ No newline at end of file diff --git a/.hypothesis/unicode_data/15.0.0/charmap.json.gz b/.hypothesis/unicode_data/15.0.0/charmap.json.gz deleted file mode 100644 index b8229995c10399be329c5155621cfaa20e021e81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21726 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 diff --git a/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz b/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz deleted file mode 100644 index c5f5cfef3f9eba3535b03226d0b5cf1b92199a30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60 zcmb2|=HOstU|?YSUy@r;S(uk;W>R3x(A{TtA<)3obdyQ%)xZujQ*$%Ji-86vCMJxJ PZbUHDMd`1Q1L^?)HsKQ9