diff --git a/agent/agent.go b/agent/agent.go index 6f4223d0..0b1d1e21 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -165,13 +165,26 @@ func (a *Agent) loadAIClientFromStoreUser(storeUserID string) (mcp.AIClient, str apiKey := strings.TrimSpace(string(model.APIKey)) customAPIURL := strings.TrimSpace(model.CustomAPIURL) modelName := strings.TrimSpace(model.CustomModelName) - customAPIURL, modelName = resolveModelRuntimeConfig(model.Provider, customAPIURL, modelName, model.ID) + provider := strings.ToLower(strings.TrimSpace(model.Provider)) + + // Use the provider registry for providers like claw402 that have their own + // client implementation (x402 payment, custom auth, etc.). + if client := mcp.NewAIClientByProvider(provider); client != nil { + if modelName == "" { + modelName = model.ID + } + client.SetAPIKey(apiKey, customAPIURL, modelName) + a.log().Info("agent AI client selected (provider registry)", "store_user_id", candidateUserID, "model_id", model.ID, "provider", provider, "model", modelName) + return client, modelName, true + } + + customAPIURL, modelName = resolveModelRuntimeConfig(provider, customAPIURL, modelName, model.ID) if apiKey == "" || customAPIURL == "" { a.log().Warn( "skipping incomplete enabled AI model", "store_user_id", candidateUserID, "model_id", model.ID, - "provider", model.Provider, + "provider", provider, "has_api_key", apiKey != "", "has_custom_api_url", customAPIURL != "", ) diff --git a/agent/planner_runtime.go b/agent/planner_runtime.go index 95839e28..cd005e17 100644 --- a/agent/planner_runtime.go +++ b/agent/planner_runtime.go @@ -4014,6 +4014,9 @@ func (a *Agent) thinkAndActLegacyWithStore(ctx context.Context, storeUserID stri if resp.Content != "" { assistantMsg.Content = resp.Content } + if resp.ReasoningContent != "" { + assistantMsg.ReasoningContent = resp.ReasoningContent + } messages = append(messages, assistantMsg) for _, tc := range resp.ToolCalls { diff --git a/mcp/client.go b/mcp/client.go index 97b4cded..3440ee91 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -278,8 +278,9 @@ func (client *Client) ParseMCPResponseFull(body []byte) (*LLMResponse, error) { var result struct { Choices []struct { Message struct { - Content string `json:"content"` - ToolCalls []ToolCall `json:"tool_calls"` + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + ToolCalls []ToolCall `json:"tool_calls"` } `json:"message"` } `json:"choices"` Usage struct { @@ -310,8 +311,9 @@ func (client *Client) ParseMCPResponseFull(body []byte) (*LLMResponse, error) { msg := result.Choices[0].Message return &LLMResponse{ - Content: msg.Content, - ToolCalls: msg.ToolCalls, + Content: msg.Content, + ReasoningContent: msg.ReasoningContent, + ToolCalls: msg.ToolCalls, }, nil } @@ -624,6 +626,11 @@ func (client *Client) BuildRequestBodyFromRequest(req *Request) map[string]any { } else { m["content"] = msg.Content } + // DeepSeek thinking models require reasoning_content to be echoed back + // in multi-turn conversations when present in assistant messages. + if msg.ReasoningContent != "" { + m["reasoning_content"] = msg.ReasoningContent + } messages = append(messages, m) } diff --git a/mcp/request.go b/mcp/request.go index 5359644c..33f57bbb 100644 --- a/mcp/request.go +++ b/mcp/request.go @@ -6,10 +6,11 @@ import "context" // Supports plain messages (Role+Content), assistant tool-call messages (ToolCalls), // and tool result messages (Role="tool", ToolCallID, Content). type Message struct { - Role string `json:"role"` // "system", "user", "assistant", "tool" - Content string `json:"content,omitempty"` // Text content (omitted when ToolCalls present) - ToolCalls []ToolCall `json:"tool_calls,omitempty"` // Set by assistant when calling tools - ToolCallID string `json:"tool_call_id,omitempty"` // Set on role="tool" result messages + Role string `json:"role"` // "system", "user", "assistant", "tool" + Content string `json:"content,omitempty"` // Text content (omitted when ToolCalls present) + ReasoningContent string `json:"reasoning_content,omitempty"` // Thinking-model reasoning (must be echoed back in multi-turn) + ToolCalls []ToolCall `json:"tool_calls,omitempty"` // Set by assistant when calling tools + ToolCallID string `json:"tool_call_id,omitempty"` // Set on role="tool" result messages } // ToolCall is a single function call requested by the LLM. @@ -29,8 +30,9 @@ type ToolCallFunction struct { // text reply (Content) and any structured tool calls (ToolCalls). // Exactly one of the two fields will be non-empty for a well-formed response. type LLMResponse struct { - Content string // Plain-text reply (final answer) - ToolCalls []ToolCall // Structured tool invocations + Content string // Plain-text reply (final answer) + ReasoningContent string // Thinking-model reasoning content + ToolCalls []ToolCall // Structured tool invocations } // Tool represents a tool/function that AI can call diff --git a/web/src/components/common/HeaderBar.tsx b/web/src/components/common/HeaderBar.tsx index 9faa919d..c60559e4 100644 --- a/web/src/components/common/HeaderBar.tsx +++ b/web/src/components/common/HeaderBar.tsx @@ -115,6 +115,7 @@ export default function HeaderBar({ page: 'agent', path: ROUTES.agent, label: 'Agent', + badge: 'Beta', requiresAuth: false, }, {