refactor(mcp): route buildRequestBodyFromRequest through hooks + full Anthropic format

Problem: callWithRequest/Full/Stream all called client.buildRequestBodyFromRequest
directly (not via hooks), so ClaudeClient could never override it. This meant
tool calling sent OpenAI format to Anthropic (wrong field names, wrong roles).

Changes:

mcp/interface.go
- Add buildRequestBodyFromRequest(*Request) map[string]any to clientHooks
- Improve comments: document what each hook group does and why

mcp/client.go
- All three paths (callWithRequest, callWithRequestFull, CallWithRequestStream)
  now call client.hooks.buildRequestBodyFromRequest — ClaudeClient picks up

mcp/claude_client.go
- Full rewrite with format comparison table in package doc
- buildRequestBodyFromRequest: produces correct Anthropic wire format
    * system prompt → top-level "system" field
    * tools: parameters → input_schema, no "type:function" wrapper
    * tool_choice "auto" → {"type":"auto"} object
    * assistant tool calls → content[{type:tool_use, id, name, input}]
    * role=tool results → role=user content[{type:tool_result,...}]
    * consecutive tool results merged into single user turn
- convertMessagesToAnthropic: handles all three message types
- parseMCPResponseFull: extracts text + tool_use blocks
- parseMCPResponse: delegates to parseMCPResponseFull

All mcp and agent tests pass.
This commit is contained in:
tinkle-community
2026-03-08 17:29:21 +08:00
parent 9fcf44af65
commit ea7b450a7e
3 changed files with 242 additions and 117 deletions

View File

@@ -22,19 +22,40 @@ type AIClient interface {
CallWithRequestFull(req *Request) (*LLMResponse, error)
}
// clientHooks internal hook interface (for subclass to override specific steps)
// These methods are only used inside the package to implement dynamic dispatch
// clientHooks is the internal dispatch interface used to implement per-provider
// polymorphism without Go's lack of virtual methods.
//
// Each method can be overridden by an embedding struct (e.g. ClaudeClient).
// The base *Client provides OpenAI-compatible defaults; providers with a
// different wire format (Anthropic, Gemini native, etc.) override only what
// differs. All call-path methods in client.go invoke these via c.hooks so
// that the override is always picked up at runtime.
type clientHooks interface {
// Hook methods that can be overridden by subclass
// ── Simple CallWithMessages path ────────────────────────────────────────
call(systemPrompt, userPrompt string) (string, error)
buildMCPRequestBody(systemPrompt, userPrompt string) map[string]any
// ── Shared request plumbing ─────────────────────────────────────────────
buildUrl() string
buildRequest(url string, jsonData []byte) (*http.Request, error)
setAuthHeader(reqHeaders http.Header)
marshalRequestBody(requestBody map[string]any) ([]byte, error)
// ── Advanced (Request-object) path ──────────────────────────────────────
// buildRequestBodyFromRequest converts a *Request into the provider's
// native wire-format map. Providers that use a different protocol (e.g.
// Anthropic uses "input_schema" for tools, "tool_use" content blocks, and
// a top-level "system" field) override this method.
buildRequestBodyFromRequest(req *Request) map[string]any
// parseMCPResponse extracts the plain-text reply from a non-streaming
// response body.
parseMCPResponse(body []byte) (string, error)
// parseMCPResponseFull extracts both text and tool calls. Providers whose
// response envelope differs from the OpenAI choices[] structure (e.g.
// Anthropic content[] with tool_use blocks) override this method.
parseMCPResponseFull(body []byte) (*LLMResponse, error)
isRetryableError(err error) bool
}