This commit is contained in:
2026-04-17 11:12:45 +03:00
commit f4b1ac5789
14 changed files with 656 additions and 0 deletions

106
openai.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// FunctionCall.Arguments is a JSON-encoded string per the OpenAI spec, not a
// JSON object — keep it as a string and decode when dispatching.
type Message struct {
Role string `json:"role"`
Content string `json:"content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Name string `json:"name,omitempty"`
}
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function FunctionCall `json:"function"`
}
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type chatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
}
type chatResponse struct {
Choices []struct {
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error,omitempty"`
}
type Client struct {
BaseURL string
APIKey string
Model string
HTTP *http.Client
}
func NewClient(baseURL, apiKey, model string) *Client {
return &Client{
BaseURL: baseURL,
APIKey: apiKey,
Model: model,
HTTP: &http.Client{Timeout: 120 * time.Second},
}
}
func (c *Client) Chat(ctx context.Context, messages []Message, tools []Tool) (Message, error) {
body, err := json.Marshal(chatRequest{Model: c.Model, Messages: messages, Tools: tools})
if err != nil {
return Message{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/chat/completions", bytes.NewReader(body))
if err != nil {
return Message{}, err
}
req.Header.Set("Content-Type", "application/json")
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return Message{}, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return Message{}, err
}
if resp.StatusCode >= 400 {
return Message{}, fmt.Errorf("api error %d: %s", resp.StatusCode, string(raw))
}
var parsed chatResponse
if err := json.Unmarshal(raw, &parsed); err != nil {
return Message{}, fmt.Errorf("decode response: %w; body=%s", err, string(raw))
}
if parsed.Error != nil {
return Message{}, fmt.Errorf("api error: %s", parsed.Error.Message)
}
if len(parsed.Choices) == 0 {
return Message{}, fmt.Errorf("no choices in response: %s", string(raw))
}
return parsed.Choices[0].Message, nil
}