mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user