package glm import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" "tm/pkg/logger" ) // Client implements the HTTPClient interface for GLM API type Client struct { config *Config httpClient *http.Client logger logger.Logger } // NewClient creates a new GLM HTTP client func NewClient(config *Config, logger logger.Logger) (*Client, error) { if config == nil { config = DefaultConfig() } if err := config.Validate(); err != nil { return nil, err } httpClient := &http.Client{ Timeout: config.Timeout, } return &Client{ config: config, httpClient: httpClient, logger: logger, }, nil } // Post performs a POST request to the GLM API func (c *Client) Post(ctx context.Context, endpoint string, body interface{}, result interface{}) error { return c.makeRequest(ctx, "POST", endpoint, body, result) } // Get performs a GET request to the GLM API func (c *Client) Get(ctx context.Context, endpoint string, result interface{}) error { return c.makeRequest(ctx, "GET", endpoint, nil, result) } // makeRequest performs an HTTP request with retry logic and logging func (c *Client) makeRequest(ctx context.Context, method, endpoint string, body interface{}, result interface{}) error { url := c.config.BaseURL + endpoint var bodyReader io.Reader var bodyBytes []byte if body != nil { var err error bodyBytes, err = json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } var lastErr error for attempt := 0; attempt <= c.config.RetryAttempts; attempt++ { if attempt > 0 { // Wait before retry select { case <-ctx.Done(): return ctx.Err() case <-time.After(c.config.RetryDelay): } } err := c.doRequest(ctx, method, url, bodyReader, bodyBytes, result) if err == nil { return nil } lastErr = err // Check if error is retryable if !c.isRetryableError(err) { break } if c.logger != nil { c.logger.Warn("Request failed, retrying", map[string]interface{}{ "attempt": attempt + 1, "max_attempts": c.config.RetryAttempts + 1, "error": err.Error(), "url": url, }) } } return lastErr } // doRequest performs the actual HTTP request func (c *Client) doRequest(ctx context.Context, method, url string, bodyReader io.Reader, bodyBytes []byte, result interface{}) error { // Reset body reader for retry if bodyReader != nil { bodyReader = bytes.NewReader(bodyBytes) } req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) if err != nil { return fmt.Errorf("failed to create request: %w", err) } // Set headers req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", c.config.UserAgent) if c.config.APIKey != "" { req.Header.Set("Authorization", "Bearer "+c.config.APIKey) } // Log request if c.logger != nil && c.config.EnableLogging { c.logger.Debug("Making GLM API request", map[string]interface{}{ "method": method, "url": url, "headers": map[string]string{ "Content-Type": req.Header.Get("Content-Type"), "User-Agent": req.Header.Get("User-Agent"), "Authorization": strings.Repeat("*", len(req.Header.Get("Authorization"))), }, }) } resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to execute request: %w", err) } defer resp.Body.Close() // Read response body respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } // Log response if c.logger != nil && c.config.EnableLogging { c.logger.Debug("GLM API response received", map[string]interface{}{ "status_code": resp.StatusCode, "headers": resp.Header, "body_size": len(respBody), }) } // Check for HTTP errors if resp.StatusCode >= 400 { return c.handleHTTPError(resp.StatusCode, respBody) } // Parse response if result != nil { if err := json.Unmarshal(respBody, result); err != nil { return ErrInvalidResponse(fmt.Sprintf("failed to unmarshal response: %v", err)) } } return nil } // handleHTTPError handles HTTP error responses func (c *Client) handleHTTPError(statusCode int, body []byte) error { var errorResp struct { Error struct { Message string `json:"message"` Type string `json:"type"` Code string `json:"code"` } `json:"error"` } if err := json.Unmarshal(body, &errorResp); err != nil { // If we can't parse the error response, return a generic error return ErrHTTPError(statusCode, string(body)) } message := errorResp.Error.Message if message == "" { message = "Unknown error" } switch statusCode { case 400: return ErrInvalidRequest(message) case 401: return ErrAuthentication(message) case 403: return ErrAuthentication(message) case 404: return ErrModelNotFound(message) case 429: return ErrRateLimit(message) case 500, 502, 503, 504: return ErrServerError(message) default: return ErrHTTPError(statusCode, message) } } // isRetryableError determines if an error should be retried func (c *Client) isRetryableError(err error) bool { var glmErr *Error if errors.As(err, &glmErr) { // Retry on server errors and rate limits switch glmErr.Code { case ErrCodeServerError, ErrCodeRateLimit: return true default: return false } } // Retry on network errors, timeouts, etc. return true }