mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-01 01:51:19 +08:00
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.
62 lines
3.0 KiB
Go
62 lines
3.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// AIClient public AI client interface (for external use)
|
|
type AIClient interface {
|
|
SetAPIKey(apiKey string, customURL string, customModel string)
|
|
SetTimeout(timeout time.Duration)
|
|
CallWithMessages(systemPrompt, userPrompt string) (string, error)
|
|
CallWithRequest(req *Request) (string, error)
|
|
// CallWithRequestStream streams the LLM response via SSE.
|
|
// onChunk is called with the full accumulated text so far (not raw deltas).
|
|
// Returns the complete final text when done.
|
|
CallWithRequestStream(req *Request, onChunk func(string)) (string, error)
|
|
// CallWithRequestFull returns both text content and tool calls.
|
|
// Use this when the request includes Tools — the LLM may respond with
|
|
// either a plain text reply (LLMResponse.Content) or tool invocations
|
|
// (LLMResponse.ToolCalls), but not both.
|
|
CallWithRequestFull(req *Request) (*LLMResponse, error)
|
|
}
|
|
|
|
// 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 {
|
|
// ── 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
|
|
}
|