mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
refactor: restructure project directories for better modularity
- Delete llm/ dead code (3 files, zero references) - Split mcp/ into sub-packages: mcp/provider/ (8 providers) and mcp/payment/ (4 payment clients) with registry pattern - Export Client internal fields and ClientHooks interface for sub-package access - Split api/server.go (3892 lines) into 8 domain-specific handler files - Split trader/auto_trader.go (2296 lines) into 5 focused files - Reorganize web/src/components/ flat files into auth/, charts/, trader/, common/, modals/, backtest/ subdirectories - Update all consumer imports to use registry-based provider creation
This commit is contained in:
298
mcp/provider/claude.go
Normal file
298
mcp/provider/claude.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// Package provider — ClaudeClient implements the Anthropic Messages API.
|
||||
//
|
||||
// Wire-format differences from the OpenAI-compatible base Client:
|
||||
//
|
||||
// ┌─────────────────────┬───────────────────────────┬─────────────────────────────────┐
|
||||
// │ Concept │ OpenAI format │ Anthropic format │
|
||||
// ├─────────────────────┼───────────────────────────┼─────────────────────────────────┤
|
||||
// │ Endpoint │ /v1/chat/completions │ /v1/messages │
|
||||
// │ Auth header │ Authorization: Bearer xxx │ x-api-key: xxx │
|
||||
// │ System prompt │ messages[0] role=system │ top-level "system" field │
|
||||
// │ Tool definition │ type=function + parameters │ name + description + input_schema│
|
||||
// │ Tool choice │ "auto" (string) │ {"type":"auto"} (object) │
|
||||
// │ Assistant tool call │ tool_calls array │ content[{type:tool_use,...}] │
|
||||
// │ Tool result │ role=tool + tool_call_id │ role=user content[tool_result] │
|
||||
// │ Max tokens │ max_tokens │ max_tokens (same) │
|
||||
// └─────────────────────┴───────────────────────────┴─────────────────────────────────┘
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultClaudeBaseURL = "https://api.anthropic.com/v1"
|
||||
DefaultClaudeModel = "claude-opus-4-6"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderClaude, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewClaudeClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
// ClaudeClient wraps the base Client and overrides the methods that differ
|
||||
// for the Anthropic Messages API. All other behaviour (retry, timeout,
|
||||
// logging) is inherited unchanged.
|
||||
type ClaudeClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *ClaudeClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewClaudeClient creates a ClaudeClient with default settings.
|
||||
func NewClaudeClient() mcp.AIClient {
|
||||
return NewClaudeClientWithOptions()
|
||||
}
|
||||
|
||||
// NewClaudeClientWithOptions creates a ClaudeClient with optional overrides.
|
||||
func NewClaudeClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
baseClient := mcp.NewClient(append([]mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderClaude),
|
||||
mcp.WithModel(DefaultClaudeModel),
|
||||
mcp.WithBaseURL(DefaultClaudeBaseURL),
|
||||
}, opts...)...).(*mcp.Client)
|
||||
|
||||
c := &ClaudeClient{Client: baseClient}
|
||||
baseClient.Hooks = c // wire dynamic dispatch to ClaudeClient
|
||||
return c
|
||||
}
|
||||
|
||||
// ── Hook overrides ────────────────────────────────────────────────────────────
|
||||
|
||||
// SetAPIKey stores credentials and optional custom endpoint / model.
|
||||
func (c *ClaudeClient) SetAPIKey(apiKey, customURL, customModel string) {
|
||||
c.APIKey = apiKey
|
||||
if len(apiKey) > 8 {
|
||||
c.Log.Infof("🔧 [MCP] Claude API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
c.BaseURL = customURL
|
||||
c.Log.Infof("🔧 [MCP] Claude BaseURL: %s", customURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] Claude Model: %s", customModel)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthHeader uses x-api-key instead of Authorization: Bearer.
|
||||
func (c *ClaudeClient) SetAuthHeader(h http.Header) {
|
||||
h.Set("x-api-key", c.APIKey)
|
||||
h.Set("anthropic-version", "2023-06-01")
|
||||
}
|
||||
|
||||
// BuildUrl targets /messages instead of /chat/completions.
|
||||
func (c *ClaudeClient) BuildUrl() string {
|
||||
return fmt.Sprintf("%s/messages", c.BaseURL)
|
||||
}
|
||||
|
||||
// BuildMCPRequestBody builds the Anthropic wire format for the simple
|
||||
// CallWithMessages path (no tool support).
|
||||
func (c *ClaudeClient) BuildMCPRequestBody(systemPrompt, userPrompt string) map[string]any {
|
||||
return map[string]any{
|
||||
"model": c.Model,
|
||||
"max_tokens": c.MaxTokens,
|
||||
"system": systemPrompt,
|
||||
"messages": []map[string]string{
|
||||
{"role": "user", "content": userPrompt},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildRequestBodyFromRequest converts a *Request into the Anthropic Messages
|
||||
// API wire format.
|
||||
func (c *ClaudeClient) BuildRequestBodyFromRequest(req *mcp.Request) map[string]any {
|
||||
// ── 1. Separate system prompt from conversation messages ──────────────────
|
||||
var systemPrompt string
|
||||
var convMsgs []mcp.Message
|
||||
for _, m := range req.Messages {
|
||||
if m.Role == "system" {
|
||||
systemPrompt = m.Content
|
||||
} else {
|
||||
convMsgs = append(convMsgs, m)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Convert messages to Anthropic format ───────────────────────────────
|
||||
anthropicMsgs := ConvertMessagesToAnthropic(convMsgs)
|
||||
|
||||
// ── 3. Convert tool definitions (parameters → input_schema) ──────────────
|
||||
var anthropicTools []map[string]any
|
||||
for _, t := range req.Tools {
|
||||
anthropicTools = append(anthropicTools, map[string]any{
|
||||
"name": t.Function.Name,
|
||||
"description": t.Function.Description,
|
||||
"input_schema": t.Function.Parameters,
|
||||
})
|
||||
}
|
||||
|
||||
// ── 4. Assemble request body ──────────────────────────────────────────────
|
||||
body := map[string]any{
|
||||
"model": req.Model,
|
||||
"max_tokens": c.MaxTokens,
|
||||
"system": systemPrompt,
|
||||
"messages": anthropicMsgs,
|
||||
}
|
||||
|
||||
if len(anthropicTools) > 0 {
|
||||
body["tools"] = anthropicTools
|
||||
}
|
||||
|
||||
// tool_choice: Anthropic uses an object, not a string.
|
||||
switch req.ToolChoice {
|
||||
case "auto":
|
||||
body["tool_choice"] = map[string]any{"type": "auto"}
|
||||
case "any":
|
||||
body["tool_choice"] = map[string]any{"type": "any"}
|
||||
case "none", "":
|
||||
// omit — no tool_choice sent
|
||||
}
|
||||
|
||||
if req.Temperature != nil {
|
||||
body["temperature"] = *req.Temperature
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// ConvertMessagesToAnthropic translates from the OpenAI-shaped mcp.Message
|
||||
// slice to Anthropic's messages array.
|
||||
func ConvertMessagesToAnthropic(msgs []mcp.Message) []map[string]any {
|
||||
var out []map[string]any
|
||||
|
||||
for i := 0; i < len(msgs); {
|
||||
msg := msgs[i]
|
||||
|
||||
switch {
|
||||
// ── Assistant message carrying tool calls ─────────────────────────────
|
||||
case msg.Role == "assistant" && len(msg.ToolCalls) > 0:
|
||||
var blocks []map[string]any
|
||||
for _, tc := range msg.ToolCalls {
|
||||
// Arguments are a JSON string; Claude wants a parsed object.
|
||||
var input map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
|
||||
input = map[string]any{"_raw": tc.Function.Arguments}
|
||||
}
|
||||
blocks = append(blocks, map[string]any{
|
||||
"type": "tool_use",
|
||||
"id": tc.ID,
|
||||
"name": tc.Function.Name,
|
||||
"input": input,
|
||||
})
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"role": "assistant",
|
||||
"content": blocks,
|
||||
})
|
||||
i++
|
||||
|
||||
// ── Tool result message(s) → single user turn ─────────────────────────
|
||||
case msg.Role == "tool":
|
||||
// Collect all consecutive tool-result messages.
|
||||
var blocks []map[string]any
|
||||
for i < len(msgs) && msgs[i].Role == "tool" {
|
||||
blocks = append(blocks, map[string]any{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": msgs[i].ToolCallID,
|
||||
"content": msgs[i].Content,
|
||||
})
|
||||
i++
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"role": "user",
|
||||
"content": blocks,
|
||||
})
|
||||
|
||||
// ── Regular user / assistant text message ─────────────────────────────
|
||||
default:
|
||||
out = append(out, map[string]any{
|
||||
"role": msg.Role,
|
||||
"content": msg.Content,
|
||||
})
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Response parsers ──────────────────────────────────────────────────────────
|
||||
|
||||
// ParseMCPResponse extracts the plain-text reply from an Anthropic response.
|
||||
func (c *ClaudeClient) ParseMCPResponse(body []byte) (string, error) {
|
||||
r, err := c.ParseMCPResponseFull(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Content, nil
|
||||
}
|
||||
|
||||
// ParseMCPResponseFull extracts both text and tool calls from an Anthropic
|
||||
// response envelope.
|
||||
func (c *ClaudeClient) ParseMCPResponseFull(body []byte) (*mcp.LLMResponse, error) {
|
||||
var raw struct {
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
} `json:"content"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
Error *struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Anthropic response: %w — body: %s", err, body)
|
||||
}
|
||||
if raw.Error != nil {
|
||||
return nil, fmt.Errorf("Anthropic API error: %s — %s", raw.Error.Type, raw.Error.Message)
|
||||
}
|
||||
|
||||
total := raw.Usage.InputTokens + raw.Usage.OutputTokens
|
||||
if mcp.TokenUsageCallback != nil && total > 0 {
|
||||
mcp.TokenUsageCallback(mcp.TokenUsage{
|
||||
Provider: c.Provider,
|
||||
Model: c.Model,
|
||||
PromptTokens: raw.Usage.InputTokens,
|
||||
CompletionTokens: raw.Usage.OutputTokens,
|
||||
TotalTokens: total,
|
||||
})
|
||||
}
|
||||
|
||||
result := &mcp.LLMResponse{}
|
||||
for _, block := range raw.Content {
|
||||
switch block.Type {
|
||||
case "text":
|
||||
result.Content = block.Text
|
||||
|
||||
case "tool_use":
|
||||
// Input is a JSON object; serialise back to a JSON string so it
|
||||
// matches the ToolCallFunction.Arguments field (always a string).
|
||||
argsJSON, err := json.Marshal(block.Input)
|
||||
if err != nil {
|
||||
argsJSON = []byte("{}")
|
||||
}
|
||||
result.ToolCalls = append(result.ToolCalls, mcp.ToolCall{
|
||||
ID: block.ID,
|
||||
Type: "function",
|
||||
Function: mcp.ToolCallFunction{
|
||||
Name: block.Name,
|
||||
Arguments: string(argsJSON),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
69
mcp/provider/deepseek.go
Normal file
69
mcp/provider/deepseek.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderDeepSeek, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewDeepSeekClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type DeepSeekClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *DeepSeekClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewDeepSeekClient creates DeepSeek client (backward compatible)
|
||||
//
|
||||
// Deprecated: Recommend using NewDeepSeekClientWithOptions for better flexibility
|
||||
func NewDeepSeekClient() mcp.AIClient {
|
||||
return NewDeepSeekClientWithOptions()
|
||||
}
|
||||
|
||||
// NewDeepSeekClientWithOptions creates DeepSeek client (supports options pattern)
|
||||
func NewDeepSeekClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
deepseekOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderDeepSeek),
|
||||
mcp.WithModel(mcp.DefaultDeepSeekModel),
|
||||
mcp.WithBaseURL(mcp.DefaultDeepSeekBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(deepseekOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
dsClient := &DeepSeekClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = dsClient
|
||||
return dsClient
|
||||
}
|
||||
|
||||
func (dsClient *DeepSeekClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
dsClient.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
dsClient.Log.Infof("🔧 [MCP] DeepSeek API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
dsClient.BaseURL = customURL
|
||||
dsClient.Log.Infof("🔧 [MCP] DeepSeek using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
dsClient.Log.Infof("🔧 [MCP] DeepSeek using default BaseURL: %s", dsClient.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
dsClient.Model = customModel
|
||||
dsClient.Log.Infof("🔧 [MCP] DeepSeek using custom Model: %s", customModel)
|
||||
} else {
|
||||
dsClient.Log.Infof("🔧 [MCP] DeepSeek using default Model: %s", dsClient.Model)
|
||||
}
|
||||
}
|
||||
|
||||
func (dsClient *DeepSeekClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
dsClient.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
73
mcp/provider/gemini.go
Normal file
73
mcp/provider/gemini.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultGeminiBaseURL = "https://generativelanguage.googleapis.com/v1beta/openai"
|
||||
DefaultGeminiModel = "gemini-3-pro-preview"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderGemini, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewGeminiClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type GeminiClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *GeminiClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewGeminiClient creates Gemini client (backward compatible)
|
||||
func NewGeminiClient() mcp.AIClient {
|
||||
return NewGeminiClientWithOptions()
|
||||
}
|
||||
|
||||
// NewGeminiClientWithOptions creates Gemini client (supports options pattern)
|
||||
func NewGeminiClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
geminiOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderGemini),
|
||||
mcp.WithModel(DefaultGeminiModel),
|
||||
mcp.WithBaseURL(DefaultGeminiBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(geminiOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
geminiClient := &GeminiClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = geminiClient
|
||||
return geminiClient
|
||||
}
|
||||
|
||||
func (c *GeminiClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
c.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
c.Log.Infof("🔧 [MCP] Gemini API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
c.BaseURL = customURL
|
||||
c.Log.Infof("🔧 [MCP] Gemini using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] Gemini using default BaseURL: %s", c.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] Gemini using custom Model: %s", customModel)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] Gemini using default Model: %s", c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini OpenAI-compatible API uses standard Bearer auth
|
||||
func (c *GeminiClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
c.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
73
mcp/provider/grok.go
Normal file
73
mcp/provider/grok.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultGrokBaseURL = "https://api.x.ai/v1"
|
||||
DefaultGrokModel = "grok-3-latest"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderGrok, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewGrokClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type GrokClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *GrokClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewGrokClient creates Grok client (backward compatible)
|
||||
func NewGrokClient() mcp.AIClient {
|
||||
return NewGrokClientWithOptions()
|
||||
}
|
||||
|
||||
// NewGrokClientWithOptions creates Grok client (supports options pattern)
|
||||
func NewGrokClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
grokOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderGrok),
|
||||
mcp.WithModel(DefaultGrokModel),
|
||||
mcp.WithBaseURL(DefaultGrokBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(grokOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
grokClient := &GrokClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = grokClient
|
||||
return grokClient
|
||||
}
|
||||
|
||||
func (c *GrokClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
c.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
c.Log.Infof("🔧 [MCP] Grok API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
c.BaseURL = customURL
|
||||
c.Log.Infof("🔧 [MCP] Grok using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] Grok using default BaseURL: %s", c.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] Grok using custom Model: %s", customModel)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] Grok using default Model: %s", c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
// Grok uses standard OpenAI-compatible API with Bearer auth
|
||||
func (c *GrokClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
c.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
73
mcp/provider/kimi.go
Normal file
73
mcp/provider/kimi.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultKimiBaseURL = "https://api.moonshot.ai/v1" // Global endpoint (use api.moonshot.cn for China)
|
||||
DefaultKimiModel = "moonshot-v1-auto"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderKimi, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewKimiClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type KimiClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *KimiClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewKimiClient creates Kimi (Moonshot) client (backward compatible)
|
||||
func NewKimiClient() mcp.AIClient {
|
||||
return NewKimiClientWithOptions()
|
||||
}
|
||||
|
||||
// NewKimiClientWithOptions creates Kimi client (supports options pattern)
|
||||
func NewKimiClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
kimiOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderKimi),
|
||||
mcp.WithModel(DefaultKimiModel),
|
||||
mcp.WithBaseURL(DefaultKimiBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(kimiOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
kimiClient := &KimiClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = kimiClient
|
||||
return kimiClient
|
||||
}
|
||||
|
||||
func (c *KimiClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
c.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
c.Log.Infof("🔧 [MCP] Kimi API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
c.BaseURL = customURL
|
||||
c.Log.Infof("🔧 [MCP] Kimi using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] Kimi using default BaseURL: %s", c.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] Kimi using custom Model: %s", customModel)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] Kimi using default Model: %s", c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
// Kimi uses standard OpenAI-compatible API
|
||||
func (c *KimiClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
c.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
73
mcp/provider/minimax.go
Normal file
73
mcp/provider/minimax.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMiniMaxBaseURL = "https://api.minimax.io/v1"
|
||||
DefaultMiniMaxModel = "MiniMax-M2.5"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderMiniMax, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewMiniMaxClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type MiniMaxClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *MiniMaxClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewMiniMaxClient creates MiniMax client (backward compatible)
|
||||
func NewMiniMaxClient() mcp.AIClient {
|
||||
return NewMiniMaxClientWithOptions()
|
||||
}
|
||||
|
||||
// NewMiniMaxClientWithOptions creates MiniMax client (supports options pattern)
|
||||
func NewMiniMaxClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
minimaxOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderMiniMax),
|
||||
mcp.WithModel(DefaultMiniMaxModel),
|
||||
mcp.WithBaseURL(DefaultMiniMaxBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(minimaxOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
minimaxClient := &MiniMaxClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = minimaxClient
|
||||
return minimaxClient
|
||||
}
|
||||
|
||||
func (c *MiniMaxClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
c.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
c.Log.Infof("🔧 [MCP] MiniMax API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
c.BaseURL = customURL
|
||||
c.Log.Infof("🔧 [MCP] MiniMax using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] MiniMax using default BaseURL: %s", c.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] MiniMax using custom Model: %s", customModel)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] MiniMax using default Model: %s", c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
// MiniMax uses standard OpenAI-compatible API with Bearer auth
|
||||
func (c *MiniMaxClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
c.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
73
mcp/provider/openai.go
Normal file
73
mcp/provider/openai.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultOpenAIBaseURL = "https://api.openai.com/v1"
|
||||
DefaultOpenAIModel = "gpt-5.4"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderOpenAI, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewOpenAIClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type OpenAIClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *OpenAIClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewOpenAIClient creates OpenAI client (backward compatible)
|
||||
func NewOpenAIClient() mcp.AIClient {
|
||||
return NewOpenAIClientWithOptions()
|
||||
}
|
||||
|
||||
// NewOpenAIClientWithOptions creates OpenAI client (supports options pattern)
|
||||
func NewOpenAIClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
openaiOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderOpenAI),
|
||||
mcp.WithModel(DefaultOpenAIModel),
|
||||
mcp.WithBaseURL(DefaultOpenAIBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(openaiOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
openaiClient := &OpenAIClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = openaiClient
|
||||
return openaiClient
|
||||
}
|
||||
|
||||
func (c *OpenAIClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
c.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
c.Log.Infof("🔧 [MCP] OpenAI API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
c.BaseURL = customURL
|
||||
c.Log.Infof("🔧 [MCP] OpenAI using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] OpenAI using default BaseURL: %s", c.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
c.Model = customModel
|
||||
c.Log.Infof("🔧 [MCP] OpenAI using custom Model: %s", customModel)
|
||||
} else {
|
||||
c.Log.Infof("🔧 [MCP] OpenAI using default Model: %s", c.Model)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI uses standard Bearer auth
|
||||
func (c *OpenAIClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
c.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
83
mcp/provider/options_test.go
Normal file
83
mcp/provider/options_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
func TestOptionsWithDeepSeekClient(t *testing.T) {
|
||||
logger := mcp.NewNoopLogger()
|
||||
|
||||
client := NewDeepSeekClientWithOptions(
|
||||
mcp.WithAPIKey("sk-deepseek-key"),
|
||||
mcp.WithLogger(logger),
|
||||
mcp.WithMaxTokens(5000),
|
||||
)
|
||||
|
||||
dsClient := client.(*DeepSeekClient)
|
||||
|
||||
// Verify DeepSeek default values
|
||||
if dsClient.Provider != mcp.ProviderDeepSeek {
|
||||
t.Error("Provider should be DeepSeek")
|
||||
}
|
||||
|
||||
if dsClient.BaseURL != mcp.DefaultDeepSeekBaseURL {
|
||||
t.Error("BaseURL should be DeepSeek default")
|
||||
}
|
||||
|
||||
if dsClient.Model != mcp.DefaultDeepSeekModel {
|
||||
t.Error("Model should be DeepSeek default")
|
||||
}
|
||||
|
||||
// Verify custom options
|
||||
if dsClient.APIKey != "sk-deepseek-key" {
|
||||
t.Error("APIKey should be set from options")
|
||||
}
|
||||
|
||||
if dsClient.Log != logger {
|
||||
t.Error("Log should be set from options")
|
||||
}
|
||||
|
||||
if dsClient.MaxTokens != 5000 {
|
||||
t.Error("MaxTokens should be 5000")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsWithQwenClient(t *testing.T) {
|
||||
logger := mcp.NewNoopLogger()
|
||||
|
||||
client := NewQwenClientWithOptions(
|
||||
mcp.WithAPIKey("sk-qwen-key"),
|
||||
mcp.WithLogger(logger),
|
||||
mcp.WithMaxTokens(6000),
|
||||
)
|
||||
|
||||
qwenClient := client.(*QwenClient)
|
||||
|
||||
// Verify Qwen default values
|
||||
if qwenClient.Provider != mcp.ProviderQwen {
|
||||
t.Error("Provider should be Qwen")
|
||||
}
|
||||
|
||||
if qwenClient.BaseURL != mcp.DefaultQwenBaseURL {
|
||||
t.Error("BaseURL should be Qwen default")
|
||||
}
|
||||
|
||||
if qwenClient.Model != mcp.DefaultQwenModel {
|
||||
t.Error("Model should be Qwen default")
|
||||
}
|
||||
|
||||
// Verify custom options
|
||||
if qwenClient.APIKey != "sk-qwen-key" {
|
||||
t.Error("APIKey should be set from options")
|
||||
}
|
||||
|
||||
if qwenClient.Log != logger {
|
||||
t.Error("Log should be set from options")
|
||||
}
|
||||
|
||||
if qwenClient.MaxTokens != 6000 {
|
||||
t.Error("MaxTokens should be 6000")
|
||||
}
|
||||
}
|
||||
74
mcp/provider/qwen.go
Normal file
74
mcp/provider/qwen.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"nofx/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultQwenBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
DefaultQwenModel = "qwen3-max"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mcp.RegisterProvider(mcp.ProviderQwen, func(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
return NewQwenClientWithOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
type QwenClient struct {
|
||||
*mcp.Client
|
||||
}
|
||||
|
||||
func (c *QwenClient) BaseClient() *mcp.Client { return c.Client }
|
||||
|
||||
// NewQwenClient creates Qwen client (backward compatible)
|
||||
//
|
||||
// Deprecated: Recommend using NewQwenClientWithOptions for better flexibility
|
||||
func NewQwenClient() mcp.AIClient {
|
||||
return NewQwenClientWithOptions()
|
||||
}
|
||||
|
||||
// NewQwenClientWithOptions creates Qwen client (supports options pattern)
|
||||
func NewQwenClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
||||
qwenOpts := []mcp.ClientOption{
|
||||
mcp.WithProvider(mcp.ProviderQwen),
|
||||
mcp.WithModel(DefaultQwenModel),
|
||||
mcp.WithBaseURL(DefaultQwenBaseURL),
|
||||
}
|
||||
|
||||
allOpts := append(qwenOpts, opts...)
|
||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||
|
||||
qwenClient := &QwenClient{
|
||||
Client: baseClient,
|
||||
}
|
||||
|
||||
baseClient.Hooks = qwenClient
|
||||
return qwenClient
|
||||
}
|
||||
|
||||
func (qwenClient *QwenClient) SetAPIKey(apiKey string, customURL string, customModel string) {
|
||||
qwenClient.APIKey = apiKey
|
||||
|
||||
if len(apiKey) > 8 {
|
||||
qwenClient.Log.Infof("🔧 [MCP] Qwen API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:])
|
||||
}
|
||||
if customURL != "" {
|
||||
qwenClient.BaseURL = customURL
|
||||
qwenClient.Log.Infof("🔧 [MCP] Qwen using custom BaseURL: %s", customURL)
|
||||
} else {
|
||||
qwenClient.Log.Infof("🔧 [MCP] Qwen using default BaseURL: %s", qwenClient.BaseURL)
|
||||
}
|
||||
if customModel != "" {
|
||||
qwenClient.Model = customModel
|
||||
qwenClient.Log.Infof("🔧 [MCP] Qwen using custom Model: %s", customModel)
|
||||
} else {
|
||||
qwenClient.Log.Infof("🔧 [MCP] Qwen using default Model: %s", qwenClient.Model)
|
||||
}
|
||||
}
|
||||
|
||||
func (qwenClient *QwenClient) SetAuthHeader(reqHeaders http.Header) {
|
||||
qwenClient.Client.SetAuthHeader(reqHeaders)
|
||||
}
|
||||
Reference in New Issue
Block a user