feat(go-models): migrate batch 3 model drivers to unified handlers (#17698)

## Summary

Relate to #17284

Migrate 10 OpenAI-compatible drivers (`minimax`, `mistral`,
`modelscope`, `moonshot`, `n1n`, `novita`, `ollama`, `openai`,
`openai_api_compatible`, `openrouter`) to use the unified response
handlers (`HandleNonStreamingResponse` / `HandleStreamingResponse`),
following the same pattern established by `deepseek` in #17634.

- Cut ~150 lines per driver (1692 lines removed, 172 added across 10
files).
- `openai_api_compatible` gains `ChatWithMessages` +
`ChatStreamlyWithSender` required by the unified handler infrastructure.
- `openai` driver preserves `reasoning_content` extraction for o-series
models.
- All drivers: pure deduplication of HTTP plumbing.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay77721
2026-08-03 15:08:55 +08:00
committed by GitHub
parent bddc941814
commit 1478aa4ced
15 changed files with 447 additions and 1904 deletions

View File

@@ -17,7 +17,6 @@
package models
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -25,31 +24,13 @@ import (
"net/http"
"ragflow/internal/common"
"strings"
"sync"
"time"
)
// modelscopeStreamIdleTimeout bounds how long a stream can go without
var modelscopeStreamIdleTimeout = 60 * time.Second
// ModelScopeModel implements ModelDriver for ModelScope chat models.
type ModelScopeModel struct {
baseModel BaseModel
}
type modelscopeChatChoice struct {
Message struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
Reasoning string `json:"reasoning"`
Thinking string `json:"thinking"`
} `json:"message"`
}
type modelscopeChatResponse struct {
Choices []modelscopeChatChoice `json:"choices"`
}
type modelscopeModelListResponse struct {
Data []ModelListItem `json:"data"`
}
@@ -85,28 +66,6 @@ func normalizeModelScopeBaseURL(base string) string {
return trimmed
}
func modelscopeReasoningFromStrings(reasoningContent string, reasoning string, thinking string) string {
switch {
case reasoningContent != "":
return reasoningContent
case reasoning != "":
return reasoning
case thinking != "":
return thinking
default:
return ""
}
}
func modelscopeReasoningFromMap(value map[string]interface{}) string {
for _, field := range []string{"reasoning_content", "reasoning", "thinking"} {
if text, ok := value[field].(string); ok && text != "" {
return text
}
}
return ""
}
// ChatWithMessages sends multiple messages with roles and returns the response.
func (m *ModelScopeModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) {
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
@@ -125,56 +84,13 @@ func (m *ModelScopeModel) ChatWithMessages(ctx context.Context, modelName string
url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Chat)
reqBody := buildRequestBody(chatModelConfig, modelName, messages, false)
jsonData, err := json.Marshal(reqBody)
body, err := m.baseModel.doRequest(ctx, url, apiConfig, reqBody, nonStreamCallTimeout)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if auth := BearerAuth(apiConfig); auth != "" {
req.Header.Set("Authorization", auth)
}
resp, err := m.baseModel.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
var result modelscopeChatResponse
if err = json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(result.Choices) == 0 {
return nil, fmt.Errorf("no choices in response")
}
content := result.Choices[0].Message.Content
reasonContent := modelscopeReasoningFromStrings(
result.Choices[0].Message.ReasoningContent,
result.Choices[0].Message.Reasoning,
result.Choices[0].Message.Thinking,
)
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
}, nil
return HandleNonStreamingResponse(body, modelUsage, chatModelConfig, OpenAIParserConfig)
}
// ChatStreamlyWithSender sends messages and streams response via sender.
@@ -189,8 +105,8 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(ctx context.Context, modelName
if len(messages) == 0 {
return fmt.Errorf("messages is empty")
}
if chatModelConfig != nil && chatModelConfig.Stream != nil && !*chatModelConfig.Stream {
return fmt.Errorf("stream must be true in ChatStreamlyWithSender")
if err := validateStreamConfig(chatModelConfig); err != nil {
return err
}
baseURL, err := m.baseModel.GetBaseURL(apiConfig)
@@ -201,105 +117,11 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(ctx context.Context, modelName
url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Chat)
reqBody := buildRequestBody(chatModelConfig, modelName, messages, true)
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
reqBody["stream_options"] = map[string]interface{}{"include_usage": true}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if auth := BearerAuth(apiConfig); auth != "" {
req.Header.Set("Authorization", auth)
}
resp, err := m.baseModel.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
lastActive := time.Now()
var lastActiveMu sync.Mutex
done := make(chan struct{})
defer close(done)
go func() {
ticker := time.NewTicker(modelscopeStreamIdleTimeout / 4)
defer ticker.Stop()
for {
select {
case <-done:
return
case now := <-ticker.C:
lastActiveMu.Lock()
idle := now.Sub(lastActive)
lastActiveMu.Unlock()
if idle >= modelscopeStreamIdleTimeout {
cancel()
return
}
}
}
}()
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
streamDone, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
lastActiveMu.Lock()
lastActive = time.Now()
lastActiveMu.Unlock()
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil
}
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
return nil
}
if delta, ok := firstChoice["delta"].(map[string]interface{}); ok {
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if reasoning := modelscopeReasoningFromMap(delta); reasoning != "" {
if err := sender(nil, &reasoning); err != nil {
return err
}
}
if content, ok := delta["content"].(string); ok && content != "" {
if err := sender(&content, nil); err != nil {
return err
}
}
}
if finishReason, ok := firstChoice["finish_reason"].(string); ok && finishReason != "" {
sawTerminal = true
}
return nil
return m.baseModel.doStreamRequest(ctx, url, apiConfig, reqBody, streamCallTimeout, func(body io.ReadCloser) error {
return HandleStreamingResponse(body, modelUsage, chatModelConfig, OpenAIParserConfig, sender)
})
if err != nil {
if ctx.Err() != nil {
return fmt.Errorf("modelscope: stream idle for more than %s, aborted", modelscopeStreamIdleTimeout)
}
return fmt.Errorf("failed to scan response body: %w", err)
}
if !streamDone && !sawTerminal {
return fmt.Errorf("modelscope: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
return sender(&endOfStream, nil)
}
func (m *ModelScopeModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) {