package openai_compat import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type ToolCall struct { ID string `json:"id"` Type string `json:"type,omitempty"` Function *FunctionCall `json:"function,omitempty"` Name string `json:"name,omitempty"` Arguments map[string]interface{} `json:"arguments,omitempty"` } type FunctionCall struct { Name string `json:"name"` Arguments string `json:"arguments"` } type LLMResponse struct { Content string `json:"content"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` FinishReason string `json:"finish_reason"` Usage *UsageInfo `json:"usage,omitempty"` } type UsageInfo struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } type Message struct { Role string `json:"role"` Content string `json:"content"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` } type ToolDefinition struct { Type string `json:"type"` Function ToolFunctionDefinition `json:"function"` } type ToolFunctionDefinition struct { Name string `json:"name"` Description string `json:"description"` Parameters map[string]interface{} `json:"parameters"` } type Provider struct { apiKey string apiBase string httpClient *http.Client } func NewProvider(apiKey, apiBase string, proxy ...string) *Provider { proxyURL := "" if len(proxy) > 0 { proxyURL = proxy[0] } client := &http.Client{ Timeout: 120 * time.Second, } if proxyURL != "" { parsed, err := url.Parse(proxyURL) if err == nil { client.Transport = &http.Transport{ Proxy: http.ProxyURL(parsed), } } } return &Provider{ apiKey: apiKey, apiBase: strings.TrimRight(apiBase, "/"), httpClient: client, } } func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { if p.apiBase == "" { return nil, fmt.Errorf("API base not configured") } // Strip provider prefix (moonshot/kimi-*, nvidia/*) for OpenAI-compatible backends. if idx := strings.Index(model, "/"); idx != -1 { prefix := model[:idx] if prefix == "moonshot" || prefix == "nvidia" { model = model[idx+1:] } } requestBody := map[string]interface{}{ "model": model, "messages": messages, } if len(tools) > 0 { requestBody["tools"] = tools requestBody["tool_choice"] = "auto" } if maxTokens, ok := options["max_tokens"].(int); ok { lowerModel := strings.ToLower(model) if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") { requestBody["max_completion_tokens"] = maxTokens } else { requestBody["max_tokens"] = maxTokens } } if temperature, ok := options["temperature"].(float64); ok { lowerModel := strings.ToLower(model) // Kimi k2 models only support temperature=1. if strings.Contains(lowerModel, "kimi") && strings.Contains(lowerModel, "k2") { requestBody["temperature"] = 1.0 } else { requestBody["temperature"] = temperature } } jsonData, err := json.Marshal(requestBody) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/chat/completions", bytes.NewReader(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") if p.apiKey != "" { req.Header.Set("Authorization", "Bearer "+p.apiKey) } resp, err := p.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API request failed:\n Status: %d\n Body: %s", resp.StatusCode, string(body)) } return parseResponse(body) } func parseResponse(body []byte) (*LLMResponse, error) { var apiResponse struct { Choices []struct { Message struct { Content string `json:"content"` ToolCalls []struct { ID string `json:"id"` Type string `json:"type"` Function *struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } `json:"tool_calls"` } `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Usage *UsageInfo `json:"usage"` } if err := json.Unmarshal(body, &apiResponse); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if len(apiResponse.Choices) == 0 { return &LLMResponse{ Content: "", FinishReason: "stop", }, nil } choice := apiResponse.Choices[0] toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls)) for _, tc := range choice.Message.ToolCalls { arguments := make(map[string]interface{}) name := "" if tc.Type == "function" && tc.Function != nil { name = tc.Function.Name if tc.Function.Arguments != "" { if err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil { arguments["raw"] = tc.Function.Arguments } } } else if tc.Function != nil { name = tc.Function.Name if tc.Function.Arguments != "" { if err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil { arguments["raw"] = tc.Function.Arguments } } } toolCalls = append(toolCalls, ToolCall{ ID: tc.ID, Name: name, Arguments: arguments, }) } return &LLMResponse{ Content: choice.Message.Content, ToolCalls: toolCalls, FinishReason: choice.FinishReason, Usage: apiResponse.Usage, }, nil }