477a811999
The 40-character truncation of tool call IDs in helper.go caused collisions when models (like deepseek-v4-flash) generated longer IDs, leading to "Duplicate value for 'tool_call_id'" errors. Removed the limit to allow full unique IDs. DeepSeek: updated reasoning_content injection to use an empty string instead of a space, better matching provider expectations for history. Improved API error reporting across all providers by capturing raw body content when response parsing fails or returns empty strings.
141 lines
3.9 KiB
Go
141 lines
3.9 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"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() {
|
|
msg := resp.String()
|
|
if msg == "" {
|
|
if body, err := io.ReadAll(resp.RawBody()); err == nil {
|
|
msg = string(body)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("Moonshot API error (%d): %s", resp.StatusCode(), msg)
|
|
}
|
|
|
|
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() {
|
|
msg := resp.String()
|
|
if msg == "" {
|
|
if body, err := io.ReadAll(resp.RawBody()); err == nil {
|
|
msg = string(body)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("Moonshot API error (%d): %s", resp.StatusCode(), msg)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
func (p *MoonshotProvider) Responses(ctx context.Context, req *models.ResponsesRequest) (*models.ResponsesResponse, error) {
|
|
return nil, fmt.Errorf("responses API not supported by moonshot")
|
|
}
|
|
|
|
func (p *MoonshotProvider) ResponsesStream(ctx context.Context, req *models.ResponsesRequest) (<-chan *models.ResponsesStreamChunk, error) {
|
|
return nil, fmt.Errorf("responses API not supported by moonshot")
|
|
}
|