Files
tm_back/pkg/glm/client.go
T
n.nakhostin a5d3a94c97 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.
2025-11-04 16:09:44 +03:30

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
}