package friendli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
// DefaultServerlessURL is the base URL for Friendli.ai serverless endpoints
DefaultServerlessURL = "https://api.friendli.ai/serverless/v1"
// DefaultTimeout is the default HTTP client timeout
DefaultTimeout = 60 * time.Second
)
// Client is the main Friendli.ai API client
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
// Services
Chat *ChatService
}
// ClientOption is a function that configures a Client
type ClientOption func(*Client)
// NewClient creates a new Friendli.ai API client
func NewClient(apiKey string, opts ...ClientOption) (*Client, error) {
if apiKey == "" {
return nil, ErrMissingAPIKey
}
c := &Client{
apiKey: apiKey,
baseURL: DefaultServerlessURL,
httpClient: &http.Client{
Timeout: DefaultTimeout,
},
}
// Apply options
for _, opt := range opts {
opt(c)
}
// Initialize services
c.Chat = &ChatService{client: c}
return c, nil
}
// WithBaseURL sets a custom base URL for the client
func WithBaseURL(url string) ClientOption {
return func(c *Client) {
c.baseURL = url
}
}
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
// WithTimeout sets the HTTP client timeout
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.httpClient.Timeout = timeout
}
}
// doRequest performs an HTTP request with authentication and error handling
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewReader(jsonBody)
}
url := c.baseURL + path
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
// Check for error status codes
if resp.StatusCode >= 400 {
defer resp.Body.Close()
return nil, parseErrorResponse(resp)
}
return resp, nil
}
// doStreamingRequest performs an HTTP request for streaming responses
func (c *Client) doStreamingRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewReader(jsonBody)
}
url := c.baseURL + path
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers for streaming
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Connection", "keep-alive")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
// Check for error status codes
if resp.StatusCode >= 400 {
defer resp.Body.Close()
return nil, parseErrorResponse(resp)
}
return resp, nil
}