feat: Add Mistral AI provider support
- Use /v1/conversations API endpoint (different rate limits) - Convert messages to inputs format for Mistral API - Handle system prompts via instructions field - Support Mistral built-in web_search tool - Convert tool results to user messages for feedback loop - Parse response with array content format Closes #<issue>
This commit is contained in:
parent
214b201bfa
commit
d51bfc3736
3 changed files with 245 additions and 21 deletions
|
|
@ -107,6 +107,10 @@
|
||||||
"moonshot": {
|
"moonshot": {
|
||||||
"api_key": "sk-xxx",
|
"api_key": "sk-xxx",
|
||||||
"api_base": ""
|
"api_base": ""
|
||||||
|
},
|
||||||
|
"mistral": {
|
||||||
|
"api_key": "",
|
||||||
|
"api_base": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ type ProvidersConfig struct {
|
||||||
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
|
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
|
||||||
DeepSeek ProviderConfig `json:"deepseek"`
|
DeepSeek ProviderConfig `json:"deepseek"`
|
||||||
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
GitHubCopilot ProviderConfig `json:"github_copilot"`
|
||||||
|
Mistral ProviderConfig `json:"mistral"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderConfig struct {
|
type ProviderConfig struct {
|
||||||
|
|
@ -304,6 +305,7 @@ func DefaultConfig() *Config {
|
||||||
Nvidia: ProviderConfig{},
|
Nvidia: ProviderConfig{},
|
||||||
Moonshot: ProviderConfig{},
|
Moonshot: ProviderConfig{},
|
||||||
ShengSuanYun: ProviderConfig{},
|
ShengSuanYun: ProviderConfig{},
|
||||||
|
Mistral: ProviderConfig{},
|
||||||
},
|
},
|
||||||
Gateway: GatewayConfig{
|
Gateway: GatewayConfig{
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
|
|
@ -405,6 +407,9 @@ func (c *Config) GetAPIKey() string {
|
||||||
if c.Providers.ShengSuanYun.APIKey != "" {
|
if c.Providers.ShengSuanYun.APIKey != "" {
|
||||||
return c.Providers.ShengSuanYun.APIKey
|
return c.Providers.ShengSuanYun.APIKey
|
||||||
}
|
}
|
||||||
|
if c.Providers.Mistral.APIKey != "" {
|
||||||
|
return c.Providers.Mistral.APIKey
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,6 +428,12 @@ func (c *Config) GetAPIBase() string {
|
||||||
if c.Providers.VLLM.APIKey != "" && c.Providers.VLLM.APIBase != "" {
|
if c.Providers.VLLM.APIKey != "" && c.Providers.VLLM.APIBase != "" {
|
||||||
return c.Providers.VLLM.APIBase
|
return c.Providers.VLLM.APIBase
|
||||||
}
|
}
|
||||||
|
if c.Providers.Mistral.APIKey != "" {
|
||||||
|
if c.Providers.Mistral.APIBase != "" {
|
||||||
|
return c.Providers.Mistral.APIBase
|
||||||
|
}
|
||||||
|
return "https://api.mistral.ai/v1"
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,108 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
// Determine the endpoint and request format
|
||||||
|
// Mistral /v1/conversations uses "inputs" and "completion_args"
|
||||||
|
useConversations := strings.Contains(p.apiBase, "/conversations")
|
||||||
|
|
||||||
|
var requestBody map[string]interface{}
|
||||||
|
if useConversations {
|
||||||
|
// Mistral conversations API: filter out system messages and put them in instructions
|
||||||
|
// Also convert tool messages to user messages
|
||||||
|
var filteredInputs []Message
|
||||||
|
var systemContent string
|
||||||
|
|
||||||
|
for _, msg := range messages {
|
||||||
|
if msg.Role == "system" {
|
||||||
|
// Collect system content
|
||||||
|
if systemContent != "" {
|
||||||
|
systemContent += "\n\n"
|
||||||
|
}
|
||||||
|
systemContent += msg.Content
|
||||||
|
} else if msg.Role == "tool" {
|
||||||
|
// Convert tool results to user messages for Mistral
|
||||||
|
// Format: "The result of tool_name is: <result>"
|
||||||
|
toolName := ""
|
||||||
|
if len(msg.ToolCalls) > 0 {
|
||||||
|
toolName = msg.ToolCalls[0].Name
|
||||||
|
}
|
||||||
|
if toolName != "" {
|
||||||
|
filteredInputs = append(filteredInputs, Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "The result of " + toolName + " is: " + msg.Content,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
filteredInputs = append(filteredInputs, Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: msg.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only keep user and assistant messages
|
||||||
|
filteredInputs = append(filteredInputs, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert filteredInputs to []map[string]interface{} for JSON
|
||||||
|
inputsForJSON := make([]map[string]interface{}, len(filteredInputs))
|
||||||
|
for i, msg := range filteredInputs {
|
||||||
|
inputsForJSON[i] = map[string]interface{}{
|
||||||
|
"role": msg.Role,
|
||||||
|
"content": msg.Content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mistral conversations API format
|
||||||
|
requestBody = map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"inputs": inputsForJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add instructions from system message
|
||||||
|
if systemContent != "" {
|
||||||
|
requestBody["instructions"] = systemContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add completion_args for Mistral conversations API
|
||||||
|
completionArgs := map[string]interface{}{}
|
||||||
|
if maxTokens, ok := options["max_tokens"].(int); ok {
|
||||||
|
completionArgs["max_tokens"] = maxTokens
|
||||||
|
}
|
||||||
|
if temperature, ok := options["temperature"].(float64); ok {
|
||||||
|
completionArgs["temperature"] = temperature
|
||||||
|
}
|
||||||
|
if topP, ok := options["top_p"].(float64); ok {
|
||||||
|
completionArgs["top_p"] = topP
|
||||||
|
}
|
||||||
|
if len(completionArgs) > 0 {
|
||||||
|
requestBody["completion_args"] = completionArgs
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard OpenAI-compatible format
|
||||||
|
requestBody = map[string]interface{}{
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(tools) > 0 {
|
// Add tools for Mistral conversations API
|
||||||
|
// For Mistral, use built-in web_search instead of custom tools
|
||||||
|
if useConversations && len(tools) > 0 {
|
||||||
|
// Convert tools to Mistral format
|
||||||
|
mistralTools := convertToolsForMistral(tools)
|
||||||
|
if len(mistralTools) > 0 {
|
||||||
|
requestBody["tools"] = mistralTools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tools for non-conversations API only
|
||||||
|
if len(tools) > 0 && !useConversations {
|
||||||
requestBody["tools"] = tools
|
requestBody["tools"] = tools
|
||||||
requestBody["tool_choice"] = "auto"
|
requestBody["tool_choice"] = "auto"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add top-level params for non-conversations API
|
||||||
|
if !useConversations {
|
||||||
if maxTokens, ok := options["max_tokens"].(int); ok {
|
if maxTokens, ok := options["max_tokens"].(int); ok {
|
||||||
lowerModel := strings.ToLower(model)
|
lowerModel := strings.ToLower(model)
|
||||||
if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") {
|
if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") {
|
||||||
|
|
@ -89,13 +181,20 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
||||||
requestBody["temperature"] = temperature
|
requestBody["temperature"] = temperature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jsonData, err := json.Marshal(requestBody)
|
jsonData, err := json.Marshal(requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/chat/completions", bytes.NewReader(jsonData))
|
// Determine the endpoint - use /chat/completions unless already using /conversations
|
||||||
|
endpoint := "/chat/completions"
|
||||||
|
if strings.Contains(p.apiBase, "/conversations") {
|
||||||
|
endpoint = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+endpoint, bytes.NewReader(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -117,13 +216,75 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("API request failed:\n Status: %d\n Body: %s", resp.StatusCode, string(body))
|
return nil, fmt.Errorf("API request failed:\n URL: %s\n Status: %d\n Body: %s", p.apiBase+endpoint, resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.parseResponse(body)
|
return p.parseResponse(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
|
func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
|
// First, try to parse as conversations API response format
|
||||||
|
// The /v1/conversations API returns {"outputs": [{"type": "message.output", "content": [...]}]}
|
||||||
|
// When tools are used, there can be multiple outputs (tool.execution + message.output)
|
||||||
|
var convResponse struct {
|
||||||
|
Outputs []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content interface{} `json:"content"` // Can be string, array of {type, text}, or nil
|
||||||
|
Name string `json:"name"` // For tool.execution
|
||||||
|
Arguments string `json:"arguments"` // For tool.execution
|
||||||
|
} `json:"outputs"`
|
||||||
|
Usage *UsageInfo `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &convResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a conversations API response
|
||||||
|
if len(convResponse.Outputs) > 0 {
|
||||||
|
// Parse as conversations API format
|
||||||
|
toolCalls := []ToolCall{}
|
||||||
|
content := ""
|
||||||
|
|
||||||
|
for _, output := range convResponse.Outputs {
|
||||||
|
if output.Type == "tool.execution" {
|
||||||
|
// This is a tool call
|
||||||
|
args := make(map[string]interface{})
|
||||||
|
if output.Arguments != "" {
|
||||||
|
json.Unmarshal([]byte(output.Arguments), &args)
|
||||||
|
}
|
||||||
|
toolCalls = append(toolCalls, ToolCall{
|
||||||
|
ID: output.Name, // Use tool name as ID
|
||||||
|
Name: output.Name,
|
||||||
|
Arguments: args,
|
||||||
|
})
|
||||||
|
} else if output.Type == "message.output" || output.Type == "message" {
|
||||||
|
// This is the actual message content
|
||||||
|
switch c := output.Content.(type) {
|
||||||
|
case string:
|
||||||
|
content = c
|
||||||
|
case []interface{}:
|
||||||
|
// Array of {type, text} objects
|
||||||
|
for _, item := range c {
|
||||||
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||||
|
if text, ok := itemMap["text"].(string); ok {
|
||||||
|
content += text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LLMResponse{
|
||||||
|
Content: content,
|
||||||
|
ToolCalls: toolCalls,
|
||||||
|
FinishReason: "stop",
|
||||||
|
Usage: convResponse.Usage,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try standard OpenAI format
|
||||||
var apiResponse struct {
|
var apiResponse struct {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Message struct {
|
Message struct {
|
||||||
|
|
@ -322,6 +483,16 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
apiBase = "localhost:4321"
|
apiBase = "localhost:4321"
|
||||||
}
|
}
|
||||||
return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model)
|
return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model)
|
||||||
|
case "mistral":
|
||||||
|
if cfg.Providers.Mistral.APIKey != "" {
|
||||||
|
apiKey = cfg.Providers.Mistral.APIKey
|
||||||
|
apiBase = cfg.Providers.Mistral.APIBase
|
||||||
|
proxy = cfg.Providers.Mistral.Proxy
|
||||||
|
// Mistral /v1/conversations endpoint for better rate limits
|
||||||
|
if apiBase == "" {
|
||||||
|
apiBase = "https://api.mistral.ai/v1/conversations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,7 +509,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
apiBase = "https://api.moonshot.cn/v1"
|
apiBase = "https://api.moonshot.cn/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(model, "openrouter/") || strings.HasPrefix(model, "anthropic/") || strings.HasPrefix(model, "openai/") || strings.HasPrefix(model, "meta-llama/") || strings.HasPrefix(model, "deepseek/") || strings.HasPrefix(model, "google/"):
|
case strings.HasPrefix(model, "openrouter/") || strings.HasPrefix(model, "anthropic/") || strings.HasPrefix(model, "openai/") || strings.HasPrefix(model, "meta-llama/") || strings.HasPrefix(model, "deepseek/") || strings.HasPrefix(model, "google/") || strings.HasPrefix(model, "mistral/"):
|
||||||
apiKey = cfg.Providers.OpenRouter.APIKey
|
apiKey = cfg.Providers.OpenRouter.APIKey
|
||||||
proxy = cfg.Providers.OpenRouter.Proxy
|
proxy = cfg.Providers.OpenRouter.Proxy
|
||||||
if cfg.Providers.OpenRouter.APIBase != "" {
|
if cfg.Providers.OpenRouter.APIBase != "" {
|
||||||
|
|
@ -401,6 +572,14 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
apiBase = "https://integrate.api.nvidia.com/v1"
|
apiBase = "https://integrate.api.nvidia.com/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case (strings.Contains(lowerModel, "mistral") || strings.HasPrefix(model, "mistral/")) && cfg.Providers.Mistral.APIKey != "":
|
||||||
|
apiKey = cfg.Providers.Mistral.APIKey
|
||||||
|
apiBase = cfg.Providers.Mistral.APIBase
|
||||||
|
proxy = cfg.Providers.Mistral.Proxy
|
||||||
|
if apiBase == "" {
|
||||||
|
apiBase = "https://api.mistral.ai/v1"
|
||||||
|
}
|
||||||
|
|
||||||
case cfg.Providers.VLLM.APIBase != "":
|
case cfg.Providers.VLLM.APIBase != "":
|
||||||
apiKey = cfg.Providers.VLLM.APIKey
|
apiKey = cfg.Providers.VLLM.APIKey
|
||||||
apiBase = cfg.Providers.VLLM.APIBase
|
apiBase = cfg.Providers.VLLM.APIBase
|
||||||
|
|
@ -431,3 +610,33 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
|
|
||||||
return NewHTTPProvider(apiKey, apiBase, proxy), nil
|
return NewHTTPProvider(apiKey, apiBase, proxy), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertToolsForMistral converts PicoClaw tools to Mistral format
|
||||||
|
// For Mistral conversations API, we use built-in web_search instead of custom tools
|
||||||
|
func convertToolsForMistral(tools []ToolDefinition) []map[string]interface{} {
|
||||||
|
mistralTools := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, tool := range tools {
|
||||||
|
if tool.Type == "function" {
|
||||||
|
// Check if this is a web_search tool - use Mistral's built-in instead
|
||||||
|
if tool.Function.Name == "web_search" || tool.Function.Name == "search" {
|
||||||
|
// Use Mistral's built-in web_search
|
||||||
|
mistralTools = append(mistralTools, map[string]interface{}{
|
||||||
|
"type": "web_search",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Keep custom function in Mistral format
|
||||||
|
mistralTools = append(mistralTools, map[string]interface{}{
|
||||||
|
"type": "function",
|
||||||
|
"function": map[string]interface{}{
|
||||||
|
"name": tool.Function.Name,
|
||||||
|
"description": tool.Function.Description,
|
||||||
|
"parameters": tool.Function.Parameters,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mistralTools
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue