-
Notifications
You must be signed in to change notification settings - Fork 936
Description
Summary
When using a custom Anthropic provider (provider.type = "anthropic" with base_url + api_key), the CLI binary handles tool execution internally instead of sending tool.call JSON-RPC requests to the Python SDK. Custom tool handlers registered via Tool(handler=...) or @define_tool are never invoked.
The binary receives a valid tool_use response from the API, but instead of delegating to Python, it fabricates a failed tool_result: "Error: External tool invocation missing toolCallId" and sends it back to the LLM.
OpenAI BYOK provider works correctly — same code, same tools, same SDK version.
No custom provider (GitHub-routed) works correctly — even for Claude models.
Environment
- Python SDK: 0.1.30 (also tested 0.1.25, 0.1.23)
- CLI binary: 0.0.420
- Platform: Windows 11 and Linux x86_64 (same behavior on both)
Reproduction
import asyncio
from copilot import CopilotClient, Tool, PermissionHandler
async def test():
handler_called = False
async def handler(invocation):
nonlocal handler_called
handler_called = True
print(f"HANDLER CALLED: {invocation['tool_name']}")
return {"textResultForLlm": "result", "resultType": "success"}
tool = Tool(
name="get_weather",
description="Get weather for a city",
handler=handler,
parameters={
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
)
client = CopilotClient({"use_logged_in_user": False})
await client.start()
session = await client.create_session({
"model": "claude-sonnet-4.6",
"provider": {
"type": "anthropic",
"base_url": "https://your-anthropic-proxy.example.com",
"api_key": "your-key",
},
"tools": [tool],
"on_permission_request": PermissionHandler.approve_all,
})
session.on(lambda e: print(f"EVENT: {e.type}"))
resp = await session.send_and_wait(
{"prompt": "Use the get_weather tool to check Seattle weather."},
timeout=60.0,
)
print(f"handler_called: {handler_called}") # Always False
await client.stop()
asyncio.run(test())Same code with provider.type = "openai" → handler_called = True.
Same code without provider (GitHub-routed Claude models) → handler_called = True.
Wire-Level Evidence
We instrumented our HTTP proxy to capture the exact request/response traffic between the CLI binary and the Anthropic-compatible API.
1. First LLM request (binary → API)
POST /v1/messages
Headers: content-type: application/json, anthropic-version: 2023-06-01
Body keys: [max_tokens, model, temperature, system, messages, tools]
18 tools defined, 1 user message
The binary correctly formats and sends an Anthropic Messages API request with tools.
2. API response (API → binary)
{
"id": "msg_...",
"type": "message",
"role": "assistant",
"content": [{"type": "tool_use", "id": "toolu_01UXvE613t95biU6E9uoxeNw", "name": "get_weather", "input": {"city": "Seattle"}}],
"stop_reason": "tool_use",
"model": "claude-sonnet-4-6-20250306",
"usage": {"input_tokens": ..., "output_tokens": ...}
}The API returns a valid tool_use response with a proper toolu_ ID.
3. What the binary does (BUG)
Instead of sending tool.call JSON-RPC to Python:
tool.execution_startSDK event firestool.execution_completeSDK event fires at the same second (no async wait)- Python handler is never called
- No
tool.callJSON-RPC is sent over stdio
4. Second LLM request (binary → API, with fabricated error)
POST /v1/messages
Messages: 3 messages
msg[0] role=user content="Use the get_weather tool..."
msg[1] role=assistant content_types=['tool_use']
msg[2] role=user tool_result: id=toolu_01UXvE613t95biU6E9uoxeNw
content="Error: External tool invocation missing toolCallId"
The binary fabricated a failed tool_result and sent it back to the LLM without ever consulting Python.
5. Final response
The LLM sees the error and apologizes to the user. The tool never actually executed.
Comparison: OpenAI vs Anthropic BYOK provider
| Behavior | OpenAI BYOK | Anthropic BYOK |
|---|---|---|
| LLM request format | /v1/chat/completions |
/v1/messages |
| Tool definitions sent | Yes | Yes |
| LLM returns tool call | Yes (tool_calls) | Yes (tool_use) |
tool.call JSON-RPC sent to Python |
Yes | No |
| Python handler invoked | Yes | No |
tool.execution_start → complete gap |
Seconds (real execution) | 0 seconds (instant) |
| Tool result source | Python handler return value | Binary internal error |
Also verified: not a proxy issue
We confirmed the issue is specific to provider.type = "anthropic" in the CLI binary, not our proxy:
- No provider set (GitHub-routed, model
claude-sonnet-4.6): Tools work.tool.callJSON-RPC sent to Python. SameTool(handler=...)API, same async handler. provider.type = "openai"with same proxy: Tools work. Handler invoked.provider.type = "anthropic"with same proxy: Tools broken. Handler never called.
The only variable is provider.type.
Expected Behavior
The CLI binary should send tool.call JSON-RPC to the Python SDK for tools registered via the tools array in session.create, regardless of provider type. The Python handler should be invoked and the return value used as the tool result.
Current workaround
We use provider.type = "openai" for all models (including Claude) and translate between OpenAI and Anthropic wire formats in our proxy layer. This works but is fragile and loses Anthropic-specific features (extended thinking, prompt caching, native system prompt handling).
Context
We are the Microsoft Copilot Studio (MCS) team building an agentic loop service (Dracarys) that runs inside ADC sandbox containers. We rely on custom tool handlers to bridge tool execution from the SDK back to the host via gRPC. This issue blocks native Anthropic provider support for external tool execution.
Internal tracking: https://microsoft.ghe.com/bic/mcs-dracarys/issues/134