package openai import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "log/slog" "net/http" "strings" "git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/ai" "git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/config" ) type Provider struct { apiKey string model string baseURL string client *http.Client log *slog.Logger } func New(cfg config.OpenAIConfig, log *slog.Logger) *Provider { baseURL := cfg.BaseURL if baseURL == "" { baseURL = "https://api.openai.com/v1" } return &Provider{ apiKey: cfg.APIKey, model: cfg.Model, baseURL: strings.TrimRight(baseURL, "/"), client: &http.Client{}, log: log, } } func (p *Provider) Name() string { return "openai" } func (p *Provider) Models() []string { return []string{p.model} } type openAIRequest struct { Model string `json:"model"` Messages []openAIMessage `json:"messages"` Tools []openAITool `json:"tools,omitempty"` Stream bool `json:"stream"` } type openAIMessage struct { Role string `json:"role"` Content string `json:"content,omitempty"` ToolCalls []openAIToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` Name string `json:"name,omitempty"` } type openAIToolCall struct { ID string `json:"id"` Type string `json:"type"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } type openAITool struct { Type string `json:"type"` Function struct { Name string `json:"name"` Description string `json:"description"` Parameters map[string]interface{} `json:"parameters"` } `json:"function"` } type openAIResponse struct { Choices []struct { Message struct { Content string `json:"content"` ToolCalls []openAIToolCall `json:"tool_calls"` } `json:"message"` Delta struct { Content string `json:"content"` ToolCalls []openAIToolCall `json:"tool_calls"` } `json:"delta"` } `json:"choices"` Model string `json:"model"` Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` } func (p *Provider) Chat(ctx context.Context, messages []ai.Message, tools []ai.Tool) (*ai.Response, error) { reqBody := openAIRequest{ Model: p.model, Messages: convertMessages(messages), Tools: convertTools(tools), Stream: false, } data, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("marshaling request: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/chat/completions", bytes.NewReader(data)) 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() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("openai returned status %d: %s", resp.StatusCode, string(body)) } var oaiResp openAIResponse if err := json.NewDecoder(resp.Body).Decode(&oaiResp); err != nil { return nil, fmt.Errorf("decoding response: %w", err) } result := &ai.Response{ Model: oaiResp.Model, Usage: ai.Usage{ PromptTokens: oaiResp.Usage.PromptTokens, CompletionTokens: oaiResp.Usage.CompletionTokens, TotalTokens: oaiResp.Usage.TotalTokens, }, } if len(oaiResp.Choices) > 0 { choice := oaiResp.Choices[0] result.Content = choice.Message.Content for _, tc := range choice.Message.ToolCalls { result.ToolCalls = append(result.ToolCalls, ai.ToolCall{ ID: tc.ID, Function: struct { Name string `json:"name"` Arguments string `json:"arguments"` }{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, }) } } return result, nil } func (p *Provider) StreamChat(ctx context.Context, messages []ai.Message, tools []ai.Tool) (<-chan ai.StreamChunk, error) { reqBody := openAIRequest{ Model: p.model, Messages: convertMessages(messages), Tools: convertTools(tools), Stream: true, } data, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("marshaling request: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/chat/completions", bytes.NewReader(data)) 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) } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) resp.Body.Close() return nil, fmt.Errorf("openai returned status %d: %s", resp.StatusCode, string(body)) } ch := make(chan ai.StreamChunk, 64) go func() { defer close(ch) defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data: ") { continue } payload := strings.TrimPrefix(line, "data: ") if payload == "[DONE]" { ch <- ai.StreamChunk{Done: true} return } var chunk openAIResponse if err := json.Unmarshal([]byte(payload), &chunk); err != nil { continue } if len(chunk.Choices) > 0 { delta := chunk.Choices[0].Delta sc := ai.StreamChunk{Content: delta.Content} for _, tc := range delta.ToolCalls { sc.ToolCalls = append(sc.ToolCalls, ai.ToolCall{ ID: tc.ID, Function: struct { Name string `json:"name"` Arguments string `json:"arguments"` }{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, }) } select { case ch <- sc: case <-ctx.Done(): return } } } }() return ch, nil } func convertMessages(msgs []ai.Message) []openAIMessage { result := make([]openAIMessage, len(msgs)) for i, m := range msgs { msg := openAIMessage{ Role: m.Role, Content: m.Content, ToolCallID: m.ToolCallID, Name: m.Name, } for _, tc := range m.ToolCalls { msg.ToolCalls = append(msg.ToolCalls, openAIToolCall{ ID: tc.ID, Type: "function", Function: struct { Name string `json:"name"` Arguments string `json:"arguments"` }{ Name: tc.Function.Name, Arguments: tc.Function.Arguments, }, }) } result[i] = msg } return result } func convertTools(tools []ai.Tool) []openAITool { result := make([]openAITool, len(tools)) for i, t := range tools { result[i] = openAITool{Type: "function"} result[i].Function.Name = t.Name result[i].Function.Description = t.Description result[i].Function.Parameters = t.Parameters } return result }