eb67287b56
30-second resty client timeout was killing long streaming responses mid-generation. Models with large output windows (e.g. deepseek-v4-pro at 384K max_tokens) routinely exceed 30s. Raised all providers to 10 minutes (Ollama already at 15min, unchanged). Circuit breaker recovery timeout raised from 30s to 5min.
120 lines
3.2 KiB
Go
120 lines
3.2 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
"gophergate/internal/config"
|
|
"gophergate/internal/models"
|
|
)
|
|
|
|
type MoonshotProvider struct {
|
|
client *resty.Client
|
|
config config.MoonshotConfig
|
|
apiKey string
|
|
}
|
|
|
|
func NewMoonshotProvider(cfg config.MoonshotConfig, apiKey string) *MoonshotProvider {
|
|
return &MoonshotProvider{
|
|
client: resty.New().SetTimeout(10 * time.Minute),
|
|
config: cfg,
|
|
apiKey: strings.TrimSpace(apiKey),
|
|
}
|
|
}
|
|
|
|
func (p *MoonshotProvider) Name() string {
|
|
return "moonshot"
|
|
}
|
|
|
|
func (p *MoonshotProvider) ChatCompletion(ctx context.Context, req *models.UnifiedRequest) (*models.ChatCompletionResponse, error) {
|
|
messagesJSON, err := MessagesToOpenAIJSON(req.Messages)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
|
}
|
|
|
|
body := BuildOpenAIBody(req, messagesJSON, false)
|
|
if strings.Contains(strings.ToLower(req.Model), "kimi-k2.5") {
|
|
if maxTokens, ok := body["max_tokens"]; ok {
|
|
delete(body, "max_tokens")
|
|
body["max_completion_tokens"] = maxTokens
|
|
}
|
|
}
|
|
|
|
baseURL := strings.TrimRight(p.config.BaseURL, "/")
|
|
|
|
resp, err := p.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("Authorization", "Bearer "+p.apiKey).
|
|
SetHeader("Content-Type", "application/json").
|
|
SetHeader("Accept", "application/json").
|
|
SetBody(body).
|
|
Post(fmt.Sprintf("%s/chat/completions", baseURL))
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
|
|
if !resp.IsSuccess() {
|
|
return nil, fmt.Errorf("Moonshot API error (%d): %s", resp.StatusCode(), resp.String())
|
|
}
|
|
|
|
var respJSON map[string]interface{}
|
|
if err := json.Unmarshal(resp.Body(), &respJSON); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return ParseOpenAIResponse(respJSON, req.Model)
|
|
}
|
|
|
|
func (p *MoonshotProvider) ChatCompletionStream(ctx context.Context, req *models.UnifiedRequest) (<-chan *models.ChatCompletionStreamResponse, error) {
|
|
messagesJSON, err := MessagesToOpenAIJSON(req.Messages)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
|
}
|
|
|
|
body := BuildOpenAIBody(req, messagesJSON, true)
|
|
if strings.Contains(strings.ToLower(req.Model), "kimi-k2.5") {
|
|
if maxTokens, ok := body["max_tokens"]; ok {
|
|
delete(body, "max_tokens")
|
|
body["max_completion_tokens"] = maxTokens
|
|
}
|
|
}
|
|
|
|
baseURL := strings.TrimRight(p.config.BaseURL, "/")
|
|
|
|
resp, err := p.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("Authorization", "Bearer "+p.apiKey).
|
|
SetHeader("Content-Type", "application/json").
|
|
SetHeader("Accept", "text/event-stream").
|
|
SetBody(body).
|
|
SetDoNotParseResponse(true).
|
|
Post(fmt.Sprintf("%s/chat/completions", baseURL))
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
|
|
if !resp.IsSuccess() {
|
|
return nil, fmt.Errorf("Moonshot API error (%d): %s", resp.StatusCode(), resp.String())
|
|
}
|
|
|
|
ch := make(chan *models.ChatCompletionStreamResponse)
|
|
go func() {
|
|
defer close(ch)
|
|
if err := StreamOpenAI(resp.RawBody(), ch); err != nil {
|
|
fmt.Printf("Moonshot Stream error: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
return ch, nil
|
|
}
|
|
|
|
func (p *MoonshotProvider) ImageGeneration(ctx context.Context, req *models.ImageGenerationRequest) (*models.ImageGenerationResponse, error) {
|
|
return nil, fmt.Errorf("moonshot does not support image generation")
|
|
}
|