Add GLM SDK Implementation
- 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.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user