mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 14:00:57 +08:00
Extract duplicated doRequestWithPayment/call/CallWithRequestFull/buildRequest/ setAuthHeader (~165 lines x3) into shared helpers in mcp/x402.go. Consolidate shared types (x402v2PaymentRequired, x402AcceptOption, x402Resource) and remove duplicate Solana types. Fix validAfter to 0 (official SDK standard), drain 402 body before retry, log Payment-Response tx hash, check Payment-Required before X-Payment-Required.
167 lines
5.4 KiB
Go
167 lines
5.4 KiB
Go
package mcp
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
)
|
|
|
|
const (
|
|
ProviderClaw402 = "claw402"
|
|
DefaultClaw402URL = "https://claw402.ai"
|
|
DefaultClaw402Model = "deepseek"
|
|
)
|
|
|
|
// claw402ModelEndpoints maps user-friendly model names to claw402 API paths.
|
|
var claw402ModelEndpoints = map[string]string{
|
|
// OpenAI
|
|
"gpt-5.4": "/api/v1/ai/openai/chat/5.4",
|
|
"gpt-5.4-pro": "/api/v1/ai/openai/chat/5.4-pro",
|
|
"gpt-5.3": "/api/v1/ai/openai/chat/5.3",
|
|
"gpt-5-mini": "/api/v1/ai/openai/chat/5-mini",
|
|
// Anthropic
|
|
"claude-opus": "/api/v1/ai/anthropic/messages/opus",
|
|
// DeepSeek
|
|
"deepseek": "/api/v1/ai/deepseek/chat",
|
|
"deepseek-reasoner": "/api/v1/ai/deepseek/chat/reasoner",
|
|
// Qwen
|
|
"qwen-max": "/api/v1/ai/qwen/chat/max",
|
|
"qwen-plus": "/api/v1/ai/qwen/chat/plus",
|
|
"qwen-turbo": "/api/v1/ai/qwen/chat/turbo",
|
|
"qwen-flash": "/api/v1/ai/qwen/chat/flash",
|
|
// Grok
|
|
"grok-4.1": "/api/v1/ai/grok/chat/4.1",
|
|
// Gemini
|
|
"gemini-3.1-pro": "/api/v1/ai/gemini/chat/3.1-pro",
|
|
// Kimi
|
|
"kimi-k2.5": "/api/v1/ai/kimi/chat/k2.5",
|
|
}
|
|
|
|
// Claw402Client implements AIClient using claw402.ai's x402 v2 USDC payment gateway.
|
|
// Reuses the same EIP-712 signing as BlockRunBaseClient (same Base chain + USDC contract).
|
|
// When the selected model routes to an Anthropic endpoint, it automatically uses
|
|
// the Anthropic wire format for requests and responses (via an internal ClaudeClient).
|
|
type Claw402Client struct {
|
|
*Client
|
|
privateKey *ecdsa.PrivateKey
|
|
claudeProxy *ClaudeClient // non-nil when endpoint is /anthropic/
|
|
}
|
|
|
|
// NewClaw402Client creates a claw402 client (backward compatible).
|
|
func NewClaw402Client() AIClient {
|
|
return NewClaw402ClientWithOptions()
|
|
}
|
|
|
|
// NewClaw402ClientWithOptions creates a claw402 client with options.
|
|
func NewClaw402ClientWithOptions(opts ...ClientOption) AIClient {
|
|
baseOpts := []ClientOption{
|
|
WithProvider(ProviderClaw402),
|
|
WithModel(DefaultClaw402Model),
|
|
WithBaseURL(DefaultClaw402URL),
|
|
}
|
|
allOpts := append(baseOpts, opts...)
|
|
baseClient := NewClient(allOpts...).(*Client)
|
|
baseClient.UseFullURL = true
|
|
baseClient.BaseURL = DefaultClaw402URL + claw402ModelEndpoints[DefaultClaw402Model]
|
|
|
|
c := &Claw402Client{Client: baseClient}
|
|
baseClient.hooks = c
|
|
return c
|
|
}
|
|
|
|
// SetAPIKey stores the EVM private key and selects the model endpoint.
|
|
func (c *Claw402Client) SetAPIKey(apiKey string, _ string, customModel string) {
|
|
hexKey := strings.TrimPrefix(apiKey, "0x")
|
|
privKey, err := crypto.HexToECDSA(hexKey)
|
|
if err != nil {
|
|
c.logger.Warnf("⚠️ [MCP] Claw402: invalid private key: %v", err)
|
|
} else {
|
|
c.privateKey = privKey
|
|
c.APIKey = apiKey
|
|
addr := crypto.PubkeyToAddress(privKey.PublicKey).Hex()
|
|
c.logger.Infof("🔧 [MCP] Claw402 wallet: %s", addr)
|
|
}
|
|
if customModel != "" {
|
|
c.Model = customModel
|
|
}
|
|
endpoint := c.resolveEndpoint()
|
|
c.BaseURL = DefaultClaw402URL + endpoint
|
|
|
|
// Anthropic endpoints need different wire format (Messages API)
|
|
if strings.Contains(endpoint, "/anthropic/") {
|
|
c.claudeProxy = &ClaudeClient{Client: c.Client}
|
|
c.logger.Infof("🔧 [MCP] Claw402 model: %s → %s (Anthropic format)", c.Model, endpoint)
|
|
} else {
|
|
c.claudeProxy = nil
|
|
c.logger.Infof("🔧 [MCP] Claw402 model: %s → %s", c.Model, endpoint)
|
|
}
|
|
}
|
|
|
|
// resolveEndpoint returns the API path for the configured model.
|
|
func (c *Claw402Client) resolveEndpoint() string {
|
|
if ep, ok := claw402ModelEndpoints[c.Model]; ok {
|
|
return ep
|
|
}
|
|
// Allow raw path override (e.g. "/api/v1/ai/openai/chat/5.4")
|
|
if strings.HasPrefix(c.Model, "/api/") {
|
|
return c.Model
|
|
}
|
|
return claw402ModelEndpoints[DefaultClaw402Model]
|
|
}
|
|
|
|
func (c *Claw402Client) setAuthHeader(h http.Header) { x402SetAuthHeader(h) }
|
|
|
|
func (c *Claw402Client) call(systemPrompt, userPrompt string) (string, error) {
|
|
return x402Call(c.Client, c.signPayment, "Claw402", systemPrompt, userPrompt)
|
|
}
|
|
|
|
func (c *Claw402Client) CallWithRequestFull(req *Request) (*LLMResponse, error) {
|
|
return x402CallFull(c.Client, c.signPayment, "Claw402", req)
|
|
}
|
|
|
|
// signPayment signs x402 v2 EIP-712 payment (same Base chain + USDC as BlockRunBase).
|
|
func (c *Claw402Client) signPayment(paymentHeaderB64 string) (string, error) {
|
|
return signBasePaymentHeader(c.privateKey, paymentHeaderB64, "Claw402")
|
|
}
|
|
|
|
// ── Format overrides for Anthropic endpoints ─────────────────────────────────
|
|
|
|
func (c *Claw402Client) buildMCPRequestBody(systemPrompt, userPrompt string) map[string]any {
|
|
if c.claudeProxy != nil {
|
|
return c.claudeProxy.buildMCPRequestBody(systemPrompt, userPrompt)
|
|
}
|
|
return c.Client.buildMCPRequestBody(systemPrompt, userPrompt)
|
|
}
|
|
|
|
func (c *Claw402Client) buildRequestBodyFromRequest(req *Request) map[string]any {
|
|
if c.claudeProxy != nil {
|
|
return c.claudeProxy.buildRequestBodyFromRequest(req)
|
|
}
|
|
return c.Client.buildRequestBodyFromRequest(req)
|
|
}
|
|
|
|
func (c *Claw402Client) parseMCPResponse(body []byte) (string, error) {
|
|
if c.claudeProxy != nil {
|
|
return c.claudeProxy.parseMCPResponse(body)
|
|
}
|
|
return c.Client.parseMCPResponse(body)
|
|
}
|
|
|
|
func (c *Claw402Client) parseMCPResponseFull(body []byte) (*LLMResponse, error) {
|
|
if c.claudeProxy != nil {
|
|
return c.claudeProxy.parseMCPResponseFull(body)
|
|
}
|
|
return c.Client.parseMCPResponseFull(body)
|
|
}
|
|
|
|
// buildUrl returns the full claw402 endpoint URL.
|
|
func (c *Claw402Client) buildUrl() string {
|
|
return c.BaseURL
|
|
}
|
|
|
|
func (c *Claw402Client) buildRequest(url string, jsonData []byte) (*http.Request, error) {
|
|
return x402BuildRequest(url, jsonData)
|
|
}
|