Skip to content

Anthropic BYOK provider handles tool execution internally — tool.call JSON-RPC never sent to python sdk #709

@sbacha

Description

@sbacha

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_start SDK event fires
  • tool.execution_complete SDK event fires at the same second (no async wait)
  • Python handler is never called
  • No tool.call JSON-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_startcomplete 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.call JSON-RPC sent to Python. Same Tool(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

Metadata

Metadata

Assignees

Labels

runtimeRequires a change in the copilot-agent-runtime repo

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions