a5d3a94c97
- Introduced a new GLM SDK for interacting with the GLM (Zhipu AI) API, designed for chat completions and text generation tasks. - Implemented core components including Client, Service, and SDK layers following Clean Architecture principles. - Added configuration management, error handling, and structured logging for improved usability and maintainability. - Included comprehensive documentation and usage examples to facilitate integration and understanding of the SDK's functionality. - Enhanced the API with features such as chat completion, streaming responses, and model management, ensuring robust interaction with the GLM service.
227 lines
5.3 KiB
Go
227 lines
5.3 KiB
Go
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
|
|
}
|