merge tenders which have the same procedure id - worker performance fix

This commit is contained in:
Mazyar
2026-05-11 15:10:01 +03:30
parent 8ba667e4f4
commit b4efb97f47
22 changed files with 661 additions and 2234 deletions
-205
View File
@@ -1,205 +0,0 @@
# GLM SDK
A Go SDK for interacting with the GLM (Zhipu AI) API, specifically designed for chat completions and text generation tasks.
## Features
- **Chat Completions**: Perform conversational AI interactions
- **Streaming Support**: Real-time streaming responses
- **Translation Helper**: Built-in convenience methods for translation tasks
- **Model Management**: List available models
- **Health Checks**: Service availability monitoring
- **Retry Logic**: Automatic retry with configurable attempts
- **Structured Logging**: Comprehensive logging support
- **Error Handling**: Detailed error types and handling
## Installation
```bash
go get tm/pkg/glm
```
## Quick Start
```go
package main
import (
"context"
"log"
"tm/pkg/glm"
"tm/pkg/logger"
)
func main() {
// Create configuration
config := &glm.Config{
APIKey: "your-api-key-here",
BaseURL: "https://api.z.ai",
}
// Create logger (you can use your preferred logger)
logger := logger.NewLogger()
// Create SDK instance
sdk, err := glm.New(config, logger)
if err != nil {
log.Fatal(err)
}
// Simple chat
ctx := context.Background()
response, err := sdk.QuickChat(ctx, "Hello, how are you?")
if err != nil {
log.Fatal(err)
}
fmt.Println(response)
// Translation example (matching the provided curl request)
translated, err := sdk.Translate(ctx, "IT-stöd för projekt- och portföljstyrning", "Swedish", "English")
if err != nil {
log.Fatal(err)
}
fmt.Println(translated)
}
```
## Configuration
```go
config := &glm.Config{
BaseURL: "https://api.z.ai", // API base URL
APIKey: "your-api-key", // Authentication key
Timeout: 300 * time.Second, // Request timeout
RetryAttempts: 3, // Number of retries
RetryDelay: 2 * time.Second, // Delay between retries
EnableLogging: true, // Enable request logging
UserAgent: "MyApp/1.0", // Custom user agent
DefaultModel: "glm-4.5", // Default model
DefaultMaxTokens: 4096, // Default max tokens
DefaultTopP: 1.0, // Default top_p
DefaultTemperature: 0.7, // Default temperature
EnableStreaming: false, // Enable streaming by default
EnableThinking: false, // Enable thinking mode
}
```
## API Methods
### Chat Completion
```go
req := &glm.ChatCompletionRequest{
Model: "glm-4.5",
Messages: []glm.Message{
{
Role: glm.MessageRoleSystem,
Content: "You are a helpful assistant.",
},
{
Role: glm.MessageRoleUser,
Content: "Explain quantum computing.",
},
},
MaxTokens: 4096,
TopP: 1.0,
Temperature: 0.7,
FrequencyPenalty: 0,
PresencePenalty: 0,
Thinking: &glm.Thinking{
Type: "false",
},
}
resp, err := sdk.ChatCompletion(ctx, req)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Choices[0].Message.Content)
```
### Streaming Chat Completion
```go
req := &glm.ChatCompletionRequest{
Model: "glm-4.5",
Messages: []glm.Message{
{
Role: glm.MessageRoleUser,
Content: "Tell me a long story.",
},
},
Stream: true,
}
err := sdk.StreamChatCompletion(ctx, req, func(chunk *glm.StreamChatCompletionResponse) error {
if len(chunk.Choices) > 0 {
fmt.Print(chunk.Choices[0].Delta.Content)
}
return nil
})
```
### Convenience Methods
```go
// Simple chat
response, err := sdk.QuickChat(ctx, "Hello!")
// Chat with system prompt
response, err := sdk.QuickChatWithSystem(ctx, "You are a poet.", "Write a haiku about coding.")
// Translation (matching the curl example)
translated, err := sdk.Translate(ctx, "IT-stöd för projekt- och portföljstyrning", "Swedish", "English")
// Streaming chat
err := sdk.StreamQuickChat(ctx, "Tell me a story", func(chunk string) error {
fmt.Print(chunk)
return nil
})
```
## Error Handling
The SDK provides detailed error types:
```go
resp, err := sdk.ChatCompletion(ctx, req)
if err != nil {
var glmErr *glm.Error
if errors.As(err, &glmErr) {
switch glmErr.Code {
case glm.ErrCodeAuthentication:
// Handle auth errors
case glm.ErrCodeRateLimit:
// Handle rate limits
case glm.ErrCodeServerError:
// Handle server errors
}
}
}
```
## Environment Variables
You can configure the SDK using environment variables:
```bash
export GLM_BASE_URL="https://api.z.ai"
export GLM_API_KEY="your-api-key"
export GLM_TIMEOUT="300s"
export GLM_RETRY_ATTEMPTS="3"
export GLM_DEFAULT_MODEL="glm-4.5"
export GLM_DEFAULT_MAX_TOKENS="4096"
```
## Testing
```bash
go test ./pkg/glm/...
```
## License
This SDK is part of the Tender Management system and follows the same licensing terms.
-280
View File
@@ -1,280 +0,0 @@
# GLM SDK Usage Guide
## Matching the Provided Curl Request
The SDK can replicate the exact curl request provided:
```go
package main
import (
"context"
"fmt"
"log"
"tm/pkg/glm"
"tm/pkg/logger"
)
func main() {
config := &glm.Config{
BaseURL: "https://api.z.ai",
APIKey: "4e267ac7897f4a789cacb3dbee15f312.coJtK3397qFv16x5",
}
logger := logger.NewLogger()
sdk, err := glm.New(config, logger)
if err != nil {
log.Fatal(err)
}
req := &glm.ChatCompletionRequest{
Model: "glm-4.5",
Messages: []glm.Message{
{
Role: glm.MessageRoleSystem,
Content: "You are a high-precision translator specializing in professional tender documents. Always respond with only the translated English text — no explanations, notes, or markdown formatting.",
},
{
Role: glm.MessageRoleUser,
Content: "Translate the following text from Swedish to English accurately and professionally:\n\"IT-stöd för projekt- och portföljstyrning\"",
},
},
Thinking: &glm.Thinking{
Type: "false",
},
MaxTokens: 4096,
TopP: 1,
FrequencyPenalty: 0,
PresencePenalty: 0,
}
ctx := context.Background()
resp, err := sdk.ChatCompletion(ctx, req)
if err != nil {
log.Fatal(err)
}
if len(resp.Choices) > 0 {
fmt.Println(resp.Choices[0].Message.Content)
}
}
```
## Using the Translate Convenience Method
For the specific translation use case:
```go
translated, err := sdk.Translate(ctx, "IT-stöd för projekt- och portföljstyrning", "Swedish", "English")
if err != nil {
log.Fatal(err)
}
fmt.Println(translated) // Output: "IT support for project and portfolio management"
```
### Custom Translation Options
For more control over translation parameters:
```go
opts := &glm.TranslateOptions{
Model: "glm-4.5",
MaxTokens: 1024,
Temperature: 0.1, // Very low for consistent translations
FrequencyPenalty: 0.2,
PresencePenalty: 0.2,
EnableThinking: &[]bool{false}[0],
}
translated, err := sdk.TranslateWithOptions(ctx, "IT-stöd för projekt- och portföljstyrning", "Swedish", "English", opts)
if err != nil {
log.Fatal(err)
}
fmt.Println(translated)
```
## Advanced Configuration
### GLM SDK Configuration
```go
config := &glm.Config{
BaseURL: "https://api.z.ai",
APIKey: "your-api-key",
Timeout: 300 * time.Second,
RetryAttempts: 3,
RetryDelay: 2 * time.Second,
EnableLogging: true,
DefaultModel: "glm-4.5",
DefaultMaxTokens: 4096,
DefaultTopP: 1.0,
DefaultTemperature: 0.7,
DefaultFrequencyPenalty: 0.0,
DefaultPresencePenalty: 0.0,
EnableStreaming: false,
EnableThinking: false,
// Translation-specific configuration
TranslationMaxTokens: 2048,
TranslationTemperature: 0.3,
TranslationFrequencyPenalty: 0.1,
TranslationPresencePenalty: 0.1,
TranslationEnableThinking: false,
}
```
### Bootstrap Configuration (Application Level)
Configure translation parameters via environment variables in your application bootstrap:
```go
// cmd/worker/bootstrap/config.go
type GLMConfig struct {
// ... basic GLM settings ...
// Translation options configured via environment variables
TranslateOptions TranslateOptions
}
type TranslateOptions struct {
MaxTokens int `env:"GLM_TRANSLATE_MAX_TOKENS" default:"2048"`
Temperature float64 `env:"GLM_TRANSLATE_TEMPERATURE" default:"0.3"`
FrequencyPenalty float64 `env:"GLM_TRANSLATE_FREQUENCY_PENALTY" default:"0.1"`
PresencePenalty float64 `env:"GLM_TRANSLATE_PRESENCE_PENALTY" default:"0.1"`
EnableThinking bool `env:"GLM_TRANSLATE_ENABLE_THINKING" default:"false"`
}
```
Set these environment variables to customize translation behavior:
```bash
export GLM_TRANSLATE_MAX_TOKENS=1024
export GLM_TRANSLATE_TEMPERATURE=0.2
export GLM_TRANSLATE_FREQUENCY_PENALTY=0.2
export GLM_TRANSLATE_PRESENCE_PENALTY=0.2
export GLM_TRANSLATE_ENABLE_THINKING=false
```
## Error Handling Examples
```go
resp, err := sdk.ChatCompletion(ctx, req)
if err != nil {
var glmErr *glm.Error
if errors.As(err, &glmErr) {
switch glmErr.Code {
case glm.ErrCodeAuthentication:
log.Printf("Authentication failed: %s", glmErr.Message)
// Handle authentication error (e.g., refresh token)
case glm.ErrCodeRateLimit:
log.Printf("Rate limit exceeded: %s", glmErr.Message)
// Implement backoff strategy
case glm.ErrCodeInvalidRequest:
log.Printf("Invalid request: %s", glmErr.Message)
// Fix request parameters
case glm.ErrCodeServerError:
log.Printf("Server error: %s", glmErr.Message)
// Retry or alert administrators
default:
log.Printf("Unknown error: %s", glmErr.Error())
}
} else {
log.Printf("Non-GLM error: %v", err)
}
return
}
```
## Streaming with Progress Tracking
```go
var fullResponse strings.Builder
err := sdk.StreamChatCompletion(ctx, req, func(chunk *glm.StreamChatCompletionResponse) error {
if len(chunk.Choices) > 0 {
content := chunk.Choices[0].Delta.Content
fmt.Print(content) // Print as it comes
fullResponse.WriteString(content)
}
// Check for completion
if len(chunk.Choices) > 0 && chunk.Choices[0].FinishReason != nil {
fmt.Printf("\n\nFinished with reason: %s\n", *chunk.Choices[0].FinishReason)
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Complete response: %s\n", fullResponse.String())
```
## Integration with Tender Management System
```go
// In your service layer
type TranslationService struct {
glmSDK *glm.SDK
logger logger.Logger
}
func NewTranslationService(glmSDK *glm.SDK, logger logger.Logger) *TranslationService {
return &TranslationService{
glmSDK: glmSDK,
logger: logger,
}
}
func (s *TranslationService) TranslateTenderDocument(ctx context.Context, text, sourceLang, targetLang string) (string, error) {
s.logger.Info("Translating tender document", map[string]interface{}{
"text_length": len(text),
"source_lang": sourceLang,
"target_lang": targetLang,
})
translated, err := s.glmSDK.Translate(ctx, text, sourceLang, targetLang)
if err != nil {
s.logger.Error("Translation failed", map[string]interface{}{
"error": err.Error(),
})
return "", err
}
s.logger.Info("Translation completed", map[string]interface{}{
"translated_length": len(translated),
})
return translated, nil
}
```
## Health Monitoring
```go
// Check service health
err := sdk.Health(ctx)
if err != nil {
log.Printf("GLM service is unhealthy: %v", err)
// Alert monitoring system
} else {
log.Println("GLM service is healthy")
}
```
## Model Management
```go
// List available models
models, err := sdk.ListModels(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Available models:")
for _, model := range models.Data {
fmt.Printf("- %s (owned by %s)\n", model.ID, model.OwnedBy)
}
```
-226
View File
@@ -1,226 +0,0 @@
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
}
-138
View File
@@ -1,138 +0,0 @@
package glm
import (
"time"
)
// Config represents the configuration for the GLM SDK
type Config struct {
// BaseURL is the base URL of the GLM API server
BaseURL string
// APIKey is the authorization token for GLM API
APIKey string
// Timeout is the timeout for HTTP requests
Timeout time.Duration
// RetryAttempts is the number of retry attempts for failed requests
RetryAttempts int
// RetryDelay is the delay between retry attempts
RetryDelay time.Duration
// EnableLogging enables request/response logging
EnableLogging bool
// UserAgent is the user agent string for HTTP requests
UserAgent string
// DefaultModel is the default model to use if none is specified
DefaultModel string
// DefaultMaxTokens is the default maximum number of tokens to generate
DefaultMaxTokens int
// DefaultTopP is the default top_p value for text generation
DefaultTopP float64
// DefaultTemperature is the default temperature for text generation
DefaultTemperature float64
// DefaultFrequencyPenalty is the default frequency penalty
DefaultFrequencyPenalty float64
// DefaultPresencePenalty is the default presence penalty
DefaultPresencePenalty float64
// EnableStreaming enables streaming responses by default
EnableStreaming bool
// EnableThinking enables thinking mode by default
EnableThinking bool
// TranslationConfig is the translation configuration
TranslationOptions TranslationOption
}
// TranslationOption represents the translation options
type TranslationOption struct {
Model string
MaxTokens int
Temperature float64
FrequencyPenalty float64
PresencePenalty float64
TopP float64
EnableThinking bool
}
// DefaultConfig returns a default configuration for the GLM SDK
func DefaultConfig() *Config {
return &Config{
BaseURL: "https://api.z.ai",
APIKey: "",
Timeout: 300 * time.Second,
RetryAttempts: 3,
RetryDelay: 2 * time.Second,
EnableLogging: true,
UserAgent: "Opplens-GLMSDK/1.0",
DefaultModel: "glm-4.5",
DefaultMaxTokens: 256,
DefaultTopP: 1.0,
DefaultTemperature: 0.5,
DefaultFrequencyPenalty: 0,
DefaultPresencePenalty: 0,
EnableStreaming: false,
EnableThinking: false,
TranslationOptions: TranslationOption{
Model: "glm-4.5",
MaxTokens: 256,
Temperature: 0.5,
FrequencyPenalty: 0,
PresencePenalty: 0,
TopP: 1.0,
EnableThinking: false,
},
}
}
// Validate validates the configuration
func (c *Config) Validate() error {
if c.BaseURL == "" {
return ErrInvalidConfig("BaseURL cannot be empty")
}
if c.Timeout <= 0 {
return ErrInvalidConfig("Timeout must be positive")
}
if c.RetryAttempts < 0 {
return ErrInvalidConfig("RetryAttempts cannot be negative")
}
if c.RetryDelay < 0 {
return ErrInvalidConfig("RetryDelay cannot be negative")
}
if c.DefaultMaxTokens < 1 || c.DefaultMaxTokens > 32768 {
return ErrInvalidConfig("DefaultMaxTokens must be between 1 and 32768")
}
if c.DefaultTopP < 0 || c.DefaultTopP > 1 {
return ErrInvalidConfig("DefaultTopP must be between 0 and 1")
}
if c.DefaultTemperature < 0 || c.DefaultTemperature > 2 {
return ErrInvalidConfig("DefaultTemperature must be between 0 and 2")
}
if c.DefaultFrequencyPenalty < -2 || c.DefaultFrequencyPenalty > 2 {
return ErrInvalidConfig("DefaultFrequencyPenalty must be between -2 and 2")
}
if c.DefaultPresencePenalty < -2 || c.DefaultPresencePenalty > 2 {
return ErrInvalidConfig("DefaultPresencePenalty must be between -2 and 2")
}
return nil
}
-92
View File
@@ -1,92 +0,0 @@
package glm
// ChatCompletionRequest represents a request for chat completion
type ChatCompletionRequest struct {
Model string `json:"model" validate:"required"`
Messages []Message `json:"messages" validate:"required,min=1"`
Thinking *Thinking `json:"thinking,omitempty"`
MaxTokens int `json:"max_tokens,omitempty" validate:"min=1,max=32768"`
TopP float64 `json:"top_p,omitempty" validate:"min=0,max=1"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty" validate:"min=-2,max=2"`
PresencePenalty float64 `json:"presence_penalty,omitempty" validate:"min=-2,max=2"`
Temperature float64 `json:"temperature,omitempty" validate:"min=0,max=2"`
Stop []string `json:"stop,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// Message represents a message in a chat conversation
type Message struct {
Role MessageRole `json:"role" validate:"required,oneof=system user assistant"`
Content string `json:"content" validate:"required"`
}
// MessageRole defines the role of a message
type MessageRole string
const (
MessageRoleSystem MessageRole = "system"
MessageRoleUser MessageRole = "user"
MessageRoleAssistant MessageRole = "assistant"
)
// Thinking represents the thinking configuration for GLM models
type Thinking struct {
Type string `json:"type" validate:"required,oneof=true false"`
}
// ChatCompletionResponse represents the response from a chat completion request
type ChatCompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage Usage `json:"usage"`
SystemFingerprint string `json:"system_fingerprint,omitempty"`
}
// Choice represents a single choice in the chat completion response
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
}
// Usage represents token usage information
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// StreamChatCompletionResponse represents a streaming response chunk
type StreamChatCompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []StreamChoice `json:"choices"`
Usage *Usage `json:"usage,omitempty"`
SystemFingerprint string `json:"system_fingerprint,omitempty"`
}
// StreamChoice represents a single choice in a streaming response
type StreamChoice struct {
Index int `json:"index"`
Delta Message `json:"delta"`
FinishReason *string `json:"finish_reason,omitempty"`
}
// Model represents information about available models
type Model struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
}
// ModelsResponse represents the response from listing available models
type ModelsResponse struct {
Object string `json:"object"`
Data []Model `json:"data"`
}
-94
View File
@@ -1,94 +0,0 @@
package glm
import (
"fmt"
)
// Error represents a GLM SDK error
type Error struct {
Code string
Message string
Cause error
}
// Error implements the error interface
func (e *Error) Error() string {
if e.Cause != nil {
return fmt.Sprintf("glm: %s: %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("glm: %s: %s", e.Code, e.Message)
}
// Unwrap returns the underlying cause of the error
func (e *Error) Unwrap() error {
return e.Cause
}
// NewError creates a new GLM error
func NewError(code, message string, cause error) *Error {
return &Error{
Code: code,
Message: message,
Cause: cause,
}
}
// Error codes
const (
ErrCodeInvalidConfig = "INVALID_CONFIG"
ErrCodeHTTPError = "HTTP_ERROR"
ErrCodeInvalidRequest = "INVALID_REQUEST"
ErrCodeAuthentication = "AUTHENTICATION_ERROR"
ErrCodeRateLimit = "RATE_LIMIT_ERROR"
ErrCodeServerError = "SERVER_ERROR"
ErrCodeInvalidResponse = "INVALID_RESPONSE"
ErrCodeChatCompletion = "CHAT_COMPLETION_ERROR"
ErrCodeModelNotFound = "MODEL_NOT_FOUND"
ErrCodeTokenLimit = "TOKEN_LIMIT_ERROR"
ErrCodeStreamError = "STREAM_ERROR"
)
// Error constructors
func ErrInvalidConfig(message string) *Error {
return NewError(ErrCodeInvalidConfig, message, nil)
}
func ErrHTTPError(statusCode int, message string) *Error {
return NewError(ErrCodeHTTPError, fmt.Sprintf("HTTP %d: %s", statusCode, message), nil)
}
func ErrInvalidRequest(message string) *Error {
return NewError(ErrCodeInvalidRequest, message, nil)
}
func ErrAuthentication(message string) *Error {
return NewError(ErrCodeAuthentication, message, nil)
}
func ErrRateLimit(message string) *Error {
return NewError(ErrCodeRateLimit, message, nil)
}
func ErrServerError(message string) *Error {
return NewError(ErrCodeServerError, message, nil)
}
func ErrInvalidResponse(message string) *Error {
return NewError(ErrCodeInvalidResponse, message, nil)
}
func ErrChatCompletion(message string, cause error) *Error {
return NewError(ErrCodeChatCompletion, message, cause)
}
func ErrModelNotFound(model string) *Error {
return NewError(ErrCodeModelNotFound, fmt.Sprintf("model '%s' not found", model), nil)
}
func ErrTokenLimit(message string) *Error {
return NewError(ErrCodeTokenLimit, message, nil)
}
func ErrStreamError(message string, cause error) *Error {
return NewError(ErrCodeStreamError, message, cause)
}
-29
View File
@@ -1,29 +0,0 @@
package glm
import (
"context"
)
// GLMService defines the interface for GLM service operations
type GLMService interface {
// ChatCompletion performs a chat completion request
ChatCompletion(ctx context.Context, req *ChatCompletionRequest) (*ChatCompletionResponse, error)
// StreamChatCompletion performs a streaming chat completion request
StreamChatCompletion(ctx context.Context, req *ChatCompletionRequest, onChunk func(*StreamChatCompletionResponse) error) error
// ListModels lists available models
ListModels(ctx context.Context) (*ModelsResponse, error)
// Health checks if the GLM service is available
Health(ctx context.Context) error
}
// HTTPClient defines the interface for HTTP operations
type HTTPClient interface {
// Post performs a POST request
Post(ctx context.Context, url string, body interface{}, result interface{}) error
// Get performs a GET request
Get(ctx context.Context, url string, result interface{}) error
}
-277
View File
@@ -1,277 +0,0 @@
package glm
import (
"context"
"fmt"
"tm/pkg/logger"
)
// SDK provides a high-level interface for the GLM service
type SDK struct {
service GLMService
client *Client
config *Config
}
// New creates a new GLM SDK with the provided configuration and logger
func New(config *Config, logger logger.Logger) (*SDK, error) {
cfg := DefaultConfig()
if config.BaseURL != "" {
cfg.BaseURL = config.BaseURL
}
if config.APIKey != "" {
cfg.APIKey = config.APIKey
}
if config.Timeout != 0 {
cfg.Timeout = config.Timeout
}
if config.RetryAttempts != 0 {
cfg.RetryAttempts = config.RetryAttempts
}
if config.RetryDelay != 0 {
cfg.RetryDelay = config.RetryDelay
}
if config.EnableLogging {
cfg.EnableLogging = config.EnableLogging
}
if config.UserAgent != "" {
cfg.UserAgent = config.UserAgent
}
if config.DefaultModel != "" {
cfg.DefaultModel = config.DefaultModel
}
if config.DefaultMaxTokens != 0 {
cfg.DefaultMaxTokens = config.DefaultMaxTokens
}
if config.DefaultTopP != 0 {
cfg.DefaultTopP = config.DefaultTopP
}
if config.DefaultTemperature != 0 {
cfg.DefaultTemperature = config.DefaultTemperature
}
if config.DefaultFrequencyPenalty != 0 {
cfg.DefaultFrequencyPenalty = config.DefaultFrequencyPenalty
}
if config.DefaultPresencePenalty != 0 {
cfg.DefaultPresencePenalty = config.DefaultPresencePenalty
}
if config.EnableStreaming {
cfg.EnableStreaming = config.EnableStreaming
}
if config.EnableThinking {
cfg.EnableThinking = config.EnableThinking
}
if config.TranslationOptions.Model != "" {
cfg.TranslationOptions.Model = config.TranslationOptions.Model
}
if config.TranslationOptions.MaxTokens != 0 {
cfg.TranslationOptions.MaxTokens = config.TranslationOptions.MaxTokens
}
if config.TranslationOptions.Temperature != 0 {
cfg.TranslationOptions.Temperature = config.TranslationOptions.Temperature
}
if config.TranslationOptions.FrequencyPenalty != 0 {
cfg.TranslationOptions.FrequencyPenalty = config.TranslationOptions.FrequencyPenalty
}
if config.TranslationOptions.PresencePenalty != 0 {
cfg.TranslationOptions.PresencePenalty = config.TranslationOptions.PresencePenalty
}
if config.TranslationOptions.TopP != 0 {
cfg.TranslationOptions.TopP = config.TranslationOptions.TopP
}
if config.TranslationOptions.EnableThinking {
cfg.TranslationOptions.EnableThinking = config.TranslationOptions.EnableThinking
}
client, err := NewClient(cfg, logger)
if err != nil {
return nil, err
}
service := NewService(client)
return &SDK{
service: service,
client: client,
config: cfg,
}, nil
}
// ChatCompletion performs a chat completion request
func (s *SDK) ChatCompletion(ctx context.Context, req *ChatCompletionRequest) (*ChatCompletionResponse, error) {
return s.service.ChatCompletion(ctx, req)
}
// StreamChatCompletion performs a streaming chat completion request
func (s *SDK) StreamChatCompletion(ctx context.Context, req *ChatCompletionRequest, onChunk func(*StreamChatCompletionResponse) error) error {
return s.service.StreamChatCompletion(ctx, req, onChunk)
}
// ListModels lists available models
func (s *SDK) ListModels(ctx context.Context) (*ModelsResponse, error) {
return s.service.ListModels(ctx)
}
// Health checks if the GLM service is available
func (s *SDK) Health(ctx context.Context) error {
return s.service.Health(ctx)
}
// QuickChat is a convenience method for simple chat interactions
func (s *SDK) QuickChat(ctx context.Context, userMessage string) (string, error) {
req := &ChatCompletionRequest{
Model: s.config.DefaultModel,
Messages: []Message{
{
Role: MessageRoleUser,
Content: userMessage,
},
},
MaxTokens: s.config.DefaultMaxTokens,
TopP: s.config.DefaultTopP,
Temperature: s.config.DefaultTemperature,
FrequencyPenalty: s.config.DefaultFrequencyPenalty,
PresencePenalty: s.config.DefaultPresencePenalty,
}
if s.config.EnableThinking {
req.Thinking = &Thinking{Type: "true"}
}
resp, err := s.ChatCompletion(ctx, req)
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", ErrInvalidResponse("no choices returned")
}
return resp.Choices[0].Message.Content, nil
}
// QuickChatWithSystem is a convenience method for chat with system prompt
func (s *SDK) QuickChatWithSystem(ctx context.Context, systemPrompt, userMessage string) (string, error) {
req := &ChatCompletionRequest{
Model: s.config.DefaultModel,
Messages: []Message{
{
Role: MessageRoleSystem,
Content: systemPrompt,
},
{
Role: MessageRoleUser,
Content: userMessage,
},
},
MaxTokens: s.config.DefaultMaxTokens,
TopP: s.config.DefaultTopP,
Temperature: s.config.DefaultTemperature,
FrequencyPenalty: s.config.DefaultFrequencyPenalty,
PresencePenalty: s.config.DefaultPresencePenalty,
}
if s.config.EnableThinking {
req.Thinking = &Thinking{Type: "true"}
}
resp, err := s.ChatCompletion(ctx, req)
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", ErrInvalidResponse("no choices returned")
}
return resp.Choices[0].Message.Content, nil
}
// TranslateOptions represents options for translation
type TranslateOptions struct {
Model string `json:"model,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
PresencePenalty float64 `json:"presence_penalty,omitempty"`
TopP float64 `json:"top_p,omitempty"`
EnableThinking *bool `json:"enable_thinking,omitempty"`
}
// Translate is a convenience method for translation tasks using dynamic configuration parameters optimized for translation
func (s *SDK) Translate(ctx context.Context, text, fromLang, toLang string) (string, error) {
systemPrompt := "You are a high-precision translator specializing in professional tender documents. Always respond with only the translated text — no explanations, notes, or markdown formatting."
userMessage := fmt.Sprintf("Translate the following text from %s to %s accurately and professionally:\n\"%s\"", fromLang, toLang, text)
// Build request with custom options or defaults
req := &ChatCompletionRequest{
Model: s.config.TranslationOptions.Model,
Messages: []Message{
{
Role: MessageRoleSystem,
Content: systemPrompt,
},
{
Role: MessageRoleUser,
Content: userMessage,
},
},
MaxTokens: s.config.TranslationOptions.MaxTokens,
TopP: s.config.TranslationOptions.TopP,
Temperature: s.config.TranslationOptions.Temperature,
FrequencyPenalty: s.config.TranslationOptions.FrequencyPenalty,
PresencePenalty: s.config.TranslationOptions.PresencePenalty,
}
resp, err := s.ChatCompletion(ctx, req)
if err != nil {
return "", err
}
if len(resp.Choices) == 0 {
return "", ErrInvalidResponse("no choices returned")
}
return resp.Choices[0].Message.Content, nil
}
// StreamQuickChat is a convenience method for streaming chat interactions
func (s *SDK) StreamQuickChat(ctx context.Context, userMessage string, onChunk func(string) error) error {
req := &ChatCompletionRequest{
Model: s.config.DefaultModel,
Messages: []Message{
{
Role: MessageRoleUser,
Content: userMessage,
},
},
MaxTokens: s.config.DefaultMaxTokens,
TopP: s.config.DefaultTopP,
Temperature: s.config.DefaultTemperature,
FrequencyPenalty: s.config.DefaultFrequencyPenalty,
PresencePenalty: s.config.DefaultPresencePenalty,
Stream: true,
}
if s.config.EnableThinking {
req.Thinking = &Thinking{Type: "true"}
}
return s.StreamChatCompletion(ctx, req, func(chunk *StreamChatCompletionResponse) error {
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
return onChunk(chunk.Choices[0].Delta.Content)
}
return nil
})
}
// GetService returns the underlying service for advanced usage
func (s *SDK) GetService() GLMService {
return s.service
}
// GetConfig returns the current configuration
func (s *SDK) GetConfig() *Config {
return s.config
}
-218
View File
@@ -1,218 +0,0 @@
package glm
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"tm/pkg/logger"
)
// Service implements the GLMService interface
type Service struct {
client *Client
config *Config
logger logger.Logger
}
// NewService creates a new GLM service
func NewService(client *Client) GLMService {
return &Service{
client: client,
config: client.config,
logger: client.logger,
}
}
// ChatCompletion performs a chat completion request
func (s *Service) ChatCompletion(ctx context.Context, req *ChatCompletionRequest) (*ChatCompletionResponse, error) {
if s.logger != nil {
s.logger.Info("Performing chat completion", map[string]interface{}{
"model": req.Model,
"message_count": len(req.Messages),
"max_tokens": req.MaxTokens,
"stream": req.Stream,
})
}
var resp ChatCompletionResponse
err := s.client.Post(ctx, "/api/coding/paas/v4/chat/completions", req, &resp)
if err != nil {
if s.logger != nil {
s.logger.Error("Chat completion failed", map[string]interface{}{
"model": req.Model,
"error": err.Error(),
})
}
return nil, ErrChatCompletion("chat completion request failed", err)
}
if s.logger != nil {
s.logger.Info("Chat completion completed", map[string]interface{}{
"model": req.Model,
"choice_count": len(resp.Choices),
"usage": resp.Usage,
})
}
return &resp, nil
}
// StreamChatCompletion performs a streaming chat completion request
func (s *Service) StreamChatCompletion(ctx context.Context, req *ChatCompletionRequest, onChunk func(*StreamChatCompletionResponse) error) error {
if s.logger != nil {
s.logger.Info("Starting streaming chat completion", map[string]interface{}{
"model": req.Model,
"message_count": len(req.Messages),
"max_tokens": req.MaxTokens,
})
}
// Force streaming
req.Stream = true
return s.streamRequest(ctx, req, onChunk)
}
// streamRequest handles streaming requests
func (s *Service) streamRequest(ctx context.Context, req *ChatCompletionRequest, onChunk func(*StreamChatCompletionResponse) error) error {
// Create a custom HTTP request for streaming
httpReq, err := s.client.createStreamingRequest(ctx, req)
if err != nil {
return ErrStreamError("failed to create streaming request", err)
}
resp, err := s.client.httpClient.Do(httpReq)
if err != nil {
return ErrStreamError("failed to execute streaming request", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return s.client.handleHTTPError(resp.StatusCode, body)
}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
// Skip empty lines
if strings.TrimSpace(line) == "" {
continue
}
// Parse SSE format
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
// Check for end of stream
if data == "[DONE]" {
break
}
var chunk StreamChatCompletionResponse
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
if s.logger != nil {
s.logger.Warn("Failed to parse streaming chunk", map[string]interface{}{
"error": err.Error(),
"data": data,
})
}
continue
}
if err := onChunk(&chunk); err != nil {
return ErrStreamError("chunk callback failed", err)
}
}
}
if err := scanner.Err(); err != nil {
return ErrStreamError("streaming scanner error", err)
}
if s.logger != nil {
s.logger.Info("Streaming chat completion finished", map[string]interface{}{})
}
return nil
}
// ListModels lists available models
func (s *Service) ListModels(ctx context.Context) (*ModelsResponse, error) {
if s.logger != nil {
s.logger.Info("Listing available models", map[string]interface{}{})
}
var resp ModelsResponse
err := s.client.Get(ctx, "/api/coding/paas/v4/models", &resp)
if err != nil {
if s.logger != nil {
s.logger.Error("Failed to list models", map[string]interface{}{
"error": err.Error(),
})
}
return nil, fmt.Errorf("failed to list models: %w", err)
}
if s.logger != nil {
s.logger.Info("Models listed successfully", map[string]interface{}{
"model_count": len(resp.Data),
})
}
return &resp, nil
}
// Health checks if the GLM service is available
func (s *Service) Health(ctx context.Context) error {
if s.logger != nil {
s.logger.Debug("Performing health check", map[string]interface{}{})
}
// Simple health check by listing models
_, err := s.ListModels(ctx)
if err != nil {
if s.logger != nil {
s.logger.Error("Health check failed", map[string]interface{}{
"error": err.Error(),
})
}
return fmt.Errorf("health check failed: %w", err)
}
if s.logger != nil {
s.logger.Debug("Health check passed", map[string]interface{}{})
}
return nil
}
// createStreamingRequest creates an HTTP request for streaming
func (c *Client) createStreamingRequest(ctx context.Context, req *ChatCompletionRequest) (*http.Request, error) {
url := c.config.BaseURL + "/api/coding/paas/v4/chat/completions"
bodyBytes, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("User-Agent", c.config.UserAgent)
if c.config.APIKey != "" {
httpReq.Header.Set("Authorization", "Bearer "+c.config.APIKey)
}
return httpReq, nil
}