package llm import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "github.com/paramah/ai_devs4/s01e03/internal/domain" ) // OpenRouterProvider implements domain.LLMProvider for OpenRouter API type OpenRouterProvider struct { apiKey string model string baseURL string client *http.Client verbose bool } // NewOpenRouterProvider creates a new OpenRouter provider func NewOpenRouterProvider(apiKey, model string, verbose bool) *OpenRouterProvider { return &OpenRouterProvider{ apiKey: apiKey, model: model, baseURL: "https://openrouter.ai/api/v1/chat/completions", client: &http.Client{}, verbose: verbose, } } type openRouterMessage struct { Role string `json:"role"` Content string `json:"content"` ToolCallID string `json:"tool_call_id,omitempty"` ToolCalls []openRouterToolCall `json:"tool_calls,omitempty"` } type openRouterToolCall struct { ID string `json:"id"` Type string `json:"type"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } type openRouterRequest struct { Model string `json:"model"` Messages []openRouterMessage `json:"messages"` Tools []domain.Tool `json:"tools,omitempty"` } type openRouterResponse struct { Choices []struct { Message struct { Content string `json:"content"` ToolCalls []openRouterToolCall `json:"tool_calls,omitempty"` } `json:"message"` } `json:"choices"` Error *struct { Message string `json:"message"` } `json:"error,omitempty"` } // Complete sends a request to OpenRouter API with function calling support func (p *OpenRouterProvider) Complete(ctx context.Context, request domain.LLMRequest) (*domain.LLMResponse, error) { // Convert domain messages to OpenRouter format messages := make([]openRouterMessage, len(request.Messages)) for i, msg := range request.Messages { // Convert tool calls if present var toolCalls []openRouterToolCall if len(msg.ToolCalls) > 0 { toolCalls = make([]openRouterToolCall, len(msg.ToolCalls)) for j, tc := range msg.ToolCalls { toolCalls[j] = openRouterToolCall{ ID: tc.ID, Type: tc.Type, Function: struct { Name string `json:"name"` Arguments string `json:"arguments"` }{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, } } } messages[i] = openRouterMessage{ Role: msg.Role, Content: msg.Content, ToolCallID: msg.ToolCallID, ToolCalls: toolCalls, } } reqBody := openRouterRequest{ Model: p.model, Messages: messages, Tools: request.Tools, } jsonData, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("marshaling request: %w", err) } if p.verbose { var prettyJSON bytes.Buffer if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil { log.Printf("\n========== LLM REQUEST ==========\nURL: %s\nBody:\n%s\n================================\n", p.baseURL, prettyJSON.String()) } } req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.baseURL, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+p.apiKey) resp, err := p.client.Do(req) if err != nil { return nil, fmt.Errorf("sending request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response: %w", err) } if p.verbose { var prettyJSON bytes.Buffer if err := json.Indent(&prettyJSON, body, "", " "); err == nil { log.Printf("\n========== LLM RESPONSE ==========\nStatus: %d\nBody:\n%s\n==================================\n", resp.StatusCode, prettyJSON.String()) } } var apiResp openRouterResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, fmt.Errorf("unmarshaling response: %w", err) } if apiResp.Error != nil { return nil, fmt.Errorf("API error: %s", apiResp.Error.Message) } if len(apiResp.Choices) == 0 { return nil, fmt.Errorf("no choices in response") } // Convert tool calls to domain format toolCalls := make([]domain.ToolCall, len(apiResp.Choices[0].Message.ToolCalls)) for i, tc := range apiResp.Choices[0].Message.ToolCalls { toolCalls[i] = domain.ToolCall{ ID: tc.ID, Type: tc.Type, Function: struct { Name string `json:"name"` Arguments string `json:"arguments"` }{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, } } return &domain.LLMResponse{ Content: apiResp.Choices[0].Message.Content, ToolCalls: toolCalls, }, nil }