Pulse/internal/ai/providers/openai.go
rcourtman 3f08bfe8d0 wip: AI chat integration with multi-provider support
- Add AI service with Anthropic, OpenAI, and Ollama providers
- Add AI chat UI component with streaming responses
- Add AI settings page for configuration
- Add agent exec framework for command execution
- Add API endpoints for AI chat and configuration
2025-12-04 20:16:53 +00:00

188 lines
4.5 KiB
Go

package providers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
openaiAPIURL = "https://api.openai.com/v1/chat/completions"
)
// OpenAIClient implements the Provider interface for OpenAI's API
type OpenAIClient struct {
apiKey string
model string
baseURL string
client *http.Client
}
// NewOpenAIClient creates a new OpenAI API client
func NewOpenAIClient(apiKey, model, baseURL string) *OpenAIClient {
if baseURL == "" {
baseURL = openaiAPIURL
}
return &OpenAIClient{
apiKey: apiKey,
model: model,
baseURL: baseURL,
client: &http.Client{
Timeout: 120 * time.Second,
},
}
}
// Name returns the provider name
func (c *OpenAIClient) Name() string {
return "openai"
}
// openaiRequest is the request body for the OpenAI API
type openaiRequest struct {
Model string `json:"model"`
Messages []openaiMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
}
type openaiMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
// openaiResponse is the response from the OpenAI API
type openaiResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []openaiChoice `json:"choices"`
Usage openaiUsage `json:"usage"`
}
type openaiChoice struct {
Index int `json:"index"`
Message openaiMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
type openaiUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type openaiError struct {
Error openaiErrorDetail `json:"error"`
}
type openaiErrorDetail struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
}
// Chat sends a chat request to the OpenAI API
func (c *OpenAIClient) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
// Convert messages to OpenAI format
messages := make([]openaiMessage, 0, len(req.Messages)+1)
// Add system message if provided
if req.System != "" {
messages = append(messages, openaiMessage{
Role: "system",
Content: req.System,
})
}
for _, m := range req.Messages {
messages = append(messages, openaiMessage{
Role: m.Role,
Content: m.Content,
})
}
// Use provided model or fall back to client default
model := req.Model
if model == "" {
model = c.model
}
openaiReq := openaiRequest{
Model: model,
Messages: messages,
}
if req.MaxTokens > 0 {
openaiReq.MaxTokens = req.MaxTokens
}
if req.Temperature > 0 {
openaiReq.Temperature = req.Temperature
}
body, err := json.Marshal(openaiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
var errResp openaiError
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error.Message != "" {
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, errResp.Error.Message)
}
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
}
var openaiResp openaiResponse
if err := json.Unmarshal(respBody, &openaiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(openaiResp.Choices) == 0 {
return nil, fmt.Errorf("no response choices returned")
}
return &ChatResponse{
Content: openaiResp.Choices[0].Message.Content,
Model: openaiResp.Model,
StopReason: openaiResp.Choices[0].FinishReason,
InputTokens: openaiResp.Usage.PromptTokens,
OutputTokens: openaiResp.Usage.CompletionTokens,
}, nil
}
// TestConnection validates the API key by making a minimal request
func (c *OpenAIClient) TestConnection(ctx context.Context) error {
_, err := c.Chat(ctx, ChatRequest{
Messages: []Message{
{Role: "user", Content: "Hi"},
},
MaxTokens: 10,
})
return err
}