diff --git a/internal/cli/response.go b/internal/cli/response.go index 7fff66b8fc..24086b0506 100644 --- a/internal/cli/response.go +++ b/internal/cli/response.go @@ -530,11 +530,11 @@ func (r *MessageResponse) PrintOut() { } type NonStreamResponse struct { - Code int `json:"code"` - ReasoningContent string `json:"reasoning_content"` - Answer string `json:"answer"` - Message string `json:"message"` - Usage *models.ChatUsage `json:"usage"` + Code int `json:"code"` + ReasoningContent string `json:"reasoning_content"` + Answer string `json:"answer"` + Message string `json:"message"` + Usage *models.TokenUsage `json:"usage"` Duration float64 OutputFormat OutputFormat } diff --git a/internal/common/model_type.go b/internal/common/model_type.go new file mode 100644 index 0000000000..83942a46de --- /dev/null +++ b/internal/common/model_type.go @@ -0,0 +1,51 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package common + +import ( + "fmt" + "time" +) + +type ModelUsage struct { + UserID string `json:"user_id"` + UserEmail string `json:"user_email"` + TenantID string `json:"tenant_id"` + TenantEmail string `json:"tenant_email"` + GroupID string `json:"group_id"` + GroupName string `json:"group_name"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + SessionID string `json:"session_id"` + ProviderName string `json:"provider_name"` + InstanceID string `json:"instance_id"` + APIKey string `json:"api_key"` + ModelName string `json:"model_name"` + Type string `json:"type"` + RequestID string `json:"request_id"` + StartAt time.Time `json:"start_at"` + ResponseTimeMS int64 `json:"response_time_ms"` + ErrorMessage string `json:"error_message"` + + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + TotalTokens int `json:"total_tokens"` +} + +func (m *ModelUsage) String() string { + return fmt.Sprintf("%#v", m) +} diff --git a/internal/engine/clickhouse/stats_ee.go b/internal/engine/clickhouse/stats_ee.go new file mode 100644 index 0000000000..76404f8cc2 --- /dev/null +++ b/internal/engine/clickhouse/stats_ee.go @@ -0,0 +1,28 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package clickhouse + +import ( + "ragflow/internal/common" +) + +func (d *Driver) CollectModelUsage(modelUsage *common.ModelUsage) error { + //if modelUsage != nil { + // common.Info("CollectModelUsage", zap.Any("modelUsage", modelUsage.String())) + //} + return nil +} diff --git a/internal/entity/models/302ai.go b/internal/entity/models/302ai.go index 85910db12b..11e4ef31a8 100644 --- a/internal/entity/models/302ai.go +++ b/internal/entity/models/302ai.go @@ -28,6 +28,7 @@ import ( "net/url" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -70,7 +71,7 @@ func validateAI302DocumentURL(rawURL string) (string, error) { return documentURL, nil } -func (a *AI302Model) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (a *AI302Model) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -218,7 +219,7 @@ func (a *AI302Model) ChatWithMessages(modelName string, messages []Message, apiC return chatResponse, nil } -func (a *AI302Model) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (a *AI302Model) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -342,14 +343,14 @@ func (a *AI302Model) ChatStreamlyWithSender(modelName string, messages []Message reasoningContent, ok := delta["reasoning_content"].(string) if ok && reasoningContent != "" { - if err := sender(nil, &reasoningContent); err != nil { + if err = sender(nil, &reasoningContent); err != nil { return err } } content, ok := delta["content"].(string) if ok && content != "" { - if err := sender(&content, nil); err != nil { + if err = sender(&content, nil); err != nil { return err } } @@ -364,7 +365,7 @@ func (a *AI302Model) ChatStreamlyWithSender(modelName string, messages []Message return sender(&endOfStream, nil) } -func (a *AI302Model) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (a *AI302Model) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -445,7 +446,7 @@ func (a *AI302Model) Embed(modelName *string, texts []string, apiConfig *APIConf return embeddings, nil } -func (a *AI302Model) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (a *AI302Model) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -534,7 +535,7 @@ func (a *AI302Model) Rerank(modelName *string, query string, documents []string, return &rerankResponse, nil } -func (a *AI302Model) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (a *AI302Model) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -662,20 +663,20 @@ func (a *AI302Model) TranscribeAudio(modelName *string, file *string, apiConfig return &ASRResponse{Text: result.Text}, nil } -func (a *AI302Model) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (a *AI302Model) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", a.Name()) } -func (a *AI302Model) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (a *AI302Model) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { // TODO https://302ai-en.apifox.cn/225254060e0 return nil, fmt.Errorf("%s no such method", a.Name()) } -func (a *AI302Model) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (a *AI302Model) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", a.Name()) } -func (a *AI302Model) OCRFile(modelName *string, content []byte, urls *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (a *AI302Model) OCRFile(modelName *string, content []byte, urls *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -770,7 +771,7 @@ func (a *AI302Model) OCRFile(modelName *string, content []byte, urls *string, ap }, nil } -func (a *AI302Model) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (a *AI302Model) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } diff --git a/internal/entity/models/302ai_test.go b/internal/entity/models/302ai_test.go index e711433c78..6052529d15 100644 --- a/internal/entity/models/302ai_test.go +++ b/internal/entity/models/302ai_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "ragflow/internal/common" "strings" "testing" ) @@ -97,6 +98,7 @@ func TestAI302ChatForcesNonStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream, Thinking: &thinking}, + &common.ModelUsage{}, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -138,6 +140,7 @@ func TestAI302StreamForcesStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(answer, reason *string) error { if answer != nil { content = append(content, *answer) @@ -252,7 +255,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "chat api key", run: func() error { - _, err := newAI302ForTest("http://unused").ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err := newAI302ForTest("http://unused").ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, &ChatConfig{}, &common.ModelUsage{}) return err }, want: "api key is required", @@ -260,7 +263,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "chat model", run: func() error { - _, err := newAI302ForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, &common.ModelUsage{}) return err }, want: "model name is required", @@ -268,21 +271,21 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "stream api key", run: func() error { - return newAI302ForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, nil, nil, send) + return newAI302ForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, nil, nil, nil, send) }, want: "api key is required", }, { name: "stream sender", run: func() error { - return newAI302ForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + return newAI302ForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) }, want: "sender is required", }, { name: "embed model", run: func() error { - _, err := newAI302ForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -290,7 +293,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "rerank api key", run: func() error { - _, err := newAI302ForTest("http://unused").Rerank(&model, "q", []string{"doc"}, nil, nil) + _, err := newAI302ForTest("http://unused").Rerank(&model, "q", []string{"doc"}, nil, nil, nil) return err }, want: "api key is required", @@ -298,7 +301,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "rerank query", run: func() error { - _, err := newAI302ForTest("http://unused").Rerank(&model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").Rerank(&model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "query is required", @@ -306,7 +309,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "asr model", run: func() error { - _, err := newAI302ForTest("http://unused").TranscribeAudio(nil, &docURL, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").TranscribeAudio(nil, &docURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -314,7 +317,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "asr file", run: func() error { - _, err := newAI302ForTest("http://unused").TranscribeAudio(&model, &file, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").TranscribeAudio(&model, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "file is missing", @@ -322,7 +325,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "ocr api key", run: func() error { - _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &docURL, nil, nil) + _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &docURL, nil, nil, nil) return err }, want: "api key is required", @@ -330,7 +333,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "ocr input", run: func() error { - _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "file url or content is required", @@ -338,7 +341,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "ocr invalid url", run: func() error { - _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &invalidURL, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &invalidURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "invalid document URL", @@ -346,7 +349,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "parse file url", run: func() error { - _, err := newAI302ForTest("http://unused").ParseFile(&model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").ParseFile(&model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "valid public document URL", @@ -354,7 +357,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "parse file invalid url", run: func() error { - _, err := newAI302ForTest("http://unused").ParseFile(&model, nil, &invalidURL, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAI302ForTest("http://unused").ParseFile(&model, nil, &invalidURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "invalid document URL", diff --git a/internal/entity/models/aliyun.go b/internal/entity/models/aliyun.go index 9aeec0f37d..0116dc2ca4 100644 --- a/internal/entity/models/aliyun.go +++ b/internal/entity/models/aliyun.go @@ -51,7 +51,7 @@ func (a *AliyunModel) Name() string { return "Tongyi-Qianwen" } -func (a *AliyunModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (a *AliyunModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -193,7 +193,7 @@ func (a *AliyunModel) ChatWithMessages(modelName string, messages []Message, api } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (a *AliyunModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (a *AliyunModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -349,7 +349,7 @@ type aliyunUsage struct { } // Embed embeds a list of texts into embeddings -func (a *AliyunModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (a *AliyunModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -437,7 +437,7 @@ type aliyunRerankResponse struct { } `json:"results"` } -func (a *AliyunModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (a *AliyunModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -510,30 +510,30 @@ func (a *AliyunModel) Rerank(modelName *string, query string, documents []string } // TranscribeAudio transcribe audio -func (a *AliyunModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (a *AliyunModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AliyunModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (a *AliyunModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } // AudioSpeech convert text to audio -func (a *AliyunModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (a *AliyunModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AliyunModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (a *AliyunModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } // OCRFile OCR file -func (a *AliyunModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (a *AliyunModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } // ParseFile parse file -func (a *AliyunModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (a *AliyunModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } diff --git a/internal/entity/models/anthropic.go b/internal/entity/models/anthropic.go index 96a45a1b64..19159ebbf9 100644 --- a/internal/entity/models/anthropic.go +++ b/internal/entity/models/anthropic.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -60,7 +61,7 @@ func (a *AnthropicModel) region(apiConfig *APIConfig) string { return "default" } -func (a *AnthropicModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (a *AnthropicModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -432,39 +433,39 @@ func (a *AnthropicModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (a *AnthropicModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (a *AnthropicModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (a *AnthropicModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (a *AnthropicModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (a *AnthropicModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (a *AnthropicModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, asrConfig *TTSConfig) (*TTSResponse, error) { +func (a *AnthropicModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, asrConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (a *AnthropicModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (a *AnthropicModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (a *AnthropicModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } diff --git a/internal/entity/models/anthropic_test.go b/internal/entity/models/anthropic_test.go index 56846f16ad..3a4a2ff533 100644 --- a/internal/entity/models/anthropic_test.go +++ b/internal/entity/models/anthropic_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "ragflow/internal/common" "strings" "testing" ) @@ -91,7 +92,8 @@ func TestAnthropicChatHappyPath(t *testing.T) { "claude-sonnet-4-5-20250929", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, - nil, + &ChatConfig{}, + &common.ModelUsage{}, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -172,6 +174,7 @@ func TestAnthropicChatMapsSystemConfigAndImages(t *testing.T) { }, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &maxTokens, Temperature: &temperature, TopP: &topP, Stop: &stop}, + &common.ModelUsage{}, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -221,7 +224,8 @@ func TestAnthropicChatMapsDataImageURL(t *testing.T) { map[string]interface{}{"type": "image_url", "image_url": map[string]interface{}{"url": "data:image/png;base64,aGVsbG8="}}, }}}, &APIConfig{ApiKey: &apiKey}, - nil, + &ChatConfig{}, + &common.ModelUsage{}, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -231,22 +235,22 @@ func TestAnthropicChatMapsDataImageURL(t *testing.T) { func TestAnthropicChatValidationErrors(t *testing.T) { m := newAnthropicForTest("http://unused") apiKey := "test-key" - if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("nil api config: got %v", err) } - if _, err := m.ChatWithMessages("claude", nil, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "messages is empty") { + if _, err := m.ChatWithMessages("claude", nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("empty messages: got %v", err) } - if _, err := m.ChatWithMessages("claude", []Message{{Role: "tool", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "unsupported message role") { + if _, err := m.ChatWithMessages("claude", []Message{{Role: "tool", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "unsupported message role") { t.Errorf("bad role: got %v", err) } - if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: []interface{}{map[string]interface{}{"type": "video_url"}}}}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "unsupported content block type") { + if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: []interface{}{map[string]interface{}{"type": "video_url"}}}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "unsupported content block type") { t.Errorf("bad block: got %v", err) } - if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: []interface{}{map[string]interface{}{"type": "text", "text": 42}}}}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "invalid text field") { + if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: []interface{}{map[string]interface{}{"type": "text", "text": 42}}}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "invalid text field") { t.Errorf("bad text block: got %v", err) } - if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: []interface{}{map[string]interface{}{"type": "image"}}}}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "image block missing source") { + if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: []interface{}{map[string]interface{}{"type": "image"}}}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "image block missing source") { t.Errorf("bad image block: got %v", err) } } @@ -259,7 +263,7 @@ func TestAnthropicChatRejectsHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newAnthropicForTest(srv.URL).ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAnthropicForTest(srv.URL).ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") || !strings.Contains(err.Error(), "bad key") { t.Errorf("expected provider error, got %v", err) } @@ -272,7 +276,7 @@ func TestAnthropicChatRejectsMalformedResponse(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newAnthropicForTest(srv.URL).ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newAnthropicForTest(srv.URL).ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no text content") { t.Errorf("expected no-text error, got %v", err) } @@ -340,38 +344,38 @@ func TestAnthropicUnsupportedMethods(t *testing.T) { name string err error }{ - {"stream", m.ChatStreamlyWithSender(modelName, []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, func(*string, *string) error { return nil })}, + {"stream", m.ChatStreamlyWithSender(modelName, []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil })}, } for _, check := range checks { if check.err == nil || !strings.Contains(check.err.Error(), "no such method") { t.Errorf("%s: want no such method, got %v", check.name, check.err) } } - if _, err := m.Embed(&modelName, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(&modelName, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed: got %v", err) } - if _, err := m.Rerank(&modelName, "q", []string{"d"}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(&modelName, "q", []string{"d"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: got %v", err) } if _, err := m.Balance(&APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: got %v", err) } - if _, err := m.TranscribeAudio(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: got %v", err) } - if err := m.TranscribeAudioWithSender(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.TranscribeAudioWithSender(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudioWithSender: got %v", err) } - if _, err := m.AudioSpeech(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: got %v", err) } - if err := m.AudioSpeechWithSender(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.AudioSpeechWithSender(&modelName, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeechWithSender: got %v", err) } - if _, err := m.OCRFile(&modelName, nil, &modelName, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&modelName, nil, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: got %v", err) } - if _, err := m.ParseFile(&modelName, nil, &modelName, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(&modelName, nil, &modelName, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ParseFile: got %v", err) } if _, err := m.ListTasks(&APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { diff --git a/internal/entity/models/astraflow.go b/internal/entity/models/astraflow.go index cbc8a9793d..3b4d23db35 100644 --- a/internal/entity/models/astraflow.go +++ b/internal/entity/models/astraflow.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -67,7 +68,7 @@ func (a *AstraflowModel) Name() string { } // ChatWithMessages sends a non-streaming chat request -func (a *AstraflowModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (a *AstraflowModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -177,7 +178,7 @@ func (a *AstraflowModel) ChatWithMessages(modelName string, messages []Message, } // ChatStreamlyWithSender opens the SSE chat-completions -func (a *AstraflowModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (a *AstraflowModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -352,7 +353,7 @@ func (a *AstraflowModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (a *AstraflowModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (a *AstraflowModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -429,7 +430,7 @@ func (a *AstraflowModel) Embed(modelName *string, texts []string, apiConfig *API return embeddings, nil } -func (a *AstraflowModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (a *AstraflowModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -511,15 +512,15 @@ func (a *AstraflowModel) Balance(apiConfig *APIConfig) (map[string]interface{}, return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (a *AstraflowModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (a *AstraflowModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (a *AstraflowModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -579,15 +580,15 @@ func (a *AstraflowModel) AudioSpeech(modelName *string, audioContent *string, ap return &TTSResponse{Audio: body}, nil } -func (a *AstraflowModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (a *AstraflowModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (a *AstraflowModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (a *AstraflowModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } diff --git a/internal/entity/models/astraflow_test.go b/internal/entity/models/astraflow_test.go index 6c1bbbe335..056a430283 100644 --- a/internal/entity/models/astraflow_test.go +++ b/internal/entity/models/astraflow_test.go @@ -124,6 +124,7 @@ func TestAstraflowChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -150,7 +151,7 @@ func TestAstraflowChatNoReasoning(t *testing.T) { resp, err := newAstraflowForTest(srv.URL).ChatWithMessages( "gpt-4o-mini", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -166,7 +167,7 @@ func TestAstraflowChatRequiresAPIKey(t *testing.T) { _, err := newAstraflowForTest("http://unused").ChatWithMessages( "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -175,7 +176,7 @@ func TestAstraflowChatRequiresAPIKey(t *testing.T) { func TestAstraflowChatRequiresMessages(t *testing.T) { apiKey := "test-key" _, err := newAstraflowForTest("http://unused").ChatWithMessages( - "claude-opus-4-7", nil, &APIConfig{ApiKey: &apiKey}, nil) + "claude-opus-4-7", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -192,7 +193,7 @@ func TestAstraflowChatPropagatesHTTPError(t *testing.T) { _, err := newAstraflowForTest(srv.URL).ChatWithMessages( "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -213,7 +214,7 @@ func TestAstraflowStreamHappyPath(t *testing.T) { err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( "claude-opus-4-7", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -251,7 +252,7 @@ func TestAstraflowStreamSplitsReasoning(t *testing.T) { err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -283,6 +284,7 @@ func TestAstraflowStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("expected stream-true guard, got %v", err) @@ -294,7 +296,7 @@ func TestAstraflowStreamRequiresSender(t *testing.T) { err := newAstraflowForTest("http://unused").ChatStreamlyWithSender( "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -310,7 +312,7 @@ func TestAstraflowStreamFailsWithoutTerminal(t *testing.T) { err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream ended before") { t.Errorf("expected truncation error, got %v", err) @@ -328,7 +330,7 @@ func TestAstraflowStreamRejectsMalformedFrame(t *testing.T) { err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "invalid SSE event") { t.Errorf("expected invalid-SSE error, got %v", err) @@ -346,7 +348,7 @@ func TestAstraflowStreamSurfacesUpstreamError(t *testing.T) { err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "upstream stream error") { t.Errorf("expected upstream-error surfacing, got %v", err) @@ -428,7 +430,7 @@ func TestAstraflowEmbedReturnsNoSuchMethod(t *testing.T) { // Embed IS implemented (not a stub). It should NOT be blocked by APIConfigCheck. // With empty input texts it short-circuits to empty result (no error). apiKey := "test-key" - embeddings, err := newAstraflowForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil) + embeddings, err := newAstraflowForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil || len(embeddings) != 0 { t.Errorf("Embed: want empty result (no error), got embeddings=%v err=%v", embeddings, err) } @@ -439,16 +441,16 @@ func TestAstraflowAudioOCRReturnNoSuchMethod(t *testing.T) { model := "x" apiKey := "test-key" // TranscribeAudio is a stub → "no such method" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, &model, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: %v", err) } // AudioSpeech IS implemented; pass nil content to hit input validation, // not api-key check (which would mean APIConfigCheck still blocks it). - if _, err := m.AudioSpeech(&model, nil, &APIConfig{ApiKey: &apiKey}, nil); err == nil || strings.Contains(err.Error(), "api key is required") { + if _, err := m.AudioSpeech(&model, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("AudioSpeech: expected non-api-key error, got %v", err) } // OCRFile is a stub → "no such method" - if _, err := m.OCRFile(&model, nil, &model, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, &model, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } diff --git a/internal/entity/models/avian.go b/internal/entity/models/avian.go index 1d757ed4c9..282669fe8b 100644 --- a/internal/entity/models/avian.go +++ b/internal/entity/models/avian.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -113,7 +114,7 @@ type avianChatResponse struct { FinishReason string `json:"finish_reason"` } -func (a *AvianModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (a *AvianModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -180,7 +181,7 @@ func (a *AvianModel) ChatWithMessages(modelName string, messages []Message, apiC }, nil } -func (a *AvianModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (a *AvianModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -321,11 +322,11 @@ func (a *AvianModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (a *AvianModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (a *AvianModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (a *AvianModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } @@ -333,27 +334,27 @@ func (a *AvianModel) Balance(apiConfig *APIConfig) (map[string]interface{}, erro return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (a *AvianModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (a *AvianModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (a *AvianModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (a *AvianModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (a *AvianModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (a *AvianModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } diff --git a/internal/entity/models/avian_test.go b/internal/entity/models/avian_test.go index 934a49de25..9322102b6d 100644 --- a/internal/entity/models/avian_test.go +++ b/internal/entity/models/avian_test.go @@ -122,6 +122,7 @@ func TestAvianChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -153,6 +154,7 @@ func TestAvianChatFallsBackToReasoningField(t *testing.T) { []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -168,6 +170,7 @@ func TestAvianChatRequiresAPIKey(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key-required error, got %v", err) @@ -181,6 +184,7 @@ func TestAvianChatRequiresMessages(t *testing.T) { []Message{}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) @@ -200,6 +204,7 @@ func TestAvianChatPropagatesUpstreamErrorStatus(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 status error, got %v", err) @@ -231,7 +236,7 @@ func TestAvianStreamHappyPath(t *testing.T) { "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, - nil, + nil, nil, func(c *string, r *string) error { if r != nil && *r != "" { reasoning = append(reasoning, *r) @@ -267,6 +272,7 @@ func TestAvianStreamRejectsFalseStreamConfig(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -282,6 +288,7 @@ func TestAvianStreamRequiresSender(t *testing.T) { &APIConfig{ApiKey: &apiKey}, nil, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) @@ -319,6 +326,7 @@ func TestAvianMissingBaseURLFailsClearly(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "no base URL") { t.Errorf("expected no-base-URL error, got %v", err) @@ -329,22 +337,22 @@ func TestAvianUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { a := newAvianForTest("http://unused") model := "deepseek/deepseek-v3.2" - if _, err := a.Embed(&model, []string{"x"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed: expected no such method, got %v", err) } - if _, err := a.Rerank(&model, "q", []string{"d"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.Rerank(&model, "q", []string{"d"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: expected no such method, got %v", err) } if _, err := a.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: expected no such method, got %v", err) } - if _, err := a.TranscribeAudio(&model, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.TranscribeAudio(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: expected no such method, got %v", err) } - if _, err := a.AudioSpeech(&model, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.AudioSpeech(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: expected no such method, got %v", err) } - if _, err := a.OCRFile(&model, nil, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.OCRFile(&model, nil, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: expected no such method, got %v", err) } } diff --git a/internal/entity/models/azure_openai.go b/internal/entity/models/azure_openai.go index b6fd2dbb17..79d277411f 100644 --- a/internal/entity/models/azure_openai.go +++ b/internal/entity/models/azure_openai.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -60,7 +61,7 @@ func (a *AzureOpenAIModel) deploymentURL(baseURL, deployment, op string) string } // ChatWithMessages sends multiple messages with roles and returns the response. -func (a *AzureOpenAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (a *AzureOpenAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -181,7 +182,7 @@ func (a *AzureOpenAIModel) ChatWithMessages(modelName string, messages []Message // ChatStreamlyWithSender sends messages and streams the response via the // sender function. Used for streaming chat responses with no extra channel. -func (a *AzureOpenAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (a *AzureOpenAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -312,7 +313,7 @@ type azureEmbeddingResponse struct { } // Embed turns a list of texts into embedding vectors -func (a *AzureOpenAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (a *AzureOpenAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -452,31 +453,31 @@ func (a *AzureOpenAIModel) Balance(apiConfig *APIConfig) (map[string]interface{} } // Rerank is not exposed by the Azure OpenAI API. -func (a *AzureOpenAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (a *AzureOpenAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (a *AzureOpenAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (a *AzureOpenAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (a *AzureOpenAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (a *AzureOpenAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (a *AzureOpenAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (a *AzureOpenAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } diff --git a/internal/entity/models/baichuan.go b/internal/entity/models/baichuan.go index 8c503143ef..fed00bf8af 100644 --- a/internal/entity/models/baichuan.go +++ b/internal/entity/models/baichuan.go @@ -49,7 +49,7 @@ func (b *BaichuanModel) Name() string { return "BaiChuan" } -func (b *BaichuanModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (b *BaichuanModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -165,7 +165,7 @@ func (b *BaichuanModel) ChatWithMessages(modelName string, messages []Message, a return chatResponse, nil } -func (b *BaichuanModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (b *BaichuanModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -290,7 +290,7 @@ func (b *BaichuanModel) ChatStreamlyWithSender(modelName string, messages []Mess return sender(&endOfStream, nil) } -func (b *BaichuanModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (b *BaichuanModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -367,35 +367,35 @@ func (b *BaichuanModel) Embed(modelName *string, texts []string, apiConfig *APIC return embeddings, nil } -func (b *BaichuanModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (b *BaichuanModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (b *BaichuanModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (b *BaichuanModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaichuanModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (b *BaichuanModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", b.Name()) } // AudioSpeech convert text to audio -func (b *BaichuanModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (b *BaichuanModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaichuanModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (b *BaichuanModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", b.Name()) } // OCRFile OCR file -func (b *BaichuanModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (b *BaichuanModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } // ParseFile parse file -func (b *BaichuanModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (b *BaichuanModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } diff --git a/internal/entity/models/baidu.go b/internal/entity/models/baidu.go index fef59b6104..46c60775b3 100644 --- a/internal/entity/models/baidu.go +++ b/internal/entity/models/baidu.go @@ -51,7 +51,7 @@ func (b *BaiduModel) Name() string { return "BaiduYiyan" } -func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -241,7 +241,7 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC ToolCalls: toolCalls, } if pt, ct, tt := extractUsageFromMap(result); tt > 0 { - chatResponse.Usage = &ChatUsage{ + chatResponse.Usage = &TokenUsage{ PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, } } @@ -249,7 +249,7 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC return chatResponse, nil } -func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -528,7 +528,7 @@ type baiduUsage struct { TotalTokens int `json:"total_tokens"` } -func (b *BaiduModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (b *BaiduModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -625,7 +625,7 @@ func (b *BaiduModel) Embed(modelName *string, texts []string, apiConfig *APIConf return embeddings, nil } -func (b *BaiduModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (b *BaiduModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -707,20 +707,20 @@ func (b *BaiduModel) Rerank(modelName *string, query string, documents []string, } // TranscribeAudio transcribe audio -func (b *BaiduModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (b *BaiduModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaiduModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (b *BaiduModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", b.Name()) } // AudioSpeech convert text to audio -func (b *BaiduModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (b *BaiduModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaiduModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (b *BaiduModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", b.Name()) } @@ -736,7 +736,7 @@ type qianfanOCRResponse struct { } `json:"result"` } -func (b *BaiduModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (b *BaiduModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -888,7 +888,7 @@ func (b *BaiduModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (b *BaiduModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (b *BaiduModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } diff --git a/internal/entity/models/bedrock.go b/internal/entity/models/bedrock.go index 39e1cd4433..fc2371102a 100644 --- a/internal/entity/models/bedrock.go +++ b/internal/entity/models/bedrock.go @@ -29,6 +29,8 @@ import ( "strings" "time" + "ragflow/internal/common" + awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" @@ -503,7 +505,7 @@ func signBedrockRequest(ctx context.Context, req *http.Request, body []byte, cre // the joined assistant answer. ReasonContent is always non-nil per the // driver contract; Bedrock surfaces no reasoning channel today, so it // is left empty rather than nil. -func (b *BedrockModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (b *BedrockModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -587,7 +589,7 @@ func (b *BedrockModel) ChatWithMessages(modelName string, messages []Message, ap // payload. For chat we only need messageStart, contentBlockDelta, // messageStop, and (for error propagation) exception frames; other // events are ignored. -func (b *BedrockModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (b *BedrockModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -862,7 +864,7 @@ type bedrockCohereEmbeddingResponse struct { // InvokeModel. Titan's embedding API accepts one inputText per call, // while Cohere accepts a texts batch and returns vectors in input // order. -func (b *BedrockModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (b *BedrockModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if len(texts) == 0 { return []EmbeddingData{}, nil } @@ -892,10 +894,10 @@ func (b *BedrockModel) Embed(modelName *string, texts []string, apiConfig *APICo } if strings.HasPrefix(modelID, "amazon.titan-embed-text-") { - return b.embedTitan(ctx, modelID, texts, region, creds, embeddingConfig) + return b.embedTitan(ctx, modelID, texts, region, creds, embeddingConfig, modelUsage) } if strings.HasPrefix(modelID, "cohere.embed-") { - return b.embedCohere(ctx, modelID, texts, region, creds, embeddingConfig) + return b.embedCohere(ctx, modelID, texts, region, creds, embeddingConfig, modelUsage) } return nil, fmt.Errorf("bedrock: unsupported embedding model %q", modelID) } @@ -936,7 +938,7 @@ func (b *BedrockModel) invokeEmbeddingModel(ctx context.Context, modelID string, return respBody, nil } -func (b *BedrockModel) embedTitan(ctx context.Context, modelID string, texts []string, region string, creds awssdk.Credentials, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (b *BedrockModel) embedTitan(ctx context.Context, modelID string, texts []string, region string, creds awssdk.Credentials, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { embeddings := make([]EmbeddingData, 0, len(texts)) for i, text := range texts { req := bedrockTitanEmbeddingRequest{ @@ -950,7 +952,7 @@ func (b *BedrockModel) embedTitan(ctx context.Context, modelID string, texts []s return nil, err } var parsed bedrockTitanEmbeddingResponse - if err := json.Unmarshal(respBody, &parsed); err != nil { + if err = json.Unmarshal(respBody, &parsed); err != nil { return nil, fmt.Errorf("bedrock: parse Titan embedding response: %w", err) } if len(parsed.Embedding) == 0 { @@ -964,7 +966,7 @@ func (b *BedrockModel) embedTitan(ctx context.Context, modelID string, texts []s return embeddings, nil } -func (b *BedrockModel) embedCohere(ctx context.Context, modelID string, texts []string, region string, creds awssdk.Credentials, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (b *BedrockModel) embedCohere(ctx context.Context, modelID string, texts []string, region string, creds awssdk.Credentials, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { req := bedrockCohereEmbeddingRequest{ Texts: texts, InputType: "search_document", @@ -1024,7 +1026,7 @@ func decodeCohereEmbeddingVectors(raw json.RawMessage) ([][]float64, error) { } // Rerank is not exposed by Bedrock. -func (b *BedrockModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (b *BedrockModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } @@ -1035,32 +1037,32 @@ func (b *BedrockModel) Balance(apiConfig *APIConfig) (map[string]interface{}, er // TranscribeAudio is not exposed by Bedrock. Speech-to-text on AWS // lives in Amazon Transcribe, a separate service. -func (b *BedrockModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (b *BedrockModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BedrockModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (b *BedrockModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", b.Name()) } // AudioSpeech is not exposed by Bedrock. Text-to-speech on AWS lives // in Amazon Polly, a separate service. -func (b *BedrockModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (b *BedrockModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BedrockModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (b *BedrockModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", b.Name()) } // OCRFile is not exposed by Bedrock. OCR on AWS lives in Amazon // Textract, a separate service. -func (b *BedrockModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (b *BedrockModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } // ParseFile is not exposed by Bedrock. -func (b *BedrockModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (b *BedrockModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } diff --git a/internal/entity/models/bedrock_test.go b/internal/entity/models/bedrock_test.go index 13124461bb..156d41ec22 100644 --- a/internal/entity/models/bedrock_test.go +++ b/internal/entity/models/bedrock_test.go @@ -352,7 +352,7 @@ func TestBedrockChatHappyPath(t *testing.T) { key := validBedrockKey() resp, err := m.ChatWithMessages("anthropic.claude-3-haiku-20240307-v1:0", []Message{{Role: "user", Content: "ping"}}, - &APIConfig{ApiKey: &key}, nil) + &APIConfig{ApiKey: &key}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -369,7 +369,7 @@ func TestBedrockChatHappyPath(t *testing.T) { func TestBedrockChatRequiresAPIKey(t *testing.T) { m := newBedrockForTest("http://unused") - _, err := m.ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil) + _, err := m.ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("want api-key error, got %v", err) } @@ -379,7 +379,7 @@ func TestBedrockChatRequiresModelID(t *testing.T) { m := newBedrockForTest("http://unused") key := validBedrockKey() _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil) + &APIConfig{ApiKey: &key}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model id is required") { t.Errorf("want model-required error, got %v", err) } @@ -398,7 +398,7 @@ func TestBedrockChatPropagatesHTTPError(t *testing.T) { key := validBedrockKey() _, err := m.ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil) + &APIConfig{ApiKey: &key}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("want 401 propagated, got %v", err) } @@ -522,7 +522,7 @@ func TestBedrockStreamDecodesContentDeltas(t *testing.T) { sawDone := false err := m.ChatStreamlyWithSender("m", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &key}, nil, + &APIConfig{ApiKey: &key}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -570,7 +570,7 @@ func TestBedrockStreamSurfacesException(t *testing.T) { key := validBedrockKey() err := m.ChatStreamlyWithSender("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil, + &APIConfig{ApiKey: &key}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "exception") { t.Errorf("want exception surfaced, got %v", err) @@ -601,7 +601,7 @@ func TestBedrockStreamFailsWithoutTerminal(t *testing.T) { key := validBedrockKey() err := m.ChatStreamlyWithSender("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil, + &APIConfig{ApiKey: &key}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream ended before") { t.Errorf("want truncation error, got %v", err) @@ -616,6 +616,7 @@ func TestBedrockStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("want stream-true guard, got %v", err) @@ -627,7 +628,7 @@ func TestBedrockStreamRequiresSender(t *testing.T) { key := validBedrockKey() err := m.ChatStreamlyWithSender("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil, nil) + &APIConfig{ApiKey: &key}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("want sender-required error, got %v", err) } @@ -673,7 +674,7 @@ func TestBedrockTitanEmbedHappyPath(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() model := "amazon.titan-embed-text-v2:0" - got, err := m.Embed(&model, []string{"alpha", "beta"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 256}) + got, err := m.Embed(&model, []string{"alpha", "beta"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 256}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -703,7 +704,7 @@ func TestBedrockTitanV1OmitsDimension(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() model := "amazon.titan-embed-text-v1" - if _, err := m.Embed(&model, []string{"alpha"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 256}); err != nil { + if _, err := m.Embed(&model, []string{"alpha"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 256}, nil); err != nil { t.Fatalf("Embed: %v", err) } } @@ -735,7 +736,7 @@ func TestBedrockCohereEmbedHappyPath(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() model := "cohere.embed-english-v3" - got, err := m.Embed(&model, []string{"first", "second"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 128}) + got, err := m.Embed(&model, []string{"first", "second"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 128}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -764,7 +765,7 @@ func TestBedrockCohereV4ForwardsDimensionAndParsesTypedResponse(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() model := "cohere.embed-v4:0" - got, err := m.Embed(&model, []string{"first"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 512}) + got, err := m.Embed(&model, []string{"first"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 512}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -775,7 +776,7 @@ func TestBedrockCohereV4ForwardsDimensionAndParsesTypedResponse(t *testing.T) { func TestBedrockEmbedShortCircuitsEmptyInput(t *testing.T) { m := newBedrockForTest("http://unused") - got, err := m.Embed(nil, nil, nil, nil) + got, err := m.Embed(nil, nil, nil, nil, nil) if err != nil { t.Fatalf("Embed empty: %v", err) } @@ -787,12 +788,12 @@ func TestBedrockEmbedShortCircuitsEmptyInput(t *testing.T) { func TestBedrockEmbedRequiresAPIKeyAndModel(t *testing.T) { m := newBedrockForTest("http://unused") model := "x" - if _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("Embed: want api-key error, got %v", err) } key := validBedrockKey() blank := " " - if _, err := m.Embed(&blank, []string{"a"}, &APIConfig{ApiKey: &key}, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := m.Embed(&blank, []string{"a"}, &APIConfig{ApiKey: &key}, nil, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("Embed: want model-required error, got %v", err) } } @@ -801,7 +802,7 @@ func TestBedrockEmbedRejectsUnsupportedModel(t *testing.T) { m := newBedrockForTest("http://unused") key := validBedrockKey() model := "anthropic.claude-3-haiku-20240307-v1:0" - if _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &key}, nil); err == nil || !strings.Contains(err.Error(), "unsupported embedding model") { + if _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &key}, nil, nil); err == nil || !strings.Contains(err.Error(), "unsupported embedding model") { t.Errorf("Embed: want unsupported-model error, got %v", err) } } @@ -818,7 +819,7 @@ func TestBedrockEmbedPropagatesHTTPError(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() model := "amazon.titan-embed-text-v2:0" - if _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &key}, nil); err == nil || !strings.Contains(err.Error(), "400") || !strings.Contains(err.Error(), "bad input") { + if _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &key}, nil, nil); err == nil || !strings.Contains(err.Error(), "400") || !strings.Contains(err.Error(), "bad input") { t.Errorf("Embed: want HTTP error with body, got %v", err) } } @@ -826,7 +827,7 @@ func TestBedrockEmbedPropagatesHTTPError(t *testing.T) { func TestBedrockRerankReturnsNoSuchMethod(t *testing.T) { m := newBedrockForTest("http://unused") model := "x" - if _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: want no-such-method, got %v", err) } } @@ -841,16 +842,16 @@ func TestBedrockBalanceReturnsNoSuchMethod(t *testing.T) { func TestBedrockAudioOCRReturnNoSuchMethod(t *testing.T) { m := newBedrockForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: want no-such-method, got %v", err) } - if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: want no-such-method, got %v", err) } - if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: want no-such-method, got %v", err) } - if _, err := m.ParseFile(&model, nil, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(&model, nil, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ParseFile: want no-such-method, got %v", err) } if _, err := m.ListTasks(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { diff --git a/internal/entity/models/builtin.go b/internal/entity/models/builtin.go index 738bb6d525..bb4c8fcf20 100644 --- a/internal/entity/models/builtin.go +++ b/internal/entity/models/builtin.go @@ -47,16 +47,16 @@ func (b *BuiltinModel) NewInstance(baseURL map[string]string) ModelDriver { } } -func (b *BuiltinModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (b *BuiltinModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("builtin model does not support chat") } -func (b *BuiltinModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (b *BuiltinModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("builtin model does not support chat") } // Embed sends texts to a TEI (Text Embeddings Inference) server and returns embeddings -func (b *BuiltinModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (b *BuiltinModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if len(texts) == 0 { return []EmbeddingData{}, nil } @@ -122,31 +122,31 @@ func (b *BuiltinModel) Embed(modelName *string, texts []string, apiConfig *APICo return result, nil } -func (b *BuiltinModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (b *BuiltinModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("builtin model does not support rerank") } -func (b *BuiltinModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (b *BuiltinModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("builtin model does not support transcription") } -func (b *BuiltinModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (b *BuiltinModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("builtin model does not support transcription") } -func (b *BuiltinModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (b *BuiltinModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("builtin model does not support TTS") } -func (b *BuiltinModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (b *BuiltinModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("builtin model does not support TTS") } -func (b *BuiltinModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (b *BuiltinModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("builtin model does not support OCR") } -func (b *BuiltinModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (b *BuiltinModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("builtin model does not support parse file") } @@ -164,7 +164,7 @@ func (b *BuiltinModel) Balance(apiConfig *APIConfig) (map[string]interface{}, er func (b *BuiltinModel) CheckConnection(apiConfig *APIConfig) error { // Try to get model info to verify connection - _, err := b.Embed(nil, []string{"test"}, apiConfig, nil) + _, err := b.Embed(nil, []string{"test"}, apiConfig, nil, nil) return err } diff --git a/internal/entity/models/chat_tools.go b/internal/entity/models/chat_tools.go index b04c1f5748..b49af5b2e0 100644 --- a/internal/entity/models/chat_tools.go +++ b/internal/entity/models/chat_tools.go @@ -99,7 +99,7 @@ func runToolLoop(ctx context.Context, cm *ChatModel, history []Message, toolsLis // without usage doesn't leak the prior call's data. cm.LastUsage = nil var totalTokens int - aggUsage := &ChatUsage{} + aggUsage := &TokenUsage{} addRoundUsage := func(resp *ChatResponse) { u := resp.Usage @@ -112,7 +112,7 @@ func runToolLoop(ctx context.Context, cm *ChatModel, history []Message, toolsLis totalTokens = aggUsage.TotalTokens // Store per-round delta (not cumulative) so RecordRunTokenUsage // records each round's contribution exactly once. - cm.LastUsage = &ChatUsage{ + cm.LastUsage = &TokenUsage{ PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens, TotalTokens: u.TotalTokens, } recordUsageFromResponse(ctx, cm) @@ -129,7 +129,7 @@ func runToolLoop(ctx context.Context, cm *ChatModel, history []Message, toolsLis tcChoice := "auto" cfg.ToolChoice = &tcChoice - resp, err := cm.ModelDriver.ChatWithMessages(*cm.ModelName, history, cm.APIConfig, &cfg) + resp, err := cm.ModelDriver.ChatWithMessages(*cm.ModelName, history, cm.APIConfig, &cfg, nil) if err != nil { return "", totalTokens, fmt.Errorf("round %d: %w", round, err) } @@ -163,7 +163,7 @@ func runToolLoop(ctx context.Context, cm *ChatModel, history []Message, toolsLis Content: fmt.Sprintf("Exceed max rounds: %d", maxRounds), }) cfg := *chatCfg - resp, err := cm.ModelDriver.ChatWithMessages(*cm.ModelName, history, cm.APIConfig, &cfg) + resp, err := cm.ModelDriver.ChatWithMessages(*cm.ModelName, history, cm.APIConfig, &cfg, nil) if err != nil { return "", totalTokens, fmt.Errorf("final call: %w", err) } @@ -231,7 +231,7 @@ func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, to // Reset stale per-call usage from a previous call. cm.LastUsage = nil var totalTokens int - aggUsage := &ChatUsage{} + aggUsage := &TokenUsage{} commitRound := func(cfg *ChatConfig, roundTokens int) { // Prefer the authoritative usage from the API (extracted via @@ -251,7 +251,7 @@ func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, to aggUsage.TotalTokens += deltaTotal } totalTokens = aggUsage.TotalTokens - cm.LastUsage = &ChatUsage{ + cm.LastUsage = &TokenUsage{ PromptTokens: deltaPrompt, CompletionTokens: deltaCompletion, TotalTokens: deltaTotal, } recordUsageFromResponse(ctx, cm) @@ -270,7 +270,7 @@ func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, to cfg.Stream = boolPtr(true) var tcs []map[string]interface{} cfg.ToolCallsResult = &tcs - var roundUsage ChatUsage + var roundUsage TokenUsage cfg.UsageResult = &roundUsage reasoningStarted := false @@ -278,7 +278,7 @@ func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, to var pendingThinkClose bool var roundTokens int - err := cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, func(delta *string, reason *string) error { + err := cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, nil, func(delta *string, reason *string) error { if reason != nil && *reason != "" { if !reasoningStarted { reasoningStarted = true @@ -348,10 +348,10 @@ func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, to }) cfg := *chatCfg cfg.Stream = boolPtr(true) - var exceedUsage ChatUsage + var exceedUsage TokenUsage cfg.UsageResult = &exceedUsage var exceedTokens int - err := cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, func(delta *string, reason *string) error { + err := cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, nil, func(delta *string, reason *string) error { if delta != nil && *delta != "" && *delta != "[DONE]" { exceedTokens += tokenizer.NumTokensFromString(*delta) } diff --git a/internal/entity/models/cohere.go b/internal/entity/models/cohere.go index e4d7a5fe7c..00b061d692 100644 --- a/internal/entity/models/cohere.go +++ b/internal/entity/models/cohere.go @@ -26,6 +26,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -52,7 +53,7 @@ func (c *CoHereModel) Name() string { return "Cohere" } -func (c *CoHereModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (c *CoHereModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -185,7 +186,7 @@ func (c *CoHereModel) ChatWithMessages(modelName string, messages []Message, api return chatResponse, nil } -func (c *CoHereModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (c *CoHereModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -336,7 +337,7 @@ func (c *CoHereModel) ChatStreamlyWithSender(modelName string, messages []Messag return sender(&endOfStream, nil) } -func (c *CoHereModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (c *CoHereModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -420,7 +421,7 @@ func (c *CoHereModel) Embed(modelName *string, texts []string, apiConfig *APICon return embeddings, nil } -func (c *CoHereModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (c *CoHereModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -505,7 +506,7 @@ func (c *CoHereModel) Rerank(modelName *string, query string, documents []string } // TranscribeAudio transcribe audio -func (c *CoHereModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (c *CoHereModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -620,26 +621,26 @@ func (c *CoHereModel) TranscribeAudio(modelName *string, file *string, apiConfig return &ASRResponse{Text: result.Text}, nil } -func (c *CoHereModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (c *CoHereModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", c.Name()) } // AudioSpeech convert text to audio -func (c *CoHereModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (c *CoHereModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } -func (c *CoHereModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (c *CoHereModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", c.Name()) } // OCRFile OCR file -func (c *CoHereModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (c *CoHereModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } // ParseFile parse file -func (c *CoHereModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (c *CoHereModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } diff --git a/internal/entity/models/cometapi.go b/internal/entity/models/cometapi.go index 4ea7ade111..5eccfa1169 100644 --- a/internal/entity/models/cometapi.go +++ b/internal/entity/models/cometapi.go @@ -27,6 +27,7 @@ import ( "net/url" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -234,7 +235,7 @@ type cometapiModelCatalogItem struct { } // ChatWithMessages sends multiple messages with roles and returns the response. -func (c *CometAPIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (c *CometAPIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -273,7 +274,7 @@ func (c *CometAPIModel) ChatWithMessages(modelName string, messages []Message, a } // ChatStreamlyWithSender sends messages and streams the response -func (c *CometAPIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (c *CometAPIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -383,7 +384,7 @@ type cometapiEmbeddingRequest struct { } // Embed turns a list of texts into embedding vectors -func (c *CometAPIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (c *CometAPIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -533,12 +534,12 @@ func (c *CometAPIModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents. -func (c *CometAPIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (c *CometAPIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (c *CometAPIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (c *CometAPIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -653,12 +654,12 @@ func (c *CometAPIModel) TranscribeAudio(modelName *string, file *string, apiConf return &ASRResponse{Text: result.Text}, nil } -func (c *CometAPIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (c *CometAPIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", c.Name()) } // AudioSpeech synthesizes speech audio from text. -func (c *CometAPIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (c *CometAPIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -718,16 +719,16 @@ func (c *CometAPIModel) AudioSpeech(modelName *string, audioContent *string, api return &TTSResponse{Audio: body}, nil } -func (c *CometAPIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (c *CometAPIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", c.Name()) } // OCRFile OCR file -func (c *CometAPIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (c *CometAPIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } -func (c *CometAPIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (c *CometAPIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } diff --git a/internal/entity/models/cometapi_test.go b/internal/entity/models/cometapi_test.go index d084388775..24f0f6343c 100644 --- a/internal/entity/models/cometapi_test.go +++ b/internal/entity/models/cometapi_test.go @@ -101,7 +101,7 @@ func TestCometAPIChatHappyPath(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("gpt-5", []Message{ {Role: "user", Content: "ping"}, - }, &APIConfig{ApiKey: &apiKey}, nil) + }, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -143,7 +143,7 @@ func TestCometAPIChatPropagatesConfig(t *testing.T) { _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop}, - ) + nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -161,7 +161,7 @@ func TestCometAPIChatReturnsReasoningContent(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -172,12 +172,12 @@ func TestCometAPIChatReturnsReasoningContent(t *testing.T) { func TestCometAPIChatRequiresAPIKey(t *testing.T) { m := newCometAPIForTest("http://unused") - _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil) + _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } emptyKey := "" - _, err = m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err = m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("empty key: expected api-key error, got %v", err) } @@ -186,11 +186,11 @@ func TestCometAPIChatRequiresAPIKey(t *testing.T) { func TestCometAPIChatRequiresModelName(t *testing.T) { m := newCometAPIForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } - err = m.ChatStreamlyWithSender(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, func(*string, *string) error { return nil }) + err = m.ChatStreamlyWithSender(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("stream: expected model-name error, got %v", err) } @@ -199,7 +199,7 @@ func TestCometAPIChatRequiresModelName(t *testing.T) { func TestCometAPIChatRequiresMessages(t *testing.T) { m := newCometAPIForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("gpt-5", nil, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.ChatWithMessages("gpt-5", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -214,7 +214,7 @@ func TestCometAPIChatRejectsHTTPError(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" - _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -235,7 +235,7 @@ func TestCometAPIChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { emptyRegion := "" _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil) + &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil, nil) if err != nil { t.Errorf("empty Region: expected fallback to default, got %v", err) } @@ -260,7 +260,7 @@ func TestCometAPIStreamRequiresSender(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -271,7 +271,7 @@ func TestCometAPIChatRejectsUnknownRegion(t *testing.T) { apiKey := "test-key" region := "eu" _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil) + &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no base URL configured for region") { t.Errorf("expected region error, got %v", err) } @@ -287,7 +287,7 @@ func TestCometAPIBaseURLNormalizesSlashes(t *testing.T) { name: "Chat", path: "/v1/chat/completions", run: func(m *CometAPIModel, apiConfig *APIConfig) error { - _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, apiConfig, nil) + _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, apiConfig, nil, nil) return err }, }, @@ -295,7 +295,7 @@ func TestCometAPIBaseURLNormalizesSlashes(t *testing.T) { name: "Stream", path: "/v1/chat/completions", run: func(m *CometAPIModel, apiConfig *APIConfig) error { - return m.ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, apiConfig, nil, func(*string, *string) error { return nil }) + return m.ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, apiConfig, nil, nil, func(*string, *string) error { return nil }) }, }, { @@ -303,7 +303,7 @@ func TestCometAPIBaseURLNormalizesSlashes(t *testing.T) { path: "/v1/embeddings", run: func(m *CometAPIModel, apiConfig *APIConfig) error { model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"x"}, apiConfig, nil) + _, err := m.Embed(&model, []string{"x"}, apiConfig, nil, nil) return err }, }, @@ -369,7 +369,7 @@ func TestCometAPIStreamHappyPath(t *testing.T) { var sawDone int32 err := m.ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, _ *string) error { if content == nil { return nil @@ -401,6 +401,7 @@ func TestCometAPIStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -421,7 +422,7 @@ func TestCometAPIStreamFailsWithoutTerminal(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream ended before") { @@ -551,7 +552,7 @@ func TestCometAPIBalanceRequiresConfiguredURL(t *testing.T) { func TestCometAPIRerankReturnsNoSuchMethod(t *testing.T) { m := newCometAPIForTest("http://unused") q := "gpt-5" - _, err := m.Rerank(&q, "what is rag?", []string{"a", "b"}, &APIConfig{}, &RerankConfig{TopN: 2}) + _, err := m.Rerank(&q, "what is rag?", []string{"a", "b"}, &APIConfig{}, &RerankConfig{TopN: 2}, nil) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: expected 'no such method', got %v", err) } @@ -582,7 +583,7 @@ func TestCometAPIEmbedHappyPath(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 256}) + vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 256}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -611,7 +612,7 @@ func TestCometAPIEmbedReordersByIndex(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -634,7 +635,7 @@ func TestCometAPIEmbedEmptyInputShortCircuits(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - vecs, err := m.Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := m.Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed([]): %v", err) } @@ -646,7 +647,7 @@ func TestCometAPIEmbedEmptyInputShortCircuits(t *testing.T) { func TestCometAPIEmbedRequiresAPIKey(t *testing.T) { m := newCometAPIForTest("http://unused") model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil) + _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -655,12 +656,12 @@ func TestCometAPIEmbedRequiresAPIKey(t *testing.T) { func TestCometAPIEmbedRequiresModelName(t *testing.T) { m := newCometAPIForTest("http://unused") apiKey := "test-key" - _, err := m.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } empty := "" - _, err = m.Embed(&empty, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err = m.Embed(&empty, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("empty model: expected model-name error, got %v", err) } @@ -682,7 +683,7 @@ func TestCometAPIEmbedRejectsDuplicateIndex(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate embedding index 0") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -701,7 +702,7 @@ func TestCometAPIEmbedRejectsOutOfRangeIndex(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -721,7 +722,7 @@ func TestCometAPIEmbedRejectsMissingSlot(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding for input index 1") { t.Errorf("expected missing-embedding error for slot 1, got %v", err) } @@ -737,7 +738,7 @@ func TestCometAPIEmbedRejectsHTTPError(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "CometAPI embeddings API error") { t.Errorf("expected CometAPI embeddings API error, got %v", err) } diff --git a/internal/entity/models/deepinfra.go b/internal/entity/models/deepinfra.go index f9192fa084..b47f6373ef 100644 --- a/internal/entity/models/deepinfra.go +++ b/internal/entity/models/deepinfra.go @@ -55,7 +55,7 @@ func (d *DeepInfraModel) Name() string { return "deepinfra" } -func (d *DeepInfraModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (d *DeepInfraModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -188,7 +188,7 @@ func (d *DeepInfraModel) ChatWithMessages(modelName string, messages []Message, return chatResponse, nil } -func (d *DeepInfraModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (d *DeepInfraModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -338,7 +338,7 @@ func (d *DeepInfraModel) ChatStreamlyWithSender(modelName string, messages []Mes return sender(&endOfStream, nil) } -func (d *DeepInfraModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (d *DeepInfraModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -421,7 +421,7 @@ type deepinfraRerankResponse struct { } // Rerank scores documents against a query using DeepInfra's inference endpoint. -func (d *DeepInfraModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (d *DeepInfraModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -510,7 +510,7 @@ func (d *DeepInfraModel) Rerank(modelName *string, query string, documents []str return &RerankResponse{Data: results}, nil } -func (d *DeepInfraModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (d *DeepInfraModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -627,11 +627,11 @@ func (d *DeepInfraModel) TranscribeAudio(modelName *string, file *string, apiCon }, nil } -func (d *DeepInfraModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (d *DeepInfraModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", d.Name()) } -func (d *DeepInfraModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (d *DeepInfraModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -708,7 +708,7 @@ func (d *DeepInfraModel) AudioSpeech(modelName *string, audioContent *string, ap return &TTSResponse{Audio: body}, nil } -func (d *DeepInfraModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (d *DeepInfraModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -806,11 +806,11 @@ func (d *DeepInfraModel) AudioSpeechWithSender(modelName *string, audioContent * return nil } -func (d *DeepInfraModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (d *DeepInfraModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s no such method", d.Name()) } -func (d *DeepInfraModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (d *DeepInfraModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s no such method", d.Name()) } diff --git a/internal/entity/models/deepinfra_test.go b/internal/entity/models/deepinfra_test.go index 477974d0c2..1c70c04450 100644 --- a/internal/entity/models/deepinfra_test.go +++ b/internal/entity/models/deepinfra_test.go @@ -64,6 +64,7 @@ func TestDeepInfraRerankHappyPath(t *testing.T) { []string{"Paris is the capital.", "Berlin is the capital."}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 1}, + nil, ) if err != nil { t.Fatalf("Rerank: %v", err) @@ -91,6 +92,7 @@ func TestDeepInfraRerankNoTopNLimit(t *testing.T) { []string{"Paris is the capital.", "Berlin is the capital."}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err != nil { t.Fatalf("Rerank: %v", err) @@ -106,7 +108,7 @@ func TestDeepInfraRerankNoTopNLimit(t *testing.T) { func TestDeepInfraRerankEmptyDocuments(t *testing.T) { apiKey := "test-key" model := "Qwen/Qwen3-Reranker-4B" - resp, err := newDeepInfraForTest("http://unused").Rerank(&model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := newDeepInfraForTest("http://unused").Rerank(&model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -118,7 +120,7 @@ func TestDeepInfraRerankEmptyDocuments(t *testing.T) { // TestDeepInfraRerankRequiresAPIKey rejects requests without an API key. func TestDeepInfraRerankRequiresAPIKey(t *testing.T) { model := "Qwen/Qwen3-Reranker-4B" - _, err := newDeepInfraForTest("http://unused").Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil) + _, err := newDeepInfraForTest("http://unused").Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -134,7 +136,7 @@ func TestDeepInfraRerankRejectsScoreCountMismatch(t *testing.T) { apiKey := "test-key" model := "cross-encoder/ms-marco-MiniLM-L-12-v2" _, err := newDeepInfraForTest(srv.URL).Rerank( - &model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + &model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "expected 2 scores") { t.Errorf("expected score-count error, got %v", err) } diff --git a/internal/entity/models/deepseek.go b/internal/entity/models/deepseek.go index 5e8b7b81f6..36eb8243ab 100644 --- a/internal/entity/models/deepseek.go +++ b/internal/entity/models/deepseek.go @@ -53,7 +53,7 @@ func (d *DeepSeekModel) Name() string { return "deepseek" } -func (d *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (d *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -242,7 +242,7 @@ func (d *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, a ToolCalls: toolCalls, } if pt, ct, tt := extractUsageFromMap(result); tt > 0 { - chatResponse.Usage = &ChatUsage{ + chatResponse.Usage = &TokenUsage{ PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, } } @@ -251,7 +251,7 @@ func (d *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, a } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (d *DeepSeekModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (d *DeepSeekModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -488,7 +488,7 @@ func (d *DeepSeekModel) ChatStreamlyWithSender(modelName string, messages []Mess } // Embed embeds a list of texts into embeddings -func (d *DeepSeekModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (d *DeepSeekModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } @@ -638,35 +638,35 @@ func (d *DeepSeekModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (d *DeepSeekModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (d *DeepSeekModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", d.Name()) } // TranscribeAudio transcribe audio -func (d *DeepSeekModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (d *DeepSeekModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DeepSeekModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (d *DeepSeekModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", d.Name()) } // AudioSpeech convert text to audio -func (d *DeepSeekModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (d *DeepSeekModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DeepSeekModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (d *DeepSeekModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", d.Name()) } // OCRFile OCR file -func (d *DeepSeekModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (d *DeepSeekModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } // ParseFile parse file -func (d *DeepSeekModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (d *DeepSeekModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } diff --git a/internal/entity/models/deepseek_test.go b/internal/entity/models/deepseek_test.go index 36340033e3..aab13f72c6 100644 --- a/internal/entity/models/deepseek_test.go +++ b/internal/entity/models/deepseek_test.go @@ -79,6 +79,7 @@ func TestDeepSeekChatWithMessagesSupportsToolCalls(t *testing.T) { }, ToolChoice: &toolChoice, }, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -142,6 +143,7 @@ func TestDeepSeekChatWithMessagesForwardsToolHistory(t *testing.T) { }, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) diff --git a/internal/entity/models/dummy.go b/internal/entity/models/dummy.go index b909b5378a..86a123ae20 100644 --- a/internal/entity/models/dummy.go +++ b/internal/entity/models/dummy.go @@ -18,6 +18,7 @@ package models import ( "fmt" + "ragflow/internal/common" ) // DummyModel implements ModelDriver for Dummy AI @@ -44,17 +45,17 @@ func (d *DummyModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns response -func (d *DummyModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (d *DummyModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("not implemented") } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (d *DummyModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (d *DummyModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("not implemented") } // Embed embeds a list of texts into embeddings -func (d *DummyModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (d *DummyModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } @@ -71,35 +72,35 @@ func (d *DummyModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (d *DummyModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (d *DummyModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", d.Name()) } // TranscribeAudio transcribe audio -func (d *DummyModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (d *DummyModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DummyModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (d *DummyModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", d.Name()) } // AudioSpeech convert text to audio -func (d *DummyModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (d *DummyModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DummyModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (d *DummyModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", d.Name()) } // OCRFile OCR file -func (d *DummyModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (d *DummyModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } // ParseFile parse file -func (d *DummyModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (d *DummyModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } diff --git a/internal/entity/models/fishaudio.go b/internal/entity/models/fishaudio.go index 56a81232af..14e358a27b 100644 --- a/internal/entity/models/fishaudio.go +++ b/internal/entity/models/fishaudio.go @@ -27,6 +27,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -53,24 +54,24 @@ func (f *FishAudioModel) Name() string { return "Fish Audio" } -func (f *FishAudioModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (f *FishAudioModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } -func (f *FishAudioModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (f *FishAudioModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", f.Name()) } -func (f *FishAudioModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (f *FishAudioModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("no such method") } -func (f *FishAudioModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (f *FishAudioModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (f *FishAudioModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (f *FishAudioModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -176,12 +177,12 @@ func (f *FishAudioModel) TranscribeAudio(modelName *string, file *string, apiCon }, nil } -func (f *FishAudioModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (f *FishAudioModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", f.Name()) } // AudioSpeech convert text to audio -func (f *FishAudioModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (f *FishAudioModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -244,7 +245,7 @@ func (f *FishAudioModel) AudioSpeech(modelName *string, audioContent *string, ap return &TTSResponse{Audio: body}, nil } -func (f *FishAudioModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (f *FishAudioModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -325,12 +326,12 @@ func (f *FishAudioModel) AudioSpeechWithSender(modelName *string, audioContent * } // OCRFile OCR file -func (f *FishAudioModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (f *FishAudioModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } // ParseFile parse file -func (f *FishAudioModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (f *FishAudioModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } diff --git a/internal/entity/models/futurmix.go b/internal/entity/models/futurmix.go index f0fef4b2a0..c46b9ade77 100644 --- a/internal/entity/models/futurmix.go +++ b/internal/entity/models/futurmix.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -145,7 +146,7 @@ type futurmixChatResponse struct { } // ChatWithMessages sends a non-streaming chat completion -func (f *FuturMixModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (f *FuturMixModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -207,7 +208,7 @@ func (f *FuturMixModel) ChatWithMessages(modelName string, messages []Message, a } // ChatStreamlyWithSender sends a streaming chat completion -func (f *FuturMixModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (f *FuturMixModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -285,12 +286,12 @@ func (f *FuturMixModel) ChatStreamlyWithSender(modelName string, messages []Mess } // Embed is not exposed by the FuturMix API per the public docs. -func (f *FuturMixModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (f *FuturMixModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } // Rerank is not exposed by the FuturMix API per the public docs. -func (f *FuturMixModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (f *FuturMixModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } @@ -310,30 +311,30 @@ func (f *FuturMixModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e } // TranscribeAudio is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (f *FuturMixModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } -func (f *FuturMixModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (f *FuturMixModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", f.Name()) } // AudioSpeech is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (f *FuturMixModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } -func (f *FuturMixModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (f *FuturMixModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", f.Name()) } // OCRFile is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (f *FuturMixModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } // ParseFile is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (f *FuturMixModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } diff --git a/internal/entity/models/gitee.go b/internal/entity/models/gitee.go index 2be4b9e716..57399f6886 100644 --- a/internal/entity/models/gitee.go +++ b/internal/entity/models/gitee.go @@ -54,7 +54,7 @@ func (g *GiteeModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns response -func (g *GiteeModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (g *GiteeModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -208,7 +208,7 @@ func (g *GiteeModel) ChatWithMessages(modelName string, messages []Message, apiC } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (g *GiteeModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (g *GiteeModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -403,7 +403,7 @@ type giteeUsage struct { } // Embed embeds a list of texts into embeddings -func (g *GiteeModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (g *GiteeModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -488,7 +488,7 @@ type giteeRerankRequest struct { } // Rerank calculates similarity scores between query and documents -func (g *GiteeModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (g *GiteeModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -562,20 +562,20 @@ func (g *GiteeModel) Rerank(modelName *string, query string, documents []string, } // TranscribeAudio transcribe audio -func (g *GiteeModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (g *GiteeModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GiteeModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (g *GiteeModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } // AudioSpeech convert text to audio -func (g *GiteeModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (g *GiteeModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GiteeModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (g *GiteeModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } @@ -585,7 +585,7 @@ type giteeOCRResponse struct { } // OCRFile OCR file -func (g *GiteeModel) OCRFile(modelName *string, content []byte, imageURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (g *GiteeModel) OCRFile(modelName *string, content []byte, imageURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -682,7 +682,7 @@ type giteeURLs struct { } // ParseFile parse file -func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } diff --git a/internal/entity/models/google.go b/internal/entity/models/google.go index 11b0fe48b6..8add9fa66b 100644 --- a/internal/entity/models/google.go +++ b/internal/entity/models/google.go @@ -119,7 +119,7 @@ func (g *GoogleModel) baseURL(apiConfig *APIConfig) string { return strings.TrimSpace(baseURL) } -func (g *GoogleModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (g *GoogleModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -193,7 +193,7 @@ func (g *GoogleModel) ChatWithMessages(modelName string, messages []Message, api } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (g *GoogleModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (g *GoogleModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -298,7 +298,7 @@ func (g *GoogleModel) ChatStreamlyWithSender(modelName string, messages []Messag // Embed generates embeddings for a batch of texts using the Gemini embeddings API. // The SDK routes to batchEmbedContents internally, so all texts are sent in one request. -func (g *GoogleModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (g *GoogleModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -370,35 +370,35 @@ func (g *GoogleModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (g *GoogleModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (g *GoogleModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", g.Name()) } // TranscribeAudio transcribe audio -func (g *GoogleModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (g *GoogleModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GoogleModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (g *GoogleModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } // AudioSpeech convert text to audio -func (g *GoogleModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (g *GoogleModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GoogleModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (g *GoogleModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } // OCRFile OCR file -func (g *GoogleModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (g *GoogleModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } // ParseFile parse file -func (g *GoogleModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (g *GoogleModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } diff --git a/internal/entity/models/google_test.go b/internal/entity/models/google_test.go index da5af43e1a..327150f8b2 100644 --- a/internal/entity/models/google_test.go +++ b/internal/entity/models/google_test.go @@ -207,7 +207,7 @@ func TestGoogleModelChatStreamlyRequiresAPIKey(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := model.ChatStreamlyWithSender("gemini-2.5-flash", messages, tc.apiConfig, nil, func(*string, *string) error { + err := model.ChatStreamlyWithSender("gemini-2.5-flash", messages, tc.apiConfig, nil, nil, func(*string, *string) error { t.Errorf("sender should not be called without an API key") return nil }) @@ -226,7 +226,7 @@ func TestGoogleModelChatRequiresModelName(t *testing.T) { apiKey := "test-api-key" messages := []Message{{Role: "user", Content: "hello"}} - response, err := model.ChatWithMessages("", messages, &APIConfig{ApiKey: &apiKey}, nil) + response, err := model.ChatWithMessages("", messages, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil { t.Fatal("expected a model name error") } @@ -237,7 +237,7 @@ func TestGoogleModelChatRequiresModelName(t *testing.T) { t.Fatalf("expected no response, got %v", response) } - err = model.ChatStreamlyWithSender("", messages, &APIConfig{ApiKey: &apiKey}, nil, func(*string, *string) error { + err = model.ChatStreamlyWithSender("", messages, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { t.Errorf("sender should not be called without a model name") return nil }) @@ -248,7 +248,7 @@ func TestGoogleModelChatRequiresModelName(t *testing.T) { t.Fatalf("expected model name error, got %v", err) } - err = model.ChatStreamlyWithSender("gemini-2.5-flash", messages, &APIConfig{ApiKey: &apiKey}, nil, nil) + err = model.ChatStreamlyWithSender("gemini-2.5-flash", messages, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil { t.Fatal("expected a sender error") } diff --git a/internal/entity/models/gpustack.go b/internal/entity/models/gpustack.go index e6d5361fe7..5ef29a047f 100644 --- a/internal/entity/models/gpustack.go +++ b/internal/entity/models/gpustack.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -51,7 +52,7 @@ func (g *GPUStackModel) Name() string { } // ChatWithMessages sends multiple messages and returns the response. -func (g *GPUStackModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (g *GPUStackModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -163,7 +164,7 @@ func (g *GPUStackModel) ChatWithMessages(modelName string, messages []Message, a } // ChatStreamlyWithSender streams the response via the sender. -func (g *GPUStackModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (g *GPUStackModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -371,6 +372,7 @@ func (g *GPUStackModel) Embed( texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, + modelUsage *common.ModelUsage, ) ([]EmbeddingData, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err @@ -467,7 +469,7 @@ func (g *GPUStackModel) Embed( return embeddings, nil } -func (g *GPUStackModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (g *GPUStackModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } @@ -475,27 +477,27 @@ func (g *GPUStackModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (g *GPUStackModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (g *GPUStackModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (g *GPUStackModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (g *GPUStackModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (g *GPUStackModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (g *GPUStackModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } diff --git a/internal/entity/models/gpustack_test.go b/internal/entity/models/gpustack_test.go index af05ae5bd9..cbbfbeeb17 100644 --- a/internal/entity/models/gpustack_test.go +++ b/internal/entity/models/gpustack_test.go @@ -115,7 +115,7 @@ func TestGPUStackChatHappyPath(t *testing.T) { resp, err := newGPUStackForTest(srv.URL).ChatWithMessages( "qwen3-8b", []Message{{Role: "user", Content: "ping"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err != nil { t.Fatalf("Chat: %v", err) @@ -146,7 +146,7 @@ func TestGPUStackChatExtractsReasoningContent(t *testing.T) { resp, err := newGPUStackForTest(srv.URL).ChatWithMessages( "qwen3-32b", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err != nil { t.Fatalf("Chat: %v", err) @@ -184,6 +184,7 @@ func TestGPUStackChatForwardsDocumentedFields(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop}, + nil, ) if err != nil { t.Fatalf("Chat: %v", err) @@ -194,7 +195,7 @@ func TestGPUStackChatAllowsEmptyAPIKey(t *testing.T) { _, err := newGPUStackForTest("http://unused").ChatWithMessages( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, ) if err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("self-hosted model should not require api key, got %v", err) @@ -206,7 +207,7 @@ func TestGPUStackChatRequiresModelName(t *testing.T) { _, err := newGPUStackForTest("http://unused").ChatWithMessages( "", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) @@ -216,7 +217,7 @@ func TestGPUStackChatRequiresModelName(t *testing.T) { func TestGPUStackChatRequiresMessages(t *testing.T) { apiKey := "test-key" _, err := newGPUStackForTest("http://unused").ChatWithMessages( - "qwen3-8b", nil, &APIConfig{ApiKey: &apiKey}, nil, + "qwen3-8b", nil, &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) @@ -234,7 +235,7 @@ func TestGPUStackChatRejectsHTTPError(t *testing.T) { _, err := newGPUStackForTest(srv.URL).ChatWithMessages( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) @@ -247,7 +248,7 @@ func TestGPUStackChatRequiresBaseURL(t *testing.T) { _, err := model.ChatWithMessages( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "no base URL configured") { t.Errorf("expected base-URL error, got %v", err) @@ -269,7 +270,7 @@ func TestGPUStackStreamHappyPath(t *testing.T) { err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( "qwen3-8b", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -307,7 +308,7 @@ func TestGPUStackStreamExtractsReasoningContent(t *testing.T) { err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if r != nil && *r != "" { reasoning = append(reasoning, *r) @@ -337,6 +338,7 @@ func TestGPUStackStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -349,7 +351,7 @@ func TestGPUStackStreamRequiresSender(t *testing.T) { err := newGPUStackForTest("http://unused").ChatStreamlyWithSender( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) @@ -366,7 +368,7 @@ func TestGPUStackStreamFailsWithoutTerminal(t *testing.T) { err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream ended before") { @@ -385,7 +387,7 @@ func TestGPUStackStreamRejectsMalformedFrame(t *testing.T) { err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "invalid SSE event") { @@ -404,7 +406,7 @@ func TestGPUStackStreamSurfacesUpstreamError(t *testing.T) { err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( "qwen3-8b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "upstream stream error") { @@ -478,7 +480,7 @@ func TestGPUStackEmbedHappyPath(t *testing.T) { apiKey := "test-key" model := "bge-m3" vecs, err := newGPUStackForTest(srv.URL).Embed( - &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 512}) + &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 512}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -506,7 +508,7 @@ func TestGPUStackEmbedReordersByIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" vecs, err := newGPUStackForTest(srv.URL).Embed( - &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -527,7 +529,7 @@ func TestGPUStackEmbedEmptyInputShortCircuits(t *testing.T) { apiKey := "test-key" model := "bge-m3" - vecs, err := newGPUStackForTest(srv.URL).Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := newGPUStackForTest(srv.URL).Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed([]): %v", err) } @@ -539,7 +541,7 @@ func TestGPUStackEmbedEmptyInputShortCircuits(t *testing.T) { // TestGPUStackEmbedRequiresAPIKey rejects requests without an API key. func TestGPUStackEmbedAllowsEmptyAPIKey(t *testing.T) { model := "bge-m3" - _, err := newGPUStackForTest("http://unused").Embed(&model, []string{"a"}, &APIConfig{}, nil) + _, err := newGPUStackForTest("http://unused").Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) if err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("self-hosted model should not require api key, got %v", err) } @@ -559,7 +561,7 @@ func TestGPUStackEmbedRejectsDuplicateIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -578,7 +580,7 @@ func TestGPUStackEmbedRejectsOutOfRangeIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -597,7 +599,7 @@ func TestGPUStackEmbedRejectsMissingIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding index") { t.Errorf("expected missing-index error, got %v", err) } @@ -616,7 +618,7 @@ func TestGPUStackEmbedRejectsEmptyVector(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "empty embedding vector") { t.Errorf("expected empty-vector error, got %v", err) } @@ -635,7 +637,7 @@ func TestGPUStackEmbedRejectsMissingSlot(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding for input index") { t.Errorf("expected missing-slot error, got %v", err) } @@ -644,19 +646,19 @@ func TestGPUStackEmbedRejectsMissingSlot(t *testing.T) { func TestGPUStackUnsupportedMethods(t *testing.T) { m := newGPUStackForTest("http://unused") model := "x" - if _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: %v", err) } if _, err := m.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: %v", err) } - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: %v", err) } - if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: %v", err) } - if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } diff --git a/internal/entity/models/groq.go b/internal/entity/models/groq.go index bec9fdf109..5f2aa09d9a 100644 --- a/internal/entity/models/groq.go +++ b/internal/entity/models/groq.go @@ -26,6 +26,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -124,7 +125,7 @@ type groqChatResponse struct { FinishReason string `json:"finish_reason"` } -func (g *GroqModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (g *GroqModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -192,7 +193,7 @@ func (g *GroqModel) ChatWithMessages(modelName string, messages []Message, apiCo }, nil } -func (g *GroqModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (g *GroqModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -341,11 +342,11 @@ func (g *GroqModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (g *GroqModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (g *GroqModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GroqModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (g *GroqModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } @@ -353,7 +354,7 @@ func (g *GroqModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GroqModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (g *GroqModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -470,11 +471,11 @@ func (g *GroqModel) TranscribeAudio(modelName *string, file *string, apiConfig * return &ASRResponse{Text: result.Text}, nil } -func (g *GroqModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (g *GroqModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } -func (g *GroqModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (g *GroqModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -534,15 +535,15 @@ func (g *GroqModel) AudioSpeech(modelName *string, audioContent *string, apiConf return &TTSResponse{Audio: body}, nil } -func (g *GroqModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (g *GroqModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", g.Name()) } -func (g *GroqModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (g *GroqModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GroqModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (g *GroqModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } diff --git a/internal/entity/models/groq_test.go b/internal/entity/models/groq_test.go index 01e61a320e..3811a35883 100644 --- a/internal/entity/models/groq_test.go +++ b/internal/entity/models/groq_test.go @@ -130,6 +130,7 @@ func TestGroqChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &maxTokens, Temperature: &temperature, TopP: &topP, Stop: &stop, Effort: &effort}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -144,7 +145,7 @@ func TestGroqChatHappyPath(t *testing.T) { func TestGroqChatRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newGroqForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGroqForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -152,7 +153,7 @@ func TestGroqChatRequiresModelName(t *testing.T) { func TestGroqChatRequiresMessages(t *testing.T) { apiKey := "test-key" - _, err := newGroqForTest("http://unused").ChatWithMessages("llama-3.3-70b-versatile", nil, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newGroqForTest("http://unused").ChatWithMessages("llama-3.3-70b-versatile", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -171,6 +172,7 @@ func TestGroqChatRejectsHTTPError(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) @@ -205,7 +207,7 @@ func TestGroqStreamHappyPath(t *testing.T) { "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, - nil, + nil, nil, func(c *string, r *string) error { if c != nil { if *c == "[DONE]" { @@ -242,6 +244,7 @@ func TestGroqStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -257,6 +260,7 @@ func TestGroqStreamRequiresSender(t *testing.T) { &APIConfig{ApiKey: &apiKey}, nil, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) @@ -276,6 +280,7 @@ func TestGroqStreamRejectsUnterminatedStream(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream ended before") { @@ -333,6 +338,7 @@ func TestGroqBaseURLTrimsTrailingSlash(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -363,6 +369,7 @@ func TestGroqUsesEmptyRegionCustomBaseURL(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -371,16 +378,16 @@ func TestGroqUsesEmptyRegionCustomBaseURL(t *testing.T) { func TestGroqUnsupportedMethods(t *testing.T) { m := newGroqForTest("http://unused") - if _, err := m.Embed(nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed error=%v", err) } - if _, err := m.Rerank(nil, "", nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(nil, "", nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank error=%v", err) } if _, err := m.Balance(nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance error=%v", err) } - if _, err := m.ParseFile(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(nil, nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ParseFile error=%v", err) } if _, err := m.ListTasks(nil); err == nil || !strings.Contains(err.Error(), "no such method") { diff --git a/internal/entity/models/huaweicloud.go b/internal/entity/models/huaweicloud.go index ff15ba0957..3a755f3ec4 100644 --- a/internal/entity/models/huaweicloud.go +++ b/internal/entity/models/huaweicloud.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" "github.com/goccy/go-json" @@ -173,7 +174,7 @@ func huaweiCloudApplyChatConfig(req map[string]any, modelName string, chatModelC } } -func (h *HuaweiCloudModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (h *HuaweiCloudModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -271,7 +272,7 @@ func (h *HuaweiCloudModel) ChatWithMessages(modelName string, messages []Message }, nil } -func (h *HuaweiCloudModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (h *HuaweiCloudModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -389,7 +390,7 @@ type huaweiCloudEmbeddingResponse struct { } `json:"data"` } -func (h *HuaweiCloudModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (h *HuaweiCloudModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -487,7 +488,7 @@ func (h *HuaweiCloudModel) Embed(modelName *string, texts []string, apiConfig *A return embeddings, nil } -func (h *HuaweiCloudModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (h *HuaweiCloudModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -580,27 +581,27 @@ func (h *HuaweiCloudModel) Rerank(modelName *string, query string, documents []s return result, nil } -func (h *HuaweiCloudModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (h *HuaweiCloudModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (h *HuaweiCloudModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (h *HuaweiCloudModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (h *HuaweiCloudModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (h *HuaweiCloudModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (h *HuaweiCloudModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } diff --git a/internal/entity/models/huggingface.go b/internal/entity/models/huggingface.go index f8b0f95717..4c7e49dd04 100644 --- a/internal/entity/models/huggingface.go +++ b/internal/entity/models/huggingface.go @@ -49,7 +49,7 @@ func (h *HuggingFaceModel) Name() string { return "huggingface" } -func (h *HuggingFaceModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (h *HuggingFaceModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -192,7 +192,7 @@ func (h *HuggingFaceModel) ChatWithMessages(modelName string, messages []Message return chatResponse, nil } -func (h *HuggingFaceModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (h *HuggingFaceModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -328,7 +328,7 @@ func (h *HuggingFaceModel) ChatStreamlyWithSender(modelName string, messages []M return sender(&endOfStream, nil) } -func (h *HuggingFaceModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (h *HuggingFaceModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -398,35 +398,35 @@ func (h *HuggingFaceModel) Embed(modelName *string, texts []string, apiConfig *A return embeddings, nil } -func (h *HuggingFaceModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (h *HuggingFaceModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (h *HuggingFaceModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (h *HuggingFaceModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuggingFaceModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (h *HuggingFaceModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", h.Name()) } // AudioSpeech convert text to audio -func (h *HuggingFaceModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (h *HuggingFaceModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuggingFaceModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (h *HuggingFaceModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", h.Name()) } // OCRFile OCR file -func (h *HuggingFaceModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (h *HuggingFaceModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } // ParseFile parse file -func (h *HuggingFaceModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (h *HuggingFaceModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } diff --git a/internal/entity/models/hunyuan.go b/internal/entity/models/hunyuan.go index d6b6c333ff..c777706711 100644 --- a/internal/entity/models/hunyuan.go +++ b/internal/entity/models/hunyuan.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -50,7 +51,7 @@ func (h *HunyuanModel) Name() string { return "Tencent Hunyuan" } -func (h *HunyuanModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (h *HunyuanModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -160,7 +161,7 @@ func (h *HunyuanModel) ChatWithMessages(modelName string, messages []Message, ap } // ChatStreamlyWithSender opens the SSE chat-completions -func (h *HunyuanModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (h *HunyuanModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -333,7 +334,7 @@ func (h *HunyuanModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (h *HunyuanModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (h *HunyuanModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -414,7 +415,7 @@ func (h *HunyuanModel) Embed(modelName *string, texts []string, apiConfig *APICo return embeddings, nil } -func (h *HunyuanModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (h *HunyuanModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } @@ -422,27 +423,27 @@ func (h *HunyuanModel) Balance(apiConfig *APIConfig) (map[string]interface{}, er return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (h *HunyuanModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (h *HunyuanModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (h *HunyuanModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (h *HunyuanModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (h *HunyuanModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (h *HunyuanModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } diff --git a/internal/entity/models/hunyuan_test.go b/internal/entity/models/hunyuan_test.go index 754c31eca8..fb31ff97c0 100644 --- a/internal/entity/models/hunyuan_test.go +++ b/internal/entity/models/hunyuan_test.go @@ -128,6 +128,7 @@ func TestHunyuanChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -154,7 +155,8 @@ func TestHunyuanChatNoReasoning(t *testing.T) { resp, err := newHunyuanForTest(srv.URL).ChatWithMessages( "hunyuan-lite", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -170,7 +172,8 @@ func TestHunyuanChatRequiresAPIKey(t *testing.T) { _, err := newHunyuanForTest("http://unused").ChatWithMessages( "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -179,7 +182,8 @@ func TestHunyuanChatRequiresAPIKey(t *testing.T) { func TestHunyuanChatRequiresMessages(t *testing.T) { apiKey := "test-key" _, err := newHunyuanForTest("http://unused").ChatWithMessages( - "hunyuan-pro", nil, &APIConfig{ApiKey: &apiKey}, nil) + "hunyuan-pro", nil, &APIConfig{ApiKey: &apiKey}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -196,7 +200,8 @@ func TestHunyuanChatPropagatesHTTPError(t *testing.T) { _, err := newHunyuanForTest(srv.URL).ChatWithMessages( "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -217,7 +222,7 @@ func TestHunyuanStreamHappyPath(t *testing.T) { err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( "hunyuan-pro", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -255,7 +260,7 @@ func TestHunyuanStreamSplitsReasoning(t *testing.T) { err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( "hunyuan-standard-256K", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -287,6 +292,7 @@ func TestHunyuanStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("expected stream-true guard, got %v", err) @@ -298,7 +304,7 @@ func TestHunyuanStreamRequiresSender(t *testing.T) { err := newHunyuanForTest("http://unused").ChatStreamlyWithSender( "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -314,7 +320,7 @@ func TestHunyuanStreamFailsWithoutTerminal(t *testing.T) { err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream ended before") { t.Errorf("expected truncation error, got %v", err) @@ -332,7 +338,7 @@ func TestHunyuanStreamRejectsMalformedFrame(t *testing.T) { err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "invalid SSE event") { t.Errorf("expected invalid-SSE error, got %v", err) @@ -350,7 +356,7 @@ func TestHunyuanStreamSurfacesUpstreamError(t *testing.T) { err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "upstream stream error") { t.Errorf("expected upstream-error surfacing, got %v", err) @@ -450,7 +456,7 @@ func TestHunyuanEmbedHappyPath(t *testing.T) { apiKey := "test-key" model := "hunyuan-embedding" - embeddings, err := newHunyuanForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + embeddings, err := newHunyuanForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -463,24 +469,24 @@ func TestHunyuanEmbedValidatesInputs(t *testing.T) { apiKey := "test-key" model := "hunyuan-embedding" - if embeddings, err := newHunyuanForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil); err != nil || len(embeddings) != 0 { + if embeddings, err := newHunyuanForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil || len(embeddings) != 0 { t.Errorf("empty input: embeddings=%+v err=%v", embeddings, err) } - if _, err := newHunyuanForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := newHunyuanForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("nil model: %v", err) } emptyModel := "" - if _, err := newHunyuanForTest("http://unused").Embed(&emptyModel, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := newHunyuanForTest("http://unused").Embed(&emptyModel, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("empty model: %v", err) } - if _, err := newHunyuanForTest("http://unused").Embed(&model, []string{"x"}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newHunyuanForTest("http://unused").Embed(&model, []string{"x"}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("nil api config: %v", err) } - if _, err := newHunyuanForTest("http://unused").Embed(&model, []string{"x"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newHunyuanForTest("http://unused").Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("missing api key: %v", err) } emptyKey := "" - if _, err := newHunyuanForTest("http://unused").Embed(&model, []string{"x"}, &APIConfig{ApiKey: &emptyKey}, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newHunyuanForTest("http://unused").Embed(&model, []string{"x"}, &APIConfig{ApiKey: &emptyKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("empty api key: %v", err) } } @@ -488,13 +494,13 @@ func TestHunyuanEmbedValidatesInputs(t *testing.T) { func TestHunyuanAudioOCRReturnNoSuchMethod(t *testing.T) { m := newHunyuanForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: %v", err) } - if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: %v", err) } - if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } diff --git a/internal/entity/models/jiekouai.go b/internal/entity/models/jiekouai.go index 8c7f8a4876..f6d7e0bdc0 100644 --- a/internal/entity/models/jiekouai.go +++ b/internal/entity/models/jiekouai.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" "time" ) @@ -60,7 +61,7 @@ func validateJieKouAIModelName(modelName *string) (string, error) { return strings.TrimSpace(*modelName), nil } -func (j *JieKouAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (j *JieKouAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -195,7 +196,7 @@ func (j *JieKouAIModel) ChatWithMessages(modelName string, messages []Message, a return chatResponse, nil } -func (j *JieKouAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (j *JieKouAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -331,7 +332,7 @@ func (j *JieKouAIModel) ChatStreamlyWithSender(modelName string, messages []Mess return nil } -func (j *JieKouAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (j *JieKouAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -410,7 +411,7 @@ func (j *JieKouAIModel) Embed(modelName *string, texts []string, apiConfig *APIC return embeddings, nil } -func (j *JieKouAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (j *JieKouAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -493,27 +494,27 @@ func (j *JieKouAIModel) Rerank(modelName *string, query string, documents []stri return &rerankResponse, nil } -func (j *JieKouAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (j *JieKouAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JieKouAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (j *JieKouAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", j.Name()) } -func (j *JieKouAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (j *JieKouAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JieKouAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (j *JieKouAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", j.Name()) } -func (j *JieKouAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (j *JieKouAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JieKouAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (j *JieKouAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } diff --git a/internal/entity/models/jiekouai_test.go b/internal/entity/models/jiekouai_test.go index 2e325eec6e..588dcfc15d 100644 --- a/internal/entity/models/jiekouai_test.go +++ b/internal/entity/models/jiekouai_test.go @@ -94,6 +94,7 @@ func TestJieKouAIChatForcesNonStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream, Thinking: &thinking}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -135,6 +136,7 @@ func TestJieKouAIStreamForcesStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(answer, reason *string) error { if answer != nil { content = append(content, *answer) @@ -225,7 +227,7 @@ func TestJieKouAIEmbedSendsValidatedRequest(t *testing.T) { apiKey := "test-key" model := " text-embedding-3-large " - embeddings, err := newJieKouAIForTest(srv.URL).Embed(&model, []string{"hello"}, &APIConfig{ApiKey: &apiKey}, nil) + embeddings, err := newJieKouAIForTest(srv.URL).Embed(&model, []string{"hello"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -259,7 +261,7 @@ func TestJieKouAIRerankHandlesNilConfig(t *testing.T) { apiKey := "test-key" model := " baai/bge-reranker-v2-m3 " - resp, err := newJieKouAIForTest(srv.URL).Rerank(&model, " question ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := newJieKouAIForTest(srv.URL).Rerank(&model, " question ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -282,7 +284,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "chat api key", run: func() error { - _, err := newJieKouAIForTest("http://unused").ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err := newJieKouAIForTest("http://unused").ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -290,7 +292,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "chat model", run: func() error { - _, err := newJieKouAIForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newJieKouAIForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -298,21 +300,21 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "stream api key", run: func() error { - return newJieKouAIForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, nil, nil, send) + return newJieKouAIForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, nil, nil, nil, send) }, want: "api key is required", }, { name: "stream sender", run: func() error { - return newJieKouAIForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + return newJieKouAIForTest("http://unused").ChatStreamlyWithSender("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) }, want: "sender is required", }, { name: "embed model", run: func() error { - _, err := newJieKouAIForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newJieKouAIForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -320,7 +322,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "embed api key", run: func() error { - _, err := newJieKouAIForTest("http://unused").Embed(&model, []string{"x"}, nil, nil) + _, err := newJieKouAIForTest("http://unused").Embed(&model, []string{"x"}, nil, nil, nil) return err }, want: "api key is required", @@ -328,7 +330,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "rerank model", run: func() error { - _, err := newJieKouAIForTest("http://unused").Rerank(nil, "q", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newJieKouAIForTest("http://unused").Rerank(nil, "q", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -336,7 +338,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "rerank api key", run: func() error { - _, err := newJieKouAIForTest("http://unused").Rerank(&model, "q", []string{"doc"}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err := newJieKouAIForTest("http://unused").Rerank(&model, "q", []string{"doc"}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -344,7 +346,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "rerank query", run: func() error { - _, err := newJieKouAIForTest("http://unused").Rerank(&model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newJieKouAIForTest("http://unused").Rerank(&model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "query is required", diff --git a/internal/entity/models/jina.go b/internal/entity/models/jina.go index 7eaf3b8cb3..4fd11f731f 100644 --- a/internal/entity/models/jina.go +++ b/internal/entity/models/jina.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" "time" ) @@ -54,7 +55,7 @@ func (j *JinaModel) Name() string { return "jina" } -func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -164,12 +165,12 @@ func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiCo }, nil } -func (j *JinaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (j *JinaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { //TODO implement me: https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post return fmt.Errorf("jina does not implement ChatStreamlyWithSender(not available for now)") } -func (j *JinaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (j *JinaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -243,7 +244,7 @@ func (j *JinaModel) Embed(modelName *string, texts []string, apiConfig *APIConfi return embeddings, nil } -func (j *JinaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (j *JinaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -380,30 +381,30 @@ func (j *JinaModel) CheckConnection(apiConfig *APIConfig) error { } // TranscribeAudio transcribe audio -func (j *JinaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (j *JinaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JinaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (j *JinaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", j.Name()) } // AudioSpeech convert text to audio -func (j *JinaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (j *JinaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JinaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (j *JinaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", j.Name()) } // OCRFile OCR file -func (j *JinaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (j *JinaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } // ParseFile parse file -func (j *JinaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (j *JinaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } diff --git a/internal/entity/models/jina_test.go b/internal/entity/models/jina_test.go index 2ae8a7be86..1f40dc1918 100644 --- a/internal/entity/models/jina_test.go +++ b/internal/entity/models/jina_test.go @@ -77,7 +77,7 @@ func TestJinaChatHappyPath(t *testing.T) { j := newJinaForTest(srv.URL) apiKey := "test-key" - resp, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -119,6 +119,7 @@ func TestJinaChatPropagatesConfig(t *testing.T) { _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &maxTokens, Temperature: &temperature, TopP: &topP, Stop: &stop}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -173,7 +174,7 @@ func TestJinaChatValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := j.ChatWithMessages(tt.modelName, tt.messages, tt.apiConfig, nil) + _, err := j.ChatWithMessages(tt.modelName, tt.messages, tt.apiConfig, nil, nil) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("expected %q error, got %v", tt.want, err) } @@ -190,7 +191,7 @@ func TestJinaChatRejectsHTTPError(t *testing.T) { j := newJinaForTest(srv.URL) apiKey := "test-key" - _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "status 401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -204,7 +205,7 @@ func TestJinaChatRejectsMalformedResponse(t *testing.T) { j := newJinaForTest(srv.URL) apiKey := "test-key" - _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no choices in response") { t.Errorf("expected malformed-response error, got %v", err) } @@ -215,7 +216,8 @@ func TestJinaChatRejectsUnknownRegion(t *testing.T) { apiKey := "test-key" region := "eu" _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil) + &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "no base URL configured for region") { t.Errorf("expected region error, got %v", err) } @@ -233,7 +235,8 @@ func TestJinaChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { apiKey := "test-key" emptyRegion := "" _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil) + &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil, nil, + ) if err != nil { t.Errorf("empty Region: expected fallback to default, got %v", err) } diff --git a/internal/entity/models/llm.go b/internal/entity/models/llm.go index 7310cd97b5..cfe163311c 100644 --- a/internal/entity/models/llm.go +++ b/internal/entity/models/llm.go @@ -147,7 +147,7 @@ func (m *EinoChatModel) Generate(ctx context.Context, msgs []*schema.Message, op if err != nil { return nil, err } - resp, err := m.inner.ModelDriver.ChatWithMessages(*m.inner.ModelName, internal, m.inner.APIConfig, chatCfg) + resp, err := m.inner.ModelDriver.ChatWithMessages(*m.inner.ModelName, internal, m.inner.APIConfig, chatCfg, nil) if err != nil { return nil, fmt.Errorf("models: EinoChatModel.Generate(%s): %w", *m.inner.ModelName, err) } @@ -155,7 +155,7 @@ func (m *EinoChatModel) Generate(ctx context.Context, msgs []*schema.Message, op // Langfuse) can compute the run total. Mirrors Python's // LLMBundle._report_usage() / self.mdl.last_usage pattern. if resp != nil && resp.Usage != nil { - m.inner.LastUsage = &ChatUsage{ + m.inner.LastUsage = &TokenUsage{ PromptTokens: resp.Usage.PromptTokens, CompletionTokens: resp.Usage.CompletionTokens, TotalTokens: resp.Usage.TotalTokens, } recordUsageFromResponse(ctx, m.inner) @@ -316,7 +316,7 @@ func (m *EinoChatModel) Stream(ctx context.Context, msgs []*schema.Message, opts } go func() { defer sw.Close() - if err := m.inner.ModelDriver.ChatStreamlyWithSender(*m.inner.ModelName, internal, m.inner.APIConfig, m.chatCfg, sender); err != nil { + if err := m.inner.ModelDriver.ChatStreamlyWithSender(*m.inner.ModelName, internal, m.inner.APIConfig, m.chatCfg, nil, sender); err != nil { _ = sw.Send(nil, err) } }() diff --git a/internal/entity/models/llm_test.go b/internal/entity/models/llm_test.go index 71fbdbd32c..147f05f8c4 100644 --- a/internal/entity/models/llm_test.go +++ b/internal/entity/models/llm_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "ragflow/internal/common" "testing" "github.com/cloudwego/eino/schema" @@ -194,7 +195,7 @@ type streamSentinelDriver struct { *captureToolDriver } -func (d *streamSentinelDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *APIConfig, _ *ChatConfig, sender func(*string, *string) error) error { +func (d *streamSentinelDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *APIConfig, _ *ChatConfig, _ *common.ModelUsage, sender func(*string, *string) error) error { answer := "answer" if err := sender(&answer, nil); err != nil { return err @@ -209,35 +210,35 @@ func (d *streamSentinelDriver) ChatStreamlyWithSender(_ string, _ []Message, _ * func (d *captureToolDriver) NewInstance(baseURL map[string]string) ModelDriver { return d } func (d *captureToolDriver) Name() string { return "capture" } -func (d *captureToolDriver) ChatWithMessages(_ string, _ []Message, _ *APIConfig, cfg *ChatConfig) (*ChatResponse, error) { +func (d *captureToolDriver) ChatWithMessages(_ string, _ []Message, _ *APIConfig, cfg *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { d.lastConfig = cfg return d.resp, nil } -func (d *captureToolDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *APIConfig, _ *ChatConfig, _ func(*string, *string) error) error { +func (d *captureToolDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *APIConfig, _ *ChatConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { return nil } -func (d *captureToolDriver) Embed(_ *string, _ []string, _ *APIConfig, _ *EmbeddingConfig) ([]EmbeddingData, error) { +func (d *captureToolDriver) Embed(_ *string, _ []string, _ *APIConfig, _ *EmbeddingConfig, _ *common.ModelUsage) ([]EmbeddingData, error) { return nil, nil } -func (d *captureToolDriver) Rerank(_ *string, _ string, _ []string, _ *APIConfig, _ *RerankConfig) (*RerankResponse, error) { +func (d *captureToolDriver) Rerank(_ *string, _ string, _ []string, _ *APIConfig, _ *RerankConfig, _ *common.ModelUsage) (*RerankResponse, error) { return nil, nil } -func (d *captureToolDriver) TranscribeAudio(_ *string, _ *string, _ *APIConfig, _ *ASRConfig) (*ASRResponse, error) { +func (d *captureToolDriver) TranscribeAudio(_ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ *common.ModelUsage) (*ASRResponse, error) { return nil, nil } -func (d *captureToolDriver) TranscribeAudioWithSender(_ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ func(*string, *string) error) error { +func (d *captureToolDriver) TranscribeAudioWithSender(_ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { return nil } -func (d *captureToolDriver) AudioSpeech(_ *string, _ *string, _ *APIConfig, _ *TTSConfig) (*TTSResponse, error) { +func (d *captureToolDriver) AudioSpeech(_ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ *common.ModelUsage) (*TTSResponse, error) { return nil, nil } -func (d *captureToolDriver) AudioSpeechWithSender(_ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ func(*string, *string) error) error { +func (d *captureToolDriver) AudioSpeechWithSender(_ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { return nil } -func (d *captureToolDriver) OCRFile(_ *string, _ []byte, _ *string, _ *APIConfig, _ *OCRConfig) (*OCRFileResponse, error) { +func (d *captureToolDriver) OCRFile(_ *string, _ []byte, _ *string, _ *APIConfig, _ *OCRConfig, _ *common.ModelUsage) (*OCRFileResponse, error) { return nil, nil } -func (d *captureToolDriver) ParseFile(_ *string, _ []byte, _ *string, _ *APIConfig, _ *ParseFileConfig) (*ParseFileResponse, error) { +func (d *captureToolDriver) ParseFile(_ *string, _ []byte, _ *string, _ *APIConfig, _ *ParseFileConfig, _ *common.ModelUsage) (*ParseFileResponse, error) { return nil, nil } func (d *captureToolDriver) ListModels(_ *APIConfig) ([]ListModelResponse, error) { diff --git a/internal/entity/models/lmstudio.go b/internal/entity/models/lmstudio.go index 5d446624d3..393f605f56 100644 --- a/internal/entity/models/lmstudio.go +++ b/internal/entity/models/lmstudio.go @@ -53,7 +53,7 @@ func (l *LmStudioModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns response -func (l *LmStudioModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (l *LmStudioModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -204,7 +204,7 @@ func (l *LmStudioModel) ChatWithMessages(modelName string, messages []Message, a } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (l *LmStudioModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (l *LmStudioModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -351,7 +351,7 @@ func (l *LmStudioModel) ChatStreamlyWithSender(modelName string, messages []Mess return nil } -func (l *LmStudioModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (l *LmStudioModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -435,35 +435,35 @@ func (l *LmStudioModel) Embed(modelName *string, texts []string, apiConfig *APIC return embeddings, nil } -func (l *LmStudioModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (l *LmStudioModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (l *LmStudioModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (l *LmStudioModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LmStudioModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (l *LmStudioModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", l.Name()) } // AudioSpeech convert text to audio -func (l *LmStudioModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (l *LmStudioModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LmStudioModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (l *LmStudioModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", l.Name()) } // OCRFile OCR file -func (l *LmStudioModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (l *LmStudioModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } // ParseFile parse file -func (l *LmStudioModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (l *LmStudioModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } diff --git a/internal/entity/models/localai.go b/internal/entity/models/localai.go index 6a79db2450..3f06c94dc6 100644 --- a/internal/entity/models/localai.go +++ b/internal/entity/models/localai.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" "sync" "time" @@ -80,7 +81,7 @@ func addLocalAIReasoningRequestParams(reqBody map[string]interface{}, cfg *ChatC } // ChatWithMessages sends multiple messages with roles and returns the response. -func (l *LocalAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (l *LocalAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -194,7 +195,7 @@ func (l *LocalAIModel) ChatWithMessages(modelName string, messages []Message, ap // ChatStreamlyWithSender sends messages and streams the response via the // sender function. The LocalAI SSE stream uses the same shape as OpenAI: // "data:" lines carrying JSON events, with a final "[DONE]" line. -func (l *LocalAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (l *LocalAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -369,7 +370,7 @@ type localAIEmbeddingResponse struct { Object string `json:"object"` } -func (l *LocalAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (l *LocalAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -471,7 +472,7 @@ type localAIRerankResponse struct { } `json:"results"` } -func (l *LocalAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (l *LocalAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -615,29 +616,29 @@ func (l *LocalAIModel) CheckConnection(apiConfig *APIConfig) error { return nil } -func (l *LocalAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (l *LocalAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LocalAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (l *LocalAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", l.Name()) } // AudioSpeech convert text to audio -func (l *LocalAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (l *LocalAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LocalAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (l *LocalAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", l.Name()) } -func (l *LocalAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (l *LocalAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } // ParseFile parse file -func (l *LocalAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (l *LocalAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } diff --git a/internal/entity/models/localai_test.go b/internal/entity/models/localai_test.go index 24f787521d..63db762e2e 100644 --- a/internal/entity/models/localai_test.go +++ b/internal/entity/models/localai_test.go @@ -80,7 +80,7 @@ func TestLocalAIStreamCancelsOnIdle(t *testing.T) { var mu sync.Mutex err := l.ChatStreamlyWithSender("gpt-4", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(content *string, _ *string) error { if content == nil || *content == "" { return nil @@ -130,7 +130,7 @@ func TestLocalAIStreamCompletesWithoutTriggeringWatchdog(t *testing.T) { var chunks []string err := l.ChatStreamlyWithSender("gpt-4", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(content *string, _ *string) error { if content != nil && *content != "" && *content != "[DONE]" { chunks = append(chunks, *content) @@ -150,7 +150,7 @@ func TestLocalAIStreamRequiresSender(t *testing.T) { l := newLocalAIForTest("http://unused") err := l.ChatStreamlyWithSender("gpt-4", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, nil) + &APIConfig{}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -163,7 +163,8 @@ func TestLocalAIChatMissingBaseURLFailsClearly(t *testing.T) { l := NewLocalAIModel(map[string]string{}, URLSuffix{Chat: "chat/completions"}) _, err := l.ChatWithMessages("gpt-4", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "base URL") { t.Errorf("expected missing-base-URL error, got %v", err) } @@ -183,7 +184,8 @@ func TestLocalAIChatOmitsAuthHeaderWhenKeyEmpty(t *testing.T) { l := newLocalAIForTest(srv.URL) resp, err := l.ChatWithMessages("gpt-4", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -207,7 +209,8 @@ func TestLocalAIChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { key := "secret" _, err := l.ChatWithMessages("gpt-4", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil) + &APIConfig{ApiKey: &key}, nil, nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -235,7 +238,7 @@ func TestLocalAIEmbedHappyPath(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - vecs, err := l.Embed(&model, []string{"a", "b", "c"}, &APIConfig{}, nil) + vecs, err := l.Embed(&model, []string{"a", "b", "c"}, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -260,7 +263,7 @@ func TestLocalAIEmbedRejectsDuplicateIndex(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil) + _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate embedding index 0") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -274,7 +277,7 @@ func TestLocalAIEmbedRejectsOutOfRangeIndex(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil) + _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -288,7 +291,7 @@ func TestLocalAIEmbedRejectsMissingSlot(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil) + _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding for input index 1") { t.Errorf("expected missing-slot error, got %v", err) } @@ -302,7 +305,7 @@ func TestLocalAIEmbedEmptyInputShortCircuits(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - vecs, err := l.Embed(&model, []string{}, &APIConfig{}, nil) + vecs, err := l.Embed(&model, []string{}, &APIConfig{}, nil, nil) if err != nil || len(vecs) != 0 { t.Errorf("Embed([])=(%v,%v) want ([],nil)", vecs, err) } @@ -369,7 +372,8 @@ func TestLocalAIChatExtractsReasoningContent(t *testing.T) { l := newLocalAIForTest(srv.URL) resp, err := l.ChatWithMessages("kimi-k2.6", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -396,7 +400,8 @@ func TestLocalAIChatExtractsThinking(t *testing.T) { l := newLocalAIForTest(srv.URL) resp, err := l.ChatWithMessages("qwen3-32b", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -419,7 +424,8 @@ func TestLocalAIChatHandlesAbsentReasoning(t *testing.T) { l := newLocalAIForTest(srv.URL) resp, err := l.ChatWithMessages("llama-3-8b-instruct", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -452,7 +458,7 @@ func TestLocalAIStreamExtractsReasoningContentDelta(t *testing.T) { var content, reasoning []string err := l.ChatStreamlyWithSender("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -495,7 +501,7 @@ func TestLocalAIStreamExtractsThinkingDelta(t *testing.T) { var got []string err := l.ChatStreamlyWithSender("qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(c *string, r *string) error { if r != nil && *r != "" { got = append(got, "R:"+*r) @@ -537,7 +543,7 @@ func TestLocalAIChatPropagatesReasoningEffort(t *testing.T) { effort := "high" _, err := l.ChatWithMessages("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, &ChatConfig{Effort: &effort}) + &APIConfig{}, &ChatConfig{Effort: &effort}, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -571,7 +577,7 @@ func TestLocalAIChatPropagatesEnableThinking(t *testing.T) { think := true _, err := l.ChatWithMessages("qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, &ChatConfig{Thinking: &think}) + &APIConfig{}, &ChatConfig{Thinking: &think}, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -606,7 +612,7 @@ func TestLocalAIStreamPropagatesReasoningParams(t *testing.T) { think := true err := l.ChatStreamlyWithSender("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, &ChatConfig{Effort: &effort, Thinking: &think}, + &APIConfig{}, &ChatConfig{Effort: &effort, Thinking: &think}, nil, func(*string, *string) error { return nil }, ) if err != nil { diff --git a/internal/entity/models/longcat.go b/internal/entity/models/longcat.go index 71612b9288..c367faabe8 100644 --- a/internal/entity/models/longcat.go +++ b/internal/entity/models/longcat.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -51,7 +52,7 @@ func (l *LongCatModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns the response. -func (l *LongCatModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (l *LongCatModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -166,7 +167,7 @@ func (l *LongCatModel) ChatWithMessages(modelName string, messages []Message, ap } // ChatStreamlyWithSender sends messages and streams the response via the -func (l *LongCatModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (l *LongCatModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -366,12 +367,12 @@ func (l *LongCatModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (l *LongCatModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (l *LongCatModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } // Rerank is not exposed by the LongCat API. -func (l *LongCatModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (l *LongCatModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } @@ -381,29 +382,29 @@ func (l *LongCatModel) Balance(apiConfig *APIConfig) (map[string]interface{}, er } // TranscribeAudio (ASR) is not exposed by the LongCat API. -func (l *LongCatModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (l *LongCatModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LongCatModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (l *LongCatModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", l.Name()) } // AudioSpeech (TTS) is not exposed by the LongCat API. -func (l *LongCatModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (l *LongCatModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LongCatModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (l *LongCatModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", l.Name()) } -func (l *LongCatModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (l *LongCatModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } // ParseFile parse file -func (l *LongCatModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (l *LongCatModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } diff --git a/internal/entity/models/longcat_test.go b/internal/entity/models/longcat_test.go index 9591d6fa93..c397f4a7ac 100644 --- a/internal/entity/models/longcat_test.go +++ b/internal/entity/models/longcat_test.go @@ -119,7 +119,7 @@ func TestLongCatChatHappyPath(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("LongCat-Flash-Chat", []Message{{Role: "user", Content: "ping"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -159,7 +159,7 @@ func TestLongCatChatExtractsReasoningContent(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("LongCat-Flash-Thinking", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -211,7 +211,9 @@ func TestLongCatChatDropsUndocumentedFields(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, // Deliberately pass Stop/Effort to prove they are filtered out. - &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}) + &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}, + nil, + ) if err != nil { t.Fatalf("Chat: %v", err) } @@ -221,7 +223,8 @@ func TestLongCatChatRequiresAPIKey(t *testing.T) { m := newLongCatForTest("http://unused") _, err := m.ChatWithMessages("LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -230,7 +233,7 @@ func TestLongCatChatRequiresAPIKey(t *testing.T) { func TestLongCatChatRequiresMessages(t *testing.T) { m := newLongCatForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("LongCat-Flash-Chat", nil, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.ChatWithMessages("LongCat-Flash-Chat", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -247,7 +250,7 @@ func TestLongCatChatRejectsHTTPError(t *testing.T) { apiKey := "test-key" _, err := m.ChatWithMessages("LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -268,7 +271,7 @@ func TestLongCatStreamHappyPath(t *testing.T) { var sawDone bool err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -309,7 +312,7 @@ func TestLongCatStreamExtractsReasoningContent(t *testing.T) { var content, reasoning []string err := m.ChatStreamlyWithSender("LongCat-Flash-Thinking", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -341,6 +344,7 @@ func TestLongCatStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("expected stream-true guard, got %v", err) @@ -352,7 +356,7 @@ func TestLongCatStreamRequiresSender(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -368,7 +372,7 @@ func TestLongCatStreamFailsWithoutTerminal(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream ended before") { t.Errorf("expected truncation error, got %v", err) @@ -389,7 +393,7 @@ func TestLongCatStreamRejectsMalformedFrame(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "invalid SSE event") { t.Errorf("expected invalid-SSE error, got %v", err) @@ -410,7 +414,7 @@ func TestLongCatStreamSurfacesUpstreamError(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "upstream stream error") { t.Errorf("expected upstream-error surfacing, got %v", err) @@ -514,7 +518,7 @@ func TestLongCatListModelsRequiresAPIKey(t *testing.T) { func TestLongCatEmbedReturnsNoSuchMethod(t *testing.T) { m := newLongCatForTest("http://unused") model := "x" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil) + _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed: want 'no such method', got %v", err) } @@ -523,7 +527,7 @@ func TestLongCatEmbedReturnsNoSuchMethod(t *testing.T) { func TestLongCatRerankReturnsNoSuchMethod(t *testing.T) { m := newLongCatForTest("http://unused") model := "x" - _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}) + _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}, nil) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: want 'no such method', got %v", err) } @@ -540,13 +544,13 @@ func TestLongCatBalanceReturnsNoSuchMethod(t *testing.T) { func TestLongCatAudioOCRReturnNoSuchMethod(t *testing.T) { m := newLongCatForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: want 'no such method', got %v", err) } - if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: want 'no such method', got %v", err) } - if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: want 'no such method', got %v", err) } } diff --git a/internal/entity/models/mineru.go b/internal/entity/models/mineru.go index 60604e67b1..bafe664e77 100644 --- a/internal/entity/models/mineru.go +++ b/internal/entity/models/mineru.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" ) type MinerUModel struct { @@ -47,39 +48,39 @@ func (m *MinerUModel) Name() string { return "mineru.net" } -func (m *MinerUModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (m *MinerUModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (m *MinerUModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (m *MinerUModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (m *MinerUModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (m *MinerUModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (m *MinerUModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (m *MinerUModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (m *MinerUModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (m *MinerUModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } @@ -104,7 +105,7 @@ type mineruTaskSubmitResponse struct { TraceID string `json:"trace_id"` } -func (m *MinerUModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (m *MinerUModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } diff --git a/internal/entity/models/mineru_local.go b/internal/entity/models/mineru_local.go index b88a7f7fc7..125fce4ebe 100644 --- a/internal/entity/models/mineru_local.go +++ b/internal/entity/models/mineru_local.go @@ -24,6 +24,7 @@ import ( "io" "mime/multipart" "net/http" + "ragflow/internal/common" ) type MinerULocalModel struct { @@ -49,39 +50,39 @@ func (m *MinerULocalModel) Name() string { return "mineru" } -func (m *MinerULocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (m *MinerULocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (m *MinerULocalModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (m *MinerULocalModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (m *MinerULocalModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (m *MinerULocalModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (m *MinerULocalModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (m *MinerULocalModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (m *MinerULocalModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (m *MinerULocalModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } @@ -97,7 +98,7 @@ func (m *MinerULocalModel) CheckConnection(apiConfig *APIConfig) error { return fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (m *MinerULocalModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go index e50227a346..20d2842f2b 100644 --- a/internal/entity/models/minimax.go +++ b/internal/entity/models/minimax.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "sort" "strings" ) @@ -60,7 +61,7 @@ func validateMinimaxModelName(modelName string) (string, error) { } // ChatWithMessages sends multiple messages with roles and returns response -func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -215,7 +216,7 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap ToolCalls: toolCalls, } if pt, ct, tt := extractUsageFromMap(result); tt > 0 { - chatResponse.Usage = &ChatUsage{ + chatResponse.Usage = &TokenUsage{ PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, } } @@ -224,7 +225,7 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -431,7 +432,7 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa } // Embed embeds a list of texts into embeddings -func (m *MinimaxModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (m *MinimaxModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } @@ -498,21 +499,21 @@ func (m *MinimaxModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (m *MinimaxModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (m *MinimaxModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", m.Name()) } // TranscribeAudio transcribe audio -func (m *MinimaxModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (m *MinimaxModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MinimaxModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (m *MinimaxModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } // AudioSpeech convert text to audio -func (m *MinimaxModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (m *MinimaxModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -602,7 +603,7 @@ func (m *MinimaxModel) AudioSpeech(modelName *string, audioContent *string, apiC }, nil } -func (m *MinimaxModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (m *MinimaxModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -697,12 +698,12 @@ func (m *MinimaxModel) AudioSpeechWithSender(modelName *string, audioContent *st } // OCRFile OCR file -func (m *MinimaxModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (m *MinimaxModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } // ParseFile parse file -func (m *MinimaxModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (m *MinimaxModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } diff --git a/internal/entity/models/minimax_test.go b/internal/entity/models/minimax_test.go index d3dd6c1b71..df24d993f0 100644 --- a/internal/entity/models/minimax_test.go +++ b/internal/entity/models/minimax_test.go @@ -117,6 +117,7 @@ func TestMinimaxChatForcesNonStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream, Thinking: &thinking}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -141,6 +142,7 @@ func TestMinimaxChatRejectsEmptyChoices(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "no choices in response") { t.Fatalf("expected choices error, got %v", err) @@ -179,6 +181,7 @@ func TestMinimaxStreamForcesStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(answer, reason *string) error { if answer != nil { content = append(content, *answer) @@ -216,6 +219,7 @@ func TestMinimaxStreamAcceptsNilConfig(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(*string, *string) error { return nil }, ) if err != nil { @@ -302,7 +306,7 @@ func TestMinimaxValidatesInputs(t *testing.T) { { name: "chat api key", run: func() error { - _, err := newMinimaxForTest("http://unused").ChatWithMessages("MiniMax-M3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err := newMinimaxForTest("http://unused").ChatWithMessages("MiniMax-M3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -310,7 +314,7 @@ func TestMinimaxValidatesInputs(t *testing.T) { { name: "chat model", run: func() error { - _, err := newMinimaxForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newMinimaxForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -318,21 +322,21 @@ func TestMinimaxValidatesInputs(t *testing.T) { { name: "stream api key", run: func() error { - return newMinimaxForTest("http://unused").ChatStreamlyWithSender("MiniMax-M3", []Message{{Role: "user", Content: "x"}}, nil, nil, send) + return newMinimaxForTest("http://unused").ChatStreamlyWithSender("MiniMax-M3", []Message{{Role: "user", Content: "x"}}, nil, nil, nil, send) }, want: "api key is required", }, { name: "stream model", run: func() error { - return newMinimaxForTest("http://unused").ChatStreamlyWithSender(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, send) + return newMinimaxForTest("http://unused").ChatStreamlyWithSender(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, send) }, want: "model name is required", }, { name: "stream sender", run: func() error { - return newMinimaxForTest("http://unused").ChatStreamlyWithSender("MiniMax-M3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + return newMinimaxForTest("http://unused").ChatStreamlyWithSender("MiniMax-M3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) }, want: "sender is required", }, diff --git a/internal/entity/models/mistral.go b/internal/entity/models/mistral.go index bc674a671d..d9197a3723 100644 --- a/internal/entity/models/mistral.go +++ b/internal/entity/models/mistral.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" "time" ) @@ -54,7 +55,7 @@ func (m *MistralModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns the response. -func (m *MistralModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (m *MistralModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -207,7 +208,7 @@ func extractMistralContent(raw interface{}) (string, string, error) { } // 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 { +func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -342,7 +343,7 @@ type mistralEmbeddingResponse struct { } // Embed turns a list of texts into embedding vectors -func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -495,30 +496,30 @@ func (m *MistralModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (m *MistralModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (m *MistralModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } // TranscribeAudio transcribe audio -func (m *MistralModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (m *MistralModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MistralModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (m *MistralModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } // AudioSpeech convert text to audio -func (m *MistralModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (m *MistralModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MistralModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (m *MistralModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } // OCRFile OCR file -func (m *MistralModel) OCRFile(modelName *string, content []byte, urls *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (m *MistralModel) OCRFile(modelName *string, content []byte, urls *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -605,7 +606,7 @@ func (m *MistralModel) OCRFile(modelName *string, content []byte, urls *string, }, nil } -func (m *MistralModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (m *MistralModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } diff --git a/internal/entity/models/mistral_test.go b/internal/entity/models/mistral_test.go index 6e9a07312b..9d15590b4c 100644 --- a/internal/entity/models/mistral_test.go +++ b/internal/entity/models/mistral_test.go @@ -89,7 +89,7 @@ func TestMistralChatHappyPath(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("mistral-large-latest", []Message{ {Role: "user", Content: "ping"}, - }, &APIConfig{ApiKey: &apiKey}, nil) + }, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -131,6 +131,7 @@ func TestMistralChatPropagatesConfig(t *testing.T) { _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -139,12 +140,12 @@ func TestMistralChatPropagatesConfig(t *testing.T) { func TestMistralChatRequiresAPIKey(t *testing.T) { m := newMistralForTest("http://unused") - _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil) + _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } emptyKey := "" - _, err = m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err = m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("empty key: expected api-key error, got %v", err) } @@ -153,7 +154,7 @@ func TestMistralChatRequiresAPIKey(t *testing.T) { func TestMistralChatRequiresMessages(t *testing.T) { m := newMistralForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("mistral-large-latest", nil, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.ChatWithMessages("mistral-large-latest", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -168,7 +169,7 @@ func TestMistralChatRejectsHTTPError(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -189,7 +190,8 @@ func TestMistralChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { emptyRegion := "" _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil) + &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil, nil, + ) if err != nil { t.Errorf("empty Region: expected fallback to default, got %v", err) } @@ -214,7 +216,7 @@ func TestMistralStreamRequiresSender(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -225,7 +227,8 @@ func TestMistralChatRejectsUnknownRegion(t *testing.T) { apiKey := "test-key" region := "eu" _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil) + &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil, nil, + ) if err == nil || !strings.Contains(err.Error(), "no base URL configured for region") { t.Errorf("expected region error, got %v", err) } @@ -261,7 +264,7 @@ func TestMistralStreamHappyPath(t *testing.T) { var sawDone int32 err := m.ChatStreamlyWithSender("mistral-large-latest", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, _ *string) error { if content == nil { return nil @@ -293,6 +296,7 @@ func TestMistralStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -313,7 +317,7 @@ func TestMistralStreamFailsWithoutTerminal(t *testing.T) { apiKey := "test-key" err := m.ChatStreamlyWithSender("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream ended before") { @@ -384,7 +388,7 @@ func TestMistralBalanceReturnsNoSuchMethod(t *testing.T) { func TestMistralRerankReturnsNoSuchMethod(t *testing.T) { m := newMistralForTest("http://unused") q := "mistral-large-latest" - _, err := m.Rerank(&q, "what is rag?", []string{"a", "b"}, &APIConfig{}, &RerankConfig{TopN: 2}) + _, err := m.Rerank(&q, "what is rag?", []string{"a", "b"}, &APIConfig{}, &RerankConfig{TopN: 2}, nil) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: expected 'no such method', got %v", err) } @@ -399,21 +403,21 @@ func TestMistralUnsupportedDefaultsReturnNoSuchMethod(t *testing.T) { call func() error }{ {"TranscribeAudio", func() error { - _, err := m.TranscribeAudio(&modelName, nil, &APIConfig{}, nil) + _, err := m.TranscribeAudio(&modelName, nil, &APIConfig{}, nil, nil) return err }}, {"TranscribeAudioWithSender", func() error { - return m.TranscribeAudioWithSender(&modelName, nil, &APIConfig{}, nil, nil) + return m.TranscribeAudioWithSender(&modelName, nil, &APIConfig{}, nil, nil, nil) }}, {"AudioSpeech", func() error { - _, err := m.AudioSpeech(&modelName, nil, &APIConfig{}, nil) + _, err := m.AudioSpeech(&modelName, nil, &APIConfig{}, nil, nil) return err }}, {"AudioSpeechWithSender", func() error { - return m.AudioSpeechWithSender(&modelName, nil, &APIConfig{}, nil, nil) + return m.AudioSpeechWithSender(&modelName, nil, &APIConfig{}, nil, nil, nil) }}, {"ParseFile", func() error { - _, err := m.ParseFile(&modelName, nil, nil, &APIConfig{}, nil) + _, err := m.ParseFile(&modelName, nil, nil, &APIConfig{}, nil, nil) return err }}, {"ListTasks", func() error { @@ -455,7 +459,7 @@ func TestMistralEmbedHappyPath(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -484,7 +488,7 @@ func TestMistralEmbedReordersByIndex(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := m.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -507,7 +511,7 @@ func TestMistralEmbedEmptyInputShortCircuits(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - vecs, err := m.Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := m.Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed([]): %v", err) } @@ -519,7 +523,7 @@ func TestMistralEmbedEmptyInputShortCircuits(t *testing.T) { func TestMistralEmbedRequiresAPIKey(t *testing.T) { m := newMistralForTest("http://unused") model := "mistral-embed" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil) + _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -528,12 +532,12 @@ func TestMistralEmbedRequiresAPIKey(t *testing.T) { func TestMistralEmbedRequiresModelName(t *testing.T) { m := newMistralForTest("http://unused") apiKey := "test-key" - _, err := m.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } empty := "" - _, err = m.Embed(&empty, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err = m.Embed(&empty, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("empty model: expected model-name error, got %v", err) } @@ -555,7 +559,7 @@ func TestMistralEmbedRejectsDuplicateIndex(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate embedding index 0") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -574,7 +578,7 @@ func TestMistralEmbedRejectsOutOfRangeIndex(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -594,7 +598,7 @@ func TestMistralEmbedRejectsMissingSlot(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding for input index 1") { t.Errorf("expected missing-embedding error for slot 1, got %v", err) } @@ -610,7 +614,7 @@ func TestMistralEmbedRejectsHTTPError(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" model := "mistral-embed" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := m.Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "Mistral embeddings API error") { t.Errorf("expected Mistral embeddings API error, got %v", err) } @@ -635,7 +639,7 @@ func TestMistralChatHandlesStringContent(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("ministral-3b-latest", []Message{{Role: "user", Content: "ping"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -679,7 +683,7 @@ func TestMistralChatExtractsReasoningFromStructuredContent(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("magistral-medium-latest", []Message{{Role: "user", Content: "When do they meet?"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -712,7 +716,7 @@ func TestMistralChatHandlesStructuredContentWithoutThinking(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("magistral-small-latest", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -746,7 +750,7 @@ func TestMistralChatIgnoresUnknownContentPartTypes(t *testing.T) { apiKey := "test-key" resp, err := m.ChatWithMessages("magistral-small-latest", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } diff --git a/internal/entity/models/modelscope.go b/internal/entity/models/modelscope.go index 491d1b88ed..83cd9237c1 100644 --- a/internal/entity/models/modelscope.go +++ b/internal/entity/models/modelscope.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" "sync" "time" @@ -140,7 +141,7 @@ func buildModelScopeChatBody(modelName string, messages []Message, stream bool, } // ChatWithMessages sends multiple messages with roles and returns the response. -func (m *ModelScopeModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (m *ModelScopeModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -210,7 +211,7 @@ func (m *ModelScopeModel) ChatWithMessages(modelName string, messages []Message, } // ChatStreamlyWithSender sends messages and streams response via sender. -func (m *ModelScopeModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (m *ModelScopeModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -330,35 +331,35 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(modelName string, messages []Me return sender(&endOfStream, nil) } -func (m *ModelScopeModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (m *ModelScopeModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (m *ModelScopeModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (m *ModelScopeModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (m *ModelScopeModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, asrConfig *TTSConfig) (*TTSResponse, error) { +func (m *ModelScopeModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, asrConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (m *ModelScopeModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (m *ModelScopeModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (m *ModelScopeModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } diff --git a/internal/entity/models/modelscope_test.go b/internal/entity/models/modelscope_test.go index d7d597cef7..1be310552d 100644 --- a/internal/entity/models/modelscope_test.go +++ b/internal/entity/models/modelscope_test.go @@ -123,7 +123,7 @@ func TestModelScopeChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) resp, err := m.ChatWithMessages("Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "ping"}}, &APIConfig{}, - &ChatConfig{MaxTokens: &maxTokens, Temperature: &temp}) + &ChatConfig{MaxTokens: &maxTokens, Temperature: &temp}, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -157,7 +157,7 @@ func TestModelScopeChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { key := "ms-test" _, err := m.ChatWithMessages("Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil) + &APIConfig{ApiKey: &key}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -175,7 +175,7 @@ func TestModelScopeChatExtractsReasoningFields(t *testing.T) { m := newModelScopeForTest(srv.URL) resp, err := m.ChatWithMessages("Qwen/Qwen3-8B", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -220,7 +220,7 @@ func TestModelScopeStreamHappyPath(t *testing.T) { var sawDone bool err := m.ChatStreamlyWithSender("Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(c *string, r *string) error { if r != nil && *r != "" { reasoning = append(reasoning, *r) @@ -254,6 +254,7 @@ func TestModelScopeStreamRejectsFalseStreamConfig(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("expected stream-must-be-true error, got %v", err) @@ -282,7 +283,7 @@ func TestModelScopeStreamCancelsOnIdle(t *testing.T) { m := newModelScopeForTest(srv.URL) err := m.ChatStreamlyWithSender("Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream idle") { t.Errorf("expected stream-idle error, got %v", err) @@ -320,7 +321,7 @@ func TestModelScopeMissingBaseURLFailsClearly(t *testing.T) { m := NewModelScopeModel(map[string]string{}, URLSuffix{Chat: "v1/chat/completions"}) _, err := m.ChatWithMessages("Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "base URL") { t.Errorf("expected missing-base-URL error, got %v", err) } @@ -330,28 +331,28 @@ func TestModelScopeUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { m := newModelScopeForTest("http://unused") model := "Qwen/Qwen2.5-7B-Instruct" - if _, err := m.Embed(&model, []string{"x"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed: expected no such method, got %v", err) } - if _, err := m.Rerank(&model, "q", []string{"d"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(&model, "q", []string{"d"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: expected no such method, got %v", err) } if _, err := m.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: expected no such method, got %v", err) } - if _, err := m.TranscribeAudio(&model, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: expected no such method, got %v", err) } - if err := m.TranscribeAudioWithSender(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.TranscribeAudioWithSender(&model, nil, &APIConfig{}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudioWithSender: expected no such method, got %v", err) } - if _, err := m.AudioSpeech(&model, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: expected no such method, got %v", err) } - if err := m.AudioSpeechWithSender(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.AudioSpeechWithSender(&model, nil, &APIConfig{}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeechWithSender: expected no such method, got %v", err) } - if _, err := m.OCRFile(&model, nil, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: expected no such method, got %v", err) } } diff --git a/internal/entity/models/moonshot.go b/internal/entity/models/moonshot.go index df848aed9e..84567d1812 100644 --- a/internal/entity/models/moonshot.go +++ b/internal/entity/models/moonshot.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "sort" "strings" ) @@ -58,7 +59,7 @@ func validateMoonshotModelName(modelName string) (string, error) { return strings.TrimSpace(modelName), nil } -func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -219,7 +220,7 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a ToolCalls: toolCalls, } if pt, ct, tt := extractUsageFromMap(result); tt > 0 { - chatResponse.Usage = &ChatUsage{ + chatResponse.Usage = &TokenUsage{ PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, } } @@ -228,7 +229,7 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -446,7 +447,7 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess } // Embed embeds a list of texts into embeddings -func (m *MoonshotModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (m *MoonshotModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } @@ -570,35 +571,35 @@ func (m *MoonshotModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (m *MoonshotModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (m *MoonshotModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", m.Name()) } // TranscribeAudio transcribe audio -func (m *MoonshotModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (m *MoonshotModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MoonshotModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (m *MoonshotModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } // AudioSpeech convert text to audio -func (m *MoonshotModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (m *MoonshotModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MoonshotModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (m *MoonshotModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", m.Name()) } // OCRFile OCR file -func (m *MoonshotModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (m *MoonshotModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } // ParseFile parse file -func (m *MoonshotModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (m *MoonshotModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } diff --git a/internal/entity/models/moonshot_test.go b/internal/entity/models/moonshot_test.go index 07e8f2f06a..cf48d520ca 100644 --- a/internal/entity/models/moonshot_test.go +++ b/internal/entity/models/moonshot_test.go @@ -116,7 +116,7 @@ func TestMoonshotChatForcesNonStreaming(t *testing.T) { " kimi-k2.6 ", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{Stream: &stream, Thinking: &thinking}, + &ChatConfig{Stream: &stream, Thinking: &thinking}, nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -167,7 +167,7 @@ func TestMoonshotChatSupportsTools(t *testing.T) { "kimi-k2.6", []Message{{Role: "user", Content: "北京今天天气怎么样?"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{Tools: tools}, + &ChatConfig{Tools: tools}, nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -213,6 +213,7 @@ func TestMoonshotStreamForcesStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(answer, reason *string) error { if answer != nil { if *answer == "[DONE]" { @@ -258,6 +259,7 @@ func TestMoonshotStreamDoesNotSendDoneAfterScannerError(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(answer, _ *string) error { if answer != nil && *answer == "[DONE]" { sawDone = true @@ -397,7 +399,7 @@ func TestMoonshotValidatesInputs(t *testing.T) { { name: "chat api key", run: func() error { - _, err := newMoonshotForTest("http://unused").ChatWithMessages("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil) + _, err := newMoonshotForTest("http://unused").ChatWithMessages("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -405,7 +407,7 @@ func TestMoonshotValidatesInputs(t *testing.T) { { name: "chat model", run: func() error { - _, err := newMoonshotForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newMoonshotForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -413,21 +415,21 @@ func TestMoonshotValidatesInputs(t *testing.T) { { name: "stream api key", run: func() error { - return newMoonshotForTest("http://unused").ChatStreamlyWithSender("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, nil, nil, send) + return newMoonshotForTest("http://unused").ChatStreamlyWithSender("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, nil, nil, nil, send) }, want: "api key is required", }, { name: "stream model", run: func() error { - return newMoonshotForTest("http://unused").ChatStreamlyWithSender(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, send) + return newMoonshotForTest("http://unused").ChatStreamlyWithSender(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, send) }, want: "model name is required", }, { name: "stream sender", run: func() error { - return newMoonshotForTest("http://unused").ChatStreamlyWithSender("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + return newMoonshotForTest("http://unused").ChatStreamlyWithSender("kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) }, want: "sender is required", }, diff --git a/internal/entity/models/n1n.go b/internal/entity/models/n1n.go index 9ea47eb3df..a9bac61596 100644 --- a/internal/entity/models/n1n.go +++ b/internal/entity/models/n1n.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -158,7 +159,7 @@ type n1nChatResponse struct { // ChatWithMessages sends a single, non-streaming chat completion // against n1n.ai's /v1/chat/completions endpoint. -func (n *N1NModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (n *N1NModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -223,7 +224,7 @@ func (n *N1NModel) ChatWithMessages(modelName string, messages []Message, apiCon } // ChatStreamlyWithSender sends a streaming chat completion. -func (n *N1NModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (n *N1NModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -321,7 +322,7 @@ type n1nEmbeddingRequest struct { } // Embed turns a list of texts into embedding vectors -func (n *N1NModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (n *N1NModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -415,7 +416,7 @@ type n1nRerankRequest struct { // Rerank scores a query against a list of documents using // n1n.ai's /v1/rerank endpoint (Cohere-shaped response). -func (n *N1NModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (n *N1NModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -548,30 +549,30 @@ func (n *N1NModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) } // TranscribeAudio: n1n.ai exposes /v1/audio/transcriptions -func (n *N1NModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (n *N1NModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *N1NModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (n *N1NModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", n.Name()) } // AudioSpeech: n1n.ai exposes /v1/audio/speech -func (n *N1NModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (n *N1NModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *N1NModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (n *N1NModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", n.Name()) } // OCRFile is not exposed by the n1n.ai API. -func (n *N1NModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (n *N1NModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } // ParseFile is not exposed by the n1n.ai API. -func (n *N1NModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (n *N1NModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } diff --git a/internal/entity/models/novita.go b/internal/entity/models/novita.go index be19bd1ca9..3617924121 100644 --- a/internal/entity/models/novita.go +++ b/internal/entity/models/novita.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strconv" "strings" ) @@ -187,7 +188,7 @@ func (e *novitaThinkExtractor) Flush() *novitaThinkSegment { } // ChatWithMessages sends multiple messages with roles and returns the response. -func (n *NovitaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (n *NovitaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -331,7 +332,7 @@ func (n *NovitaModel) ChatWithMessages(modelName string, messages []Message, api // inline-style models): a stateful extractor splits tag bytes // across SSE chunk boundaries, then routes content/reasoning to // the first/second sender arg respectively. -func (n *NovitaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (n *NovitaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -561,7 +562,7 @@ type novitaEmbeddingResponse struct { // Embed turns a list of texts into embedding vectors using the Novita // /v3/embeddings endpoint. The output has one vector per input, in the // same order the inputs were given. -func (n *NovitaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (n *NovitaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -665,7 +666,7 @@ type novitaRerankResponse struct { // /openai/v1/rerank endpoint and returns one RerankResult per scored // document in the API's ranking order. Caller may sort by Index to // recover original input order. -func (n *NovitaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (n *NovitaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -825,29 +826,29 @@ func (n *NovitaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, err return response, nil } -func (n *NovitaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (n *NovitaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *NovitaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (n *NovitaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", n.Name()) } -func (n *NovitaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (n *NovitaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *NovitaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (n *NovitaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", n.Name()) } // OCRFile OCR file -func (n *NovitaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (n *NovitaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } // ParseFile parse file -func (n *NovitaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (n *NovitaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } diff --git a/internal/entity/models/novita_test.go b/internal/entity/models/novita_test.go index 81ad829f7e..e0ad544d86 100644 --- a/internal/entity/models/novita_test.go +++ b/internal/entity/models/novita_test.go @@ -268,7 +268,7 @@ func TestNovitaChatPureText(t *testing.T) { resp, err := newNovitaForTest(srv.URL).ChatWithMessages( "meta-llama/llama-3.3-70b-instruct", []Message{{Role: "user", Content: "ping"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -296,7 +296,7 @@ func TestNovitaChatExtractsThinkTags(t *testing.T) { resp, err := newNovitaForTest(srv.URL).ChatWithMessages( "qwen/qwen3-30b-a3b-fp8", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -331,7 +331,7 @@ func TestNovitaChatExtractsReasoningContentField(t *testing.T) { resp, err := newNovitaForTest(srv.URL).ChatWithMessages( "deepseek/deepseek-v3.1", []Message{{Role: "user", Content: "2+2?"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -363,7 +363,7 @@ func TestNovitaStreamExtractsDeltaReasoningContent(t *testing.T) { err := newNovitaForTest(srv.URL).ChatStreamlyWithSender( "deepseek/deepseek-v3.1", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -424,7 +424,7 @@ func TestNovitaChatPropagatesEnableThinking(t *testing.T) { "qwen/qwen3-30b-a3b-fp8", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{Thinking: &thinking}) + &ChatConfig{Thinking: &thinking}, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -452,7 +452,7 @@ func TestNovitaChatOmitsEnableThinkingWhenUnset(t *testing.T) { _, err := newNovitaForTest(srv.URL).ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{}) // no Thinking + &ChatConfig{}, nil) // no Thinking if err != nil { t.Fatalf("Chat: %v", err) } @@ -481,6 +481,7 @@ func TestNovitaStreamPropagatesEnableThinking(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Thinking: &thinking}, + nil, func(*string, *string) error { return nil }) if err != nil { t.Fatalf("Stream: %v", err) @@ -492,7 +493,7 @@ func TestNovitaStreamPropagatesEnableThinking(t *testing.T) { func TestNovitaChatRequiresAPIKey(t *testing.T) { _, err := newNovitaForTest("http://unused").ChatWithMessages("m", - []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil) + []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("got %v", err) } @@ -501,7 +502,7 @@ func TestNovitaChatRequiresAPIKey(t *testing.T) { func TestNovitaChatRequiresMessages(t *testing.T) { apiKey := "test-key" _, err := newNovitaForTest("http://unused").ChatWithMessages("m", nil, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("got %v", err) } @@ -517,7 +518,7 @@ func TestNovitaChatRejectsHTTPError(t *testing.T) { apiKey := "test-key" _, err := newNovitaForTest(srv.URL).ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("got %v", err) } @@ -547,7 +548,7 @@ func TestNovitaStreamSplitsThinkTags(t *testing.T) { err := newNovitaForTest(srv.URL).ChatStreamlyWithSender( "qwen/qwen3-30b-a3b-fp8", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -589,7 +590,7 @@ func TestNovitaStreamPureContent(t *testing.T) { var sawDone bool err := newNovitaForTest(srv.URL).ChatStreamlyWithSender("meta-llama/llama-3.3-70b-instruct", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -616,7 +617,7 @@ func TestNovitaStreamRequiresSender(t *testing.T) { apiKey := "test-key" err := newNovitaForTest("http://unused").ChatStreamlyWithSender("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("got %v", err) } @@ -629,6 +630,7 @@ func TestNovitaStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("got %v", err) @@ -690,7 +692,7 @@ func TestNovitaRerankHappyPathReordersByIndex(t *testing.T) { apiKey := "test-key" model := "baai/bge-reranker-v2-m3" - resp, err := newNovitaForTest(srv.URL).Rerank(&model, "what is rag", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := newNovitaForTest(srv.URL).Rerank(&model, "what is rag", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -716,7 +718,7 @@ func TestNovitaRerankRespectsTopNConfig(t *testing.T) { apiKey := "test-key" model := "baai/bge-reranker-v2-m3" - if _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}); err != nil { + if _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, nil); err != nil { t.Fatalf("Rerank: %v", err) } } @@ -724,7 +726,7 @@ func TestNovitaRerankRespectsTopNConfig(t *testing.T) { func TestNovitaRerankEmptyDocumentsShortCircuits(t *testing.T) { apiKey := "test-key" model := "x" - resp, err := newNovitaForTest("http://unused").Rerank(&model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := newNovitaForTest("http://unused").Rerank(&model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("expected nil error for empty docs, got %v", err) } @@ -735,7 +737,7 @@ func TestNovitaRerankEmptyDocumentsShortCircuits(t *testing.T) { func TestNovitaRerankRequiresApiKey(t *testing.T) { model := "x" - _, err := newNovitaForTest("http://unused").Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil) + _, err := newNovitaForTest("http://unused").Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("got %v", err) } @@ -743,7 +745,7 @@ func TestNovitaRerankRequiresApiKey(t *testing.T) { func TestNovitaRerankRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newNovitaForTest("http://unused").Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newNovitaForTest("http://unused").Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("got %v", err) } @@ -757,7 +759,7 @@ func TestNovitaRerankRejectsOutOfRangeIndex(t *testing.T) { apiKey := "test-key" model := "x" - _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("got %v", err) } @@ -771,7 +773,7 @@ func TestNovitaRerankRejectsDuplicateIndex(t *testing.T) { apiKey := "test-key" model := "x" - _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate") { t.Errorf("got %v", err) } @@ -786,7 +788,7 @@ func TestNovitaRerankSurfacesHTTPError(t *testing.T) { apiKey := "test-key" model := "x" - _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "Novita rerank API error") { t.Errorf("got %v", err) } @@ -799,7 +801,7 @@ func TestNovitaRerankRejectsMissingRerankSuffix(t *testing.T) { map[string]string{"default": "http://unused"}, URLSuffix{Chat: "openai/v1/chat/completions"}, ) - _, err := driver.Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := driver.Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no rerank URL suffix configured") { t.Errorf("got %v", err) } @@ -819,13 +821,13 @@ func TestNovitaAudioOCRReturnNoSuchMethod(t *testing.T) { m := "x" apiKey := "test-key" v := newNovitaForTest("http://unused") - if _, err := v.TranscribeAudio(&m, &m, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := v.TranscribeAudio(&m, &m, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: %v", err) } - if _, err := v.AudioSpeech(&m, &m, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := v.AudioSpeech(&m, &m, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: %v", err) } - if _, err := v.OCRFile(&m, nil, &m, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := v.OCRFile(&m, nil, &m, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } @@ -854,7 +856,7 @@ func TestNovitaBaseURLTrimsTrailingSlash(t *testing.T) { invoke: func(n *NovitaModel, apiKey string) error { _, err := n.ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, respBody: `{"choices":[{"message":{"content":"ok"}}]}`, @@ -878,7 +880,7 @@ func TestNovitaBaseURLTrimsTrailingSlash(t *testing.T) { invoke: func(n *NovitaModel, apiKey string) error { return n.ChatStreamlyWithSender("m", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) }, respBody: `data: {"choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":"stop"}]}` + "\n" + diff --git a/internal/entity/models/nvidia.go b/internal/entity/models/nvidia.go index 90752fd781..f699924cc6 100644 --- a/internal/entity/models/nvidia.go +++ b/internal/entity/models/nvidia.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -50,7 +51,7 @@ func (n NvidiaModel) Name() string { return "nvidia" } -func (n *NvidiaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (n *NvidiaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -183,7 +184,7 @@ func (n *NvidiaModel) ChatWithMessages(modelName string, messages []Message, api return chatResponse, nil } -func (n *NvidiaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (n *NvidiaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -323,7 +324,7 @@ type nvidiaEmbeddingResponse struct { } `json:"data"` } -func (n NvidiaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (n NvidiaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -440,7 +441,7 @@ type nvidiaRerankResponse struct { // RerankResult entries are in the API's ranking order; callers that // need original-input order should sort by Index. Same return-shape // contract as the Aliyun and ZhipuAI Rerank drivers. -func (n NvidiaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (n NvidiaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -532,30 +533,30 @@ func (n NvidiaModel) Rerank(modelName *string, query string, documents []string, } // TranscribeAudio transcribe audio -func (n *NvidiaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (n *NvidiaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *NvidiaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (n *NvidiaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", n.Name()) } // AudioSpeech convert text to audio -func (n *NvidiaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (n *NvidiaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *NvidiaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (n *NvidiaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", n.Name()) } // OCRFile OCR file -func (n *NvidiaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (n *NvidiaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } // ParseFile parse file -func (n *NvidiaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (n *NvidiaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } diff --git a/internal/entity/models/nvidia_rerank_test.go b/internal/entity/models/nvidia_rerank_test.go index c92249bfbb..10edee00dc 100644 --- a/internal/entity/models/nvidia_rerank_test.go +++ b/internal/entity/models/nvidia_rerank_test.go @@ -91,6 +91,7 @@ func TestNvidiaRerankHappyPath(t *testing.T) { []string{"doc-zero", "doc-one", "doc-two"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, + nil, ) if err != nil { t.Fatalf("Rerank failed: %v", err) @@ -123,6 +124,7 @@ func TestNvidiaRerankTopNClamp(t *testing.T) { []string{"a", "b", "c", "d"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, + nil, ); err != nil { t.Fatalf("Rerank failed: %v", err) } @@ -132,7 +134,7 @@ func TestNvidiaRerankEmptyDocuments(t *testing.T) { model := newNvidiaModelForTest("http://unused") apiKey := "test-key" modelName := "nvidia/nv-rerankqa-mistral-4b-v3" - resp, err := model.Rerank(&modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + resp, err := model.Rerank(&modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err != nil { t.Fatalf("expected nil error for empty documents, got %v", err) } @@ -144,7 +146,7 @@ func TestNvidiaRerankEmptyDocuments(t *testing.T) { func TestNvidiaRerankRequiresAPIKey(t *testing.T) { model := newNvidiaModelForTest("http://unused") modelName := "nvidia/nv-rerankqa-mistral-4b-v3" - _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{}, &RerankConfig{}) + _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -153,7 +155,7 @@ func TestNvidiaRerankRequiresAPIKey(t *testing.T) { func TestNvidiaRerankRequiresModelName(t *testing.T) { model := newNvidiaModelForTest("http://unused") apiKey := "test-key" - _, err := model.Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + _, err := model.Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -169,7 +171,7 @@ func TestNvidiaRerankRejectsHTTPError(t *testing.T) { model := newNvidiaModelForTest(srv.URL) apiKey := "test-key" modelName := "nvidia/nv-rerankqa-mistral-4b-v3" - _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "Nvidia rerank API error") { t.Errorf("expected API error, got %v", err) } @@ -188,7 +190,7 @@ func TestNvidiaRerankRejectsOutOfRangeIndex(t *testing.T) { model := newNvidiaModelForTest(srv.URL) apiKey := "test-key" modelName := "nvidia/nv-rerankqa-mistral-4b-v3" - _, err := model.Rerank(&modelName, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + _, err := model.Rerank(&modelName, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "unexpected rerank index") { t.Errorf("expected out-of-range error, got %v", err) } diff --git a/internal/entity/models/ollama.go b/internal/entity/models/ollama.go index 734562752d..0c0878ae32 100644 --- a/internal/entity/models/ollama.go +++ b/internal/entity/models/ollama.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -72,7 +73,7 @@ func (o *OllamaModel) Name() string { return "Ollama" } -func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if len(messages) == 0 { return nil, fmt.Errorf("message is nil") } @@ -194,7 +195,7 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api return chatResponse, nil } -func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if len(messages) == 0 { return fmt.Errorf("messages is empty") } @@ -329,7 +330,7 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag return scanner.Err() } -func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -426,35 +427,35 @@ func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APICon return embeddings, nil } -func (o *OllamaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (o *OllamaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (o *OllamaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (o *OllamaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } -func (o *OllamaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (o *OllamaModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", o.Name()) } // AudioSpeech convert text to audio -func (o *OllamaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (o *OllamaModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } -func (o *OllamaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (o *OllamaModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", o.Name()) } // OCRFile OCR file -func (o *OllamaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (o *OllamaModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } // ParseFile parse file -func (o *OllamaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (o *OllamaModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } diff --git a/internal/entity/models/openai.go b/internal/entity/models/openai.go index c8d2e5f37d..46b52df406 100644 --- a/internal/entity/models/openai.go +++ b/internal/entity/models/openai.go @@ -28,6 +28,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -57,7 +58,7 @@ func (o *OpenAIModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns the response -func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -216,7 +217,7 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api // providers that implement the OpenAI-compat API surface (DeepSeek, // Moonshot, etc.) also return a "usage" key with the same shape. if pt, ct, tt := extractUsageFromMap(result); tt > 0 { - chatResponse.Usage = &ChatUsage{ + chatResponse.Usage = &TokenUsage{ PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt, } } @@ -225,7 +226,7 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api } // ChatStreamlyWithSender sends messages and streams the response -func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -337,7 +338,7 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag // The last chunk in the stream carries the "usage" key alongside // empty choices; we overwrite on every chunk so the final frame // wins, matching Python's chat_model.py usage_from_response loop. - var streamUsage *ChatUsage + var streamUsage *TokenUsage scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() @@ -366,7 +367,7 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag // is true, the final chunk carries the full usage breakdown at the // top level of the event alongside (possibly empty) choices. if pt, ct, tt := extractUsageFromMap(event); tt > 0 { - streamUsage = &ChatUsage{PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt} + streamUsage = &TokenUsage{PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt} } choices, ok := event["choices"].([]interface{}) @@ -482,7 +483,7 @@ type openaiUsage struct { TotalTokens int `json:"total_tokens"` } -func (o *OpenAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (o *OpenAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -624,12 +625,12 @@ func (o *OpenAIModel) CheckConnection(apiConfig *APIConfig) error { // Rerank calculates similarity scores between query and documents. OpenAI does // not expose a rerank API, so this is left unimplemented. -func (o *OpenAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (o *OpenAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", o.Name()) } // TranscribeAudio transcribe audio -func (o *OpenAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (o *OpenAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() @@ -658,7 +659,7 @@ func (o *OpenAIModel) TranscribeAudio(modelName *string, file *string, apiConfig return decodeOpenAIASRResponse(respBody, responseFormat) } -func (o *OpenAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (o *OpenAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") } @@ -750,7 +751,7 @@ func decodeOpenAIASRResponse(respBody []byte, responseFormat string) (*ASRRespon } // AudioSpeech convert text to audio -func (o *OpenAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (o *OpenAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() @@ -777,7 +778,7 @@ func (o *OpenAIModel) AudioSpeech(modelName *string, audioContent *string, apiCo return &TTSResponse{Audio: body}, nil } -func (o *OpenAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (o *OpenAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") } @@ -1041,12 +1042,12 @@ func writeOpenAIMultipartField(writer *multipart.Writer, key string, value inter } // OCRFile OCR file -func (o *OpenAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (o *OpenAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } // ParseFile parse file -func (o *OpenAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (o *OpenAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } diff --git a/internal/entity/models/openai_test.go b/internal/entity/models/openai_test.go index 8483c4d94c..b7c0ceb4a3 100644 --- a/internal/entity/models/openai_test.go +++ b/internal/entity/models/openai_test.go @@ -133,6 +133,7 @@ func TestOpenAITranscribeAudioPostsMultipartToAudioEndpoint(t *testing.T) { "language": "en", "temperature": 0.2, }}, + nil, ) if err != nil { t.Fatalf("TranscribeAudio: %v", err) @@ -192,6 +193,7 @@ func TestOpenAITranscribeAudioWithSenderStreamsDeltas(t *testing.T) { &audioPath, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(content, _ *string) error { if content != nil { chunks = append(chunks, *content) @@ -262,6 +264,7 @@ func TestOpenAIAudioSpeechPostsJSONToAudioEndpoint(t *testing.T) { "speed": 1.25, }, }, + nil, ) if err != nil { t.Fatalf("AudioSpeech: %v", err) @@ -281,6 +284,7 @@ func TestOpenAIAudioSpeechRequiresVoice(t *testing.T) { &input, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "voice is required") { t.Fatalf("err=%v, want voice is required", err) @@ -297,6 +301,7 @@ func TestOpenAIAudioSpeechRejectsNonStringVoice(t *testing.T) { &input, &APIConfig{ApiKey: &apiKey}, &TTSConfig{Params: map[string]interface{}{"voice": 123}}, + nil, ) if err == nil || !strings.Contains(err.Error(), "voice is required") { t.Fatalf("err=%v, want voice is required", err) @@ -349,6 +354,7 @@ func TestOpenAIAudioSpeechWithSenderStreamsRawAudio(t *testing.T) { &input, &APIConfig{ApiKey: &apiKey}, &TTSConfig{Params: map[string]interface{}{"voice": "alloy"}}, + nil, func(content, _ *string) error { if content != nil { chunks = append(chunks, *content) @@ -398,6 +404,7 @@ func TestOpenAIAudioSpeechWithSenderStreamsSSEAudioDeltas(t *testing.T) { "voice": "alloy", "stream_format": "sse", }}, + nil, func(content, _ *string) error { if content != nil { chunks = append(chunks, *content) diff --git a/internal/entity/models/openrouter.go b/internal/entity/models/openrouter.go index c7192450b3..638aa6cd26 100644 --- a/internal/entity/models/openrouter.go +++ b/internal/entity/models/openrouter.go @@ -54,7 +54,7 @@ func (o *OpenRouterModel) Name() string { return "openrouter" } -func (o *OpenRouterModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (o *OpenRouterModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -189,7 +189,7 @@ func (o *OpenRouterModel) ChatWithMessages(modelName string, messages []Message, return chatResponse, nil } -func (o *OpenRouterModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (o *OpenRouterModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -353,7 +353,7 @@ type openrouterUsage struct { TotalTokens int `json:"total_tokens"` } -func (o *OpenRouterModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (o *OpenRouterModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -448,7 +448,7 @@ type OpenRouterRerankResponse struct { } `json:"results"` } -func (o *OpenRouterModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (o *OpenRouterModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -543,7 +543,7 @@ func openRouterAudioFormat(file string, asrConfig *ASRConfig) string { } // TranscribeAudio transcribe audio -func (o *OpenRouterModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (o *OpenRouterModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -625,12 +625,12 @@ func (o *OpenRouterModel) TranscribeAudio(modelName *string, file *string, apiCo return &ASRResponse{Text: result.Text}, nil } -func (o *OpenRouterModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (o *OpenRouterModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", o.Name()) } // AudioSpeech convert text to audio -func (o *OpenRouterModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (o *OpenRouterModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -693,17 +693,17 @@ func (o *OpenRouterModel) AudioSpeech(modelName *string, audioContent *string, a return &TTSResponse{Audio: body}, nil } -func (o *OpenRouterModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (o *OpenRouterModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", o.Name()) } // OCRFile OCR file -func (o *OpenRouterModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (o *OpenRouterModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } // ParseFile parse file -func (o *OpenRouterModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (o *OpenRouterModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } diff --git a/internal/entity/models/openrouter_test.go b/internal/entity/models/openrouter_test.go index 598195a1d4..3aef619eb7 100644 --- a/internal/entity/models/openrouter_test.go +++ b/internal/entity/models/openrouter_test.go @@ -130,6 +130,7 @@ func TestOpenRouterTranscribeAudioHappyPath(t *testing.T) { "model": "wrong-model", "input_audio": map[string]interface{}{"data": "bad-data", "format": "bad"}, }}, + nil, ) if err != nil { t.Fatalf("TranscribeAudio: %v", err) @@ -156,7 +157,7 @@ func TestOpenRouterTranscribeAudioInfersFormat(t *testing.T) { apiKey := "test-key" modelName := "openai/whisper-large-v3" file := writeOpenRouterAudioFile(t, "sample.mp3", []byte("audio")) - _, err := newOpenRouterForTest(srv.URL).TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newOpenRouterForTest(srv.URL).TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("TranscribeAudio: %v", err) } @@ -181,7 +182,7 @@ func TestOpenRouterTranscribeAudioValidatesInputs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := newOpenRouterForTest("http://unused").TranscribeAudio(tt.modelName, tt.file, tt.apiConfig, nil) + _, err := newOpenRouterForTest("http://unused").TranscribeAudio(tt.modelName, tt.file, tt.apiConfig, nil, nil) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("err=%v, want %q", err, tt.want) } @@ -195,7 +196,7 @@ func TestOpenRouterTranscribeAudioValidatesASRSuffix(t *testing.T) { file := "sample.wav" model := NewOpenRouterModel(map[string]string{"default": "http://unused"}, URLSuffix{}) - _, err := model.TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil) + _, err := model.TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "OpenRouter ASR url suffix is missing") { t.Fatalf("err=%v", err) } @@ -210,7 +211,7 @@ func TestOpenRouterTranscribeAudioHTTPError(t *testing.T) { apiKey := "test-key" modelName := "openai/whisper-large-v3" file := writeOpenRouterAudioFile(t, "sample.wav", []byte("audio")) - _, err := newOpenRouterForTest(srv.URL).TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newOpenRouterForTest(srv.URL).TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "OpenRouter ASR API error") || !strings.Contains(err.Error(), "401 Unauthorized") || @@ -245,6 +246,7 @@ func TestOpenRouterChatStreamlyRequest(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Thinking: &thinking}, + nil, func(content, reason *string) error { if reason != nil { reasoning.WriteString(*reason) diff --git a/internal/entity/models/orcarouter.go b/internal/entity/models/orcarouter.go index 84e68aaf6a..27d2d185f1 100644 --- a/internal/entity/models/orcarouter.go +++ b/internal/entity/models/orcarouter.go @@ -48,7 +48,7 @@ func (o *OrcaRouterModel) Name() string { return "orcarouter" } -func (o *OrcaRouterModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (o *OrcaRouterModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -168,7 +168,7 @@ func (o *OrcaRouterModel) ChatWithMessages(modelName string, messages []Message, return chatResponse, nil } -func (o *OrcaRouterModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (o *OrcaRouterModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -296,23 +296,23 @@ func (o *OrcaRouterModel) ChatStreamlyWithSender(modelName string, messages []Me return nil } -func (o *OrcaRouterModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (o *OrcaRouterModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (o *OrcaRouterModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (o *OrcaRouterModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (o *OrcaRouterModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (o *OrcaRouterModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -376,15 +376,15 @@ func (o *OrcaRouterModel) AudioSpeech(modelName *string, audioContent *string, a return &TTSResponse{Audio: body}, nil } -func (o *OrcaRouterModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (o *OrcaRouterModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (o *OrcaRouterModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (o *OrcaRouterModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } diff --git a/internal/entity/models/paddleocr.go b/internal/entity/models/paddleocr.go index 6a9b147e73..51a11cf39e 100644 --- a/internal/entity/models/paddleocr.go +++ b/internal/entity/models/paddleocr.go @@ -25,6 +25,7 @@ import ( "io" "mime/multipart" "net/http" + "ragflow/internal/common" "strings" "time" ) @@ -52,35 +53,35 @@ func (p *PaddleOCRModel) Name() string { return "paddle_ocr.net" } -func (p *PaddleOCRModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (p *PaddleOCRModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (p *PaddleOCRModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (p *PaddleOCRModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (p *PaddleOCRModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (p *PaddleOCRModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (p *PaddleOCRModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (p *PaddleOCRModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (p *PaddleOCRModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } @@ -115,7 +116,7 @@ type paddleJsonlLine struct { } `json:"result"` } -func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -318,7 +319,7 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str return &OCRFileResponse{Text: &extractedText}, nil } -func (p *PaddleOCRModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (p *PaddleOCRModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } diff --git a/internal/entity/models/paddleocr_local.go b/internal/entity/models/paddleocr_local.go index a457a2e0e9..8e78554677 100644 --- a/internal/entity/models/paddleocr_local.go +++ b/internal/entity/models/paddleocr_local.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -49,35 +50,35 @@ func (p *PaddleOCRLocalModel) Name() string { return "paddleocr" } -func (p *PaddleOCRLocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (p *PaddleOCRLocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (p *PaddleOCRLocalModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (p *PaddleOCRLocalModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (p *PaddleOCRLocalModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (p *PaddleOCRLocalModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (p *PaddleOCRLocalModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (p *PaddleOCRLocalModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (p *PaddleOCRLocalModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", p.Name()) } @@ -97,7 +98,7 @@ type paddleLocalOCRResponse struct { } `json:"result"` } -func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if len(content) == 0 { return nil, fmt.Errorf("local PaddleOCR requires file content, but content is empty") } @@ -184,7 +185,7 @@ func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL }, nil } -func (p *PaddleOCRLocalModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (p *PaddleOCRLocalModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } diff --git a/internal/entity/models/perplexity.go b/internal/entity/models/perplexity.go index 03205e2cf9..40d8c10e5d 100644 --- a/internal/entity/models/perplexity.go +++ b/internal/entity/models/perplexity.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -113,7 +114,7 @@ type perplexityChatResponse struct { FinishReason string `json:"finish_reason"` } -func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -180,7 +181,7 @@ func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, }, nil } -func (p *PerplexityModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (p *PerplexityModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -349,7 +350,7 @@ type perplexityEmbeddingResponse struct { Error interface{} `json:"error"` } -func (p *PerplexityModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (p *PerplexityModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -423,7 +424,7 @@ func (p *PerplexityModel) Embed(modelName *string, texts []string, apiConfig *AP return embeddings, nil } -func (p *PerplexityModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (p *PerplexityModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } @@ -431,27 +432,27 @@ func (p *PerplexityModel) Balance(apiConfig *APIConfig) (map[string]interface{}, return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (p *PerplexityModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (p *PerplexityModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (p *PerplexityModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (p *PerplexityModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (p *PerplexityModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (p *PerplexityModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } diff --git a/internal/entity/models/perplexity_test.go b/internal/entity/models/perplexity_test.go index a219c9770c..81030a4624 100644 --- a/internal/entity/models/perplexity_test.go +++ b/internal/entity/models/perplexity_test.go @@ -95,6 +95,7 @@ func TestPerplexityChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -132,6 +133,7 @@ func TestPerplexityChatSkipsReasoningEffortForNonReasoningModel(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Effort: &effort}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -143,14 +145,14 @@ func TestPerplexityChatSkipsReasoningEffortForNonReasoningModel(t *testing.T) { func TestPerplexityChatRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newPerplexityForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newPerplexityForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } } func TestPerplexityChatRequiresApiKey(t *testing.T) { - _, err := newPerplexityForTest("http://unused").ChatWithMessages("sonar", []Message{{Role: "user", Content: "x"}}, nil, nil) + _, err := newPerplexityForTest("http://unused").ChatWithMessages("sonar", []Message{{Role: "user", Content: "x"}}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -182,7 +184,7 @@ func TestPerplexityStreamHappyPath(t *testing.T) { err := newPerplexityForTest(srv.URL).ChatStreamlyWithSender( "sonar", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil { content = append(content, *c) @@ -219,7 +221,7 @@ func TestPerplexityStreamStopsOnDoneMarker(t *testing.T) { err := newPerplexityForTest(srv.URL).ChatStreamlyWithSender( "sonar", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c != nil { chunks = append(chunks, *c) @@ -317,6 +319,7 @@ func TestPerplexityEmbedHappyPath(t *testing.T) { []string{"hello", "world"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 16}, + nil, ) if err != nil { t.Fatalf("Embed: %v", err) @@ -335,7 +338,7 @@ func TestPerplexityEmbedHappyPath(t *testing.T) { func TestPerplexityEmbedEmptyTextsReturnsEmpty(t *testing.T) { modelName := "pplx-embed-v1-0.6b" apiKey := "test-key" - out, err := newPerplexityForTest("http://unused").Embed(&modelName, nil, &APIConfig{ApiKey: &apiKey}, nil) + out, err := newPerplexityForTest("http://unused").Embed(&modelName, nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -346,7 +349,7 @@ func TestPerplexityEmbedEmptyTextsReturnsEmpty(t *testing.T) { func TestPerplexityEmbedRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newPerplexityForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newPerplexityForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -354,7 +357,7 @@ func TestPerplexityEmbedRequiresModelName(t *testing.T) { func TestPerplexityUnsupportedMethods(t *testing.T) { m := newPerplexityForTest("http://unused") - if _, err := m.Rerank(nil, "", nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(nil, "", nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank error=%v", err) } if _, err := m.Balance(nil); err == nil || !strings.Contains(err.Error(), "no such method") { diff --git a/internal/entity/models/ppio.go b/internal/entity/models/ppio.go index a1f09dbd6a..181b4489a6 100644 --- a/internal/entity/models/ppio.go +++ b/internal/entity/models/ppio.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -111,7 +112,7 @@ type ppioChatResponse struct { FinishReason string `json:"finish_reason"` } -func (p *PPIOModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (p *PPIOModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -179,7 +180,7 @@ func (p *PPIOModel) ChatWithMessages(modelName string, messages []Message, apiCo }, nil } -func (p *PPIOModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (p *PPIOModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -328,11 +329,11 @@ func (p *PPIOModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (p *PPIOModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (p *PPIOModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (p *PPIOModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } @@ -340,27 +341,27 @@ func (p *PPIOModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (p *PPIOModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (p *PPIOModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (p *PPIOModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (p *PPIOModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (p *PPIOModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (p *PPIOModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } diff --git a/internal/entity/models/ppio_test.go b/internal/entity/models/ppio_test.go index f9e6ff256d..9c8f6921bf 100644 --- a/internal/entity/models/ppio_test.go +++ b/internal/entity/models/ppio_test.go @@ -134,6 +134,7 @@ func TestPPIOChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -165,6 +166,7 @@ func TestPPIOChatUsesReasoningFallback(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -176,7 +178,7 @@ func TestPPIOChatUsesReasoningFallback(t *testing.T) { func TestPPIOChatRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newPPIOForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newPPIOForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -184,7 +186,7 @@ func TestPPIOChatRequiresModelName(t *testing.T) { func TestPPIOChatRequiresMessages(t *testing.T) { apiKey := "test-key" - _, err := newPPIOForTest("http://unused").ChatWithMessages("deepseek/deepseek-r1", nil, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newPPIOForTest("http://unused").ChatWithMessages("deepseek/deepseek-r1", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages error, got %v", err) } @@ -197,7 +199,7 @@ func TestPPIOChatSurfacesHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newPPIOForTest(srv.URL).ChatWithMessages("deepseek/deepseek-r1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newPPIOForTest(srv.URL).ChatWithMessages("deepseek/deepseek-r1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "status 401") { t.Errorf("expected HTTP status error, got %v", err) } @@ -212,7 +214,7 @@ func TestPPIOChatRejectsProviderError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newPPIOForTest(srv.URL).ChatWithMessages("deepseek/deepseek-r1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newPPIOForTest(srv.URL).ChatWithMessages("deepseek/deepseek-r1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "upstream error") { t.Errorf("expected upstream error, got %v", err) } @@ -245,7 +247,7 @@ func TestPPIOStreamHappyPath(t *testing.T) { err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil { content = append(content, *c) @@ -280,7 +282,7 @@ func TestPPIOStreamSurfacesHTTPError(t *testing.T) { err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "status 401") { @@ -299,7 +301,7 @@ func TestPPIOStreamStopsOnSenderError(t *testing.T) { err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return io.ErrUnexpectedEOF }, ) if err == nil || !strings.Contains(err.Error(), "unexpected EOF") { @@ -315,6 +317,7 @@ func TestPPIOStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -327,7 +330,7 @@ func TestPPIOStreamRequiresSender(t *testing.T) { err := newPPIOForTest("http://unused").ChatStreamlyWithSender( "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender error, got %v", err) @@ -345,7 +348,7 @@ func TestPPIOStreamRequiresTerminalEvent(t *testing.T) { err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream ended before") { @@ -471,31 +474,31 @@ func TestPPIOMissingRegionBaseURL(t *testing.T) { func TestPPIOUnsupportedMethods(t *testing.T) { m := newPPIOForTest("http://unused") - if _, err := m.Embed(nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed error=%v", err) } - if _, err := m.Rerank(nil, "", nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(nil, "", nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank error=%v", err) } if _, err := m.Balance(nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance error=%v", err) } - if _, err := m.TranscribeAudio(nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio error=%v", err) } - if err := m.TranscribeAudioWithSender(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.TranscribeAudioWithSender(nil, nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudioWithSender error=%v", err) } - if _, err := m.AudioSpeech(nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech error=%v", err) } - if err := m.AudioSpeechWithSender(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.AudioSpeechWithSender(nil, nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeechWithSender error=%v", err) } - if _, err := m.OCRFile(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(nil, nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile error=%v", err) } - if _, err := m.ParseFile(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(nil, nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ParseFile error=%v", err) } if _, err := m.ListTasks(nil); err == nil || !strings.Contains(err.Error(), "no such method") { diff --git a/internal/entity/models/qiniu.go b/internal/entity/models/qiniu.go index 79ce00c5ef..219fabd4cb 100644 --- a/internal/entity/models/qiniu.go +++ b/internal/entity/models/qiniu.go @@ -140,7 +140,7 @@ func applyQiniuThinkingConfig(reqBody map[string]interface{}, modelName string, } } -func (q *QiniuModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (q *QiniuModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := q.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -264,7 +264,7 @@ func (q *QiniuModel) ChatWithMessages(modelName string, messages []Message, apiC return chatResponse, nil } -func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := q.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -381,35 +381,35 @@ func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message return nil } -func (q *QiniuModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (q *QiniuModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (q *QiniuModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (q *QiniuModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (q *QiniuModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (q *QiniuModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (q *QiniuModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (q *QiniuModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (q *QiniuModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } diff --git a/internal/entity/models/ragcon.go b/internal/entity/models/ragcon.go index b99208e50c..92e1f5c60c 100644 --- a/internal/entity/models/ragcon.go +++ b/internal/entity/models/ragcon.go @@ -27,6 +27,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strings" ) @@ -126,7 +127,7 @@ func ragconChatPayload(modelName string, messages []Message, stream bool, chatMo return reqBody } -func (r *RAGconModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (r *RAGconModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -216,13 +217,13 @@ func (r *RAGconModel) ChatWithMessages(modelName string, messages []Message, api ToolCalls: toolCalls, } if pt, ct, tt := extractUsageFromMap(result); tt > 0 { - chatResponse.Usage = &ChatUsage{PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt} + chatResponse.Usage = &TokenUsage{PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt} } return chatResponse, nil } -func (r *RAGconModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (r *RAGconModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -269,7 +270,7 @@ func (r *RAGconModel) ChatStreamlyWithSender(modelName string, messages []Messag sawTerminal := false accumulatedToolCalls := make(map[int]map[string]interface{}) - var streamUsage *ChatUsage + var streamUsage *TokenUsage scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 64*1024), 1024*1024) @@ -293,7 +294,7 @@ func (r *RAGconModel) ChatStreamlyWithSender(modelName string, messages []Messag } if pt, ct, tt := extractUsageFromMap(event); tt > 0 { - streamUsage = &ChatUsage{PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt} + streamUsage = &TokenUsage{PromptTokens: pt, CompletionTokens: ct, TotalTokens: tt} } choices, ok := event["choices"].([]interface{}) @@ -382,7 +383,7 @@ type ragconEmbeddingResponse struct { } `json:"data"` } -func (r *RAGconModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (r *RAGconModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -450,7 +451,7 @@ func (r *RAGconModel) Embed(modelName *string, texts []string, apiConfig *APICon } // Rerank POSTs to RAGcon's /rerank endpoint (LiteLLM proxy passthrough). -func (r *RAGconModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (r *RAGconModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -656,7 +657,7 @@ func (r *RAGconModel) newASRRequest(ctx context.Context, modelName *string, file return req, responseFormat, nil } -func (r *RAGconModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (r *RAGconModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() @@ -683,7 +684,7 @@ func (r *RAGconModel) TranscribeAudio(modelName *string, file *string, apiConfig return decodeOpenAIASRResponse(respBody, responseFormat) } -func (r *RAGconModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (r *RAGconModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") } @@ -819,7 +820,7 @@ func (r *RAGconModel) newTTSRequest(ctx context.Context, modelName *string, audi return req, streamFormat, nil } -func (r *RAGconModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (r *RAGconModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() @@ -845,7 +846,7 @@ func (r *RAGconModel) AudioSpeech(modelName *string, audioContent *string, apiCo return &TTSResponse{Audio: body}, nil } -func (r *RAGconModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (r *RAGconModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") } @@ -876,12 +877,12 @@ func (r *RAGconModel) AudioSpeechWithSender(modelName *string, audioContent *str } // OCRFile is not offered by RAGcon. -func (r *RAGconModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (r *RAGconModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } // ParseFile is not offered by RAGcon. -func (r *RAGconModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (r *RAGconModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } diff --git a/internal/entity/models/ragcon_test.go b/internal/entity/models/ragcon_test.go index 655f53ac6a..e30a23465c 100644 --- a/internal/entity/models/ragcon_test.go +++ b/internal/entity/models/ragcon_test.go @@ -113,7 +113,7 @@ func TestRAGconChatHappyPath(t *testing.T) { "llama-4-maverick", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, - nil, + nil, nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -134,7 +134,7 @@ func TestRAGconChatRequiresAPIKey(t *testing.T) { "llama-4-maverick", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, - nil, + nil, nil, ) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key-required error, got %v", err) @@ -147,7 +147,7 @@ func TestRAGconChatRequiresMessages(t *testing.T) { "llama-4-maverick", []Message{}, &APIConfig{ApiKey: &apiKey}, - nil, + nil, nil, ) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) @@ -166,7 +166,7 @@ func TestRAGconChatSurfacesHTTPError(t *testing.T) { "llama-4-maverick", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, - nil, + nil, nil, ) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 status error, got %v", err) @@ -199,6 +199,7 @@ func TestRAGconStreamHappyPath(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(c *string, r *string) error { if r != nil && *r != "" { reasoning = append(reasoning, *r) @@ -234,6 +235,7 @@ func TestRAGconStreamRejectsNilSender(t *testing.T) { &APIConfig{ApiKey: &apiKey}, nil, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) @@ -256,7 +258,7 @@ func TestRAGconEmbed(t *testing.T) { apiKey := "test-key" model := "text-embedding-3-small" - embeddings, err := newRAGconForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + embeddings, err := newRAGconForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -284,7 +286,7 @@ func TestRAGconRerank(t *testing.T) { apiKey := "test-key" model := "rerank-v1" - resp, err := newRAGconForTest(srv.URL).Rerank(&model, "q", []string{"doc0", "doc1"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}) + resp, err := newRAGconForTest(srv.URL).Rerank(&model, "q", []string{"doc0", "doc1"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -350,7 +352,7 @@ func TestRAGconTranscribeAudioPostsMultipart(t *testing.T) { apiKey := "test-key" model := "whisper-1" - resp, err := newRAGconForTest(srv.URL).TranscribeAudio(&model, &audioPath, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := newRAGconForTest(srv.URL).TranscribeAudio(&model, &audioPath, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("TranscribeAudio: %v", err) } @@ -374,7 +376,7 @@ func TestRAGconAudioSpeechPostsJSON(t *testing.T) { apiKey := "test-key" model := "tts-1" text := "hello" - resp, err := newRAGconForTest(srv.URL).AudioSpeech(&model, &text, &APIConfig{ApiKey: &apiKey}, &TTSConfig{Params: map[string]interface{}{"voice": "alloy"}}) + resp, err := newRAGconForTest(srv.URL).AudioSpeech(&model, &text, &APIConfig{ApiKey: &apiKey}, &TTSConfig{Params: map[string]interface{}{"voice": "alloy"}}, nil) if err != nil { t.Fatalf("AudioSpeech: %v", err) } @@ -390,10 +392,10 @@ func TestRAGconUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { if _, err := r.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: expected no such method, got %v", err) } - if _, err := r.OCRFile(&model, nil, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := r.OCRFile(&model, nil, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: expected no such method, got %v", err) } - if _, err := r.ParseFile(&model, nil, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := r.ParseFile(&model, nil, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ParseFile: expected no such method, got %v", err) } if _, err := r.ListTasks(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { diff --git a/internal/entity/models/reasoning_family_provider_test.go b/internal/entity/models/reasoning_family_provider_test.go index c81e881764..2687429925 100644 --- a/internal/entity/models/reasoning_family_provider_test.go +++ b/internal/entity/models/reasoning_family_provider_test.go @@ -76,6 +76,7 @@ func TestGiteeChatExtractsQwenThinkingFromInlineContent(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Thinking: &thinking, ModelClass: &modelClass}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -113,6 +114,7 @@ func TestSiliconflowChatExtractsProviderPrefixedQwenThinkingFromInlineContent(t []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Thinking: &thinking, ModelClass: &modelClass}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) diff --git a/internal/entity/models/replicate.go b/internal/entity/models/replicate.go index 3f543b2548..ae0b16f7d8 100644 --- a/internal/entity/models/replicate.go +++ b/internal/entity/models/replicate.go @@ -25,6 +25,7 @@ import ( "io" "net/http" "net/url" + "ragflow/internal/common" "sort" "strings" "time" @@ -324,7 +325,7 @@ func (r *ReplicateModel) waitForPrediction(ctx context.Context, prediction *repl } } -func (r *ReplicateModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (r *ReplicateModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -363,7 +364,7 @@ func (r *ReplicateModel) ChatWithMessages(modelName string, messages []Message, return &ChatResponse{Answer: &answer, ReasonContent: &reasonContent}, nil } -func (r *ReplicateModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (r *ReplicateModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -673,7 +674,7 @@ func replicateKeys(m map[string]interface{}) []string { } // Embed turns a list of texts into embedding vectors via Replicate's -func (r *ReplicateModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (r *ReplicateModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -764,7 +765,7 @@ func replicateScoresFromInterface(arr []interface{}, n int) ([]float64, error) { } // Rerank scores a query against a list of documents -func (r *ReplicateModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (r *ReplicateModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -832,27 +833,27 @@ func (r *ReplicateModel) Balance(apiConfig *APIConfig) (map[string]interface{}, return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (r *ReplicateModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (r *ReplicateModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (r *ReplicateModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (r *ReplicateModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (r *ReplicateModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (r *ReplicateModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } diff --git a/internal/entity/models/replicate_test.go b/internal/entity/models/replicate_test.go index 50701e8983..020a4fb91a 100644 --- a/internal/entity/models/replicate_test.go +++ b/internal/entity/models/replicate_test.go @@ -127,6 +127,7 @@ func TestReplicateOfficialChatHappyPath(t *testing.T) { []Message{{Role: "system", Content: "be helpful"}, {Role: "user", Content: "hello"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &maxTokens, Stop: &stop}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -165,7 +166,7 @@ func TestReplicateCommunityChatUsesVersionEndpoint(t *testing.T) { resp, err := newReplicateForTest(srv.URL).ChatWithMessages( version, []Message{{Role: "user", Content: "hello"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -205,7 +206,7 @@ func TestReplicateChatPollsUntilSucceeded(t *testing.T) { resp, err := newReplicateForTest(srv.URL).ChatWithMessages( "meta/meta-llama-3-70b-instruct", []Message{{Role: "user", Content: "hello"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -261,7 +262,7 @@ func TestReplicateStreamHappyPath(t *testing.T) { err := newReplicateForTest(apiSrv.URL).ChatStreamlyWithSender( "meta/meta-llama-3-70b-instruct", []Message{{Role: "user", Content: "hello"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c != nil { chunks = append(chunks, *c) @@ -312,7 +313,7 @@ func TestReplicateUnsupportedMethods(t *testing.T) { apiKey := "test-key" // Rerank IS implemented; with empty documents it short-circuits (no error). // Pass non-empty docs + nil modelName to trigger model-name validation. - if _, err := m.Rerank(nil, "", []string{"d"}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := m.Rerank(nil, "", []string{"d"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("Rerank error=%v", err) } // Balance IS a stub → "no such method" diff --git a/internal/entity/models/siliconflow.go b/internal/entity/models/siliconflow.go index 0f09768aad..3abfbc3663 100644 --- a/internal/entity/models/siliconflow.go +++ b/internal/entity/models/siliconflow.go @@ -67,7 +67,7 @@ type SiliconflowRerankRequest struct { } // ChatWithMessages sends multiple messages with roles and returns response -func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -218,7 +218,7 @@ func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (s *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (s *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -388,7 +388,7 @@ type siliconflowUsage struct { const siliconflowMaxBatchSize = 32 // Embed embeds a list of texts into embeddings -func (s *SiliconflowModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (s *SiliconflowModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -638,7 +638,7 @@ type SiliconflowRerankResponse struct { } // Rerank calculates similarity scores between query and documents -func (s *SiliconflowModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (s *SiliconflowModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -717,7 +717,7 @@ func (s *SiliconflowModel) Rerank(modelName *string, query string, documents []s } // TranscribeAudio transcribe audio -func (s *SiliconflowModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (s *SiliconflowModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -838,12 +838,12 @@ func (s *SiliconflowModel) TranscribeAudio(modelName *string, file *string, apiC return &ASRResponse{Text: result.Text}, nil } -func (s *SiliconflowModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (s *SiliconflowModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", s.Name()) } // AudioSpeech convert text to audio -func (s *SiliconflowModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (s *SiliconflowModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -907,7 +907,7 @@ func (s *SiliconflowModel) AudioSpeech(modelName *string, audioContent *string, return &TTSResponse{Audio: body}, nil } -func (s *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (s *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -987,12 +987,12 @@ func (s *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent } // OCRFile OCR file -func (s *SiliconflowModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (s *SiliconflowModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } // ParseFile parse file -func (s *SiliconflowModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (s *SiliconflowModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } diff --git a/internal/entity/models/sse_scanner_buffer_test.go b/internal/entity/models/sse_scanner_buffer_test.go index 0e76d90a36..e2aee7de24 100644 --- a/internal/entity/models/sse_scanner_buffer_test.go +++ b/internal/entity/models/sse_scanner_buffer_test.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "net/http/httptest" + "ragflow/internal/common" "strings" "testing" ) @@ -12,7 +13,7 @@ import ( // provider. The buffer regression below exercises it through a table so a new // provider only needs one row. type chatStreamer interface { - ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error + ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error } // largeSSEStreamServer streams a single SSE "data:" line whose content delta is @@ -79,6 +80,7 @@ func TestChatStreamLargeChunkNotTruncated(t *testing.T) { // them while avoiding a nil-config deref in providers that read // modelConfig unconditionally. &ChatConfig{}, + nil, func(c *string, _ *string) error { if c != nil && *c != "[DONE]" { got.WriteString(*c) diff --git a/internal/entity/models/stepfun.go b/internal/entity/models/stepfun.go index f8ef70535d..7b8f1004a8 100644 --- a/internal/entity/models/stepfun.go +++ b/internal/entity/models/stepfun.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -52,7 +53,7 @@ func (s *StepFunModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns the response. -func (s *StepFunModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (s *StepFunModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -161,7 +162,7 @@ func (s *StepFunModel) ChatWithMessages(modelName string, messages []Message, ap } // ChatStreamlyWithSender sends messages and streams the response -func (s *StepFunModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (s *StepFunModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -284,7 +285,7 @@ func (s *StepFunModel) ChatStreamlyWithSender(modelName string, messages []Messa } // Embed is left as a stub. -func (s *StepFunModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (s *StepFunModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } @@ -354,21 +355,21 @@ func (s *StepFunModel) CheckConnection(apiConfig *APIConfig) error { // Rerank calculates similarity scores between query and documents. StepFun // does not expose a public rerank API, so this returns "no such method". -func (s *StepFunModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (s *StepFunModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (s *StepFunModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (s *StepFunModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } -func (s *StepFunModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (s *StepFunModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", s.Name()) } // AudioSpeech convert text to audio -func (s *StepFunModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (s *StepFunModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -430,7 +431,7 @@ func (s *StepFunModel) AudioSpeech(modelName *string, audioContent *string, apiC } // AudioSpeechWithSender for Streaming TTS -func (s *StepFunModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (s *StepFunModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -512,12 +513,12 @@ func (s *StepFunModel) AudioSpeechWithSender(modelName *string, audioContent *st } // OCRFile OCR file -func (s *StepFunModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (s *StepFunModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } // ParseFile parse file -func (s *StepFunModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (s *StepFunModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } diff --git a/internal/entity/models/timeout_test.go b/internal/entity/models/timeout_test.go index 3d4908a4b1..2b2097970d 100644 --- a/internal/entity/models/timeout_test.go +++ b/internal/entity/models/timeout_test.go @@ -90,6 +90,7 @@ func TestStreamNotTruncatedByNonStreamTimeout(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(c *string, _ *string) error { if c != nil && *c != "[DONE]" { got.WriteString(*c) @@ -132,7 +133,7 @@ func TestNonStreamHonorsShortDeadline(t *testing.T) { "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, - nil, + nil, nil, ) done <- err }() diff --git a/internal/entity/models/togetherai.go b/internal/entity/models/togetherai.go index 68f5616c09..c1d9fae969 100644 --- a/internal/entity/models/togetherai.go +++ b/internal/entity/models/togetherai.go @@ -27,6 +27,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" ) @@ -126,7 +127,7 @@ type togetherAIChatResponse struct { FinishReason string `json:"finish_reason"` } -func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -193,7 +194,7 @@ func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, }, nil } -func (t *TogetherAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (t *TogetherAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -349,7 +350,7 @@ type togetherAIEmbeddingResponse struct { Object string `json:"object"` } -func (t *TogetherAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (t *TogetherAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -437,7 +438,7 @@ func (t *TogetherAIModel) Embed(modelName *string, texts []string, apiConfig *AP return embeddings, nil } -func (t *TogetherAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (t *TogetherAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -519,7 +520,7 @@ func (t *TogetherAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TogetherAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (t *TogetherAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -622,11 +623,11 @@ func (t *TogetherAIModel) TranscribeAudio(modelName *string, file *string, apiCo }, nil } -func (t *TogetherAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (t *TogetherAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", t.Name()) } -func (t *TogetherAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (t *TogetherAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -686,7 +687,7 @@ func (t *TogetherAIModel) AudioSpeech(modelName *string, audioContent *string, a return &TTSResponse{Audio: body}, nil } -func (t *TogetherAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (t *TogetherAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -772,11 +773,11 @@ func (t *TogetherAIModel) AudioSpeechWithSender(modelName *string, audioContent return nil } -func (t *TogetherAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (t *TogetherAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TogetherAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (t *TogetherAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } diff --git a/internal/entity/models/togetherai_test.go b/internal/entity/models/togetherai_test.go index 336bbb1d5e..f9f5a7b79d 100644 --- a/internal/entity/models/togetherai_test.go +++ b/internal/entity/models/togetherai_test.go @@ -95,6 +95,7 @@ func TestTogetherAIChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -139,6 +140,7 @@ func TestTogetherAIChatForwardsReasoningEnabled(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Thinking: &thinking}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -150,7 +152,7 @@ func TestTogetherAIChatForwardsReasoningEnabled(t *testing.T) { func TestTogetherAIChatRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newTogetherAIForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newTogetherAIForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -182,7 +184,7 @@ func TestTogetherAIStreamHappyPath(t *testing.T) { err := newTogetherAIForTest(srv.URL).ChatStreamlyWithSender( "meta-llama/Llama-3.3-70B-Instruct-Turbo", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil { content = append(content, *c) @@ -218,7 +220,7 @@ func TestTogetherAIStreamStopsOnRootFinishReason(t *testing.T) { err := newTogetherAIForTest(srv.URL).ChatStreamlyWithSender( "meta-llama/Llama-3.3-70B-Instruct-Turbo", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c != nil { chunks = append(chunks, *c) @@ -268,7 +270,7 @@ func TestTogetherAIUnsupportedMethods(t *testing.T) { apiKey := "test-key" // Rerank IS implemented; with nil documents it short-circuits to empty response (no error). // It should NOT be blocked by APIConfigCheck. - if _, err := m.Rerank(nil, "", nil, &APIConfig{ApiKey: &apiKey}, nil); err != nil { + if _, err := m.Rerank(nil, "", nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { t.Errorf("Rerank error=%v (expected no error for empty documents)", err) } // Balance IS a stub → "no such method" diff --git a/internal/entity/models/tokenhub.go b/internal/entity/models/tokenhub.go index 5675f59c0f..a44a24198e 100644 --- a/internal/entity/models/tokenhub.go +++ b/internal/entity/models/tokenhub.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -58,7 +59,7 @@ func validateTokenHubChatRequest(modelName string, messages []Message) error { return nil } -func (t *TokenHubModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (t *TokenHubModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -183,7 +184,7 @@ func (t *TokenHubModel) ChatWithMessages(modelName string, messages []Message, a return chatResponse, nil } -func (t *TokenHubModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (t *TokenHubModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -314,7 +315,7 @@ func (t *TokenHubModel) ChatStreamlyWithSender(modelName string, messages []Mess return sender(&endOfStream, nil) } -func (t *TokenHubModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (t *TokenHubModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -394,31 +395,31 @@ func (t *TokenHubModel) Embed(modelName *string, texts []string, apiConfig *APIC return embeddings, nil } -func (t *TokenHubModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (t *TokenHubModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (t *TokenHubModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (t *TokenHubModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (t *TokenHubModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (t *TokenHubModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (t *TokenHubModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (t *TokenHubModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } diff --git a/internal/entity/models/tokenhub_test.go b/internal/entity/models/tokenhub_test.go index 3a17a1a4d0..ccdf0f8fd0 100644 --- a/internal/entity/models/tokenhub_test.go +++ b/internal/entity/models/tokenhub_test.go @@ -120,6 +120,7 @@ func TestTokenHubChatWithMessagesForcesNonStreaming(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -133,7 +134,7 @@ func TestTokenHubChatWithMessagesForcesNonStreaming(t *testing.T) { } func TestTokenHubChatRequiresAPIKey(t *testing.T) { - _, err := newTokenHubForTest("http://unused").ChatWithMessages("gpt-4o-mini", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil) + _, err := newTokenHubForTest("http://unused").ChatWithMessages("gpt-4o-mini", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Fatalf("expected api-key error, got %v", err) } @@ -141,7 +142,7 @@ func TestTokenHubChatRequiresAPIKey(t *testing.T) { func TestTokenHubChatRequiresModelName(t *testing.T) { apiKey := "test-key" - _, err := newTokenHubForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newTokenHubForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Fatalf("expected model-name error, got %v", err) } @@ -164,6 +165,7 @@ func TestTokenHubStreamHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(c *string, r *string) error { if c != nil { content = append(content, *c) @@ -193,6 +195,7 @@ func TestTokenHubStreamRejectsFalseStreamConfig(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "stream must be true") { @@ -208,6 +211,7 @@ func TestTokenHubStreamRequiresSender(t *testing.T) { &APIConfig{ApiKey: &apiKey}, nil, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Fatalf("expected sender error, got %v", err) @@ -220,6 +224,7 @@ func TestTokenHubStreamRequiresAPIKey(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{}, nil, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "api key is required") { @@ -234,6 +239,7 @@ func TestTokenHubStreamRequiresModelName(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(*string, *string) error { return nil }, ) if err == nil || !strings.Contains(err.Error(), "model name is required") { @@ -263,7 +269,7 @@ func TestTokenHubEmbedHappyPath(t *testing.T) { apiKey := "test-key" model := "text-embedding-3-small" - embeddings, err := newTokenHubForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + embeddings, err := newTokenHubForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -274,14 +280,14 @@ func TestTokenHubEmbedHappyPath(t *testing.T) { func TestTokenHubEmbedValidatesInputs(t *testing.T) { apiKey := "test-key" - if embeddings, err := newTokenHubForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil); err != nil || len(embeddings) != 0 { + if embeddings, err := newTokenHubForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil || len(embeddings) != 0 { t.Fatalf("empty input should return empty embeddings, got %#v err=%v", embeddings, err) } - if _, err := newTokenHubForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := newTokenHubForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { t.Fatalf("expected model-name error, got %v", err) } model := "text-embedding-3-small" - if _, err := newTokenHubForTest("http://unused").Embed(&model, []string{"x"}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newTokenHubForTest("http://unused").Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Fatalf("expected api-key error, got %v", err) } } diff --git a/internal/entity/models/tokenpony.go b/internal/entity/models/tokenpony.go index 1f2407236a..8c2fcead5e 100644 --- a/internal/entity/models/tokenpony.go +++ b/internal/entity/models/tokenpony.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -51,7 +52,7 @@ func (t *TokenPonyModel) Name() string { } // ChatWithMessages sends a non-streaming chat request -func (t *TokenPonyModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (t *TokenPonyModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -161,7 +162,7 @@ func (t *TokenPonyModel) ChatWithMessages(modelName string, messages []Message, } // ChatStreamlyWithSender opens the SSE chat-completions -func (t *TokenPonyModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (t *TokenPonyModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -336,11 +337,11 @@ func (t *TokenPonyModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (t *TokenPonyModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (t *TokenPonyModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (t *TokenPonyModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } @@ -348,27 +349,27 @@ func (t *TokenPonyModel) Balance(apiConfig *APIConfig) (map[string]interface{}, return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (t *TokenPonyModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (t *TokenPonyModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (t *TokenPonyModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (t *TokenPonyModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (t *TokenPonyModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (t *TokenPonyModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } diff --git a/internal/entity/models/tokenpony_test.go b/internal/entity/models/tokenpony_test.go index 82724ff54c..5f1094f40a 100644 --- a/internal/entity/models/tokenpony_test.go +++ b/internal/entity/models/tokenpony_test.go @@ -124,6 +124,7 @@ func TestTokenPonyChatHappyPath(t *testing.T) { []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp}, + nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -150,7 +151,7 @@ func TestTokenPonyChatNoReasoning(t *testing.T) { resp, err := newTokenPonyForTest(srv.URL).ChatWithMessages( "qwen3-8b", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -166,7 +167,7 @@ func TestTokenPonyChatRequiresAPIKey(t *testing.T) { _, err := newTokenPonyForTest("http://unused").ChatWithMessages( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -175,7 +176,7 @@ func TestTokenPonyChatRequiresAPIKey(t *testing.T) { func TestTokenPonyChatRequiresMessages(t *testing.T) { apiKey := "test-key" _, err := newTokenPonyForTest("http://unused").ChatWithMessages( - "qwen3-32b", nil, &APIConfig{ApiKey: &apiKey}, nil) + "qwen3-32b", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("expected messages-empty error, got %v", err) } @@ -192,7 +193,7 @@ func TestTokenPonyChatPropagatesHTTPError(t *testing.T) { _, err := newTokenPonyForTest(srv.URL).ChatWithMessages( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -213,7 +214,7 @@ func TestTokenPonyStreamHappyPath(t *testing.T) { err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( "qwen3-32b", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { if c == nil { return nil @@ -251,7 +252,7 @@ func TestTokenPonyStreamSplitsReasoning(t *testing.T) { err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( "deepseek-r1-0528", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if c != nil && r != nil { t.Errorf("sender called with both args non-nil") @@ -283,6 +284,7 @@ func TestTokenPonyStreamRejectsExplicitFalse(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("expected stream-true guard, got %v", err) @@ -294,7 +296,7 @@ func TestTokenPonyStreamRequiresSender(t *testing.T) { err := newTokenPonyForTest("http://unused").ChatStreamlyWithSender( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { t.Errorf("expected sender-required error, got %v", err) } @@ -310,7 +312,7 @@ func TestTokenPonyStreamFailsWithoutTerminal(t *testing.T) { err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream ended before") { t.Errorf("expected truncation error, got %v", err) @@ -328,7 +330,7 @@ func TestTokenPonyStreamRejectsMalformedFrame(t *testing.T) { err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "invalid SSE event") { t.Errorf("expected invalid-SSE error, got %v", err) @@ -346,7 +348,7 @@ func TestTokenPonyStreamSurfacesUpstreamError(t *testing.T) { err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( "qwen3-32b", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "upstream stream error") { t.Errorf("expected upstream-error surfacing, got %v", err) @@ -426,7 +428,7 @@ func TestTokenPonyBaseURLForRegionUnknown(t *testing.T) { func TestTokenPonyEmbedReturnsNoSuchMethod(t *testing.T) { model := "x" - _, err := newTokenPonyForTest("http://unused").Embed(&model, []string{"a"}, &APIConfig{}, nil) + _, err := newTokenPonyForTest("http://unused").Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed: want 'no such method', got %v", err) } @@ -435,13 +437,13 @@ func TestTokenPonyEmbedReturnsNoSuchMethod(t *testing.T) { func TestTokenPonyAudioOCRReturnNoSuchMethod(t *testing.T) { m := newTokenPonyForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudio: %v", err) } - if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeech: %v", err) } - if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } diff --git a/internal/entity/models/types.go b/internal/entity/models/types.go index 9d9730073e..60f293222f 100644 --- a/internal/entity/models/types.go +++ b/internal/entity/models/types.go @@ -1,6 +1,9 @@ package models -import "encoding/json" +import ( + "encoding/json" + "ragflow/internal/common" +) // Message represents a chat message with role and content // @@ -27,23 +30,23 @@ type ModelDriver interface { Name() string // ChatWithMessages sends multiple messages synchronously - ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) + ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) // ChatStreamlyWithSender sends multiple messages asynchronously - ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error + ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error // Embed a list of texts into embeddings - Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) + Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) // Rerank calculates similarity scores between query and texts - Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) + Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) // TranscribeAudio transcribe audio - TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) - TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error + TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) + TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error // AudioSpeech convert text to audio - AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) - AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error + AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) + AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error // OCRFile OCR file - OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) + OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) // ParseFile parse file - ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) + ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) // ListModels List supported models ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) @@ -60,16 +63,16 @@ type ChatResponse struct { Answer *string `json:"answer"` ReasonContent *string `json:"reason_content"` ToolCalls []map[string]interface{} `json:"tool_calls,omitempty"` - Usage *ChatUsage `json:"usage,omitempty"` + Usage *TokenUsage `json:"usage,omitempty"` } // ChatUsage holds token usage split for one LLM call. Consumed by // LLMBundle for accurate Langfuse reporting and run aggregation. // Mirrors Python's common.token_utils.usage_from_response() split. -type ChatUsage struct { - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` +type TokenUsage struct { + PromptTokens int `json:"prompt_tokens" mapstructure:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens" mapstructure:"completion_tokens"` + TotalTokens int `json:"total_tokens" mapstructure:"total_tokens"` } type EmbeddingData struct { @@ -171,34 +174,12 @@ type ChatConfig struct { // The ChatStreamlyWithSender driver writes to this pointer (if // non-nil) after the stream completes; callers read it the same // way they read ToolCallsResult. - UsageResult *ChatUsage `json:"-"` + UsageResult *TokenUsage `json:"-"` // StreamCallback receives raw content/reasoning deltas as soon as // the model driver streams them. StreamCallback func(contentDelta, reasoningDelta string) `json:"-"` } -type ChatAppConfig struct { - UserID string `json:"user_id"` - UserEmail string `json:"user_email"` - TenantID string `json:"tenant_id"` - TenantEmail string `json:"tenant_email"` - GroupID *string `json:"group_id"` - GroupName *string `json:"group_name"` - AppID string `json:"app_id"` - AppName string `json:"app_name"` - SessionID *string `json:"session_id"` - ProviderName string `json:"provider_name"` - InstanceID string `json:"instance_id"` - ModelName string `json:"model_name"` - Type string `json:"type"` - InputTokens uint64 `json:"input_tokens"` - OutputTokens uint64 `json:"output_tokens"` - TotalTokens uint64 `json:"total_tokens"` - RequestID *string `json:"request_id"` - ResponseTimeMS uint64 `json:"response_time_ms"` - ErrorMessage *string `json:"error_message"` -} - type APIConfig struct { ApiKey *string Region *string @@ -265,8 +246,8 @@ func NewRerankModel(driver ModelDriver, modelName *string, apiConfig *APIConfig) } // Rerank calculates similarity between query and texts -func (r *RerankModel) Rerank(query string, texts []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { - return r.ModelDriver.Rerank(r.ModelName, query, texts, apiConfig, rerankConfig) +func (r *RerankModel) Rerank(query string, texts []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { + return r.ModelDriver.Rerank(r.ModelName, query, texts, apiConfig, rerankConfig, modelUsage) } // ToolConfig bundles tool-calling configuration for a ChatModel. @@ -286,7 +267,7 @@ type ChatModel struct { // LastUsage holds the token usage (prompt/completion/total) of the most // recent chat call. Consumed by callers for accurate Langfuse reporting // and per-run token aggregation. Reset before each call. - LastUsage *ChatUsage + LastUsage *TokenUsage } // NewChatModel creates a new ChatModel diff --git a/internal/entity/models/upstage.go b/internal/entity/models/upstage.go index b03d72d9db..de0751d4d1 100644 --- a/internal/entity/models/upstage.go +++ b/internal/entity/models/upstage.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -51,7 +52,7 @@ func (u *UpstageModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns the response. -func (u *UpstageModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (u *UpstageModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := u.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -167,7 +168,7 @@ func (u *UpstageModel) ChatWithMessages(modelName string, messages []Message, ap } // ChatStreamlyWithSender sends messages and streams the response -func (u *UpstageModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (u *UpstageModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := u.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -311,7 +312,7 @@ type upstageEmbeddingResponse struct { } // Embed turns a list of texts into embedding vectors -func (u *UpstageModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (u *UpstageModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := u.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -462,35 +463,35 @@ func (u *UpstageModel) CheckConnection(apiConfig *APIConfig) error { // Rerank calculates similarity scores between query and documents. Upstage // does not expose a public rerank API, so this returns "no such method". -func (u *UpstageModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (u *UpstageModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method") } // TranscribeAudio transcribe audio -func (u *UpstageModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (u *UpstageModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", u.Name()) } -func (u *UpstageModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (u *UpstageModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", u.Name()) } // AudioSpeech convert text to audio -func (u *UpstageModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (u *UpstageModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", u.Name()) } -func (u *UpstageModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (u *UpstageModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", u.Name()) } // OCRFile OCR file -func (u *UpstageModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (u *UpstageModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", u.Name()) } // ParseFile parse file -func (u *UpstageModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (u *UpstageModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", u.Name()) } diff --git a/internal/entity/models/upstage_test.go b/internal/entity/models/upstage_test.go index f9a8bba36a..8e81cec111 100644 --- a/internal/entity/models/upstage_test.go +++ b/internal/entity/models/upstage_test.go @@ -41,7 +41,7 @@ func TestUpstageChatPropagatesReasoningEffort(t *testing.T) { _, err := u.ChatWithMessages("solar-pro2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{Effort: &effort}) + &ChatConfig{Effort: &effort}, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -68,6 +68,7 @@ func TestUpstageChatOmitsReasoningEffortWhenUnset(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, // no Effort + nil, ) if err != nil { t.Fatalf("Chat: %v", err) @@ -97,6 +98,7 @@ func TestUpstageStreamPropagatesReasoningEffort(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Effort: &effort}, + nil, func(*string, *string) error { return nil }, ) if err != nil { @@ -124,7 +126,7 @@ func TestUpstageChatExtractsReasoningField(t *testing.T) { apiKey := "test-key" resp, err := u.ChatWithMessages("solar-pro3", []Message{{Role: "user", Content: "What is 15% of 80?"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -149,7 +151,7 @@ func TestUpstageChatHandlesAbsentReasoning(t *testing.T) { apiKey := "test-key" resp, err := u.ChatWithMessages("solar-mini", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil) + &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -183,7 +185,7 @@ func TestUpstageRequestBodyMatchesSolarAPIShape(t *testing.T) { _, err := u.ChatWithMessages("solar-pro2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}) + &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}, nil) if err != nil { t.Fatalf("Chat: %v", err) } @@ -225,7 +227,7 @@ func TestUpstageEmbedRejectsDuplicateIndex(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" model := "solar-embedding-1-large-passage" - _, err := u.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := u.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate embedding index 0") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -240,7 +242,7 @@ func TestUpstageEmbedRejectsOutOfRangeIndex(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" model := "solar-embedding-1-large-passage" - _, err := u.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := u.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -259,7 +261,7 @@ func TestUpstageEmbedHappyPathReordersByIndex(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" model := "solar-embedding-1-large-passage" - vecs, err := u.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := u.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -299,7 +301,7 @@ func TestUpstageStreamExtractsReasoningDelta(t *testing.T) { var contentChunks, reasoningChunks []string err := u.ChatStreamlyWithSender("solar-pro3", []Message{{Role: "user", Content: "What is 15% of 80?"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, reason *string) error { // At most one of (content, reason) is set per call: callers // need this contract to route to the right UI channel. @@ -354,7 +356,7 @@ func TestUpstageStreamReasoningChunksArriveBeforeContent(t *testing.T) { var seq []string err := u.ChatStreamlyWithSender("solar-pro3", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, reason *string) error { if reason != nil && *reason != "" { seq = append(seq, "R:"+*reason) @@ -400,7 +402,7 @@ func TestUpstageStreamWithoutReasoningStillWorks(t *testing.T) { var reasonCalled bool err := u.ChatStreamlyWithSender("solar-mini", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &apiKey}, nil, + &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { if r != nil && *r != "" { reasonCalled = true diff --git a/internal/entity/models/vllm.go b/internal/entity/models/vllm.go index af608fcd94..ff8792bca0 100644 --- a/internal/entity/models/vllm.go +++ b/internal/entity/models/vllm.go @@ -53,7 +53,7 @@ func (v *VllmModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns response -func (v *VllmModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (v *VllmModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -204,7 +204,7 @@ func (v *VllmModel) ChatWithMessages(modelName string, messages []Message, apiCo } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (v *VllmModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (v *VllmModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -360,7 +360,7 @@ type vllmEmbeddingResponse struct { } // Embed embeds a list of texts into embeddings -func (v *VllmModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (v *VllmModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -550,7 +550,7 @@ type vllmRerankResponse struct { // Authorization header is sent only when APIConfig.ApiKey is non-empty, // matching the existing Embed/ListModels behaviour for this local // driver. -func (v *VllmModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (v *VllmModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -641,30 +641,30 @@ func (v *VllmModel) Rerank(modelName *string, query string, documents []string, } // TranscribeAudio transcribe audio -func (v *VllmModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (v *VllmModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VllmModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (v *VllmModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } // AudioSpeech convert text to audio -func (v *VllmModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (v *VllmModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VllmModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (v *VllmModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } // OCRFile OCR file -func (v *VllmModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (v *VllmModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } // ParseFile parse file -func (v *VllmModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (v *VllmModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } diff --git a/internal/entity/models/vllm_rerank_test.go b/internal/entity/models/vllm_rerank_test.go index 42fda948c2..19203a1e76 100644 --- a/internal/entity/models/vllm_rerank_test.go +++ b/internal/entity/models/vllm_rerank_test.go @@ -90,6 +90,7 @@ func TestVllmRerankHappyPath(t *testing.T) { []string{"doc-zero", "doc-one", "doc-two"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, + nil, ) if err != nil { t.Fatalf("Rerank failed: %v", err) @@ -122,6 +123,7 @@ func TestVllmRerankTopNClamp(t *testing.T) { []string{"a", "b", "c", "d"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, + nil, ); err != nil { t.Fatalf("Rerank failed: %v", err) } @@ -131,7 +133,7 @@ func TestVllmRerankEmptyDocuments(t *testing.T) { model := newVllmModelForTest("http://unused") apiKey := "test-key" modelName := "BAAI/bge-reranker-v2-m3" - resp, err := model.Rerank(&modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + resp, err := model.Rerank(&modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err != nil { t.Fatalf("expected nil error for empty documents, got %v", err) } @@ -155,7 +157,7 @@ func TestVllmRerankWithoutAPIKey(t *testing.T) { model := newVllmModelForTest(srv.URL) modelName := "BAAI/bge-reranker-v2-m3" - resp, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{}, &RerankConfig{}) + resp, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{}, &RerankConfig{}, nil) if err != nil { t.Fatalf("Rerank failed without api key: %v", err) } @@ -167,7 +169,7 @@ func TestVllmRerankWithoutAPIKey(t *testing.T) { func TestVllmRerankRequiresModelName(t *testing.T) { model := newVllmModelForTest("http://unused") apiKey := "test-key" - _, err := model.Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + _, err := model.Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -183,7 +185,7 @@ func TestVllmRerankRejectsHTTPError(t *testing.T) { model := newVllmModelForTest(srv.URL) apiKey := "test-key" modelName := "BAAI/bge-reranker-v2-m3" - _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "vLLM rerank API error") { t.Errorf("expected API error, got %v", err) } @@ -202,7 +204,7 @@ func TestVllmRerankRejectsOutOfRangeIndex(t *testing.T) { model := newVllmModelForTest(srv.URL) apiKey := "test-key" modelName := "BAAI/bge-reranker-v2-m3" - _, err := model.Rerank(&modelName, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + _, err := model.Rerank(&modelName, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err == nil || !strings.Contains(err.Error(), "unexpected rerank index") { t.Errorf("expected out-of-range error, got %v", err) } diff --git a/internal/entity/models/volcengine.go b/internal/entity/models/volcengine.go index c4d9481690..db318b7d62 100644 --- a/internal/entity/models/volcengine.go +++ b/internal/entity/models/volcengine.go @@ -52,7 +52,7 @@ func (v *VolcEngine) Name() string { } // ChatWithMessages sends multiple messages with roles and returns response -func (v *VolcEngine) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (v *VolcEngine) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -216,7 +216,7 @@ func (v *VolcEngine) ChatWithMessages(modelName string, messages []Message, apiC } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (v *VolcEngine) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (v *VolcEngine) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -417,7 +417,7 @@ type volcenginePromptTokensDetails struct { } // Embed embeds a list of texts into embeddings -func (v *VolcEngine) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (v *VolcEngine) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -509,35 +509,35 @@ func (v *VolcEngine) Embed(modelName *string, texts []string, apiConfig *APIConf } // Rerank calculates similarity scores between query and documents -func (v *VolcEngine) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (v *VolcEngine) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", v.Name()) } // TranscribeAudio transcribe audio -func (v *VolcEngine) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (v *VolcEngine) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VolcEngine) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (v *VolcEngine) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } // AudioSpeech convert text to audio -func (v *VolcEngine) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (v *VolcEngine) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VolcEngine) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (v *VolcEngine) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } // OCRFile OCR file -func (v *VolcEngine) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (v *VolcEngine) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } // ParseFile parse file -func (v *VolcEngine) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (v *VolcEngine) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } diff --git a/internal/entity/models/voyage.go b/internal/entity/models/voyage.go index 95b009baaa..247f0dded3 100644 --- a/internal/entity/models/voyage.go +++ b/internal/entity/models/voyage.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "ragflow/internal/common" "strings" ) @@ -62,7 +63,7 @@ type voyageEmbeddingResponse struct { Model string `json:"model"` } -func (v *VoyageModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (v *VoyageModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -166,7 +167,7 @@ type voyageRerankResponse struct { Model string `json:"model"` } -func (v *VoyageModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (v *VoyageModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -261,11 +262,11 @@ func (v *VoyageModel) CheckConnection(apiConfig *APIConfig) error { } // ChatWithMessages is not exposed by the Voyage AI API. -func (v *VoyageModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (v *VoyageModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (v *VoyageModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } @@ -276,28 +277,28 @@ func (v *VoyageModel) Balance(apiConfig *APIConfig) (map[string]interface{}, err // TranscribeAudio / AudioSpeech / OCRFile: Voyage does not host any of // these surfaces. -func (v *VoyageModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (v *VoyageModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (v *VoyageModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (v *VoyageModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (v *VoyageModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (v *VoyageModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } // ParseFile parse file -func (v *VoyageModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (v *VoyageModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } diff --git a/internal/entity/models/voyage_test.go b/internal/entity/models/voyage_test.go index 7fad9cf2e6..e4f978fd82 100644 --- a/internal/entity/models/voyage_test.go +++ b/internal/entity/models/voyage_test.go @@ -85,7 +85,7 @@ func TestVoyageEmbedHappyPath(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - vecs, err := v.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := v.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -121,7 +121,7 @@ func TestVoyageEmbedPropagatesOutputDimension(t *testing.T) { apiKey := "test-key" model := "voyage-3.5" _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, - &EmbeddingConfig{Dimension: 256}) + &EmbeddingConfig{Dimension: 256}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -144,7 +144,7 @@ func TestVoyageEmbedOmitsOutputDimensionWhenUnset(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -165,7 +165,7 @@ func TestVoyageEmbedReordersByIndex(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - vecs, err := v.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := v.Embed(&model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -185,7 +185,7 @@ func TestVoyageEmbedEmptyInputShortCircuits(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - vecs, err := v.Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil) + vecs, err := v.Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil || len(vecs) != 0 { t.Errorf("Embed([])=(%v,%v)", vecs, err) } @@ -194,7 +194,7 @@ func TestVoyageEmbedEmptyInputShortCircuits(t *testing.T) { func TestVoyageEmbedRequiresAPIKey(t *testing.T) { v := newVoyageForTest("http://unused") model := "voyage-3.5" - _, err := v.Embed(&model, []string{"a"}, &APIConfig{}, nil) + _, err := v.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } @@ -203,7 +203,7 @@ func TestVoyageEmbedRequiresAPIKey(t *testing.T) { func TestVoyageEmbedRequiresModelName(t *testing.T) { v := newVoyageForTest("http://unused") apiKey := "test-key" - _, err := v.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := v.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -223,7 +223,7 @@ func TestVoyageEmbedRejectsDuplicateIndex(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - _, err := v.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := v.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate embedding index 0") { t.Errorf("expected duplicate error, got %v", err) } @@ -242,7 +242,7 @@ func TestVoyageEmbedRejectsOutOfRangeIndex(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - _, err := v.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := v.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -261,7 +261,7 @@ func TestVoyageEmbedRejectsMissingSlot(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - _, err := v.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := v.Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding for input index 1") { t.Errorf("expected missing-slot error, got %v", err) } @@ -292,7 +292,7 @@ func TestVoyageRerankHappyPath(t *testing.T) { apiKey := "test-key" model := "rerank-2" resp, err := v.Rerank(&model, "x", []string{"a", "b", "c"}, - &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 3}) + &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 3}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -320,7 +320,7 @@ func TestVoyageRerankTopKDefaultsToLenDocuments(t *testing.T) { apiKey := "test-key" model := "rerank-2" _, err := v.Rerank(&model, "x", []string{"a", "b", "c", "d"}, - &APIConfig{ApiKey: &apiKey}, &RerankConfig{}) + &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -331,7 +331,7 @@ func TestVoyageRerankEmptyDocuments(t *testing.T) { apiKey := "test-key" model := "rerank-2" resp, err := v.Rerank(&model, "x", nil, - &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 0}) + &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 0}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -354,7 +354,7 @@ func TestVoyageRerankRejectsOutOfRangeIndex(t *testing.T) { apiKey := "test-key" model := "rerank-2" _, err := v.Rerank(&model, "x", []string{"a", "b"}, - &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}) + &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -378,7 +378,7 @@ func TestVoyageRerankRejectsDuplicateIndex(t *testing.T) { apiKey := "test-key" model := "rerank-2" _, err := v.Rerank(&model, "x", []string{"a", "b"}, - &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}) + &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, nil) if err == nil || !strings.Contains(err.Error(), "duplicate rerank index 0") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -404,7 +404,7 @@ func TestVoyageEmbedTrimsTrailingSlashInBaseURL(t *testing.T) { ) apiKey := "test-key" model := "voyage-3.5" - if _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil); err != nil { + if _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { t.Fatalf("Embed: %v", err) } if sawPath != "/v1/embeddings" { diff --git a/internal/entity/models/xai.go b/internal/entity/models/xai.go index ab64310ec1..303e7e5d5c 100644 --- a/internal/entity/models/xai.go +++ b/internal/entity/models/xai.go @@ -26,6 +26,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" "time" @@ -62,7 +63,7 @@ func (x *XAIModel) Name() string { } // ChatWithMessages sends multiple messages with roles and returns the response -func (x *XAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (x *XAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -189,7 +190,7 @@ func (x *XAIModel) ChatWithMessages(modelName string, messages []Message, apiCon } // ChatStreamlyWithSender sends messages and streams the response -func (x *XAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (x *XAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -325,7 +326,7 @@ func (x *XAIModel) ChatStreamlyWithSender(modelName string, messages []Message, // Embed embeds a list of texts into embeddings. xAI does not expose a // public embedding API yet, so this is left unimplemented. -func (x *XAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (x *XAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } @@ -400,12 +401,12 @@ func (x *XAIModel) CheckConnection(apiConfig *APIConfig) error { } // Rerank calculates similarity scores between query and documents -func (x *XAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (x *XAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, Rerank not implemented", x.Name()) } // TranscribeAudio transcribe audio -func (x *XAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (x *XAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -534,12 +535,12 @@ func (x *XAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *A return &ASRResponse{Text: result.Text}, nil } -func (x *XAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (x *XAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", x.Name()) } // AudioSpeech convert text to audio -func (x *XAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (x *XAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -601,17 +602,17 @@ func (x *XAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfi return &TTSResponse{Audio: body}, nil } -func (x *XAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (x *XAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", x.Name()) } // OCRFile OCR file -func (x *XAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (x *XAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } // ParseFile parse file -func (x *XAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (x *XAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } diff --git a/internal/entity/models/xiaomi.go b/internal/entity/models/xiaomi.go index 5ed0c0e592..867fe985d0 100644 --- a/internal/entity/models/xiaomi.go +++ b/internal/entity/models/xiaomi.go @@ -27,6 +27,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "sort" "strings" ) @@ -53,7 +54,7 @@ func (x *XiaomiModel) Name() string { return "xiaomi" } -func (x *XiaomiModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (x *XiaomiModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -167,7 +168,7 @@ func (x *XiaomiModel) ChatWithMessages(modelName string, messages []Message, api // Parse response var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { + if err = json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } @@ -224,7 +225,7 @@ func (x *XiaomiModel) ChatWithMessages(modelName string, messages []Message, api return chatResponse, nil } -func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -339,7 +340,7 @@ func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Messag modelConfig.ToolCallsResult = nil } accumulatedToolCalls := make(map[int]map[string]interface{}) - if _, err := ParseSSEStreamTolerant[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { + if _, err = ParseSSEStreamTolerant[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { choices, ok := event["choices"].([]interface{}) if !ok || len(choices) == 0 { return nil @@ -402,14 +403,14 @@ func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Messag } reasoningContent, ok := delta["reasoning_content"].(string) if ok && reasoningContent != "" { - if err := sender(nil, &reasoningContent); err != nil { + if err = sender(nil, &reasoningContent); err != nil { return err } } content, ok := delta["content"].(string) if ok && content != "" { - if err := sender(&content, nil); err != nil { + if err = sender(&content, nil); err != nil { return err } } @@ -441,15 +442,15 @@ func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Messag return nil } -func (x *XiaomiModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (x *XiaomiModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } -func (x *XiaomiModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (x *XiaomiModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } -func (x *XiaomiModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (x *XiaomiModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) defer cancel() @@ -484,7 +485,7 @@ func (x *XiaomiModel) TranscribeAudio(modelName *string, file *string, apiConfig return &ASRResponse{Text: result.Choices[0].Message.Content}, nil } -func (x *XiaomiModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (x *XiaomiModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") } @@ -695,7 +696,7 @@ func readXiaomiASRStream(body io.Reader, sender func(*string, *string) error) er return sender(&done, nil) } -func (x *XiaomiModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (x *XiaomiModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) defer cancel() @@ -721,7 +722,7 @@ func (x *XiaomiModel) AudioSpeech(modelName *string, audioContent *string, apiCo return decodeXiaomiTTSResponse(body) } -func (x *XiaomiModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (x *XiaomiModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") } @@ -894,11 +895,11 @@ func decodeXiaomiAudioData(data string) ([]byte, error) { return base64.StdEncoding.DecodeString(data) } -func (x *XiaomiModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (x *XiaomiModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } -func (x *XiaomiModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (x *XiaomiModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } diff --git a/internal/entity/models/xiaomi_test.go b/internal/entity/models/xiaomi_test.go index f72baa52be..03b1caca01 100644 --- a/internal/entity/models/xiaomi_test.go +++ b/internal/entity/models/xiaomi_test.go @@ -115,7 +115,7 @@ func TestXiaomiChatHappyPath(t *testing.T) { "mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, - &ChatConfig{MaxTokens: &maxTokens, Thinking: &thinking}, + &ChatConfig{MaxTokens: &maxTokens, Thinking: &thinking}, nil, ) if err != nil { t.Fatalf("ChatWithMessages: %v", err) @@ -145,7 +145,7 @@ func TestXiaomiUsesEmptyRegionBaseURLOverride(t *testing.T) { map[string]string{"default": srv.URL}, URLSuffix{Chat: "v1/chat/completions"}, ) - resp, err := m.ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := m.ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -172,7 +172,7 @@ func TestXiaomiAPIConfigBaseURLOverridesRegionMap(t *testing.T) { map[string]string{"default": "http://unused"}, URLSuffix{Chat: "override/chat"}, ) - resp, err := m.ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey, BaseURL: &baseURL}, nil) + resp, err := m.ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey, BaseURL: &baseURL}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -195,7 +195,7 @@ func TestXiaomiChatExtractsReasoningContent(t *testing.T) { defer srv.Close() apiKey := "test-key" - resp, err := newXiaomiForTest(srv.URL).ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := newXiaomiForTest(srv.URL).ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -207,13 +207,13 @@ func TestXiaomiChatExtractsReasoningContent(t *testing.T) { func TestXiaomiChatRequiresInputs(t *testing.T) { apiKey := "test-key" m := newXiaomiForTest("http://unused") - if _, err := m.ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := m.ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("api key guard: %v", err) } - if _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("model guard: %v", err) } - if _, err := m.ChatWithMessages("mimo-v2.5-pro", nil, &APIConfig{ApiKey: &apiKey}, nil); err == nil || !strings.Contains(err.Error(), "messages is empty") { + if _, err := m.ChatWithMessages("mimo-v2.5-pro", nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("messages guard: %v", err) } } @@ -226,7 +226,7 @@ func TestXiaomiChatRejectsHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newXiaomiForTest(srv.URL).ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newXiaomiForTest(srv.URL).ChatWithMessages("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } @@ -254,6 +254,7 @@ func TestXiaomiStreamHappyPath(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(c *string, r *string) error { if c != nil && *c == "[DONE]" { sawDone = true @@ -299,6 +300,7 @@ func TestXiaomiStreamHandlesCRLFFrames(t *testing.T) { []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(c *string, _ *string) error { if c != nil && *c != "[DONE]" { content = append(content, *c) @@ -323,7 +325,7 @@ func TestXiaomiStreamRejectsMalformedFrame(t *testing.T) { apiKey := "test-key" // Malformed SSE frames are silently skipped; the stream completes and sends [DONE]. - err := newXiaomiForTest(srv.URL).ChatStreamlyWithSender("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, func(*string, *string) error { return nil }) + err := newXiaomiForTest(srv.URL).ChatStreamlyWithSender("mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) if err != nil { t.Errorf("expected no error on malformed frame, got %v", err) } @@ -335,10 +337,10 @@ func TestXiaomiUnsupportedMethods(t *testing.T) { apiKey := "test-key" cfg := &APIConfig{ApiKey: &apiKey} - if _, err := m.Embed(&model, []string{"x"}, cfg, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(&model, []string{"x"}, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Embed: %v", err) } - if _, err := m.Rerank(&model, "q", []string{"d"}, cfg, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(&model, "q", []string{"d"}, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Rerank: %v", err) } // CheckConnection IS implemented — verifies API config and base URL are reachable. @@ -346,14 +348,14 @@ func TestXiaomiUnsupportedMethods(t *testing.T) { t.Errorf("CheckConnection: %v", err) } // TranscribeAudio IS implemented; with nil file it returns input validation error. - if _, err := m.TranscribeAudio(&model, nil, cfg, nil); err == nil || !strings.Contains(err.Error(), "file is missing") { + if _, err := m.TranscribeAudio(&model, nil, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "file is missing") { t.Errorf("TranscribeAudio: %v", err) } // AudioSpeech IS implemented; with nil content it returns input validation error. - if _, err := m.AudioSpeech(&model, nil, cfg, nil); err == nil || !strings.Contains(err.Error(), "audio content is empty") { + if _, err := m.AudioSpeech(&model, nil, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "audio content is empty") { t.Errorf("AudioSpeech: %v", err) } - if _, err := m.OCRFile(&model, nil, nil, cfg, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(&model, nil, nil, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } diff --git a/internal/entity/models/xinference.go b/internal/entity/models/xinference.go index 7908f92f1e..8aa492b5da 100644 --- a/internal/entity/models/xinference.go +++ b/internal/entity/models/xinference.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "path/filepath" + "ragflow/internal/common" "strconv" "strings" "sync" @@ -142,7 +143,7 @@ func buildXinferenceChatBody(modelName string, messages []Message, stream bool, } // ChatWithMessages sends multiple messages with roles and returns the response. -func (x *XinferenceModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (x *XinferenceModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -212,7 +213,7 @@ func (x *XinferenceModel) ChatWithMessages(modelName string, messages []Message, } // ChatStreamlyWithSender sends messages and streams response via sender. -func (x *XinferenceModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (x *XinferenceModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -341,7 +342,7 @@ type xinferenceEmbeddingResponse struct { } // Embed POSTs the input texts to the tenant's Xinference -func (x *XinferenceModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (x *XinferenceModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -449,7 +450,7 @@ type xinferenceRerankResponse struct { // in the API's ranking order. Caller may sort by Index to recover // original input order. Xinference rerank models are launched with // --model-type rerank and exposed under the OpenAI-compatible base URL. -func (x *XinferenceModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (x *XinferenceModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -540,7 +541,7 @@ func (x *XinferenceModel) Rerank(modelName *string, query string, documents []st return &rerankResponse, nil } -func (x *XinferenceModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (x *XinferenceModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -649,11 +650,11 @@ func (x *XinferenceModel) TranscribeAudio(modelName *string, file *string, apiCo }, nil } -func (x *XinferenceModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (x *XinferenceModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", x.Name()) } -func (x *XinferenceModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (x *XinferenceModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -715,15 +716,15 @@ func (x *XinferenceModel) AudioSpeech(modelName *string, audioContent *string, a return &TTSResponse{Audio: body}, nil } -func (x *XinferenceModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (x *XinferenceModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", x.Name()) } -func (x *XinferenceModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (x *XinferenceModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XinferenceModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (x *XinferenceModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } diff --git a/internal/entity/models/xinference_test.go b/internal/entity/models/xinference_test.go index 81a03f222d..4ff820c900 100644 --- a/internal/entity/models/xinference_test.go +++ b/internal/entity/models/xinference_test.go @@ -93,7 +93,7 @@ func TestXinferenceChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) resp, err := x.ChatWithMessages("qwen2.5-instruct", []Message{{Role: "user", Content: "ping"}}, &APIConfig{}, - &ChatConfig{MaxTokens: &maxTokens, Temperature: &temp}) + &ChatConfig{MaxTokens: &maxTokens, Temperature: &temp}, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -124,7 +124,7 @@ func TestXinferenceChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { key := "sk-test" _, err := x.ChatWithMessages("qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, - &APIConfig{ApiKey: &key}, nil) + &APIConfig{ApiKey: &key}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -142,7 +142,7 @@ func TestXinferenceChatExtractsReasoningFields(t *testing.T) { x := newXinferenceForTest(srv.URL) resp, err := x.ChatWithMessages("qwen3", []Message{{Role: "user", Content: "15% of 80?"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -178,7 +178,7 @@ func TestXinferenceStreamHappyPath(t *testing.T) { var sawDone bool err := x.ChatStreamlyWithSender("qwen2.5-instruct", []Message{{Role: "user", Content: "hi"}}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, func(c *string, r *string) error { if r != nil && *r != "" { reasoning = append(reasoning, *r) @@ -212,6 +212,7 @@ func TestXinferenceStreamRejectsFalseStreamConfig(t *testing.T) { []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Stream: &stream}, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream must be true") { t.Errorf("expected stream-must-be-true error, got %v", err) @@ -241,6 +242,7 @@ func TestXinferenceStreamCancelsOnIdle(t *testing.T) { err := x.ChatStreamlyWithSender("qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, + nil, func(*string, *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream idle") { t.Errorf("expected stream-idle error, got %v", err) @@ -320,7 +322,7 @@ func TestXinferenceEmbedHappyPathAndOmitsEmptyAuth(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - got, err := x.Embed(&model, []string{"hello", "world"}, &APIConfig{}, nil) + got, err := x.Embed(&model, []string{"hello", "world"}, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -346,7 +348,7 @@ func TestXinferenceEmbedSendsAuthWhenKeyConfigured(t *testing.T) { x := newXinferenceForTest(srv.URL) key := "sk-test" model := "bge-m3" - if _, err := x.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &key}, nil); err != nil { + if _, err := x.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &key}, nil, nil); err != nil { t.Fatalf("Embed: %v", err) } if gotAuth != "Bearer sk-test" { @@ -365,7 +367,7 @@ func TestXinferenceEmbedNormalizesBaseURLWithV1Suffix(t *testing.T) { URLSuffix{Chat: "v1/chat/completions", Embedding: "v1/embeddings", Models: "v1/models"}, ) model := "bge-m3" - if _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil); err != nil { + if _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err != nil { t.Fatalf("Embed: %v", err) } } @@ -381,7 +383,7 @@ func TestXinferenceEmbedForwardsDimension(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - if _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, &EmbeddingConfig{Dimension: 384}); err != nil { + if _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, &EmbeddingConfig{Dimension: 384}, nil); err != nil { t.Fatalf("Embed: %v", err) } } @@ -394,7 +396,7 @@ func TestXinferenceEmbedRejectsDuplicateIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil) + _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate embedding index") { t.Errorf("expected duplicate-index error, got %v", err) } @@ -408,7 +410,7 @@ func TestXinferenceEmbedRejectsOutOfRangeIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil) + _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("expected out-of-range error, got %v", err) } @@ -423,7 +425,7 @@ func TestXinferenceEmbedRejectsMissingIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil) + _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "missing embedding") { t.Errorf("expected missing-embedding error, got %v", err) } @@ -432,7 +434,7 @@ func TestXinferenceEmbedRejectsMissingIndex(t *testing.T) { func TestXinferenceEmbedEmptyTextsShortCircuits(t *testing.T) { x := newXinferenceForTest("http://unused") model := "bge-m3" - got, err := x.Embed(&model, nil, &APIConfig{}, nil) + got, err := x.Embed(&model, nil, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("expected nil error for empty inputs, got %v", err) } @@ -443,7 +445,7 @@ func TestXinferenceEmbedEmptyTextsShortCircuits(t *testing.T) { func TestXinferenceEmbedRequiresModelName(t *testing.T) { x := newXinferenceForTest("http://unused") - _, err := x.Embed(nil, []string{"x"}, &APIConfig{}, nil) + _, err := x.Embed(nil, []string{"x"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("expected model-name error, got %v", err) } @@ -458,7 +460,7 @@ func TestXinferenceEmbedSurfacesHTTPError(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil) + _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "Xinference embeddings API error") { t.Errorf("expected API error, got %v", err) } @@ -470,7 +472,7 @@ func TestXinferenceEmbedRejectsMissingEmbeddingSuffix(t *testing.T) { URLSuffix{Chat: "v1/chat/completions"}, // no Embedding suffix ) model := "bge-m3" - _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil) + _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no embedding URL suffix configured") { t.Errorf("expected missing-suffix error, got %v", err) } @@ -480,7 +482,7 @@ func TestXinferenceMissingBaseURLFailsClearly(t *testing.T) { x := NewXinferenceModel(map[string]string{}, URLSuffix{Chat: "v1/chat/completions"}) _, err := x.ChatWithMessages("qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, - &APIConfig{}, nil) + &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "base URL") { t.Errorf("expected missing-base-URL error, got %v", err) } @@ -496,20 +498,20 @@ func TestXinferenceUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { // TranscribeAudio IS implemented; it validates inputs after APIConfigCheck. // The test passes nil file, which should yield an input-validation error, // not an api-key error. - if _, err := x.TranscribeAudio(&model, nil, &APIConfig{}, nil); err == nil || strings.Contains(err.Error(), "api key is required") { + if _, err := x.TranscribeAudio(&model, nil, &APIConfig{}, nil, nil); err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("TranscribeAudio: expected input validation error (file missing), got %v", err) } - if err := x.TranscribeAudioWithSender(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := x.TranscribeAudioWithSender(&model, nil, &APIConfig{}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("TranscribeAudioWithSender: expected no such method, got %v", err) } // AudioSpeech IS implemented; it validates inputs after APIConfigCheck. - if _, err := x.AudioSpeech(&model, nil, &APIConfig{}, nil); err == nil || strings.Contains(err.Error(), "api key is required") { + if _, err := x.AudioSpeech(&model, nil, &APIConfig{}, nil, nil); err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("AudioSpeech: expected input validation error (text missing), got %v", err) } - if err := x.AudioSpeechWithSender(&model, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := x.AudioSpeechWithSender(&model, nil, &APIConfig{}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("AudioSpeechWithSender: expected no such method, got %v", err) } - if _, err := x.OCRFile(&model, nil, nil, &APIConfig{}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := x.OCRFile(&model, nil, nil, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: expected no such method, got %v", err) } } @@ -568,7 +570,7 @@ func TestXinferenceRerankHappyPathReordersByIndex(t *testing.T) { model := "bge-reranker-v2-m3" resp, err := x.Rerank(&model, "capital of France", []string{"Paris is the capital of France.", "Eiffel Tower.", "Berlin is the capital of Germany."}, - &APIConfig{}, nil, + &APIConfig{}, nil, nil, ) if err != nil { t.Fatalf("Rerank: %v", err) @@ -596,7 +598,7 @@ func TestXinferenceRerankNormalizesV1BaseURL(t *testing.T) { ) apiKey := "test-key" model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil) + _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -613,7 +615,7 @@ func TestXinferenceRerankRespectsTopNConfig(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a", "b", "c", "d"}, &APIConfig{}, &RerankConfig{TopN: 2}) + _, err := x.Rerank(&model, "q", []string{"a", "b", "c", "d"}, &APIConfig{}, &RerankConfig{TopN: 2}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -622,7 +624,7 @@ func TestXinferenceRerankRespectsTopNConfig(t *testing.T) { func TestXinferenceRerankEmptyDocumentsShortCircuits(t *testing.T) { x := newXinferenceForTest("http://unused") model := "bge-reranker-v2-m3" - resp, err := x.Rerank(&model, "q", nil, &APIConfig{}, nil) + resp, err := x.Rerank(&model, "q", nil, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -633,7 +635,7 @@ func TestXinferenceRerankEmptyDocumentsShortCircuits(t *testing.T) { func TestXinferenceRerankRequiresModelName(t *testing.T) { x := newXinferenceForTest("http://unused") - _, err := x.Rerank(nil, "q", []string{"a"}, &APIConfig{}, nil) + _, err := x.Rerank(nil, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("err=%v", err) } @@ -649,7 +651,7 @@ func TestXinferenceRerankRejectsOutOfRangeIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a", "b"}, &APIConfig{}, nil) + _, err := x.Rerank(&model, "q", []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("err=%v", err) } @@ -668,7 +670,7 @@ func TestXinferenceRerankRejectsDuplicateIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a", "b"}, &APIConfig{}, nil) + _, err := x.Rerank(&model, "q", []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate") { t.Errorf("err=%v", err) } @@ -683,7 +685,7 @@ func TestXinferenceRerankSurfacesHTTPError(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil) + _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "Xinference rerank API error") { t.Errorf("err=%v", err) } @@ -695,7 +697,7 @@ func TestXinferenceRerankRejectsMissingRerankSuffix(t *testing.T) { URLSuffix{Chat: "v1/chat/completions"}, ) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil) + _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "no rerank URL suffix configured") { t.Errorf("err=%v", err) } diff --git a/internal/entity/models/xunfei.go b/internal/entity/models/xunfei.go index c331e39a2c..80bbe35d78 100644 --- a/internal/entity/models/xunfei.go +++ b/internal/entity/models/xunfei.go @@ -48,7 +48,7 @@ func (x *XunFeiModel) Name() string { return "XunFei Spark" } -func (x *XunFeiModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (x *XunFeiModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -180,7 +180,7 @@ func (x *XunFeiModel) ChatWithMessages(modelName string, messages []Message, api return chatResponse, nil } -func (x *XunFeiModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { +func (x *XunFeiModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -321,35 +321,35 @@ func (x *XunFeiModel) ChatStreamlyWithSender(modelName string, messages []Messag return nil } -func (x *XunFeiModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +func (x *XunFeiModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (x *XunFeiModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (x *XunFeiModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (x *XunFeiModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (x *XunFeiModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (x *XunFeiModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (x *XunFeiModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (x *XunFeiModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } diff --git a/internal/entity/models/xunfei_test.go b/internal/entity/models/xunfei_test.go index 3b79fca74e..a1ae98ee46 100644 --- a/internal/entity/models/xunfei_test.go +++ b/internal/entity/models/xunfei_test.go @@ -13,33 +13,33 @@ func TestXunFeiUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { call func() error }{ {"Embed", func() error { - _, err := driver.Embed(&modelName, []string{text}, &APIConfig{}, nil) + _, err := driver.Embed(&modelName, []string{text}, &APIConfig{}, nil, nil) return err }}, {"Rerank", func() error { - _, err := driver.Rerank(&modelName, text, []string{text}, &APIConfig{}, nil) + _, err := driver.Rerank(&modelName, text, []string{text}, &APIConfig{}, nil, nil) return err }}, {"TranscribeAudio", func() error { - _, err := driver.TranscribeAudio(&modelName, &text, &APIConfig{}, nil) + _, err := driver.TranscribeAudio(&modelName, &text, &APIConfig{}, nil, nil) return err }}, {"TranscribeAudioWithSender", func() error { - return driver.TranscribeAudioWithSender(&modelName, &text, &APIConfig{}, nil, nil) + return driver.TranscribeAudioWithSender(&modelName, &text, &APIConfig{}, nil, nil, nil) }}, {"AudioSpeech", func() error { - _, err := driver.AudioSpeech(&modelName, &text, &APIConfig{}, nil) + _, err := driver.AudioSpeech(&modelName, &text, &APIConfig{}, nil, nil) return err }}, {"AudioSpeechWithSender", func() error { - return driver.AudioSpeechWithSender(&modelName, &text, &APIConfig{}, nil, nil) + return driver.AudioSpeechWithSender(&modelName, &text, &APIConfig{}, nil, nil, nil) }}, {"OCRFile", func() error { - _, err := driver.OCRFile(&modelName, nil, &text, &APIConfig{}, nil) + _, err := driver.OCRFile(&modelName, nil, &text, &APIConfig{}, nil, nil) return err }}, {"ParseFile", func() error { - _, err := driver.ParseFile(&modelName, nil, &text, &APIConfig{}, nil) + _, err := driver.ParseFile(&modelName, nil, &text, &APIConfig{}, nil, nil) return err }}, {"Balance", func() error { diff --git a/internal/entity/models/zhipu-ai.go b/internal/entity/models/zhipu-ai.go index e108e25a37..9801fcae84 100644 --- a/internal/entity/models/zhipu-ai.go +++ b/internal/entity/models/zhipu-ai.go @@ -28,7 +28,11 @@ import ( "os" "path/filepath" "ragflow/internal/common" + "ragflow/internal/engine/clickhouse" "strings" + "time" + + "github.com/mitchellh/mapstructure" ) // ZhipuAIModel implements ModelDriver for Zhipu AI @@ -81,7 +85,7 @@ type ZhipuChatResponse struct { } // ChatWithMessages sends multiple messages with roles and returns response -func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { +func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -185,6 +189,10 @@ func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to parse response: %w", err) } + if result.Choices == nil || len(result.Choices) == 0 { + return nil, fmt.Errorf("empty response") + } + content := &result.Choices[0].Message.Content var reasonContent *string @@ -192,12 +200,24 @@ func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, ap reasonContent = &result.Choices[0].Message.ReasoningContent } - usage := &ChatUsage{ + usage := &TokenUsage{ PromptTokens: result.Usage.PromptTokens, CompletionTokens: result.Usage.CompletionTokens, TotalTokens: result.Usage.TotalTokens, } + modelUsage.RequestID = result.RequestId + modelUsage.InputTokens = result.Usage.PromptTokens + modelUsage.OutputTokens = result.Usage.CompletionTokens + modelUsage.TotalTokens = result.Usage.TotalTokens + modelUsage.ResponseTimeMS = time.Since(modelUsage.StartAt).Milliseconds() + + clickhouseDriver := clickhouse.GetDriver() + err = clickhouseDriver.CollectModelUsage(modelUsage) + if err != nil { + return nil, fmt.Errorf("failed to collect model usage: %w", err) + } + chatResponse := &ChatResponse{ Answer: content, ReasonContent: reasonContent, @@ -208,7 +228,7 @@ func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, ap } // ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel) -func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { +func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -307,7 +327,7 @@ func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName string, messages []Messa } // SSE parsing: read line by line - if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { + if _, err = ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error { common.Info(fmt.Sprintf("%v", event)) choices, ok := event["choices"].([]interface{}) @@ -327,18 +347,31 @@ func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName string, messages []Messa reasoningContent, ok := delta["reasoning_content"].(string) if ok && reasoningContent != "" { - if err := sender(nil, &reasoningContent); err != nil { + if err = sender(nil, &reasoningContent); err != nil { return err } } content, ok := delta["content"].(string) if ok && content != "" { - if err := sender(&content, nil); err != nil { + if err = sender(&content, nil); err != nil { return err } } + tokenUsageMap, ok := event["usage"].(map[string]interface{}) + if ok { + tokenUsage := TokenUsage{} + err = mapstructure.Decode(tokenUsageMap, &tokenUsage) + modelUsage.InputTokens = tokenUsage.PromptTokens + modelUsage.OutputTokens = tokenUsage.CompletionTokens + modelUsage.TotalTokens = tokenUsage.TotalTokens + modelUsage.ResponseTimeMS = time.Since(modelUsage.StartAt).Milliseconds() + clickhouseDriver := clickhouse.GetDriver() + err = clickhouseDriver.CollectModelUsage(modelUsage) + return nil + } + return nil }); err != nil { return fmt.Errorf("failed to scan response body: %w", err) @@ -372,8 +405,8 @@ type zhipuUsage struct { TotalTokens int `json:"total_tokens"` } -// Encode encodes a list of texts into embeddings -func (z *ZhipuAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { +// Embed embeddings a list of texts into embeddings +func (z *ZhipuAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -576,7 +609,7 @@ type zhipuOCRResponse struct { // Rerank calculates similarity scores between query and documents using // the ZhipuAI /rerank endpoint (e.g. glm-rerank). The result is one // score per input text, in the same order the documents were given. -func (z *ZhipuAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { +func (z *ZhipuAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -658,7 +691,7 @@ func (z *ZhipuAIModel) Rerank(modelName *string, query string, documents []strin } // TranscribeAudio transcribe audio -func (z *ZhipuAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { +func (z *ZhipuAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -682,13 +715,13 @@ func (z *ZhipuAIModel) TranscribeAudio(modelName *string, file *string, apiConfi var body bytes.Buffer writer := multipart.NewWriter(&body) - if err := writer.WriteField("model", *modelName); err != nil { + if err = writer.WriteField("model", *modelName); err != nil { return nil, fmt.Errorf("failed to write model field: %w", err) } - if err := writer.WriteField("stream", "false"); err != nil { + if err = writer.WriteField("stream", "false"); err != nil { return nil, fmt.Errorf("failed to write stream field: %w", err) } - if err := writeZhipuASRParams(writer, asrConfig); err != nil { + if err = writeZhipuASRParams(writer, asrConfig); err != nil { return nil, err } @@ -785,12 +818,12 @@ func writeZhipuASRField(writer *multipart.Writer, key string, value interface{}) } } -func (z *ZhipuAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { +func (z *ZhipuAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("%s, no such method", z.Name()) } // AudioSpeech convert text to audio -func (z *ZhipuAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { +func (z *ZhipuAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -832,7 +865,7 @@ func (z *ZhipuAIModel) AudioSpeech(modelName *string, audioContent *string, apiC return &TTSResponse{Audio: body}, nil } -func (z *ZhipuAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { +func (z *ZhipuAIModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -930,7 +963,7 @@ func (z *ZhipuAIModel) buildTTSRequest(modelName *string, audioContent *string, } // OCRFile OCR file -func (z *ZhipuAIModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { +func (z *ZhipuAIModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -1013,7 +1046,7 @@ func (z *ZhipuAIModel) OCRFile(modelName *string, content []byte, fileURL *strin } // ParseFile parse file -func (z *ZhipuAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { +func (z *ZhipuAIModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { return nil, fmt.Errorf("%s, no such method", z.Name()) } diff --git a/internal/entity/models/zhipu-ai_test.go b/internal/entity/models/zhipu-ai_test.go index 44b302bc19..4e4c0fbafd 100644 --- a/internal/entity/models/zhipu-ai_test.go +++ b/internal/entity/models/zhipu-ai_test.go @@ -52,7 +52,7 @@ func TestZhipuAIOCRFileSendsLayoutParsingRequest(t *testing.T) { defer server.Close() model := NewZhipuAIModel(map[string]string{"default": server.URL}, URLSuffix{OCR: "layout_parsing"}) - resp, err := model.OCRFile(&modelName, nil, &fileURL, &APIConfig{ApiKey: &apiKey}, nil) + resp, err := model.OCRFile(&modelName, nil, &fileURL, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("OCRFile returned error: %v", err) } @@ -81,7 +81,7 @@ func TestZhipuAIOCRFileEncodesContent(t *testing.T) { defer server.Close() model := NewZhipuAIModel(map[string]string{"default": server.URL}, URLSuffix{OCR: "layout_parsing"}) - if _, err := model.OCRFile(&modelName, content, nil, &APIConfig{ApiKey: &apiKey}, nil); err != nil { + if _, err := model.OCRFile(&modelName, content, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { t.Fatalf("OCRFile returned error: %v", err) } } @@ -106,7 +106,7 @@ func TestZhipuAIOCRFileDetectsPDFContent(t *testing.T) { defer server.Close() model := NewZhipuAIModel(map[string]string{"default": server.URL}, URLSuffix{OCR: "layout_parsing"}) - if _, err := model.OCRFile(&modelName, content, nil, &APIConfig{ApiKey: &apiKey}, nil); err != nil { + if _, err := model.OCRFile(&modelName, content, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { t.Fatalf("OCRFile returned error: %v", err) } } @@ -149,7 +149,7 @@ func TestZhipuAIOCRFileValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := model.OCRFile(tt.modelName, nil, tt.fileURL, tt.apiConfig, nil) + _, err := model.OCRFile(tt.modelName, nil, tt.fileURL, tt.apiConfig, nil, nil) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("error = %v, want containing %q", err, tt.want) } diff --git a/internal/entity/models/zhipu_ai_test.go b/internal/entity/models/zhipu_ai_test.go index f68d6aa3c4..73a38c246a 100644 --- a/internal/entity/models/zhipu_ai_test.go +++ b/internal/entity/models/zhipu_ai_test.go @@ -104,6 +104,7 @@ func TestZhipuAITranscribeAudio(t *testing.T) { "user_id": 12345, "nil_value": nil, }}, + nil, ) if err != nil { t.Fatalf("TranscribeAudio: %v", err) @@ -132,7 +133,7 @@ func TestZhipuAITranscribeAudioValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := newZhipuAIForTest("http://unused").TranscribeAudio(tt.modelName, tt.file, tt.apiConfig, nil) + _, err := newZhipuAIForTest("http://unused").TranscribeAudio(tt.modelName, tt.file, tt.apiConfig, nil, nil) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("error=%v, want %q", err, tt.want) } @@ -149,6 +150,7 @@ func TestZhipuAITranscribeAudioRequiresASRSuffix(t *testing.T) { &file, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "ASR URL suffix is not configured") { t.Fatalf("error=%v", err) @@ -165,7 +167,7 @@ func TestZhipuAITranscribeAudioHTTPError(t *testing.T) { apiKey := "test-key" modelName := "glm-asr-2512" file := writeZhipuAITestAudio(t) - _, err := newZhipuAIForTest(srv.URL).TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newZhipuAIForTest(srv.URL).TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "ZhipuAI ASR API error: 400 Bad Request") { t.Fatalf("error=%v", err) } @@ -229,6 +231,7 @@ func TestZhipuAIAudioSpeech(t *testing.T) { "stream": true, "response_format": "wav", }}, + nil, ) if err != nil { t.Fatalf("AudioSpeech: %v", err) @@ -262,6 +265,7 @@ func TestZhipuAIAudioSpeechWithSender(t *testing.T) { &content, &APIConfig{ApiKey: &apiKey}, nil, + nil, func(content *string, reasoning *string) error { if content != nil { chunks = append(chunks, *content) @@ -299,7 +303,7 @@ func TestZhipuAIAudioSpeechValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := newZhipuAIForTest("http://unused").AudioSpeech(tt.modelName, tt.audioContent, tt.apiConfig, nil) + _, err := newZhipuAIForTest("http://unused").AudioSpeech(tt.modelName, tt.audioContent, tt.apiConfig, nil, nil) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("error=%v, want %q", err, tt.want) } @@ -316,6 +320,7 @@ func TestZhipuAIAudioSpeechRequiresTTSSuffix(t *testing.T) { &content, &APIConfig{ApiKey: &apiKey}, nil, + nil, ) if err == nil || !strings.Contains(err.Error(), "TTS URL suffix is not configured") { t.Fatalf("error=%v", err) @@ -332,7 +337,7 @@ func TestZhipuAIAudioSpeechHTTPError(t *testing.T) { apiKey := "test-key" modelName := "glm-tts" content := "hello" - _, err := newZhipuAIForTest(srv.URL).AudioSpeech(&modelName, &content, &APIConfig{ApiKey: &apiKey}, nil) + _, err := newZhipuAIForTest(srv.URL).AudioSpeech(&modelName, &content, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "ZhipuAI TTS API error: 400 Bad Request") { t.Fatalf("error=%v", err) } diff --git a/internal/handler/chat_audio.go b/internal/handler/chat_audio.go index 40e86d7e86..62000e6637 100644 --- a/internal/handler/chat_audio.go +++ b/internal/handler/chat_audio.go @@ -84,7 +84,7 @@ func (h *ChatHandler) ChatAudioSpeech(c *gin.Context) { if seg == "" { continue } - resp, err := driver.AudioSpeech(&modelName, &seg, apiConfig, &modelModule.TTSConfig{Format: "mp3"}) + resp, err := driver.AudioSpeech(&modelName, &seg, apiConfig, &modelModule.TTSConfig{Format: "mp3"}, nil) if err != nil { common.Warn("chat TTS synthesis failed", zap.Int("segmentIndex", i), @@ -234,7 +234,7 @@ func (h *ChatHandler) ChatAudioTranscription(c *gin.Context) { return nil } - if err := driver.TranscribeAudioWithSender(&modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, sender); err != nil { + if err = driver.TranscribeAudioWithSender(&modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil, sender); err != nil { errEvent := map[string]interface{}{"event": "error", "text": err.Error()} data, _ := json.Marshal(errEvent) _, _ = c.Writer.WriteString(fmt.Sprintf("data: %s\n\n", data)) @@ -252,7 +252,7 @@ func (h *ChatHandler) ChatAudioTranscription(c *gin.Context) { return } - resp, err := driver.TranscribeAudio(&modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}) + resp, err := driver.TranscribeAudio(&modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil) if err != nil { common.ErrorWithCode(c, common.CodeServerError, err.Error()) return diff --git a/internal/handler/providers.go b/internal/handler/providers.go index 8f35a1c6b7..47cb33c5ec 100644 --- a/internal/handler/providers.go +++ b/internal/handler/providers.go @@ -25,6 +25,7 @@ import ( "ragflow/internal/entity/models" "ragflow/internal/service" "strings" + "time" "github.com/gin-gonic/gin" ) @@ -722,8 +723,6 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { } } - userID := c.GetString("user_id") - if !req.Thinking { req.Effort = nil req.Verbosity = nil @@ -747,6 +746,16 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { Verbosity: req.Verbosity, } + userID := c.GetString("user_id") + email := c.GetString("email") + modelUsage := common.ModelUsage{ + UserID: userID, + UserEmail: email, + ProviderName: *req.ProviderName, + ModelName: *req.ModelName, + Type: "chat", + StartAt: time.Now(), + } // Check if it's a stream request if req.Stream { // Set SSE headers @@ -789,7 +798,18 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { } // Stream response using sender function (the best performance, no channel) - errorCode, err := h.modelProviderService.ChatToModelStreamWithSender(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, messages, &apiConfig, &chatConfig, sender) + errorCode, err := h.modelProviderService.ChatToModelStreamWithSender( + req.ProviderName, + req.InstanceName, + req.ModelName, + req.ModelID, + userID, + messages, + &apiConfig, + &chatConfig, + &modelUsage, + sender, + ) if errorCode != common.CodeSuccess { c.SSEvent("error", err.Error()) @@ -809,7 +829,17 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { content := msg["content"] messages[i] = models.Message{Role: role, Content: content} } - response, errorCode, err = h.modelProviderService.ChatToModelWithMessages(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, messages, &apiConfig, &chatConfig) + response, errorCode, err = h.modelProviderService.ChatToModelWithMessages( + req.ProviderName, + req.InstanceName, + req.ModelName, + req.ModelID, + userID, + messages, + &apiConfig, + &chatConfig, + &modelUsage, + ) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) diff --git a/internal/ingestion/component/docx_vision_dispatch.go b/internal/ingestion/component/docx_vision_dispatch.go index 3b6a389464..0cd81882f5 100644 --- a/internal/ingestion/component/docx_vision_dispatch.go +++ b/internal/ingestion/component/docx_vision_dispatch.go @@ -296,5 +296,5 @@ func defaultVisionChatInvoker( apiConfig *modelModule.APIConfig, ) (*modelModule.ChatResponse, error) { vision := true - return driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}) + return driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) } diff --git a/internal/ingestion/component/media_dispatch.go b/internal/ingestion/component/media_dispatch.go index 027b874dd3..09a4c9b378 100644 --- a/internal/ingestion/component/media_dispatch.go +++ b/internal/ingestion/component/media_dispatch.go @@ -94,7 +94,7 @@ func maybeDispatchVideo( }, }} vision := true - resp, err := driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}) + resp, err := driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) if err != nil { return parserDispatchResult{}, true, fmt.Errorf("Parser: video describe: %w", err) @@ -221,7 +221,7 @@ func maybeDispatchImage( }, }} vision := true - resp, err := driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}) + resp, err := driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) if err != nil { if ocrText != "" { return parserDispatchResult{ @@ -294,7 +294,7 @@ func maybeDispatchAudio( } defer os.Remove(tmpFile) - resp, err := driver.TranscribeAudio(&modelName, &tmpFile, apiConfig, nil) + resp, err := driver.TranscribeAudio(&modelName, &tmpFile, apiConfig, nil, nil) if err != nil { return parserDispatchResult{}, true, fmt.Errorf("Parser: audio transcription: %w", err) diff --git a/internal/ingestion/component/parser_dispatch_test.go b/internal/ingestion/component/parser_dispatch_test.go index 1cbfcd523b..117273096c 100644 --- a/internal/ingestion/component/parser_dispatch_test.go +++ b/internal/ingestion/component/parser_dispatch_test.go @@ -34,6 +34,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "ragflow/internal/common" "reflect" "strings" "testing" @@ -579,34 +580,34 @@ type mineruTestDriver struct{} func (d *mineruTestDriver) NewInstance(baseURL map[string]string) modelModule.ModelDriver { return d } func (d *mineruTestDriver) Name() string { return "mineru" } -func (d *mineruTestDriver) ChatWithMessages(modelName string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, chatModelConfig *modelModule.ChatConfig) (*modelModule.ChatResponse, error) { +func (d *mineruTestDriver) ChatWithMessages(modelName string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, chatModelConfig *modelModule.ChatConfig, usage *common.ModelUsage) (*modelModule.ChatResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) ChatStreamlyWithSender(modelName string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, sender func(*string, *string) error) error { +func (d *mineruTestDriver) ChatStreamlyWithSender(modelName string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, usage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("not implemented") } -func (d *mineruTestDriver) Embed(modelName *string, texts []string, apiConfig *modelModule.APIConfig, embeddingConfig *modelModule.EmbeddingConfig) ([]modelModule.EmbeddingData, error) { +func (d *mineruTestDriver) Embed(modelName *string, texts []string, apiConfig *modelModule.APIConfig, embeddingConfig *modelModule.EmbeddingConfig, usage *common.ModelUsage) ([]modelModule.EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) Rerank(modelName *string, query string, documents []string, apiConfig *modelModule.APIConfig, rerankConfig *modelModule.RerankConfig) (*modelModule.RerankResponse, error) { +func (d *mineruTestDriver) Rerank(modelName *string, query string, documents []string, apiConfig *modelModule.APIConfig, rerankConfig *modelModule.RerankConfig, usage *common.ModelUsage) (*modelModule.RerankResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) TranscribeAudio(modelName *string, file *string, apiConfig *modelModule.APIConfig, asrConfig *modelModule.ASRConfig) (*modelModule.ASRResponse, error) { +func (d *mineruTestDriver) TranscribeAudio(modelName *string, file *string, apiConfig *modelModule.APIConfig, asrConfig *modelModule.ASRConfig, usage *common.ModelUsage) (*modelModule.ASRResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *modelModule.APIConfig, asrConfig *modelModule.ASRConfig, sender func(*string, *string) error) error { +func (d *mineruTestDriver) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *modelModule.APIConfig, asrConfig *modelModule.ASRConfig, usage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("not implemented") } -func (d *mineruTestDriver) AudioSpeech(modelName *string, audioContent *string, apiConfig *modelModule.APIConfig, ttsConfig *modelModule.TTSConfig) (*modelModule.TTSResponse, error) { +func (d *mineruTestDriver) AudioSpeech(modelName *string, audioContent *string, apiConfig *modelModule.APIConfig, ttsConfig *modelModule.TTSConfig, usage *common.ModelUsage) (*modelModule.TTSResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *modelModule.APIConfig, ttsConfig *modelModule.TTSConfig, sender func(*string, *string) error) error { +func (d *mineruTestDriver) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *modelModule.APIConfig, ttsConfig *modelModule.TTSConfig, usage *common.ModelUsage, sender func(*string, *string) error) error { return fmt.Errorf("not implemented") } -func (d *mineruTestDriver) OCRFile(modelName *string, content []byte, url *string, apiConfig *modelModule.APIConfig, ocrConfig *modelModule.OCRConfig) (*modelModule.OCRFileResponse, error) { +func (d *mineruTestDriver) OCRFile(modelName *string, content []byte, url *string, apiConfig *modelModule.APIConfig, ocrConfig *modelModule.OCRConfig, usage *common.ModelUsage) (*modelModule.OCRFileResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) ParseFile(modelName *string, content []byte, url *string, apiConfig *modelModule.APIConfig, parseFileConfig *modelModule.ParseFileConfig) (*modelModule.ParseFileResponse, error) { +func (d *mineruTestDriver) ParseFile(modelName *string, content []byte, url *string, apiConfig *modelModule.APIConfig, parseFileConfig *modelModule.ParseFileConfig, usage *common.ModelUsage) (*modelModule.ParseFileResponse, error) { return nil, fmt.Errorf("not implemented") } func (d *mineruTestDriver) ListModels(apiConfig *modelModule.APIConfig) ([]modelModule.ListModelResponse, error) { diff --git a/internal/ingestion/component/pdf_vision_dispatch.go b/internal/ingestion/component/pdf_vision_dispatch.go index 1240a20f2b..b47d1f8db8 100644 --- a/internal/ingestion/component/pdf_vision_dispatch.go +++ b/internal/ingestion/component/pdf_vision_dispatch.go @@ -478,7 +478,7 @@ func defaultPDFVisionChatInvoker( apiConfig *modelModule.APIConfig, ) (*modelModule.ChatResponse, error) { vision := true - return driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}) + return driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) } func loadPDFVisionPrompt(name string) (string, error) { diff --git a/internal/ingestion/task/embedder.go b/internal/ingestion/task/embedder.go index cb5447c236..99c9423049 100644 --- a/internal/ingestion/task/embedder.go +++ b/internal/ingestion/task/embedder.go @@ -42,7 +42,7 @@ func (e *embedder) Encode(texts []string) ([]componentpkg.EmbeddingResult, error return nil, fmt.Errorf("embedder: embedding model driver is nil for model %v", e.model.ModelName) } config := &models.EmbeddingConfig{Dimension: 0} - embeds, err := e.model.ModelDriver.Embed(e.model.ModelName, texts, e.model.APIConfig, config) + embeds, err := e.model.ModelDriver.Embed(e.model.ModelName, texts, e.model.APIConfig, config, nil) if err != nil { return nil, err } diff --git a/internal/ingestion/task/embedder_test.go b/internal/ingestion/task/embedder_test.go index 619392d4f3..61618c9156 100644 --- a/internal/ingestion/task/embedder_test.go +++ b/internal/ingestion/task/embedder_test.go @@ -17,6 +17,7 @@ package task import ( + "ragflow/internal/common" "testing" "ragflow/internal/entity/models" @@ -73,7 +74,7 @@ type stubDriver struct { capturedTexts []string } -func (d *stubDriver) Embed(modelName *string, texts []string, apiConfig *models.APIConfig, embeddingConfig *models.EmbeddingConfig) ([]models.EmbeddingData, error) { +func (d *stubDriver) Embed(modelName *string, texts []string, apiConfig *models.APIConfig, embeddingConfig *models.EmbeddingConfig, usage *common.ModelUsage) ([]models.EmbeddingData, error) { d.capturedTexts = texts result := make([]models.EmbeddingData, len(texts)) for i := range texts { @@ -87,31 +88,31 @@ func (d *stubDriver) Embed(modelName *string, texts []string, apiConfig *models. } func (d *stubDriver) NewInstance(baseURL map[string]string) models.ModelDriver { return d } func (d *stubDriver) Name() string { return "stub" } -func (d *stubDriver) ChatWithMessages(modelName string, messages []models.Message, apiConfig *models.APIConfig, chatModelConfig *models.ChatConfig) (*models.ChatResponse, error) { +func (d *stubDriver) ChatWithMessages(modelName string, messages []models.Message, apiConfig *models.APIConfig, chatModelConfig *models.ChatConfig, usage *common.ModelUsage) (*models.ChatResponse, error) { return nil, nil } -func (d *stubDriver) ChatStreamlyWithSender(modelName string, messages []models.Message, apiConfig *models.APIConfig, modelConfig *models.ChatConfig, sender func(*string, *string) error) error { +func (d *stubDriver) ChatStreamlyWithSender(modelName string, messages []models.Message, apiConfig *models.APIConfig, modelConfig *models.ChatConfig, usage *common.ModelUsage, sender func(*string, *string) error) error { return nil } -func (d *stubDriver) Rerank(modelName *string, query string, documents []string, apiConfig *models.APIConfig, rerankConfig *models.RerankConfig) (*models.RerankResponse, error) { +func (d *stubDriver) Rerank(modelName *string, query string, documents []string, apiConfig *models.APIConfig, rerankConfig *models.RerankConfig, usage *common.ModelUsage) (*models.RerankResponse, error) { return nil, nil } -func (d *stubDriver) TranscribeAudio(modelName *string, file *string, apiConfig *models.APIConfig, asrConfig *models.ASRConfig) (*models.ASRResponse, error) { +func (d *stubDriver) TranscribeAudio(modelName *string, file *string, apiConfig *models.APIConfig, asrConfig *models.ASRConfig, usage *common.ModelUsage) (*models.ASRResponse, error) { return nil, nil } -func (d *stubDriver) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *models.APIConfig, asrConfig *models.ASRConfig, sender func(*string, *string) error) error { +func (d *stubDriver) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *models.APIConfig, asrConfig *models.ASRConfig, usage *common.ModelUsage, sender func(*string, *string) error) error { return nil } -func (d *stubDriver) AudioSpeech(modelName *string, audioContent *string, apiConfig *models.APIConfig, ttsConfig *models.TTSConfig) (*models.TTSResponse, error) { +func (d *stubDriver) AudioSpeech(modelName *string, audioContent *string, apiConfig *models.APIConfig, ttsConfig *models.TTSConfig, usage *common.ModelUsage) (*models.TTSResponse, error) { return nil, nil } -func (d *stubDriver) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *models.APIConfig, ttsConfig *models.TTSConfig, sender func(*string, *string) error) error { +func (d *stubDriver) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *models.APIConfig, ttsConfig *models.TTSConfig, usage *common.ModelUsage, sender func(*string, *string) error) error { return nil } -func (d *stubDriver) OCRFile(modelName *string, content []byte, url *string, apiConfig *models.APIConfig, ocrConfig *models.OCRConfig) (*models.OCRFileResponse, error) { +func (d *stubDriver) OCRFile(modelName *string, content []byte, url *string, apiConfig *models.APIConfig, ocrConfig *models.OCRConfig, usage *common.ModelUsage) (*models.OCRFileResponse, error) { return nil, nil } -func (d *stubDriver) ParseFile(modelName *string, content []byte, url *string, apiConfig *models.APIConfig, parseFileConfig *models.ParseFileConfig) (*models.ParseFileResponse, error) { +func (d *stubDriver) ParseFile(modelName *string, content []byte, url *string, apiConfig *models.APIConfig, parseFileConfig *models.ParseFileConfig, usage *common.ModelUsage) (*models.ParseFileResponse, error) { return nil, nil } func (d *stubDriver) ListModels(apiConfig *models.APIConfig) ([]models.ListModelResponse, error) { diff --git a/internal/parser/parser/pdf_parser_mineru.go b/internal/parser/parser/pdf_parser_mineru.go index 8f954f2cb9..dcf2ff76f6 100644 --- a/internal/parser/parser/pdf_parser_mineru.go +++ b/internal/parser/parser/pdf_parser_mineru.go @@ -50,7 +50,7 @@ func parsePDFWithMinerU(filename string, data []byte, parser *PDFParser) ParseRe apiConfig.ApiKey = &apiKey } - task, err := driver.ParseFile(&backend, data, nil, apiConfig, &models.ParseFileConfig{}) + task, err := driver.ParseFile(&backend, data, nil, apiConfig, &models.ParseFileConfig{}, nil) if err != nil { return ParseResult{Err: fmt.Errorf("parser: MinerU submit: %w", err)} } diff --git a/internal/parser/parser/pdf_parser_paddleocr.go b/internal/parser/parser/pdf_parser_paddleocr.go index 6e9d032b6a..c48a27ff9b 100644 --- a/internal/parser/parser/pdf_parser_paddleocr.go +++ b/internal/parser/parser/pdf_parser_paddleocr.go @@ -47,7 +47,7 @@ func parsePDFWithPaddleOCR(filename string, data []byte, parser *PDFParser) Pars resp, err := driver.OCRFile(&algorithm, data, &filename, apiConfig, &models.OCRConfig{ Algorithm: algorithm, - }) + }, nil) if err != nil { return ParseResult{Err: fmt.Errorf("parser: PaddleOCR OCRFile: %w", err)} } diff --git a/internal/service/bot_completion.go b/internal/service/bot_completion.go index 65ab8bd7c5..f045c2f2f6 100644 --- a/internal/service/bot_completion.go +++ b/internal/service/bot_completion.go @@ -375,7 +375,7 @@ func (s *BotService) ChatbotCompletion( go func() { defer close(out) resp, callErr := chatModel.ModelDriver.ChatWithMessages( - modelName, messages, chatModel.APIConfig, &modelModule.ChatConfig{}, + modelName, messages, chatModel.APIConfig, &modelModule.ChatConfig{}, nil, ) if callErr != nil { // Log the real error with structured context so diff --git a/internal/service/chat_pipeline.go b/internal/service/chat_pipeline.go index d5978bf945..150803587a 100644 --- a/internal/service/chat_pipeline.go +++ b/internal/service/chat_pipeline.go @@ -1116,7 +1116,7 @@ func (s *ChatPipelineService) AsyncChat( }) } else { driverErr = chatDriver.ModelDriver.ChatStreamlyWithSender( - *chatDriver.ModelName, chatMessages, chatDriver.APIConfig, chatCfg, + *chatDriver.ModelName, chatMessages, chatDriver.APIConfig, chatCfg, nil, func(answer *string, reason *string) error { if reason != nil && *reason != "" { if thinkState.EnterReasoning() { @@ -1253,7 +1253,7 @@ func (s *ChatPipelineService) AsyncChat( answer, _, err = chatDriver.ChatWithTools(ctx, prompt+prompt4citation, chatMessages, chatCfg) } else { resp, respErr := chatDriver.ModelDriver.ChatWithMessages( - *chatDriver.ModelName, chatMessages, chatDriver.APIConfig, chatCfg, + *chatDriver.ModelName, chatMessages, chatDriver.APIConfig, chatCfg, nil, ) if respErr != nil { err = respErr @@ -1434,7 +1434,7 @@ func (s *ChatPipelineService) AsyncChatSolo( timer.Enter(common.PhaseGenerateAnswer) driverErr := chatModel.ModelDriver.ChatStreamlyWithSender( - *chatModel.ModelName, chatMessages, chatModel.APIConfig, chatCfg, + *chatModel.ModelName, chatMessages, chatModel.APIConfig, chatCfg, nil, func(answer *string, reason *string) error { if reason != nil && *reason != "" { if thinkState.EnterReasoning() { @@ -1560,7 +1560,7 @@ func (s *ChatPipelineService) AsyncChatSolo( chatCfg := BuildChatConfig(chat, nil) timer.Enter(common.PhaseGenerateAnswer) resp, err := chatModel.ModelDriver.ChatWithMessages( - *chatModel.ModelName, chatMessages, chatModel.APIConfig, chatCfg, + *chatModel.ModelName, chatMessages, chatModel.APIConfig, chatCfg, nil, ) timer.Exit(common.PhaseGenerateAnswer) if err != nil { @@ -2251,7 +2251,7 @@ func (s *ChatPipelineService) synthesizeTTS(ttsModel *modelModule.ChatModel, tex return nil } ttsResp, err := ttsModel.ModelDriver.AudioSpeech( - ttsModel.ModelName, &text, ttsModel.APIConfig, &modelModule.TTSConfig{Format: "mp3"}, + ttsModel.ModelName, &text, ttsModel.APIConfig, &modelModule.TTSConfig{Format: "mp3"}, nil, ) if err != nil { common.Warn("TTS synthesis failed", zap.Error(err)) @@ -2642,7 +2642,7 @@ type embeddingModelEmbedder struct { func (e *embeddingModelEmbedder) Encode(texts []string) ([][]float64, error) { config := &modelModule.EmbeddingConfig{Dimension: 0} - embeds, err := e.embModel.ModelDriver.Embed(e.embModel.ModelName, texts, e.embModel.APIConfig, config) + embeds, err := e.embModel.ModelDriver.Embed(e.embModel.ModelName, texts, e.embModel.APIConfig, config, nil) if err != nil { return nil, err } @@ -3599,7 +3599,7 @@ func chatForSQL( modelModule.Message{Role: "user", Content: userPrompt}, } resp, err := chatModel.ModelDriver.ChatWithMessages( - modelName, msgs, chatModel.APIConfig, cfg, + modelName, msgs, chatModel.APIConfig, cfg, nil, ) if err != nil { return "", err diff --git a/internal/service/chunk/chunk.go b/internal/service/chunk/chunk.go index 63d14d444b..fe57c6a745 100644 --- a/internal/service/chunk/chunk.go +++ b/internal/service/chunk/chunk.go @@ -1352,7 +1352,7 @@ func (s *ChunkService) AddChunk(req *service.AddChunkRequest, userID string) (*s if len(questionKwd) > 0 { embeddingText = strings.Join(questionKwd, "\n") } - embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{docName, embeddingText}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}) + embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{docName, embeddingText}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}, nil) if err != nil { return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("encode chunk embedding: %v", err)} } diff --git a/internal/service/chunk/chunk_test.go b/internal/service/chunk/chunk_test.go index fc6c57b034..a3c868266c 100644 --- a/internal/service/chunk/chunk_test.go +++ b/internal/service/chunk/chunk_test.go @@ -1081,34 +1081,34 @@ type stubEmbeddingDriver struct { func (d *stubEmbeddingDriver) NewInstance(map[string]string) models.ModelDriver { return d } func (d *stubEmbeddingDriver) Name() string { return "stub" } -func (d *stubEmbeddingDriver) ChatWithMessages(string, []models.Message, *models.APIConfig, *models.ChatConfig) (*models.ChatResponse, error) { +func (d *stubEmbeddingDriver) ChatWithMessages(string, []models.Message, *models.APIConfig, *models.ChatConfig, *common.ModelUsage) (*models.ChatResponse, error) { return nil, nil } -func (d *stubEmbeddingDriver) ChatStreamlyWithSender(string, []models.Message, *models.APIConfig, *models.ChatConfig, func(*string, *string) error) error { +func (d *stubEmbeddingDriver) ChatStreamlyWithSender(string, []models.Message, *models.APIConfig, *models.ChatConfig, *common.ModelUsage, func(*string, *string) error) error { return nil } -func (d *stubEmbeddingDriver) Embed(*string, []string, *models.APIConfig, *models.EmbeddingConfig) ([]models.EmbeddingData, error) { +func (d *stubEmbeddingDriver) Embed(*string, []string, *models.APIConfig, *models.EmbeddingConfig, *common.ModelUsage) ([]models.EmbeddingData, error) { return d.embeddings, d.embedErr } -func (d *stubEmbeddingDriver) Rerank(*string, string, []string, *models.APIConfig, *models.RerankConfig) (*models.RerankResponse, error) { +func (d *stubEmbeddingDriver) Rerank(*string, string, []string, *models.APIConfig, *models.RerankConfig, *common.ModelUsage) (*models.RerankResponse, error) { return nil, nil } -func (d *stubEmbeddingDriver) TranscribeAudio(*string, *string, *models.APIConfig, *models.ASRConfig) (*models.ASRResponse, error) { +func (d *stubEmbeddingDriver) TranscribeAudio(*string, *string, *models.APIConfig, *models.ASRConfig, *common.ModelUsage) (*models.ASRResponse, error) { return nil, nil } -func (d *stubEmbeddingDriver) TranscribeAudioWithSender(*string, *string, *models.APIConfig, *models.ASRConfig, func(*string, *string) error) error { +func (d *stubEmbeddingDriver) TranscribeAudioWithSender(*string, *string, *models.APIConfig, *models.ASRConfig, *common.ModelUsage, func(*string, *string) error) error { return nil } -func (d *stubEmbeddingDriver) AudioSpeech(*string, *string, *models.APIConfig, *models.TTSConfig) (*models.TTSResponse, error) { +func (d *stubEmbeddingDriver) AudioSpeech(*string, *string, *models.APIConfig, *models.TTSConfig, *common.ModelUsage) (*models.TTSResponse, error) { return nil, nil } -func (d *stubEmbeddingDriver) AudioSpeechWithSender(*string, *string, *models.APIConfig, *models.TTSConfig, func(*string, *string) error) error { +func (d *stubEmbeddingDriver) AudioSpeechWithSender(*string, *string, *models.APIConfig, *models.TTSConfig, *common.ModelUsage, func(*string, *string) error) error { return nil } -func (d *stubEmbeddingDriver) OCRFile(*string, []byte, *string, *models.APIConfig, *models.OCRConfig) (*models.OCRFileResponse, error) { +func (d *stubEmbeddingDriver) OCRFile(*string, []byte, *string, *models.APIConfig, *models.OCRConfig, *common.ModelUsage) (*models.OCRFileResponse, error) { return nil, nil } -func (d *stubEmbeddingDriver) ParseFile(*string, []byte, *string, *models.APIConfig, *models.ParseFileConfig) (*models.ParseFileResponse, error) { +func (d *stubEmbeddingDriver) ParseFile(*string, []byte, *string, *models.APIConfig, *models.ParseFileConfig, *common.ModelUsage) (*models.ParseFileResponse, error) { return nil, nil } func (d *stubEmbeddingDriver) ListModels(*models.APIConfig) ([]models.ListModelResponse, error) { diff --git a/internal/service/dataset.go b/internal/service/dataset.go index 5992ca17e6..0e549d705a 100644 --- a/internal/service/dataset.go +++ b/internal/service/dataset.go @@ -942,7 +942,7 @@ func datasetCleanEmbeddingText(s string) string { func datasetEncodeEmbedding(embeddingModel *models.EmbeddingModel, texts []string) ([][]float64, error) { embeddingConfig := &models.EmbeddingConfig{Dimension: 0} - embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, texts, embeddingModel.APIConfig, embeddingConfig) + embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, texts, embeddingModel.APIConfig, embeddingConfig, nil) if err != nil { return nil, err } diff --git a/internal/service/deep_researcher.go b/internal/service/deep_researcher.go index 06bff70c12..bc9a93193c 100644 --- a/internal/service/deep_researcher.go +++ b/internal/service/deep_researcher.go @@ -671,7 +671,7 @@ func (dr *DeepResearcher) chatOnce( modelModule.Message{Role: "user", Content: userPrompt}, } resp, err := dr.ChatModel.ModelDriver.ChatWithMessages( - modelName, msgs, dr.ChatModel.APIConfig, cfg, + modelName, msgs, dr.ChatModel.APIConfig, cfg, nil, ) if err != nil { return "", err diff --git a/internal/service/generator.go b/internal/service/generator.go index 8145c1314f..18e7bb70ce 100644 --- a/internal/service/generator.go +++ b/internal/service/generator.go @@ -73,7 +73,7 @@ func KeywordExtraction(ctx context.Context, chatModel *modelModule.ChatModel, co } // Call LLM using ChatModel - response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, modelConfig) + response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, modelConfig, nil) if err != nil { return "", fmt.Errorf("failed to extract keywords: %w", err) } @@ -176,7 +176,7 @@ func CrossLanguages(ctx context.Context, tenantID string, llmID string, query st } // Call LLM using ChatModel - response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, modelConfig) + response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, modelConfig, nil) if err != nil { return query, fmt.Errorf("failed to translate question: %w", err) } @@ -341,7 +341,7 @@ func FullQuestion( {Role: "user", Content: "Output: "}, } resp, err := chatModel.ModelDriver.ChatWithMessages( - modelName, msgs, chatModel.APIConfig, nil, + modelName, msgs, chatModel.APIConfig, nil, nil, ) if err != nil { return fallbackToLatestUser(messages), err diff --git a/internal/service/graph/retrieval.go b/internal/service/graph/retrieval.go index abc9ca843f..7bbb276d2f 100644 --- a/internal/service/graph/retrieval.go +++ b/internal/service/graph/retrieval.go @@ -199,7 +199,7 @@ func buildSearchExprs(embModel *modelModule.EmbeddingModel, matchText *types.Mat return []interface{}{matchText} } embeddingConfig := &modelModule.EmbeddingConfig{Dimension: 0} - embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{matchText.MatchingText}, embModel.APIConfig, embeddingConfig) + embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{matchText.MatchingText}, embModel.APIConfig, embeddingConfig, nil) if err != nil || len(embeddings) == 0 { return []interface{}{matchText} } @@ -243,7 +243,7 @@ func queryRewrite(chatModel *modelModule.ChatModel, question string, ty2entsJSON {Role: "system", Content: prompt}, {Role: "user", Content: "Output:"}, } - response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, nil) + response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, nil, nil) if err == nil && response != nil && response.Answer != nil { result, parseErr := common.ParseQueryRewriteResponse(*response.Answer) if parseErr == nil && result != nil { diff --git a/internal/service/graph/retrieval_test.go b/internal/service/graph/retrieval_test.go index 1b827aa1b9..26ce25d6ad 100644 --- a/internal/service/graph/retrieval_test.go +++ b/internal/service/graph/retrieval_test.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "ragflow/internal/common" "strings" "testing" @@ -321,7 +322,7 @@ type spyEmbedDriver struct { err error } -func (s *spyEmbedDriver) Embed(_ *string, texts []string, _ *modelModule.APIConfig, _ *modelModule.EmbeddingConfig) ([]modelModule.EmbeddingData, error) { +func (s *spyEmbedDriver) Embed(_ *string, texts []string, _ *modelModule.APIConfig, _ *modelModule.EmbeddingConfig, _ *common.ModelUsage) ([]modelModule.EmbeddingData, error) { s.capturedTexts = texts if s.err != nil { return nil, s.err diff --git a/internal/service/graph/search.go b/internal/service/graph/search.go index 369ca7974a..21a1ec64bb 100644 --- a/internal/service/graph/search.go +++ b/internal/service/graph/search.go @@ -99,7 +99,7 @@ func buildDenseExpr(embModel *modelModule.EmbeddingModel, question string, topN return nil, nil } embCfg := &modelModule.EmbeddingConfig{Dimension: 0} - embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{question}, embModel.APIConfig, embCfg) + embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{question}, embModel.APIConfig, embCfg, nil) if err != nil { return nil, fmt.Errorf("KG entity embed failed: %w", err) } diff --git a/internal/service/graph/search_test.go b/internal/service/graph/search_test.go index eb4398c248..1eb59e3f4c 100644 --- a/internal/service/graph/search_test.go +++ b/internal/service/graph/search_test.go @@ -18,6 +18,7 @@ package graph import ( "context" + "ragflow/internal/common" "testing" "ragflow/internal/engine" @@ -343,7 +344,7 @@ type fakeEmbedDriver struct { func (f *fakeEmbedDriver) Name() string { return f.name } -func (f *fakeEmbedDriver) Embed(modelName *string, texts []string, apiConfig *modelModule.APIConfig, config *modelModule.EmbeddingConfig) ([]modelModule.EmbeddingData, error) { +func (f *fakeEmbedDriver) Embed(modelName *string, texts []string, apiConfig *modelModule.APIConfig, config *modelModule.EmbeddingConfig, usage *common.ModelUsage) ([]modelModule.EmbeddingData, error) { return []modelModule.EmbeddingData{{Embedding: f.vector}}, nil } diff --git a/internal/service/memory.go b/internal/service/memory.go index 3d012e4442..40491b4b65 100644 --- a/internal/service/memory.go +++ b/internal/service/memory.go @@ -1263,7 +1263,7 @@ func (s *MemoryService) memoryMessageDenseExpr(question string, memory *entity.M return nil, err } embeddingModel := models.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens) - embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{question}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}) + embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{question}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}, nil) if err != nil { return nil, err } diff --git a/internal/service/memory_message_service.go b/internal/service/memory_message_service.go index db188ff850..27c84f53ad 100644 --- a/internal/service/memory_message_service.go +++ b/internal/service/memory_message_service.go @@ -267,7 +267,7 @@ func (s *MemoryMessageService) embedAndSave(ctx context.Context, mem *CreateMemo return err } embeddingModel := models.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens) - embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{content}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}) + embeddings, err := embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{content}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}, nil) if err != nil { return err } diff --git a/internal/service/metadata_filter.go b/internal/service/metadata_filter.go index 4ed676d665..decc83a06a 100644 --- a/internal/service/metadata_filter.go +++ b/internal/service/metadata_filter.go @@ -25,11 +25,12 @@ import ( "ragflow/internal/engine" "regexp" - "github.com/kaptinlin/jsonrepair" "strconv" "strings" "time" + "github.com/kaptinlin/jsonrepair" + "go.uber.org/zap" modelModule "ragflow/internal/entity/models" @@ -203,7 +204,7 @@ func GenMetaFilter(ctx context.Context, chatModel *modelModule.ChatModel, metaDa } // Call LLM using ChatModel - response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, nil) + response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, nil, nil) if err != nil { common.Warn("ChatWithMessages failed for GenMetaFilter", zap.String("model", diff --git a/internal/service/model_chat.go b/internal/service/model_chat.go index 5e83e1c1f0..9319fb6105 100644 --- a/internal/service/model_chat.go +++ b/internal/service/model_chat.go @@ -31,7 +31,7 @@ func (m *ModelProviderService) Chat(tenantID, modelID string, messages []modelMo if err != nil { return nil, err } - return chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, config) + return chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, config, nil) } func (m *ModelProviderService) ChatStream(ctx context.Context, tenantID, modelID string, messages []modelModule.Message, config *modelModule.ChatConfig) (<-chan string, error) { @@ -46,7 +46,7 @@ func chatStreamWithContext(ctx context.Context, chatModel *modelModule.ChatModel ch := make(chan string, 256) go func() { defer close(ch) - if err := chatModel.ModelDriver.ChatStreamlyWithSender(*chatModel.ModelName, messages, chatModel.APIConfig, config, + if err := chatModel.ModelDriver.ChatStreamlyWithSender(*chatModel.ModelName, messages, chatModel.APIConfig, config, nil, func(delta *string, _ *string) error { if delta == nil { return nil diff --git a/internal/service/model_service.go b/internal/service/model_service.go index f0ca72b687..302070eeaa 100644 --- a/internal/service/model_service.go +++ b/internal/service/model_service.go @@ -2268,7 +2268,7 @@ func (m *ModelProviderService) getModelInstanceAndProviderByID(modelID *string, } // ChatToModelWithMessages sends messages to the model with messages array -func (m *ModelProviderService) ChatToModelWithMessages(providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig) (*modelModule.ChatResponse, common.ErrorCode, error) { +func (m *ModelProviderService) ChatToModelWithMessages(providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, modelUsage *common.ModelUsage) (*modelModule.ChatResponse, common.ErrorCode, error) { var err error var info *ModelInstanceAndProviderInfo @@ -2323,7 +2323,11 @@ func (m *ModelProviderService) ChatToModelWithMessages(providerName, instanceNam } } - response, err = modelDriver.ChatWithMessages(resolvedModelName, messages, info.APIConfig, modelConfig) + modelUsage.TenantID = info.ProviderEntity.TenantID + modelUsage.InstanceID = info.InstanceEntity.ID + modelUsage.APIKey = info.InstanceEntity.APIKey + + response, err = modelDriver.ChatWithMessages(resolvedModelName, messages, info.APIConfig, modelConfig, modelUsage) if err != nil { return nil, common.CodeServerError, err } @@ -2335,7 +2339,7 @@ func (m *ModelProviderService) ChatToModelWithMessages(providerName, instanceNam } // ChatToModelStreamWithSender streams chat response directly via sender function ( the best performance, no channel) -func (m *ModelProviderService) ChatToModelStreamWithSender(providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, sender func(*string, *string) error) (common.ErrorCode, error) { +func (m *ModelProviderService) ChatToModelStreamWithSender(providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) (common.ErrorCode, error) { var err error var info *ModelInstanceAndProviderInfo @@ -2386,7 +2390,11 @@ func (m *ModelProviderService) ChatToModelStreamWithSender(providerName, instanc } } - err = modelDriver.ChatStreamlyWithSender(resolvedModelName, messages, info.APIConfig, modelConfig, sender) + modelUsage.TenantID = info.ProviderEntity.TenantID + modelUsage.InstanceID = info.InstanceEntity.ID + modelUsage.APIKey = info.InstanceEntity.APIKey + + err = modelDriver.ChatStreamlyWithSender(resolvedModelName, messages, info.APIConfig, modelConfig, modelUsage, sender) if err != nil { return common.CodeServerError, err } @@ -2478,7 +2486,7 @@ func (m *ModelProviderService) EmbedText(providerName, instanceName, modelName, } var response []modelModule.EmbeddingData - response, err = modelDriver.Embed(&resolvedModelName, texts, info.APIConfig, modelConfig) + response, err = modelDriver.Embed(&resolvedModelName, texts, info.APIConfig, modelConfig, nil) if err != nil { return nil, common.CodeServerError, err } @@ -2540,7 +2548,7 @@ func (m *ModelProviderService) RerankDocument(providerName, instanceName, modelN } var response *modelModule.RerankResponse - response, err = modelDriver.Rerank(&resolvedModelName, query, documents, info.APIConfig, modelConfig) + response, err = modelDriver.Rerank(&resolvedModelName, query, documents, info.APIConfig, modelConfig, nil) if err != nil { return nil, common.CodeServerError, err } @@ -2594,7 +2602,7 @@ func (m *ModelProviderService) TranscribeAudio(providerName, instanceName, model } var response *modelModule.ASRResponse - response, err = modelDriver.TranscribeAudio(modelName, audioFile, apiConfig, modelConfig) + response, err = modelDriver.TranscribeAudio(modelName, audioFile, apiConfig, modelConfig, nil) if err != nil { return nil, common.CodeServerError, err } @@ -2650,7 +2658,7 @@ func (m *ModelProviderService) TranscribeAudioStream(providerName, instanceName, } } - err = modelDriver.TranscribeAudioWithSender(modelName, audioFile, apiConfig, modelConfig, sender) + err = modelDriver.TranscribeAudioWithSender(modelName, audioFile, apiConfig, modelConfig, nil, sender) if err != nil { return common.CodeServerError, err } @@ -2704,7 +2712,7 @@ func (m *ModelProviderService) AudioSpeech(providerName, instanceName, modelName } var response *modelModule.TTSResponse - response, err = modelDriver.AudioSpeech(modelName, audioContent, apiConfig, modelConfig) + response, err = modelDriver.AudioSpeech(modelName, audioContent, apiConfig, modelConfig, nil) if err != nil { return nil, common.CodeServerError, err } @@ -2759,7 +2767,7 @@ func (m *ModelProviderService) AudioSpeechStream(providerName, instanceName, mod } } - err = modelDriver.AudioSpeechWithSender(modelName, audioContent, apiConfig, modelConfig, sender) + err = modelDriver.AudioSpeechWithSender(modelName, audioContent, apiConfig, modelConfig, nil, sender) if err != nil { return common.CodeServerError, err } @@ -2812,7 +2820,7 @@ func (m *ModelProviderService) OCRFile(providerName, instanceName, modelName, mo } var response *modelModule.OCRFileResponse - response, err = modelDriver.OCRFile(modelName, content, url, apiConfig, modelConfig) + response, err = modelDriver.OCRFile(modelName, content, url, apiConfig, modelConfig, nil) if err != nil { return nil, common.CodeServerError, err } @@ -2867,7 +2875,7 @@ func (m *ModelProviderService) ParseFile(providerName, instanceName, modelName, } var response *modelModule.ParseFileResponse - response, err = modelDriver.ParseFile(modelName, content, url, apiConfig, modelConfig) + response, err = modelDriver.ParseFile(modelName, content, url, apiConfig, modelConfig, nil) if err != nil { return nil, common.CodeServerError, err } diff --git a/internal/service/nlp/reranker.go b/internal/service/nlp/reranker.go index d638e61811..b19ccfd587 100644 --- a/internal/service/nlp/reranker.go +++ b/internal/service/nlp/reranker.go @@ -148,7 +148,7 @@ func RerankByModel( tsim = TokenSimilarity(keywords, insTw, qb) // Get similarity scores from reranker model - rerankResponse, err := rerankModel.ModelDriver.Rerank(rerankModel.ModelName, query, docs, rerankModel.APIConfig, &models.RerankConfig{}) + rerankResponse, err := rerankModel.ModelDriver.Rerank(rerankModel.ModelName, query, docs, rerankModel.APIConfig, &models.RerankConfig{}, nil) if err != nil { common.Error("RerankByModel: rerankModel.Rerank failed; falling back to token-only similarity", err) // If model fails, fall back to token similarity only diff --git a/internal/service/nlp/retrieval.go b/internal/service/nlp/retrieval.go index c746a8d4f9..afb8b4f86c 100644 --- a/internal/service/nlp/retrieval.go +++ b/internal/service/nlp/retrieval.go @@ -785,7 +785,7 @@ func (s *RetrievalService) GetVector(txt string, embModel *models.EmbeddingModel embeddingConfig := &models.EmbeddingConfig{ Dimension: 0, } - embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{txt}, embModel.APIConfig, embeddingConfig) + embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{txt}, embModel.APIConfig, embeddingConfig, nil) if err != nil { return nil, err } diff --git a/internal/service/skill_indexer.go b/internal/service/skill_indexer.go index 10d76d667d..fa63daee0a 100644 --- a/internal/service/skill_indexer.go +++ b/internal/service/skill_indexer.go @@ -935,7 +935,7 @@ func (s *SkillIndexerService) generateEmbedding(ctx context.Context, text, embdI truncatedText := truncate(text, maxLen-10) var response []models.EmbeddingData - response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{truncatedText}, embeddingModel.APIConfig, nil) + response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{truncatedText}, embeddingModel.APIConfig, nil, nil) if err != nil { return nil, fmt.Errorf("failed to encode text: %w", err) } @@ -979,7 +979,7 @@ func (s *SkillIndexerService) generateEmbeddings(ctx context.Context, texts []st common.Info(fmt.Sprintf("Encoding %d texts", len(truncatedTexts))) // Use batch encode API (consistent with Python's encode(texts: list)) var response []models.EmbeddingData - response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, truncatedTexts, embeddingModel.APIConfig, nil) + response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, truncatedTexts, embeddingModel.APIConfig, nil, nil) if err != nil { common.Error(fmt.Sprintf("Failed to encode texts: %v", err), err) return nil, fmt.Errorf("failed to encode texts: %w", err) @@ -1026,7 +1026,7 @@ func (s *SkillIndexerService) getEmbeddingDimension(ctx context.Context, tenantI // Use simple test text like Python does: embedding_model.encode(["ok"]) testText := "ok" var response []models.EmbeddingData - response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{testText}, embeddingModel.APIConfig, nil) + response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{testText}, embeddingModel.APIConfig, nil, nil) if err != nil { return 0, fmt.Errorf("failed to encode test text: %w", err) } diff --git a/internal/service/skill_search.go b/internal/service/skill_search.go index ca763b8a52..33d24019a4 100644 --- a/internal/service/skill_search.go +++ b/internal/service/skill_search.go @@ -680,7 +680,7 @@ func (s *SkillSearchService) getEmbedding(ctx context.Context, text, embdID, ten truncatedText := truncate(text, maxLen-10) var response []models.EmbeddingData - response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{truncatedText}, embeddingModel.APIConfig, nil) + response, err = embeddingModel.ModelDriver.Embed(embeddingModel.ModelName, []string{truncatedText}, embeddingModel.APIConfig, nil, nil) if err != nil { return nil, fmt.Errorf("failed to encode query: %w", err) } diff --git a/internal/service/toc_enhancer.go b/internal/service/toc_enhancer.go index ef0fd4c5c5..1533cd7f0a 100644 --- a/internal/service/toc_enhancer.go +++ b/internal/service/toc_enhancer.go @@ -479,7 +479,7 @@ func (e *TOCEnhancer) scoreEntries(ctx context.Context, entries []tocEntry, limi modelName = *e.chatModel.ModelName } resp, err := e.chatModel.ModelDriver.ChatWithMessages( - modelName, msgs, e.chatModel.APIConfig, cfg, + modelName, msgs, e.chatModel.APIConfig, cfg, nil, ) if err != nil { return nil, err diff --git a/internal/tokenizer/tokenizer_concurrent_test.go b/internal/tokenizer/tokenizer_concurrent_test.go index 24261a97d0..166bcce54f 100644 --- a/internal/tokenizer/tokenizer_concurrent_test.go +++ b/internal/tokenizer/tokenizer_concurrent_test.go @@ -30,7 +30,7 @@ import ( func init() { // Initialize logger for tests - if err := common.Init("info", common.FileOutput{}, ""); err != nil { + if err := common.Init("info", common.FileOutput{}, "tokenizer_test"); err != nil { fmt.Printf("Failed to initialize logger: %v\n", err) } }