mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-09-08 10:14:35 +08:00
Refactor[Go Model Provider]: refactor baseURL and modelConfig (#15627)
### What problem does this PR solve? As Title ### Type of change - [x] Refactoring
This commit is contained in:
@@ -30,30 +30,12 @@ import (
|
||||
)
|
||||
|
||||
// MistralModel implements ModelDriver for Mistral AI.
|
||||
//
|
||||
// Mistral exposes an OpenAI-compatible REST API at https://api.mistral.ai/v1
|
||||
// (chat completions at /chat/completions, list models at /models). The wire
|
||||
// shape matches OpenAI closely enough that the chat path here is a direct
|
||||
// port of the OpenAI driver, with the differences kept small on purpose:
|
||||
// no reasoning_content pass-through (Mistral does not expose one), and a
|
||||
// distinct Name() so the factory can route to this driver.
|
||||
|
||||
type MistralModel struct {
|
||||
BaseURL map[string]string
|
||||
URLSuffix URLSuffix
|
||||
httpClient *http.Client
|
||||
baseModel BaseModel
|
||||
}
|
||||
|
||||
// NewMistralModel creates a new Mistral model instance.
|
||||
//
|
||||
// We clone http.DefaultTransport so we keep Go's defaults for
|
||||
// ProxyFromEnvironment, DialContext (with KeepAlive), HTTP/2,
|
||||
// TLSHandshakeTimeout, and ExpectContinueTimeout, and only override
|
||||
// the connection-pool fields we care about.
|
||||
//
|
||||
// The Client itself has no Timeout. http.Client.Timeout would also
|
||||
// cap the time spent reading the response body, which would cut off
|
||||
// long-lived SSE streams in ChatStreamlyWithSender. Non-streaming
|
||||
// callers wrap each request with context.WithTimeout instead.
|
||||
func NewMistralModel(baseURL map[string]string, urlSuffix URLSuffix) *MistralModel {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.MaxIdleConns = 100
|
||||
@@ -63,54 +45,40 @@ func NewMistralModel(baseURL map[string]string, urlSuffix URLSuffix) *MistralMod
|
||||
transport.ResponseHeaderTimeout = 60 * time.Second
|
||||
|
||||
return &MistralModel{
|
||||
BaseURL: baseURL,
|
||||
URLSuffix: urlSuffix,
|
||||
httpClient: &http.Client{
|
||||
Transport: transport,
|
||||
baseModel: BaseModel{
|
||||
BaseURL: baseURL,
|
||||
URLSuffix: urlSuffix,
|
||||
httpClient: &http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MistralModel) NewInstance(baseURL map[string]string) ModelDriver {
|
||||
return NewMistralModel(baseURL, m.URLSuffix)
|
||||
return NewMistralModel(baseURL, m.baseModel.URLSuffix)
|
||||
}
|
||||
|
||||
func (m *MistralModel) Name() string {
|
||||
return "mistral"
|
||||
}
|
||||
|
||||
// baseURLForRegion returns the base URL for the given region, or an
|
||||
// error if no entry exists. This makes a misconfigured region fail
|
||||
// fast with a clear message, instead of silently producing a relative
|
||||
// URL that the HTTP transport then rejects.
|
||||
func (m *MistralModel) baseURLForRegion(region string) (string, error) {
|
||||
base, ok := m.BaseURL[region]
|
||||
if !ok || base == "" {
|
||||
return "", fmt.Errorf("mistral: no base URL configured for region %q", region)
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// ChatWithMessages sends multiple messages with roles and returns the response.
|
||||
func (m *MistralModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) {
|
||||
if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" {
|
||||
return nil, fmt.Errorf("api key is required")
|
||||
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
return nil, fmt.Errorf("messages is empty")
|
||||
}
|
||||
|
||||
region := "default"
|
||||
if apiConfig.Region != nil && *apiConfig.Region != "" {
|
||||
region = *apiConfig.Region
|
||||
}
|
||||
|
||||
baseURL, err := m.baseURLForRegion(region)
|
||||
baseURL, err := m.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.URLSuffix.Chat)
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Chat)
|
||||
|
||||
apiMessages := make([]map[string]interface{}, len(messages))
|
||||
for i, msg := range messages {
|
||||
@@ -160,7 +128,7 @@ func (m *MistralModel) ChatWithMessages(modelName string, messages []Message, ap
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
resp, err := m.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
@@ -206,27 +174,6 @@ func (m *MistralModel) ChatWithMessages(modelName string, messages []Message, ap
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractMistralContent normalizes the two shapes Mistral can return in
|
||||
// choices[0].message.content.
|
||||
//
|
||||
// 1. Plain string. The historical shape, used by every non-reasoning
|
||||
// Mistral model (mistral-large, mistral-medium, ministral-*, etc.):
|
||||
//
|
||||
// "content": "Pong."
|
||||
//
|
||||
// 2. Structured array of typed parts. Used by the magistral reasoning
|
||||
// family (magistral-small-*, magistral-medium-*) when the model
|
||||
// actually produces a chain-of-thought:
|
||||
//
|
||||
// "content": [
|
||||
// {"type": "thinking", "thinking": [{"type": "text", "text": "..."}]},
|
||||
// {"type": "text", "text": "The final answer is ..."}
|
||||
// ]
|
||||
//
|
||||
// The function concatenates the visible text parts into the assistant
|
||||
// answer and the inner thinking text into the reasoning trace. Unknown
|
||||
// part types are skipped rather than failing, so a new part shape from
|
||||
// Mistral does not break the driver for tenants that don't use it.
|
||||
func extractMistralContent(raw interface{}) (string, string, error) {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
@@ -269,10 +216,12 @@ func extractMistralContent(raw interface{}) (string, string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ChatStreamlyWithSender sends messages and streams the response via the
|
||||
// sender function. The Mistral SSE stream uses the same shape as OpenAI:
|
||||
// "data:" lines carrying JSON events, with a final "[DONE]" line.
|
||||
// ChatStreamlyWithSender sends messages and streams the response
|
||||
func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error {
|
||||
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sender == nil {
|
||||
return fmt.Errorf("sender is required")
|
||||
}
|
||||
@@ -281,20 +230,12 @@ func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Messa
|
||||
return fmt.Errorf("messages is empty")
|
||||
}
|
||||
|
||||
if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" {
|
||||
return fmt.Errorf("api key is required")
|
||||
}
|
||||
|
||||
var region = "default"
|
||||
if apiConfig.Region != nil && *apiConfig.Region != "" {
|
||||
region = *apiConfig.Region
|
||||
}
|
||||
|
||||
baseURL, err := m.baseURLForRegion(region)
|
||||
baseURL, err := m.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.URLSuffix.Chat)
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Chat)
|
||||
|
||||
apiMessages := make([]map[string]interface{}, len(messages))
|
||||
for i, msg := range messages {
|
||||
@@ -311,10 +252,6 @@ func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Messa
|
||||
}
|
||||
|
||||
if chatModelConfig != nil {
|
||||
// Refuse to run if the caller explicitly asked for stream=false.
|
||||
// The body of this method only knows how to read SSE, so a
|
||||
// non-SSE JSON response would be parsed as if it were a stream
|
||||
// and produce no chunks. Better to fail clearly.
|
||||
if chatModelConfig.Stream != nil && !*chatModelConfig.Stream {
|
||||
return fmt.Errorf("stream must be true in ChatStreamlyWithSender")
|
||||
}
|
||||
@@ -338,9 +275,6 @@ func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Messa
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
// Use an explicit background context. SSE streams are long-lived
|
||||
// so we do not attach a hard deadline here; the transport's
|
||||
// ResponseHeaderTimeout caps the connection-establishment phase.
|
||||
req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
@@ -349,7 +283,7 @@ func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Messa
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
resp, err := m.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
@@ -440,32 +374,26 @@ type mistralEmbeddingResponse struct {
|
||||
Object string `json:"object"`
|
||||
}
|
||||
|
||||
// Embed turns a list of texts into embedding vectors using the
|
||||
// Mistral /v1/embeddings endpoint (mistral-embed). The output has
|
||||
// one vector per input, in the same order the inputs were given.
|
||||
// Embed turns a list of texts into embedding vectors
|
||||
func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) {
|
||||
if len(texts) == 0 {
|
||||
return []EmbeddingData{}, nil
|
||||
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" {
|
||||
return nil, fmt.Errorf("api key is required")
|
||||
if len(texts) == 0 {
|
||||
return []EmbeddingData{}, nil
|
||||
}
|
||||
|
||||
if modelName == nil || *modelName == "" {
|
||||
return nil, fmt.Errorf("model name is required")
|
||||
}
|
||||
|
||||
region := "default"
|
||||
if apiConfig.Region != nil && *apiConfig.Region != "" {
|
||||
region = *apiConfig.Region
|
||||
}
|
||||
|
||||
baseURL, err := m.baseURLForRegion(region)
|
||||
baseURL, err := m.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.URLSuffix.Embedding)
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Embedding)
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"model": *modelName,
|
||||
@@ -488,7 +416,7 @@ func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APICo
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
resp, err := m.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
@@ -508,10 +436,6 @@ func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APICo
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
// Reorder the returned vectors by their reported index so the output
|
||||
// always lines up with the input texts, even if the upstream API ever
|
||||
// returns items out of order. A nil slot at the end indicates the
|
||||
// upstream did not return an embedding for that input.
|
||||
embeddings := make([]EmbeddingData, len(texts))
|
||||
filled := make([]bool, len(texts))
|
||||
for _, item := range parsed.Data {
|
||||
@@ -519,9 +443,6 @@ func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APICo
|
||||
return nil, fmt.Errorf("mistral: response index %d out of range for %d inputs", item.Index, len(texts))
|
||||
}
|
||||
if filled[item.Index] {
|
||||
// A malformed response that repeats the same index would
|
||||
// silently overwrite the earlier vector. Fail loudly so
|
||||
// the caller never uses ambiguous output.
|
||||
return nil, fmt.Errorf("mistral: duplicate embedding index %d in response", item.Index)
|
||||
}
|
||||
embeddings[item.Index] = EmbeddingData{
|
||||
@@ -541,20 +462,16 @@ func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APICo
|
||||
|
||||
// ListModels returns the list of model ids visible to the API key.
|
||||
func (m *MistralModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
||||
if apiConfig == nil || apiConfig.ApiKey == nil || *apiConfig.ApiKey == "" {
|
||||
return nil, fmt.Errorf("api key is required")
|
||||
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
region := "default"
|
||||
if apiConfig.Region != nil && *apiConfig.Region != "" {
|
||||
region = *apiConfig.Region
|
||||
}
|
||||
|
||||
baseURL, err := m.baseURLForRegion(region)
|
||||
baseURL, err := m.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.URLSuffix.Models)
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Models)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout)
|
||||
defer cancel()
|
||||
@@ -566,7 +483,7 @@ func (m *MistralModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
resp, err := m.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
@@ -621,8 +538,7 @@ func (m *MistralModel) CheckConnection(apiConfig *APIConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rerank calculates similarity scores between query and documents. Mistral
|
||||
// does not expose a public rerank API, so this returns "no such method".
|
||||
// Rerank calculates similarity scores between query and documents
|
||||
func (m *MistralModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) {
|
||||
return nil, fmt.Errorf("%s, no such method", m.Name())
|
||||
}
|
||||
@@ -647,16 +563,19 @@ func (m *MistralModel) AudioSpeechWithSender(modelName *string, audioContent *st
|
||||
|
||||
// OCRFile OCR file
|
||||
func (m *MistralModel) OCRFile(modelName *string, content []byte, urls *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) {
|
||||
if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if (urls == nil || *urls == "") && (content == nil || len(content) == 0) {
|
||||
return nil, fmt.Errorf("file url or content is required")
|
||||
}
|
||||
|
||||
region := "default"
|
||||
if apiConfig.Region != nil && *apiConfig.Region != "" {
|
||||
region = *apiConfig.Region
|
||||
resolvedBaseURL, err := m.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s", m.BaseURL[region], m.URLSuffix.OCR)
|
||||
url := fmt.Sprintf("%s/%s", resolvedBaseURL, m.baseModel.URLSuffix.OCR)
|
||||
|
||||
var docURL string
|
||||
if urls != nil && *urls != "" {
|
||||
@@ -691,7 +610,7 @@ func (m *MistralModel) OCRFile(modelName *string, content []byte, urls *string,
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
resp, err := m.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user