diff --git a/internal/admin/handler.go b/internal/admin/handler.go index 058ba2ce94..d0835febdd 100644 --- a/internal/admin/handler.go +++ b/internal/admin/handler.go @@ -92,6 +92,7 @@ func (h *Handler) Ping(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /admin/login [post] func (h *Handler) Login(c *gin.Context) { + ctx := c.Request.Context() var req service.EmailLoginRequest if err := c.ShouldBindJSON(&req); err != nil { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, err.Error()) @@ -100,7 +101,7 @@ func (h *Handler) Login(c *gin.Context) { // Use userService.LoginByEmail with adminLogin=true // This allows default admin account to log in admin system - user, code, err := h.userService.LoginByEmail(&req) + user, code, err := h.userService.LoginByEmail(ctx, &req) if err != nil { common.ErrorWithCode(c, code, err.Error()) return @@ -816,6 +817,7 @@ func (h *Handler) TestSandboxConnection(c *gin.Context) { // Validates that the user is authenticated and is a superuser (admin) func (h *Handler) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + ctx := c.Request.Context() token := c.GetHeader("Authorization") if token == "" { common.ErrorWithCode(c, common.CodeUnauthorized, "Missing authorization header") @@ -824,7 +826,7 @@ func (h *Handler) AuthMiddleware() gin.HandlerFunc { } // Get user by access token - user, code, err := h.userService.GetUserByToken(token) + user, code, err := h.userService.GetUserByToken(ctx, token) if err != nil { common.ResponseWithHttpCodeData(c, http.StatusUnauthorized, code, nil, "Invalid access token") c.Abort() diff --git a/internal/agent/audio/tts_dispatch.go b/internal/agent/audio/tts_dispatch.go index 272fdaf435..9952e67aa9 100644 --- a/internal/agent/audio/tts_dispatch.go +++ b/internal/agent/audio/tts_dispatch.go @@ -61,6 +61,7 @@ import ( // consumer. type TTSDispatcher interface { AudioSpeech( + ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, audioContent *string, @@ -123,6 +124,7 @@ func NewTTSDispatchFunc(d TTSDispatcher) ModelProviderFunc { text := req.Text resp, code, err := d.AudioSpeech( + ctx, nil, // providerName — let the dispatcher resolve by name nil, // instanceName — same modelName, diff --git a/internal/agent/audio/tts_dispatch_test.go b/internal/agent/audio/tts_dispatch_test.go index 43e17dc929..22e092b245 100644 --- a/internal/agent/audio/tts_dispatch_test.go +++ b/internal/agent/audio/tts_dispatch_test.go @@ -51,6 +51,7 @@ type fakeTTSDispatcher struct { } func (f *fakeTTSDispatcher) AudioSpeech( + ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, audioContent *string, diff --git a/internal/cli/user_command.go b/internal/cli/user_command.go index 779b54c40e..12ed05d9a7 100644 --- a/internal/cli/user_command.go +++ b/internal/cli/user_command.go @@ -18,6 +18,7 @@ package cli import ( "bufio" + "context" "encoding/base64" "encoding/json" "errors" @@ -3558,7 +3559,8 @@ func (c *CLI) APIParseLocalFileCommand(cmd *Command) (ResponseIf, error) { return nil, fmt.Errorf("failed to read dsl file: %w", err) } - parseResult := fileParser.ParseWithResult(filename, fileContent) + ctx := context.Background() + parseResult := fileParser.ParseWithResult(ctx, filename, fileContent) if parseResult.Err != nil { return nil, formatRequestError("parse local file", parseResult.Err) } diff --git a/internal/dao/user.go b/internal/dao/user.go index 6ecbabc1ea..2aedbc50b3 100644 --- a/internal/dao/user.go +++ b/internal/dao/user.go @@ -36,7 +36,7 @@ func (dao *UserDAO) Create(user *entity.User) error { } // GetByID get user by ID -func (dao *UserDAO) GetByID(id uint) (*entity.User, error) { +func (dao *UserDAO) GetByID(ctx context.Context, id uint) (*entity.User, error) { var user entity.User err := DB.First(&user, id).Error if err != nil { diff --git a/internal/entity/models/302ai.go b/internal/entity/models/302ai.go index 11e4ef31a8..960e1a17e4 100644 --- a/internal/entity/models/302ai.go +++ b/internal/entity/models/302ai.go @@ -71,7 +71,7 @@ func validateAI302DocumentURL(rawURL string) (string, error) { return documentURL, nil } -func (a *AI302Model) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (a *AI302Model) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -147,7 +147,7 @@ func (a *AI302Model) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -219,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AI302Model) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -302,7 +302,7 @@ func (a *AI302Model) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -365,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (a *AI302Model) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -394,7 +394,7 @@ func (a *AI302Model) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -446,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (a *AI302Model) Rerank(ctx context.Context, 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 } @@ -486,7 +486,7 @@ func (a *AI302Model) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -535,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (a *AI302Model) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -623,7 +623,7 @@ func (a *AI302Model) TranscribeAudio(modelName *string, file *string, apiConfig } // build request - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, &body) @@ -663,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AI302Model) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (a *AI302Model) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AI302Model) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (a *AI302Model) OCRFile(ctx context.Context, 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 } @@ -721,7 +721,7 @@ func (a *AI302Model) OCRFile(modelName *string, content []byte, urls *string, ap return nil, fmt.Errorf("failed to marshal json payload: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -771,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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (a *AI302Model) ParseFile(ctx context.Context, 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 } @@ -803,7 +803,7 @@ func (a *AI302Model) ParseFile(modelName *string, content []byte, documentURL *s return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData)) @@ -843,7 +843,7 @@ func (a *AI302Model) ParseFile(modelName *string, content []byte, documentURL *s }, nil } -func (a *AI302Model) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (a *AI302Model) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -855,7 +855,7 @@ func (a *AI302Model) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro } url := fmt.Sprintf("%s/%s", resolvedBaseURL, a.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -895,20 +895,20 @@ func (a *AI302Model) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return models, nil } -func (a *AI302Model) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (a *AI302Model) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", a.Name()) } -func (a *AI302Model) CheckConnection(apiConfig *APIConfig) error { - _, err := a.ListModels(apiConfig) +func (a *AI302Model) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := a.ListModels(ctx, apiConfig) return err } -func (a *AI302Model) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (a *AI302Model) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", a.Name()) } -func (a *AI302Model) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (a *AI302Model) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -924,7 +924,7 @@ func (a *AI302Model) ShowTask(taskID string, apiConfig *APIConfig) (*TaskRespons } apiURL := fmt.Sprintf("%s/%s/%s", resolvedBaseURL, a.baseModel.URLSuffix.DocumentParse, url.PathEscape(strings.TrimSpace(taskID))) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) diff --git a/internal/entity/models/302ai_test.go b/internal/entity/models/302ai_test.go index 6052529d15..1c7eace058 100644 --- a/internal/entity/models/302ai_test.go +++ b/internal/entity/models/302ai_test.go @@ -89,11 +89,13 @@ func TestAI302ChatForcesNonStreaming(t *testing.T) { }) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" stream := true thinking := true resp, err := newAI302ForTest(srv.URL).ChatWithMessages( + ctx, "gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -131,11 +133,13 @@ func TestAI302StreamForcesStreaming(t *testing.T) { }, "\n")) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" stream := false var content, reasoning []string err := newAI302ForTest(srv.URL).ChatStreamlyWithSender( + ctx, "gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -178,9 +182,10 @@ func TestAI302ListModelsHappyPath(t *testing.T) { }) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" - models, err := newAI302ForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newAI302ForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -196,12 +201,13 @@ func TestAI302ListModelsRejectsMalformedResponse(t *testing.T) { "empty id": map[string]interface{}{"data": []map[string]string{{"id": ""}}}, } { t.Run(name, func(t *testing.T) { + ctx := t.Context() srv := newAI302Server(t, func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(response) }) defer srv.Close() - if _, err := newAI302ForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}); err == nil { + if _, err := newAI302ForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}); err == nil { t.Fatal("expected malformed response error") } }) @@ -226,9 +232,10 @@ func TestAI302ShowTaskEscapesTaskID(t *testing.T) { }) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" - resp, err := newAI302ForTest(srv.URL).ShowTask(" task/with?query#fragment ", &APIConfig{ApiKey: &apiKey}) + resp, err := newAI302ForTest(srv.URL).ShowTask(ctx, " task/with?query#fragment ", &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ShowTask: %v", err) } @@ -246,7 +253,7 @@ func TestAI302ValidatesInputs(t *testing.T) { docURL := "https://example.com/doc.pdf" invalidURL := "ftp://example.com/doc.pdf" send := func(*string, *string) error { return nil } - + ctx := t.Context() tests := []struct { name string run func() error @@ -255,7 +262,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}, &ChatConfig{}, &common.ModelUsage{}) + _, err := newAI302ForTest("http://unused").ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, &ChatConfig{}, &common.ModelUsage{}) return err }, want: "api key is required", @@ -263,7 +270,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}, &ChatConfig{}, &common.ModelUsage{}) + _, err := newAI302ForTest("http://unused").ChatWithMessages(ctx, " ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, &common.ModelUsage{}) return err }, want: "model name is required", @@ -271,21 +278,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, nil, send) + return newAI302ForTest("http://unused").ChatStreamlyWithSender(ctx, "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, nil) + return newAI302ForTest("http://unused").ChatStreamlyWithSender(ctx, "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, nil) + _, err := newAI302ForTest("http://unused").Embed(ctx, nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -293,7 +300,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "rerank api key", run: func() error { - _, err := newAI302ForTest("http://unused").Rerank(&model, "q", []string{"doc"}, nil, nil, nil) + _, err := newAI302ForTest("http://unused").Rerank(ctx, &model, "q", []string{"doc"}, nil, nil, nil) return err }, want: "api key is required", @@ -301,7 +308,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "rerank query", run: func() error { - _, err := newAI302ForTest("http://unused").Rerank(&model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newAI302ForTest("http://unused").Rerank(ctx, &model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "query is required", @@ -309,7 +316,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "asr model", run: func() error { - _, err := newAI302ForTest("http://unused").TranscribeAudio(nil, &docURL, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newAI302ForTest("http://unused").TranscribeAudio(ctx, nil, &docURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -317,7 +324,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "asr file", run: func() error { - _, err := newAI302ForTest("http://unused").TranscribeAudio(&model, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newAI302ForTest("http://unused").TranscribeAudio(ctx, &model, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "file is missing", @@ -325,7 +332,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "ocr api key", run: func() error { - _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &docURL, nil, nil, nil) + _, err := newAI302ForTest("http://unused").OCRFile(ctx, &model, nil, &docURL, nil, nil, nil) return err }, want: "api key is required", @@ -333,7 +340,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "ocr input", run: func() error { - _, err := newAI302ForTest("http://unused").OCRFile(&model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newAI302ForTest("http://unused").OCRFile(ctx, &model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "file url or content is required", @@ -341,7 +348,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, nil) + _, err := newAI302ForTest("http://unused").OCRFile(ctx, &model, nil, &invalidURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "invalid document URL", @@ -349,7 +356,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, nil) + _, err := newAI302ForTest("http://unused").ParseFile(ctx, &model, nil, &blankURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "valid public document URL", @@ -357,7 +364,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, nil) + _, err := newAI302ForTest("http://unused").ParseFile(ctx, &model, nil, &invalidURL, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "invalid document URL", @@ -365,7 +372,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "models api key", run: func() error { - _, err := newAI302ForTest("http://unused").ListModels(&APIConfig{}) + _, err := newAI302ForTest("http://unused").ListModels(ctx, &APIConfig{}) return err }, want: "api key is required", @@ -373,7 +380,7 @@ func TestAI302ValidatesInputs(t *testing.T) { { name: "show task id", run: func() error { - _, err := newAI302ForTest("http://unused").ShowTask(" ", &APIConfig{ApiKey: &apiKey}) + _, err := newAI302ForTest("http://unused").ShowTask(ctx, " ", &APIConfig{ApiKey: &apiKey}) return err }, want: "task id is required", diff --git a/internal/entity/models/aliyun.go b/internal/entity/models/aliyun.go index 0d4acdcbfd..80292015ed 100644 --- a/internal/entity/models/aliyun.go +++ b/internal/entity/models/aliyun.go @@ -52,7 +52,7 @@ func (a *AliyunModel) Name() string { return "Tongyi-Qianwen" } -func (a *AliyunModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (a *AliyunModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -117,7 +117,7 @@ func (a *AliyunModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -192,7 +192,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AliyunModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -258,7 +258,7 @@ func (a *AliyunModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -381,7 +381,7 @@ type aliyunUsage struct { } // Embed embeds a list of texts into embeddings -func (a *AliyunModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (a *AliyunModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -412,7 +412,7 @@ func (a *AliyunModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -469,7 +469,7 @@ type aliyunRerankResponse struct { } `json:"results"` } -func (a *AliyunModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (a *AliyunModel) Rerank(ctx context.Context, 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 } @@ -507,7 +507,7 @@ func (a *AliyunModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -542,30 +542,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (a *AliyunModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AliyunModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (a *AliyunModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AliyunModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (a *AliyunModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (a *AliyunModel) ParseFile(ctx context.Context, 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()) } @@ -586,7 +586,7 @@ type AliyunModelList struct { Output AliyunModelOutput `json:"output"` } -func (a *AliyunModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (a *AliyunModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -607,7 +607,7 @@ func (a *AliyunModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -641,22 +641,22 @@ func (a *AliyunModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return ParseListModel(modelList), nil } -func (a *AliyunModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (a *AliyunModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AliyunModel) CheckConnection(apiConfig *APIConfig) error { - _, err := a.ListModels(apiConfig) +func (a *AliyunModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := a.ListModels(ctx, apiConfig) if err != nil { return err } return nil } -func (a *AliyunModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (a *AliyunModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AliyunModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (a *AliyunModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } diff --git a/internal/entity/models/aliyun_test.go b/internal/entity/models/aliyun_test.go index dad23a9252..4d5d86c569 100644 --- a/internal/entity/models/aliyun_test.go +++ b/internal/entity/models/aliyun_test.go @@ -39,6 +39,7 @@ func TestAliyunChatWithMessagesSupportsToolCalls(t *testing.T) { _, _ = w.Write([]byte(`{"choices":[{"message":{"content":null,"tool_calls":[{"id":"call-1","type":"function","function":{"name":"retrieval","arguments":"{\"query\":\"ragflow\"}"}}]}}]}`)) })) defer server.Close() + ctx := t.Context() model := NewAliyunModel( map[string]string{"default": server.URL}, @@ -56,6 +57,7 @@ func TestAliyunChatWithMessagesSupportsToolCalls(t *testing.T) { messages := []Message{{Role: "user", Content: "find ragflow"}} response, err := model.ChatWithMessages( + ctx, "qwen-flash", messages, &APIConfig{ApiKey: &apiKey}, @@ -85,6 +87,7 @@ func TestAliyunChatWithMessagesSupportsToolCalls(t *testing.T) { } func TestAliyunChatWithMessagesStopsQwenFlashAfterToolResult(t *testing.T) { + ctx := t.Context() requestBody := make(chan map[string]interface{}, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]interface{} @@ -121,6 +124,7 @@ func TestAliyunChatWithMessagesStopsQwenFlashAfterToolResult(t *testing.T) { {Role: "tool", Content: "retrieved text", ToolCallID: "previous-call"}, } response, err := model.ChatWithMessages( + ctx, "qwen-flash", messages, &APIConfig{ApiKey: &apiKey}, @@ -184,6 +188,7 @@ func TestAliyunToolChoiceStopsOnlyQwenFlashAfterToolResult(t *testing.T) { } func TestAliyunChatStreamlyWithSenderSupportsToolCalls(t *testing.T) { + ctx := t.Context() requestBody := make(chan map[string]interface{}, 1) requestPath := make(chan string, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -221,6 +226,7 @@ data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ra } var streamed []string err := model.ChatStreamlyWithSender( + ctx, "qwen-flash", []Message{{Role: "user", Content: "find ragflow"}}, &APIConfig{ApiKey: &apiKey}, @@ -276,6 +282,7 @@ data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ra } func TestAliyunChatStreamlyWithSenderRejectsStreamFalse(t *testing.T) { + ctx := t.Context() model := NewAliyunModel( map[string]string{"default": "https://dashscope.example"}, URLSuffix{Chat: "compatible-mode/v1/chat/completions"}, @@ -283,6 +290,7 @@ func TestAliyunChatStreamlyWithSenderRejectsStreamFalse(t *testing.T) { apiKey := "test-key" stream := false err := model.ChatStreamlyWithSender( + ctx, "qwen-flash", []Message{{Role: "user", Content: "hello"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/anthropic.go b/internal/entity/models/anthropic.go index 19159ebbf9..b754ccd6cb 100644 --- a/internal/entity/models/anthropic.go +++ b/internal/entity/models/anthropic.go @@ -61,7 +61,7 @@ func (a *AnthropicModel) region(apiConfig *APIConfig) string { return "default" } -func (a *AnthropicModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (a *AnthropicModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -102,7 +102,7 @@ func (a *AnthropicModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -368,7 +368,7 @@ func parseAnthropicChatResponse(body []byte) (string, string, error) { return answer.String(), reasoning.String(), nil } -func (a *AnthropicModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (a *AnthropicModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -386,7 +386,7 @@ func (a *AnthropicModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = strings.TrimSpace(strings.TrimSuffix(baseURL, "/")) url := fmt.Sprintf("%s/%s", baseURL, strings.TrimLeft(a.baseModel.URLSuffix.Models, "/")) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -428,55 +428,55 @@ func (a *AnthropicModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return models, nil } -func (a *AnthropicModel) CheckConnection(apiConfig *APIConfig) error { - _, err := a.ListModels(apiConfig) +func (a *AnthropicModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := a.ListModels(ctx, apiConfig) return err } -func (a *AnthropicModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AnthropicModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (a *AnthropicModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (a *AnthropicModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (a *AnthropicModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AnthropicModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (a *AnthropicModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AnthropicModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (a *AnthropicModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (a *AnthropicModel) ParseFile(ctx context.Context, 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()) } -func (a *AnthropicModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (a *AnthropicModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (a *AnthropicModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AnthropicModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (a *AnthropicModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 3a4a2ff533..d26629319e 100644 --- a/internal/entity/models/anthropic_test.go +++ b/internal/entity/models/anthropic_test.go @@ -86,9 +86,11 @@ func TestAnthropicChatHappyPath(t *testing.T) { }) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" resp, err := newAnthropicForTest(srv.URL).ChatWithMessages( + ctx, "claude-sonnet-4-5-20250929", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -157,6 +159,7 @@ func TestAnthropicChatMapsSystemConfigAndImages(t *testing.T) { }) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" maxTokens := 64 @@ -164,6 +167,7 @@ func TestAnthropicChatMapsSystemConfigAndImages(t *testing.T) { topP := 0.8 stop := []string{"END"} _, err := newAnthropicForTest(srv.URL).ChatWithMessages( + ctx, "claude-opus-4-5-20251101", []Message{ {Role: "system", Content: "be concise"}, @@ -216,9 +220,11 @@ func TestAnthropicChatMapsDataImageURL(t *testing.T) { }) }) defer srv.Close() + ctx := t.Context() apiKey := "test-key" _, err := newAnthropicForTest(srv.URL).ChatWithMessages( + ctx, "claude-sonnet-4-5-20250929", []Message{{Role: "user", Content: []interface{}{ map[string]interface{}{"type": "image_url", "image_url": map[string]interface{}{"url": "data:image/png;base64,aGVsbG8="}}, @@ -233,29 +239,31 @@ func TestAnthropicChatMapsDataImageURL(t *testing.T) { } func TestAnthropicChatValidationErrors(t *testing.T) { + ctx := t.Context() m := newAnthropicForTest("http://unused") apiKey := "test-key" - if _, err := m.ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := m.ChatWithMessages(ctx, "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, nil); err == nil || !strings.Contains(err.Error(), "messages is empty") { + if _, err := m.ChatWithMessages(ctx, "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, nil); err == nil || !strings.Contains(err.Error(), "unsupported message role") { + if _, err := m.ChatWithMessages(ctx, "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, nil); err == nil || !strings.Contains(err.Error(), "unsupported content block type") { + if _, err := m.ChatWithMessages(ctx, "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, nil); err == nil || !strings.Contains(err.Error(), "invalid text field") { + if _, err := m.ChatWithMessages(ctx, "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, nil); err == nil || !strings.Contains(err.Error(), "image block missing source") { + if _, err := m.ChatWithMessages(ctx, "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) } } func TestAnthropicChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newAnthropicServer(t, "/v1/messages", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":{"message":"bad key"}}`)) @@ -263,26 +271,28 @@ 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, nil) + _, err := newAnthropicForTest(srv.URL).ChatWithMessages(ctx, "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) } } func TestAnthropicChatRejectsMalformedResponse(t *testing.T) { + ctx := t.Context() srv := newAnthropicServer(t, "/v1/messages", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"content": []map[string]interface{}{{"type": "tool_use"}}}) }) defer srv.Close() apiKey := "test-key" - _, err := newAnthropicForTest(srv.URL).ChatWithMessages("claude", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newAnthropicForTest(srv.URL).ChatWithMessages(ctx, "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) } } func TestAnthropicListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() var calls int srv := newAnthropicServer(t, "/v1/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { calls++ @@ -297,14 +307,14 @@ func TestAnthropicListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" m := newAnthropicForTest(srv.URL) - models, err := m.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "claude-sonnet-4-5-20250929,claude-haiku-4-5-20251001" { t.Errorf("models=%v", models) } - if err := m.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err = m.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection: %v", err) } if calls != 2 { @@ -313,6 +323,7 @@ func TestAnthropicListModelsAndCheckConnection(t *testing.T) { } func TestAnthropicListModelsRejectsProviderError(t *testing.T) { + ctx := t.Context() srv := newAnthropicServer(t, "/v1/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"error":"forbidden"}`)) @@ -320,7 +331,7 @@ func TestAnthropicListModelsRejectsProviderError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newAnthropicForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := newAnthropicForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "403") { t.Errorf("expected 403 error, got %v", err) } @@ -337,6 +348,7 @@ func TestAnthropicFactoryRegistration(t *testing.T) { } func TestAnthropicUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newAnthropicForTest("http://unused") apiKey := "test-key" modelName := "claude" @@ -344,44 +356,44 @@ func TestAnthropicUnsupportedMethods(t *testing.T) { name string err error }{ - {"stream", m.ChatStreamlyWithSender(modelName, []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil })}, + {"stream", m.ChatStreamlyWithSender(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, &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") { + if _, err := m.Balance(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.TranscribeAudioWithSender(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.AudioSpeechWithSender(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(ctx, &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") { + if _, err := m.ListTasks(ctx, &APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ListTasks: got %v", err) } - if _, err := m.ShowTask("task-id", &APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ShowTask(ctx, "task-id", &APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ShowTask: got %v", err) } } diff --git a/internal/entity/models/astraflow.go b/internal/entity/models/astraflow.go index 3b4d23db35..bc28e5ab15 100644 --- a/internal/entity/models/astraflow.go +++ b/internal/entity/models/astraflow.go @@ -68,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (a *AstraflowModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -117,7 +117,7 @@ func (a *AstraflowModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -178,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AstraflowModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -236,7 +236,7 @@ func (a *AstraflowModel) ChatStreamlyWithSender(modelName string, messages []Mes // SSE is long-lived; rely on the transport's ResponseHeaderTimeout // to cap connection-establishment instead of a hard deadline. - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -301,7 +301,7 @@ func (a *AstraflowModel) ChatStreamlyWithSender(modelName string, messages []Mes return nil } -func (a *AstraflowModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (a *AstraflowModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -313,7 +313,7 @@ func (a *AstraflowModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, a.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -348,12 +348,12 @@ func (a *AstraflowModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(modelList), nil } -func (a *AstraflowModel) CheckConnection(apiConfig *APIConfig) error { - _, err := a.ListModels(apiConfig) +func (a *AstraflowModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := a.ListModels(ctx, apiConfig) return err } -func (a *AstraflowModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (a *AstraflowModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -430,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (a *AstraflowModel) Rerank(ctx context.Context, 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 } @@ -508,19 +508,19 @@ func (a *AstraflowModel) Rerank(modelName *string, query string, documents []str return &rerankResponse, nil } -func (a *AstraflowModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (a *AstraflowModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (a *AstraflowModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AstraflowModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (a *AstraflowModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -580,22 +580,22 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AstraflowModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (a *AstraflowModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (a *AstraflowModel) ParseFile(ctx context.Context, 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()) } -func (a *AstraflowModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (a *AstraflowModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AstraflowModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (a *AstraflowModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 056a430283..3a41ac73f4 100644 --- a/internal/entity/models/astraflow_test.go +++ b/internal/entity/models/astraflow_test.go @@ -92,6 +92,7 @@ func TestAstraflowFactory(t *testing.T) { } func TestAstraflowChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newAstraflowServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "claude-opus-4-7" { t.Errorf("model=%v", body["model"]) @@ -120,6 +121,7 @@ func TestAstraflowChatHappyPath(t *testing.T) { mt := 64 temp := 0.3 resp, err := newAstraflowForTest(srv.URL).ChatWithMessages( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -138,6 +140,7 @@ func TestAstraflowChatHappyPath(t *testing.T) { } func TestAstraflowChatNoReasoning(t *testing.T) { + ctx := t.Context() srv := newAstraflowServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -149,6 +152,7 @@ func TestAstraflowChatNoReasoning(t *testing.T) { apiKey := "test-key" resp, err := newAstraflowForTest(srv.URL).ChatWithMessages( + ctx, "gpt-4o-mini", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -164,7 +168,9 @@ func TestAstraflowChatNoReasoning(t *testing.T) { } func TestAstraflowChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() _, err := newAstraflowForTest("http://unused").ChatWithMessages( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) @@ -174,15 +180,17 @@ func TestAstraflowChatRequiresAPIKey(t *testing.T) { } func TestAstraflowChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" _, err := newAstraflowForTest("http://unused").ChatWithMessages( - "claude-opus-4-7", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx, "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) } } func TestAstraflowChatPropagatesHTTPError(t *testing.T) { + ctx := t.Context() srv := newAstraflowServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"bad key"}`)) @@ -191,6 +199,7 @@ func TestAstraflowChatPropagatesHTTPError(t *testing.T) { apiKey := "test-key" _, err := newAstraflowForTest(srv.URL).ChatWithMessages( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -200,6 +209,7 @@ func TestAstraflowChatPropagatesHTTPError(t *testing.T) { } func TestAstraflowStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newAstraflowSSEServer(t, "/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant"}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"content":"Hello"}}]}`+"\n"+ @@ -212,6 +222,7 @@ func TestAstraflowStreamHappyPath(t *testing.T) { var chunks []string var sawDone bool err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -238,6 +249,7 @@ func TestAstraflowStreamHappyPath(t *testing.T) { } func TestAstraflowStreamSplitsReasoning(t *testing.T) { + ctx := t.Context() srv := newAstraflowSSEServer(t, "/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant"}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"reasoning_content":"step 1. "}}]}`+"\n"+ @@ -250,6 +262,7 @@ func TestAstraflowStreamSplitsReasoning(t *testing.T) { apiKey := "test-key" var content, reasoning []string err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( + ctx, "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -277,9 +290,11 @@ func TestAstraflowStreamSplitsReasoning(t *testing.T) { } func TestAstraflowStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() apiKey := "test-key" stream := false err := newAstraflowForTest("http://unused").ChatStreamlyWithSender( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -292,8 +307,10 @@ func TestAstraflowStreamRejectsExplicitFalse(t *testing.T) { } func TestAstraflowStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newAstraflowForTest("http://unused").ChatStreamlyWithSender( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) @@ -303,6 +320,7 @@ func TestAstraflowStreamRequiresSender(t *testing.T) { } func TestAstraflowStreamFailsWithoutTerminal(t *testing.T) { + ctx := t.Context() srv := newAstraflowSSEServer(t, "/chat/completions", `data: {"choices":[{"delta":{"content":"half"}}]}`+"\n", ) @@ -310,6 +328,7 @@ func TestAstraflowStreamFailsWithoutTerminal(t *testing.T) { apiKey := "test-key" err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -320,6 +339,7 @@ func TestAstraflowStreamFailsWithoutTerminal(t *testing.T) { } func TestAstraflowStreamRejectsMalformedFrame(t *testing.T) { + ctx := t.Context() srv := newAstraflowSSEServer(t, "/chat/completions", `data: {"choices":[{"delta":{"content":"ok"}}]}`+"\n"+ `data: {oops not json}`+"\n", @@ -328,6 +348,7 @@ func TestAstraflowStreamRejectsMalformedFrame(t *testing.T) { apiKey := "test-key" err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -338,6 +359,7 @@ func TestAstraflowStreamRejectsMalformedFrame(t *testing.T) { } func TestAstraflowStreamSurfacesUpstreamError(t *testing.T) { + ctx := t.Context() srv := newAstraflowSSEServer(t, "/chat/completions", `data: {"choices":[{"delta":{"content":"partial "}}]}`+"\n"+ `data: {"error":{"message":"rate limit","type":"rate_limit_error"}}`+"\n", @@ -346,6 +368,7 @@ func TestAstraflowStreamSurfacesUpstreamError(t *testing.T) { apiKey := "test-key" err := newAstraflowForTest(srv.URL).ChatStreamlyWithSender( + ctx, "claude-opus-4-7", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -359,6 +382,7 @@ func TestAstraflowStreamSurfacesUpstreamError(t *testing.T) { } func TestAstraflowListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newAstraflowServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -371,7 +395,7 @@ func TestAstraflowListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newAstraflowForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newAstraflowForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -382,13 +406,15 @@ func TestAstraflowListModelsHappyPath(t *testing.T) { } func TestAstraflowListModelsRequiresAPIKey(t *testing.T) { - _, err := newAstraflowForTest("http://unused").ListModels(&APIConfig{}) + ctx := t.Context() + _, err := newAstraflowForTest("http://unused").ListModels(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } } func TestAstraflowCheckConnectionDelegatesToListModels(t *testing.T) { + ctx := t.Context() srv := newAstraflowServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{{"id": "claude-opus-4-7"}}, @@ -397,12 +423,13 @@ func TestAstraflowCheckConnectionDelegatesToListModels(t *testing.T) { defer srv.Close() apiKey := "test-key" - if err := newAstraflowForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newAstraflowForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection: %v", err) } } func TestAstraflowCheckConnectionPropagatesError(t *testing.T) { + ctx := t.Context() srv := newAstraflowServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"bad key"}`)) @@ -410,47 +437,50 @@ func TestAstraflowCheckConnectionPropagatesError(t *testing.T) { defer srv.Close() apiKey := "test-key" - err := newAstraflowForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}) + err := newAstraflowForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } } func TestAstraflowBaseURLForRegionUnknown(t *testing.T) { + ctx := t.Context() m := newAstraflowForTest("http://unused") apiKey := "test-key" region := "missing" - _, err := m.ListModels(&APIConfig{ApiKey: &apiKey, Region: ®ion}) + _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey, Region: ®ion}) if err == nil || !strings.Contains(err.Error(), "no base URL configured") { t.Errorf("expected base-URL error, got %v", err) } } func TestAstraflowEmbedReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() // 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, nil) + embeddings, err := newAstraflowForTest("http://unused").Embed(ctx, 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) } } func TestAstraflowAudioOCRReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newAstraflowForTest("http://unused") model := "x" apiKey := "test-key" // TranscribeAudio is a stub → "no such method" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || strings.Contains(err.Error(), "api key is required") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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 282669fe8b..4818965d45 100644 --- a/internal/entity/models/avian.go +++ b/internal/entity/models/avian.go @@ -114,7 +114,7 @@ type avianChatResponse struct { FinishReason string `json:"finish_reason"` } -func (a *AvianModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (a *AvianModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -135,7 +135,7 @@ func (a *AvianModel) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -181,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AvianModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -209,7 +209,7 @@ func (a *AvianModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -273,7 +273,7 @@ type avianModelInfo struct { ID string `json:"id"` } -func (a *AvianModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (a *AvianModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -285,7 +285,7 @@ func (a *AvianModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, a.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -317,51 +317,51 @@ func (a *AvianModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return ParseListModel(modelList), nil } -func (a *AvianModel) CheckConnection(apiConfig *APIConfig) error { - _, err := a.ListModels(apiConfig) +func (a *AvianModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := a.ListModels(ctx, apiConfig) return err } -func (a *AvianModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (a *AvianModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (a *AvianModel) Rerank(ctx context.Context, 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 *AvianModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (a *AvianModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (a *AvianModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AvianModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (a *AvianModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AvianModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (a *AvianModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (a *AvianModel) ParseFile(ctx context.Context, 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()) } -func (a *AvianModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (a *AvianModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AvianModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (a *AvianModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 9322102b6d..cc1302a17a 100644 --- a/internal/entity/models/avian_test.go +++ b/internal/entity/models/avian_test.go @@ -79,6 +79,7 @@ func TestAvianFactory(t *testing.T) { } func TestAvianChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newAvianServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/v1/chat/completions" { t.Errorf("path=%s, want /v1/chat/completions", r.URL.Path) @@ -118,6 +119,7 @@ func TestAvianChatHappyPath(t *testing.T) { topP := 0.9 stop := []string{"END"} resp, err := newAvianForTest(srv.URL).ChatWithMessages( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -136,6 +138,7 @@ func TestAvianChatHappyPath(t *testing.T) { } func TestAvianChatFallsBackToReasoningField(t *testing.T) { + ctx := t.Context() srv := newAvianServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -150,6 +153,7 @@ func TestAvianChatFallsBackToReasoningField(t *testing.T) { apiKey := "test-key" resp, err := newAvianForTest(srv.URL).ChatWithMessages( + ctx, "moonshotai/kimi-k2.5", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, @@ -165,7 +169,9 @@ func TestAvianChatFallsBackToReasoningField(t *testing.T) { } func TestAvianChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() _, err := newAvianForTest("http://unused").ChatWithMessages( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, @@ -178,8 +184,10 @@ func TestAvianChatRequiresAPIKey(t *testing.T) { } func TestAvianChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" _, err := newAvianForTest("http://unused").ChatWithMessages( + ctx, "deepseek/deepseek-v3.2", []Message{}, &APIConfig{ApiKey: &apiKey}, @@ -192,6 +200,7 @@ func TestAvianChatRequiresMessages(t *testing.T) { } func TestAvianChatPropagatesUpstreamErrorStatus(t *testing.T) { + ctx := t.Context() srv := newAvianServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = io.WriteString(w, `{"error":"invalid key"}`) @@ -200,6 +209,7 @@ func TestAvianChatPropagatesUpstreamErrorStatus(t *testing.T) { apiKey := "test-key" _, err := newAvianForTest(srv.URL).ChatWithMessages( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -212,6 +222,7 @@ func TestAvianChatPropagatesUpstreamErrorStatus(t *testing.T) { } func TestAvianStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newAvianServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/v1/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -233,6 +244,7 @@ func TestAvianStreamHappyPath(t *testing.T) { var content, reasoning []string var sawDone bool err := newAvianForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -265,9 +277,11 @@ func TestAvianStreamHappyPath(t *testing.T) { } func TestAvianStreamRejectsFalseStreamConfig(t *testing.T) { + ctx := t.Context() apiKey := "test-key" stream := false err := newAvianForTest("http://unused").ChatStreamlyWithSender( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -281,8 +295,10 @@ func TestAvianStreamRejectsFalseStreamConfig(t *testing.T) { } func TestAvianStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newAvianForTest("http://unused").ChatStreamlyWithSender( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -296,6 +312,7 @@ func TestAvianStreamRequiresSender(t *testing.T) { } func TestAvianListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := newAvianServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/v1/models" { t.Errorf("path=%s, want /v1/models", r.URL.Path) @@ -306,22 +323,24 @@ func TestAvianListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" cfg := &APIConfig{ApiKey: &apiKey} - models, err := newAvianForTest(srv.URL).ListModels(cfg) + models, err := newAvianForTest(srv.URL).ListModels(ctx, cfg) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "deepseek/deepseek-v3.2,moonshotai/kimi-k2.5" { t.Errorf("models=%v", models) } - if err := newAvianForTest(srv.URL).CheckConnection(cfg); err != nil { + if err = newAvianForTest(srv.URL).CheckConnection(ctx, cfg); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestAvianMissingBaseURLFailsClearly(t *testing.T) { + ctx := t.Context() a := NewAvianModel(map[string]string{}, URLSuffix{Chat: "v1/chat/completions"}) apiKey := "test-key" _, err := a.ChatWithMessages( + ctx, "deepseek/deepseek-v3.2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -334,25 +353,26 @@ func TestAvianMissingBaseURLFailsClearly(t *testing.T) { } func TestAvianUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() a := newAvianForTest("http://unused") model := "deepseek/deepseek-v3.2" - if _, err := a.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.Rerank(ctx, &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") { + if _, err := a.Balance(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := a.OCRFile(ctx, &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 79d277411f..55c5febcb5 100644 --- a/internal/entity/models/azure_openai.go +++ b/internal/entity/models/azure_openai.go @@ -61,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (a *AzureOpenAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -115,7 +115,7 @@ func (a *AzureOpenAIModel) ChatWithMessages(modelName string, messages []Message return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -182,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AzureOpenAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -241,7 +241,7 @@ func (a *AzureOpenAIModel) ChatStreamlyWithSender(modelName string, messages []M if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -313,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (a *AzureOpenAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -346,7 +346,7 @@ func (a *AzureOpenAIModel) Embed(modelName *string, texts []string, apiConfig *A return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -391,7 +391,7 @@ func (a *AzureOpenAIModel) Embed(modelName *string, texts []string, apiConfig *A } // ListModels returns the deployment names visible to the configured API key. -func (a *AzureOpenAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (a *AzureOpenAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := a.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -404,7 +404,7 @@ func (a *AzureOpenAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse url := fmt.Sprintf("%s/%s?api-version=%s", strings.TrimRight(baseURL, "/"), a.baseModel.URLSuffix.Models, azureAPIVersion) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -442,49 +442,49 @@ func (a *AzureOpenAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse } // CheckConnection runs a lightweight ListModels call to verify the endpoint -func (a *AzureOpenAIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := a.ListModels(apiConfig) +func (a *AzureOpenAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := a.ListModels(ctx, apiConfig) return err } // Balance is not exposed by the Azure OpenAI API. -func (a *AzureOpenAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (a *AzureOpenAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // Rerank is not exposed by the Azure OpenAI API. -func (a *AzureOpenAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (a *AzureOpenAIModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (a *AzureOpenAIModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AzureOpenAIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (a *AzureOpenAIModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (a *AzureOpenAIModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (a *AzureOpenAIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (a *AzureOpenAIModel) ParseFile(ctx context.Context, 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()) } -func (a *AzureOpenAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (a *AzureOpenAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", a.Name()) } -func (a *AzureOpenAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (a *AzureOpenAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 fed00bf8af..f85c1d50fe 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (b *BaichuanModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -103,7 +103,7 @@ func (b *BaichuanModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BaichuanModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -223,7 +223,7 @@ func (b *BaichuanModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (b *BaichuanModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -315,7 +315,7 @@ func (b *BaichuanModel) Embed(modelName *string, texts []string, apiConfig *APIC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -367,54 +367,54 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (b *BaichuanModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (b *BaichuanModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BaichuanModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (b *BaichuanModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BaichuanModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (b *BaichuanModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (b *BaichuanModel) ParseFile(ctx context.Context, 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()) } -func (b *BaichuanModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (b *BaichuanModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("no such method") } -func (b *BaichuanModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (b *BaichuanModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } -func (b *BaichuanModel) CheckConnection(apiConfig *APIConfig) error { +func (b *BaichuanModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("no such method") } -func (b *BaichuanModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (b *BaichuanModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaichuanModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (b *BaichuanModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 6f9da4bc63..1db8a39457 100644 --- a/internal/entity/models/baidu.go +++ b/internal/entity/models/baidu.go @@ -50,7 +50,7 @@ func (b *BaiduModel) Name() string { return "BaiduYiyan" } -func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (b *BaiduModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -146,7 +146,7 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -233,7 +233,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BaiduModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -334,7 +334,7 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -438,7 +438,7 @@ type baiduUsage struct { TotalTokens int `json:"total_tokens"` } -func (b *BaiduModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (b *BaiduModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -468,7 +468,7 @@ func (b *BaiduModel) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -535,7 +535,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (b *BaiduModel) Rerank(ctx context.Context, 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 } @@ -567,7 +567,7 @@ func (b *BaiduModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -617,20 +617,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (b *BaiduModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BaiduModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (b *BaiduModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BaiduModel) AudioSpeechWithSender(ctx context.Context, 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()) } @@ -646,7 +646,7 @@ type qianfanOCRResponse struct { } `json:"result"` } -func (b *BaiduModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (b *BaiduModel) OCRFile(ctx context.Context, 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 } @@ -691,7 +691,7 @@ func (b *BaiduModel) OCRFile(modelName *string, content []byte, fileURL *string, return nil, fmt.Errorf("failed to marshal json payload: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -736,7 +736,7 @@ func (b *BaiduModel) OCRFile(modelName *string, content []byte, fileURL *string, return &ocrResponse, nil } -func (b *BaiduModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (b *BaiduModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -754,7 +754,7 @@ func (b *BaiduModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -789,23 +789,23 @@ func (b *BaiduModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return ParseListModel(modelList), nil } -func (b *BaiduModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (b *BaiduModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaiduModel) CheckConnection(apiConfig *APIConfig) error { - _, err := b.ListModels(apiConfig) +func (b *BaiduModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := b.ListModels(ctx, apiConfig) return err } -func (b *BaiduModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (b *BaiduModel) ParseFile(ctx context.Context, 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()) } -func (b *BaiduModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (b *BaiduModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } -func (b *BaiduModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (b *BaiduModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 fc2371102a..6b42401368 100644 --- a/internal/entity/models/bedrock.go +++ b/internal/entity/models/bedrock.go @@ -505,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (b *BedrockModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -531,7 +531,7 @@ func (b *BedrockModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("bedrock: marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() creds, err := resolveBedrockCredentials(ctx, key, region) @@ -589,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BedrockModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -625,7 +625,6 @@ func (b *BedrockModel) ChatStreamlyWithSender(modelName string, messages []Messa // Background context: event streams are long-lived so we attach // no overall deadline. ResponseHeaderTimeout (set in the // constructor) still caps connection setup. - ctx := context.Background() creds, err := resolveBedrockCredentials(ctx, key, region) if err != nil { return err @@ -767,7 +766,7 @@ type bedrockListModelsResponse struct { // configured credentials. The control plane lives at // bedrock.{region}.amazonaws.com (not bedrock-runtime), signs against // the "bedrock" service, and is GET-only. -func (b *BedrockModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (b *BedrockModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := b.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -781,7 +780,7 @@ func (b *BedrockModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() creds, err := resolveBedrockCredentials(ctx, key, region) @@ -836,8 +835,8 @@ func (b *BedrockModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er // CheckConnection delegates to ListModels: a successful catalog query // proves credentials, region, and network reachability in one round // trip without burning a chat completion. -func (b *BedrockModel) CheckConnection(apiConfig *APIConfig) error { - _, err := b.ListModels(apiConfig) +func (b *BedrockModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := b.ListModels(ctx, apiConfig) return err } @@ -864,7 +863,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (b *BedrockModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if len(texts) == 0 { return []EmbeddingData{}, nil } @@ -885,7 +884,7 @@ func (b *BedrockModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() creds, err := resolveBedrockCredentials(ctx, key, region) @@ -1026,52 +1025,52 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (b *BedrockModel) Rerank(ctx context.Context, 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()) } // Balance is not exposed by Bedrock. -func (b *BedrockModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (b *BedrockModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } // 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (b *BedrockModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BedrockModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (b *BedrockModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BedrockModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (b *BedrockModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (b *BedrockModel) ParseFile(ctx context.Context, 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()) } // ListTasks is not exposed by Bedrock through the Converse API. -func (b *BedrockModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (b *BedrockModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", b.Name()) } // ShowTask is not exposed by Bedrock through the Converse API. -func (b *BedrockModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (b *BedrockModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 156d41ec22..9687e1dec0 100644 --- a/internal/entity/models/bedrock_test.go +++ b/internal/entity/models/bedrock_test.go @@ -327,6 +327,7 @@ func newBedrockServer(t *testing.T, wantMethod, wantPath string, handler http.Ha } func TestBedrockChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodPost, "/model/anthropic.claude-3-haiku-20240307-v1:0/converse", func(w http.ResponseWriter, r *http.Request) { @@ -350,7 +351,7 @@ func TestBedrockChatHappyPath(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() - resp, err := m.ChatWithMessages("anthropic.claude-3-haiku-20240307-v1:0", + resp, err := m.ChatWithMessages(ctx, "anthropic.claude-3-haiku-20240307-v1:0", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &key}, nil, nil) if err != nil { @@ -368,17 +369,19 @@ func TestBedrockChatHappyPath(t *testing.T) { } func TestBedrockChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") - _, err := m.ChatWithMessages("m", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) + _, err := m.ChatWithMessages(ctx, "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) } } func TestBedrockChatRequiresModelID(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") key := validBedrockKey() - _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, + _, err := m.ChatWithMessages(ctx, "", []Message{{Role: "user", Content: "x"}}, &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) @@ -386,6 +389,7 @@ func TestBedrockChatRequiresModelID(t *testing.T) { } func TestBedrockChatPropagatesHTTPError(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodPost, "/model/m/converse", func(w http.ResponseWriter, r *http.Request) { @@ -396,7 +400,7 @@ func TestBedrockChatPropagatesHTTPError(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() - _, err := m.ChatWithMessages("m", + _, err := m.ChatWithMessages(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { @@ -408,6 +412,7 @@ func TestBedrockChatPropagatesHTTPError(t *testing.T) { } func TestBedrockListModelsParsesCatalog(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodGet, "/foundation-models", func(w http.ResponseWriter, r *http.Request) { @@ -424,7 +429,7 @@ func TestBedrockListModelsParsesCatalog(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() - got, err := m.ListModels(&APIConfig{ApiKey: &key}) + got, err := m.ListModels(ctx, &APIConfig{ApiKey: &key}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -445,6 +450,7 @@ func TestBedrockListModelsParsesCatalog(t *testing.T) { } func TestBedrockCheckConnectionDelegates(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodGet, "/foundation-models", func(w http.ResponseWriter, r *http.Request) { @@ -453,7 +459,7 @@ func TestBedrockCheckConnectionDelegates(t *testing.T) { defer srv.Close() m := newBedrockForTest(srv.URL) key := validBedrockKey() - if err := m.CheckConnection(&APIConfig{ApiKey: &key}); err == nil || !strings.Contains(err.Error(), "403") { + if err := m.CheckConnection(ctx, &APIConfig{ApiKey: &key}); err == nil || !strings.Contains(err.Error(), "403") { t.Errorf("want 403 surfaced via ListModels, got %v", err) } } @@ -497,6 +503,7 @@ func encodeBedrockEventFrames(t *testing.T, events []struct { } func TestBedrockStreamDecodesContentDeltas(t *testing.T) { + ctx := t.Context() frames := encodeBedrockEventFrames(t, []struct { eventType string messageType string @@ -520,7 +527,7 @@ func TestBedrockStreamDecodesContentDeltas(t *testing.T) { key := validBedrockKey() var chunks []string sawDone := false - err := m.ChatStreamlyWithSender("m", + err := m.ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &key}, nil, nil, func(c *string, _ *string) error { @@ -546,6 +553,7 @@ func TestBedrockStreamDecodesContentDeltas(t *testing.T) { } func TestBedrockStreamSurfacesException(t *testing.T) { + ctx := t.Context() frames := encodeBedrockEventFrames(t, []struct { eventType string messageType string @@ -568,7 +576,7 @@ func TestBedrockStreamSurfacesException(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() - err := m.ChatStreamlyWithSender("m", + err := m.ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil, func(*string, *string) error { return nil }) @@ -578,6 +586,7 @@ func TestBedrockStreamSurfacesException(t *testing.T) { } func TestBedrockStreamFailsWithoutTerminal(t *testing.T) { + ctx := t.Context() // Connection closed cleanly after a delta but before messageStop. // This used to be silently treated as success, masking truncated // answers; the driver must now surface a "stream ended before @@ -599,7 +608,7 @@ func TestBedrockStreamFailsWithoutTerminal(t *testing.T) { m := newBedrockForTest(srv.URL) key := validBedrockKey() - err := m.ChatStreamlyWithSender("m", + err := m.ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil, func(*string, *string) error { return nil }) @@ -609,10 +618,11 @@ func TestBedrockStreamFailsWithoutTerminal(t *testing.T) { } func TestBedrockStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") key := validBedrockKey() stream := false - err := m.ChatStreamlyWithSender("m", + err := m.ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, &ChatConfig{Stream: &stream}, @@ -624,9 +634,10 @@ func TestBedrockStreamRejectsExplicitFalse(t *testing.T) { } func TestBedrockStreamRequiresSender(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") key := validBedrockKey() - err := m.ChatStreamlyWithSender("m", + err := m.ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { @@ -648,6 +659,7 @@ func TestLookupBedrockEventHeader(t *testing.T) { } func TestBedrockTitanEmbedHappyPath(t *testing.T) { + ctx := t.Context() var seenInputs []string srv := newBedrockServer(t, http.MethodPost, "/model/amazon.titan-embed-text-v2:0/invoke", @@ -674,7 +686,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}, nil) + got, err := m.Embed(ctx, &model, []string{"alpha", "beta"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 256}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -690,6 +702,7 @@ func TestBedrockTitanEmbedHappyPath(t *testing.T) { } func TestBedrockTitanV1OmitsDimension(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodPost, "/model/amazon.titan-embed-text-v1/invoke", func(w http.ResponseWriter, r *http.Request) { @@ -704,12 +717,13 @@ 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}, nil); err != nil { + if _, err := m.Embed(ctx, &model, []string{"alpha"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 256}, nil); err != nil { t.Fatalf("Embed: %v", err) } } func TestBedrockCohereEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodPost, "/model/cohere.embed-english-v3/invoke", func(w http.ResponseWriter, r *http.Request) { @@ -736,7 +750,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}, nil) + got, err := m.Embed(ctx, &model, []string{"first", "second"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 128}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -746,6 +760,7 @@ func TestBedrockCohereEmbedHappyPath(t *testing.T) { } func TestBedrockCohereV4ForwardsDimensionAndParsesTypedResponse(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodPost, "/model/cohere.embed-v4:0/invoke", func(w http.ResponseWriter, r *http.Request) { @@ -765,7 +780,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}, nil) + got, err := m.Embed(ctx, &model, []string{"first"}, &APIConfig{ApiKey: &key}, &EmbeddingConfig{Dimension: 512}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -775,8 +790,9 @@ func TestBedrockCohereV4ForwardsDimensionAndParsesTypedResponse(t *testing.T) { } func TestBedrockEmbedShortCircuitsEmptyInput(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") - got, err := m.Embed(nil, nil, nil, nil, nil) + got, err := m.Embed(ctx, nil, nil, nil, nil, nil) if err != nil { t.Fatalf("Embed empty: %v", err) } @@ -786,28 +802,31 @@ func TestBedrockEmbedShortCircuitsEmptyInput(t *testing.T) { } func TestBedrockEmbedRequiresAPIKeyAndModel(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") model := "x" - if _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := m.Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := m.Embed(ctx, &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) } } func TestBedrockEmbedRejectsUnsupportedModel(t *testing.T) { + ctx := t.Context() 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, nil); err == nil || !strings.Contains(err.Error(), "unsupported embedding model") { + if _, err := m.Embed(ctx, &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) } } func TestBedrockEmbedPropagatesHTTPError(t *testing.T) { + ctx := t.Context() srv := newBedrockServer(t, http.MethodPost, "/model/amazon.titan-embed-text-v2:0/invoke", func(w http.ResponseWriter, r *http.Request) { @@ -819,45 +838,48 @@ 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, nil); err == nil || !strings.Contains(err.Error(), "400") || !strings.Contains(err.Error(), "bad input") { + if _, err := m.Embed(ctx, &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) } } func TestBedrockRerankReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") model := "x" - if _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, &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) } } func TestBedrockBalanceReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") - if _, err := m.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Balance(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: want no-such-method, got %v", err) } } func TestBedrockAudioOCRReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newBedrockForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(ctx, &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") { + if _, err := m.ListTasks(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ListTasks: want no-such-method, got %v", err) } - if _, err := m.ShowTask("t", &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ShowTask(ctx, "t", &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ShowTask: want no-such-method, got %v", err) } } diff --git a/internal/entity/models/builtin.go b/internal/entity/models/builtin.go index bb4c8fcf20..cdefc00794 100644 --- a/internal/entity/models/builtin.go +++ b/internal/entity/models/builtin.go @@ -2,6 +2,7 @@ package models import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -47,16 +48,16 @@ func (b *BuiltinModel) NewInstance(baseURL map[string]string) ModelDriver { } } -func (b *BuiltinModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (b *BuiltinModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BuiltinModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (b *BuiltinModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if len(texts) == 0 { return []EmbeddingData{}, nil } @@ -122,35 +123,35 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (b *BuiltinModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (b *BuiltinModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BuiltinModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (b *BuiltinModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (b *BuiltinModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (b *BuiltinModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (b *BuiltinModel) ParseFile(ctx context.Context, 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") } -func (b *BuiltinModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (b *BuiltinModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return []ListModelResponse{ { Name: b.model, @@ -158,21 +159,21 @@ func (b *BuiltinModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er }, nil } -func (b *BuiltinModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (b *BuiltinModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("builtin model does not support balance") } -func (b *BuiltinModel) CheckConnection(apiConfig *APIConfig) error { +func (b *BuiltinModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { // Try to get model info to verify connection - _, err := b.Embed(nil, []string{"test"}, apiConfig, nil, nil) + _, err := b.Embed(ctx, nil, []string{"test"}, apiConfig, nil, nil) return err } -func (b *BuiltinModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (b *BuiltinModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("builtin model does not support tasks") } -func (b *BuiltinModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (b *BuiltinModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("builtin model does not support tasks") } diff --git a/internal/entity/models/chat_tools.go b/internal/entity/models/chat_tools.go index b49af5b2e0..933cf3f806 100644 --- a/internal/entity/models/chat_tools.go +++ b/internal/entity/models/chat_tools.go @@ -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, nil) + resp, err := cm.ModelDriver.ChatWithMessages(ctx, *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, nil) + resp, err := cm.ModelDriver.ChatWithMessages(ctx, *cm.ModelName, history, cm.APIConfig, &cfg, nil) if err != nil { return "", totalTokens, fmt.Errorf("final call: %w", err) } @@ -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, nil, func(delta *string, reason *string) error { + err := cm.ModelDriver.ChatStreamlyWithSender(ctx, *cm.ModelName, history, cm.APIConfig, &cfg, nil, func(delta *string, reason *string) error { if reason != nil && *reason != "" { if !reasoningStarted { reasoningStarted = true @@ -351,7 +351,7 @@ func runStreamToolLoop(ctx context.Context, cm *ChatModel, history []Message, to var exceedUsage TokenUsage cfg.UsageResult = &exceedUsage var exceedTokens int - err := cm.ModelDriver.ChatStreamlyWithSender(*cm.ModelName, history, cm.APIConfig, &cfg, nil, func(delta *string, reason *string) error { + err := cm.ModelDriver.ChatStreamlyWithSender(ctx, *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 5f4877ebe1..5b963285d4 100644 --- a/internal/entity/models/cohere.go +++ b/internal/entity/models/cohere.go @@ -53,7 +53,7 @@ func (c *CoHereModel) Name() string { return "Cohere" } -func (c *CoHereModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (c *CoHereModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -119,7 +119,7 @@ func (c *CoHereModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -186,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (c *CoHereModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -263,7 +263,7 @@ func (c *CoHereModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -337,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (c *CoHereModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -370,7 +370,7 @@ func (c *CoHereModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -421,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (c *CoHereModel) Rerank(ctx context.Context, 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 } @@ -455,7 +455,7 @@ func (c *CoHereModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -506,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (c *CoHereModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -584,7 +584,7 @@ func (c *CoHereModel) TranscribeAudio(modelName *string, file *string, apiConfig } // build request - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, &body) @@ -621,30 +621,30 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (c *CoHereModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (c *CoHereModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (c *CoHereModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (c *CoHereModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (c *CoHereModel) ParseFile(ctx context.Context, 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()) } -func (c *CoHereModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (c *CoHereModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -655,7 +655,7 @@ func (c *CoHereModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err } url := fmt.Sprintf("%s/%s", resolvedBaseURL, c.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -704,19 +704,19 @@ func (c *CoHereModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return ParseListModel(ModelList{Models: models}), nil } -func (c *CoHereModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (c *CoHereModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } -func (c *CoHereModel) CheckConnection(apiConfig *APIConfig) error { - _, err := c.ListModels(apiConfig) +func (c *CoHereModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := c.ListModels(ctx, apiConfig) return err } -func (c *CoHereModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (c *CoHereModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } -func (c *CoHereModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (c *CoHereModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 5eccfa1169..992bc5e7ae 100644 --- a/internal/entity/models/cometapi.go +++ b/internal/entity/models/cometapi.go @@ -235,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (c *CometAPIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -255,7 +255,7 @@ func (c *CometAPIModel) ChatWithMessages(modelName string, messages []Message, a reqBody := buildCometAPIChatRequest(modelName, messages, false, chatModelConfig) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newCometAPIJSONRequest(ctx, "POST", url, reqBody, apiKey) @@ -274,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (c *CometAPIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -309,7 +309,7 @@ func (c *CometAPIModel) ChatStreamlyWithSender(modelName string, messages []Mess } reqBody := buildCometAPIChatRequest(modelName, messages, true, chatModelConfig) - req, err := newCometAPIJSONRequest(context.Background(), "POST", url, reqBody, apiKey) + req, err := newCometAPIJSONRequest(ctx, "POST", url, reqBody, apiKey) if err != nil { return err } @@ -384,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (c *CometAPIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -412,7 +412,7 @@ func (c *CometAPIModel) Embed(modelName *string, texts []string, apiConfig *APIC reqBody.Dimensions = embeddingConfig.Dimension } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newCometAPIJSONRequest(ctx, "POST", url, reqBody, apiKey) @@ -459,13 +459,13 @@ func (c *CometAPIModel) Embed(modelName *string, texts []string, apiConfig *APIC } // ListModels returns the public CometAPI model catalog. -func (c *CometAPIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (c *CometAPIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { url, err := c.endpointURL(cometapiRegion(apiConfig), c.baseModel.URLSuffix.Models) if err != nil { return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -491,7 +491,7 @@ func (c *CometAPIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e } // Balance queries CometAPI's quota service. -func (c *CometAPIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (c *CometAPIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -499,7 +499,7 @@ func (c *CometAPIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e return nil, fmt.Errorf("balance URL is required") } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", c.balanceURL(*apiConfig.ApiKey), nil) @@ -525,8 +525,8 @@ func (c *CometAPIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e } // CheckConnection runs a quota query to verify the API key. -func (c *CometAPIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := c.Balance(apiConfig) +func (c *CometAPIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := c.Balance(ctx, apiConfig) if err != nil { return err } @@ -534,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (c *CometAPIModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (c *CometAPIModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -654,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (c *CometAPIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (c *CometAPIModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := c.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -719,23 +719,23 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (c *CometAPIModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (c *CometAPIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (c *CometAPIModel) ParseFile(ctx context.Context, 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()) } -func (c *CometAPIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (c *CometAPIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", c.Name()) } -func (c *CometAPIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (c *CometAPIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 24f0f6343c..1a93719b17 100644 --- a/internal/entity/models/cometapi_test.go +++ b/internal/entity/models/cometapi_test.go @@ -77,6 +77,7 @@ func TestCometAPIFactoryRoute(t *testing.T) { } func TestCometAPIChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "gpt-5" { t.Errorf("expected model=gpt-5, got %v", body["model"]) @@ -99,7 +100,7 @@ func TestCometAPIChatHappyPath(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("gpt-5", []Message{ + resp, err := m.ChatWithMessages(ctx, "gpt-5", []Message{ {Role: "user", Content: "ping"}, }, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -114,6 +115,7 @@ func TestCometAPIChatHappyPath(t *testing.T) { } func TestCometAPIChatPropagatesConfig(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["max_tokens"] != float64(64) { t.Errorf("max_tokens=%v want 64", body["max_tokens"]) @@ -140,7 +142,7 @@ func TestCometAPIChatPropagatesConfig(t *testing.T) { temp := 0.3 topP := 0.9 stop := []string{"END"} - _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "ping"}}, + _, err := m.ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop}, nil) @@ -150,6 +152,7 @@ func TestCometAPIChatPropagatesConfig(t *testing.T) { } func TestCometAPIChatReturnsReasoningContent(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{ @@ -161,7 +164,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, nil) + resp, err := m.ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -171,41 +174,45 @@ func TestCometAPIChatReturnsReasoningContent(t *testing.T) { } func TestCometAPIChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") - _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) + _, err := m.ChatWithMessages(ctx, "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, nil) + _, err = m.ChatWithMessages(ctx, "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) } } func TestCometAPIChatRequiresModelName(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := m.ChatWithMessages(ctx, "", []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, nil, func(*string, *string) error { return nil }) + err = m.ChatStreamlyWithSender(ctx, " ", []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) } } func TestCometAPIChatRequiresMessages(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("gpt-5", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := m.ChatWithMessages(ctx, "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) } } func TestCometAPIChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) @@ -214,13 +221,14 @@ 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, nil) + _, err := m.ChatWithMessages(ctx, "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) } } func TestCometAPIChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { + ctx := t.Context() // Empty *Region pointer must fall back to the "default" entry, not // be treated as an explicit "" region (which would miss the lookup). srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { @@ -233,7 +241,7 @@ func TestCometAPIChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" emptyRegion := "" - _, err := m.ChatWithMessages("gpt-5", + _, err := m.ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil, nil) if err != nil { @@ -242,6 +250,7 @@ func TestCometAPIChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { } func TestCometAPIListModelsFallsBackToDefaultOnEmptyRegion(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/api/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"data": []map[string]interface{}{{"id": "x"}}}) }) @@ -250,15 +259,16 @@ func TestCometAPIListModelsFallsBackToDefaultOnEmptyRegion(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" emptyRegion := "" - if _, err := m.ListModels(&APIConfig{ApiKey: &apiKey, Region: &emptyRegion}); err != nil { + if _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}); err != nil { t.Errorf("empty Region: expected fallback to default, got %v", err) } } func TestCometAPIStreamRequiresSender(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") apiKey := "test-key" - err := m.ChatStreamlyWithSender("gpt-5", + err := m.ChatStreamlyWithSender(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { @@ -267,10 +277,11 @@ func TestCometAPIStreamRequiresSender(t *testing.T) { } func TestCometAPIChatRejectsUnknownRegion(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") apiKey := "test-key" region := "eu" - _, err := m.ChatWithMessages("gpt-5", []Message{{Role: "user", Content: "x"}}, + _, err := m.ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &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) @@ -278,6 +289,7 @@ func TestCometAPIChatRejectsUnknownRegion(t *testing.T) { } func TestCometAPIBaseURLNormalizesSlashes(t *testing.T) { + ctx := t.Context() tests := []struct { name string path string @@ -287,7 +299,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, nil) + _, err := m.ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, apiConfig, nil, nil) return err }, }, @@ -295,7 +307,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, nil, func(*string, *string) error { return nil }) + return m.ChatStreamlyWithSender(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, apiConfig, nil, nil, func(*string, *string) error { return nil }) }, }, { @@ -303,7 +315,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, nil) + _, err := m.Embed(ctx, &model, []string{"x"}, apiConfig, nil, nil) return err }, }, @@ -311,7 +323,7 @@ func TestCometAPIBaseURLNormalizesSlashes(t *testing.T) { name: "ListModels", path: "/api/models", run: func(m *CometAPIModel, apiConfig *APIConfig) error { - _, err := m.ListModels(apiConfig) + _, err := m.ListModels(ctx, apiConfig) return err }, }, @@ -347,6 +359,7 @@ func TestCometAPIBaseURLNormalizesSlashes(t *testing.T) { } func TestCometAPIStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["stream"] != true { t.Errorf("expected stream=true, got %v", body["stream"]) @@ -367,7 +380,7 @@ func TestCometAPIStreamHappyPath(t *testing.T) { apiKey := "test-key" var chunks []string var sawDone int32 - err := m.ChatStreamlyWithSender("gpt-5", + err := m.ChatStreamlyWithSender(ctx, "gpt-5", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, _ *string) error { @@ -394,10 +407,11 @@ func TestCometAPIStreamHappyPath(t *testing.T) { } func TestCometAPIStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") apiKey := "test-key" stream := false - err := m.ChatStreamlyWithSender("gpt-5", + err := m.ChatStreamlyWithSender(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, @@ -410,6 +424,7 @@ func TestCometAPIStreamRejectsExplicitFalse(t *testing.T) { } func TestCometAPIStreamFailsWithoutTerminal(t *testing.T) { + ctx := t.Context() // Body closes before [DONE] or a finish_reason -> driver must complain // instead of pretending the stream finished cleanly. srv := newCometAPIServer(t, "/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { @@ -420,7 +435,7 @@ func TestCometAPIStreamFailsWithoutTerminal(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" - err := m.ChatStreamlyWithSender("gpt-5", + err := m.ChatStreamlyWithSender(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, @@ -431,6 +446,7 @@ func TestCometAPIStreamFailsWithoutTerminal(t *testing.T) { } func TestCometAPIListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/api/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -444,7 +460,7 @@ func TestCometAPIListModelsHappyPath(t *testing.T) { m := newCometAPIForTest(srv.URL) apiKey := "test-key" - ids, err := m.ListModels(&APIConfig{ApiKey: &apiKey}) + ids, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -454,13 +470,14 @@ func TestCometAPIListModelsHappyPath(t *testing.T) { } func TestCometAPIListModelsAllowsNilAPIConfig(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/api/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"data": []map[string]interface{}{{"id": "gpt-5"}}}) }) defer srv.Close() m := newCometAPIForTest(srv.URL) - ids, err := m.ListModels(nil) + ids, err := m.ListModels(ctx, nil) if err != nil { t.Fatalf("ListModels(nil): %v", err) } @@ -470,6 +487,7 @@ func TestCometAPIListModelsAllowsNilAPIConfig(t *testing.T) { } func TestCometAPICheckConnectionDelegatesToBalance(t *testing.T) { + ctx := t.Context() // 200 -> CheckConnection succeeds; 401 -> CheckConnection propagates. okSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/user/quota" { @@ -489,17 +507,18 @@ func TestCometAPICheckConnectionDelegatesToBalance(t *testing.T) { apiKey := "test-key" mOK := newCometAPIForTest(okSrv.URL) mOK.baseModel.URLSuffix.Balance = okSrv.URL + "/user/quota" - if err := mOK.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := mOK.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection(ok): %v", err) } mFail := newCometAPIForTest(failSrv.URL) mFail.baseModel.URLSuffix.Balance = failSrv.URL + "/user/quota" - if err := mFail.CheckConnection(&APIConfig{ApiKey: &apiKey}); err == nil { + if err := mFail.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err == nil { t.Error("CheckConnection(fail): expected error, got nil") } } func TestCometAPIBalanceHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/user/quota" { t.Errorf("path=%s want /user/quota", r.URL.Path) @@ -522,7 +541,7 @@ func TestCometAPIBalanceHappyPath(t *testing.T) { m := newCometAPIForTest("http://unused") m.baseModel.URLSuffix.Balance = srv.URL + "/user/quota" apiKey := "test-key" - balance, err := m.Balance(&APIConfig{ApiKey: &apiKey}) + balance, err := m.Balance(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("Balance: %v", err) } @@ -532,33 +551,37 @@ func TestCometAPIBalanceHappyPath(t *testing.T) { } func TestCometAPIBalanceRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") - _, err := m.Balance(&APIConfig{}) + _, err := m.Balance(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("Balance: expected api-key error, got %v", err) } } func TestCometAPIBalanceRequiresConfiguredURL(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") m.baseModel.URLSuffix.Balance = "" apiKey := "test-key" - _, err := m.Balance(&APIConfig{ApiKey: &apiKey}) + _, err := m.Balance(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "balance URL is required") { t.Errorf("Balance: expected balance URL error, got %v", err) } } func TestCometAPIRerankReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") q := "gpt-5" - _, err := m.Rerank(&q, "what is rag?", []string{"a", "b"}, &APIConfig{}, &RerankConfig{TopN: 2}, nil) + _, err := m.Rerank(ctx, &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) } } func TestCometAPIEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "text-embedding-3-small" { t.Errorf("model=%v want text-embedding-3-small", body["model"]) @@ -583,7 +606,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}, nil) + vecs, err := m.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 256}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -596,6 +619,7 @@ func TestCometAPIEmbedHappyPath(t *testing.T) { } func TestCometAPIEmbedReordersByIndex(t *testing.T) { + ctx := t.Context() // Upstream returns the three vectors in shuffled order. The driver // must reorder them so the slot at position i corresponds to input i. srv := newCometAPIServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { @@ -612,7 +636,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, nil) + vecs, err := m.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -624,6 +648,7 @@ func TestCometAPIEmbedReordersByIndex(t *testing.T) { } func TestCometAPIEmbedEmptyInputShortCircuits(t *testing.T) { + ctx := t.Context() // Empty input must NOT make an HTTP call; the test fails the request // rather than the assertion if it does. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -635,7 +660,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, nil) + vecs, err := m.Embed(ctx, &model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed([]): %v", err) } @@ -645,29 +670,32 @@ func TestCometAPIEmbedEmptyInputShortCircuits(t *testing.T) { } func TestCometAPIEmbedRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") model := "text-embedding-3-small" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) + _, err := m.Embed(ctx, &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) } } func TestCometAPIEmbedRequiresModelName(t *testing.T) { + ctx := t.Context() m := newCometAPIForTest("http://unused") apiKey := "test-key" - _, err := m.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := m.Embed(ctx, 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, nil) + _, err = m.Embed(ctx, &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) } } func TestCometAPIEmbedRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() // A malformed upstream that repeats data[*].index would silently // overwrite the earlier vector; the driver must fail loudly instead. srv := newCometAPIServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { @@ -683,13 +711,14 @@ 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, nil) + _, err := m.Embed(ctx, &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) } } func TestCometAPIEmbedRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -702,13 +731,14 @@ 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, nil) + _, err := m.Embed(ctx, &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) } } func TestCometAPIEmbedRejectsMissingSlot(t *testing.T) { + ctx := t.Context() // Upstream returns only one of the two requested embeddings. srv := newCometAPIServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ @@ -722,13 +752,14 @@ 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, nil) + _, err := m.Embed(ctx, &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) } } func TestCometAPIEmbedRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newCometAPIServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) @@ -738,7 +769,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, nil) + _, err := m.Embed(ctx, &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 60c827f7d3..fb5554b735 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (d *DeepInfraModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -125,7 +125,7 @@ func (d *DeepInfraModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DeepInfraModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -265,7 +265,7 @@ func (d *DeepInfraModel) ChatStreamlyWithSender(modelName string, messages []Mes return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (d *DeepInfraModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -367,7 +367,7 @@ func (d *DeepInfraModel) Embed(modelName *string, texts []string, apiConfig *API return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (d *DeepInfraModel) Rerank(ctx context.Context, 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 } @@ -452,7 +452,7 @@ func (d *DeepInfraModel) Rerank(modelName *string, query string, documents []str return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (d *DeepInfraModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -586,7 +586,7 @@ func (d *DeepInfraModel) TranscribeAudio(modelName *string, file *string, apiCon return nil, fmt.Errorf("failed to close multipart writer: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, &body) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DeepInfraModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (d *DeepInfraModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -679,7 +679,7 @@ func (d *DeepInfraModel) AudioSpeech(modelName *string, audioContent *string, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DeepInfraModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -757,7 +757,7 @@ func (d *DeepInfraModel) AudioSpeechWithSender(modelName *string, audioContent * return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -806,15 +806,15 @@ func (d *DeepInfraModel) AudioSpeechWithSender(modelName *string, audioContent * return nil } -func (d *DeepInfraModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (d *DeepInfraModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (d *DeepInfraModel) ParseFile(ctx context.Context, 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()) } -func (d *DeepInfraModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (d *DeepInfraModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { resolvedBaseURL, err := d.baseModel.GetBaseURL(apiConfig) if err != nil { @@ -829,7 +829,7 @@ func (d *DeepInfraModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -875,7 +875,7 @@ func (d *DeepInfraModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(ModelList{Models: models}), nil } -func (d *DeepInfraModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (d *DeepInfraModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -886,7 +886,7 @@ func (d *DeepInfraModel) Balance(apiConfig *APIConfig) (map[string]interface{}, } url := fmt.Sprintf("%s/%s", baseURL, d.baseModel.URLSuffix.Balance) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -926,15 +926,15 @@ func (d *DeepInfraModel) Balance(apiConfig *APIConfig) (map[string]interface{}, }, nil } -func (d *DeepInfraModel) CheckConnection(apiConfig *APIConfig) error { - _, err := d.ListModels(apiConfig) +func (d *DeepInfraModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := d.ListModels(ctx, apiConfig) return err } -func (d *DeepInfraModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (d *DeepInfraModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", d.Name()) } -func (d *DeepInfraModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (d *DeepInfraModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 1c70c04450..354d53443c 100644 --- a/internal/entity/models/deepinfra_test.go +++ b/internal/entity/models/deepinfra_test.go @@ -23,6 +23,7 @@ func newDeepInfraForTest(baseURL string) *DeepInfraModel { // TestDeepInfraRerankHappyPath verifies request shape and score mapping. func TestDeepInfraRerankHappyPath(t *testing.T) { + ctx := t.Context() const modelPath = "/v1/inference/Qwen/Qwen3-Reranker-4B" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != modelPath { @@ -59,6 +60,7 @@ func TestDeepInfraRerankHappyPath(t *testing.T) { apiKey := "test-key" model := "Qwen/Qwen3-Reranker-4B" resp, err := newDeepInfraForTest(srv.URL).Rerank( + ctx, &model, "capital of France?", []string{"Paris is the capital.", "Berlin is the capital."}, @@ -77,6 +79,7 @@ func TestDeepInfraRerankHappyPath(t *testing.T) { // TestDeepInfraRerankNoTopNLimit returns every scored document when TopN is unset. func TestDeepInfraRerankNoTopNLimit(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "scores": []float64{0.9, 0.1}, @@ -87,6 +90,7 @@ func TestDeepInfraRerankNoTopNLimit(t *testing.T) { apiKey := "test-key" model := "Qwen/Qwen3-Reranker-4B" resp, err := newDeepInfraForTest(srv.URL).Rerank( + ctx, &model, "capital of France?", []string{"Paris is the capital.", "Berlin is the capital."}, @@ -106,9 +110,10 @@ func TestDeepInfraRerankNoTopNLimit(t *testing.T) { // TestDeepInfraRerankEmptyDocuments returns an empty result without calling the API. func TestDeepInfraRerankEmptyDocuments(t *testing.T) { + ctx := t.Context() apiKey := "test-key" model := "Qwen/Qwen3-Reranker-4B" - resp, err := newDeepInfraForTest("http://unused").Rerank(&model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + resp, err := newDeepInfraForTest("http://unused").Rerank(ctx, &model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -119,8 +124,9 @@ func TestDeepInfraRerankEmptyDocuments(t *testing.T) { // TestDeepInfraRerankRequiresAPIKey rejects requests without an API key. func TestDeepInfraRerankRequiresAPIKey(t *testing.T) { + ctx := t.Context() model := "Qwen/Qwen3-Reranker-4B" - _, err := newDeepInfraForTest("http://unused").Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) + _, err := newDeepInfraForTest("http://unused").Rerank(ctx, &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) } @@ -128,6 +134,7 @@ func TestDeepInfraRerankRequiresAPIKey(t *testing.T) { // TestDeepInfraRerankRejectsScoreCountMismatch errors when scores length mismatches documents. func TestDeepInfraRerankRejectsScoreCountMismatch(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"scores": []float64{0.5}}) })) @@ -136,7 +143,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, nil) + ctx, &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 7d54adcefa..3ec8e61ed8 100644 --- a/internal/entity/models/deepseek.go +++ b/internal/entity/models/deepseek.go @@ -52,7 +52,7 @@ func (d *DeepSeekModel) Name() string { return "deepseek" } -func (d *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (d *DeepSeekModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -149,7 +149,7 @@ func (d *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -234,7 +234,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DeepSeekModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -336,7 +336,7 @@ func (d *DeepSeekModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -417,11 +417,11 @@ 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (d *DeepSeekModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DeepSeekModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (d *DeepSeekModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -440,7 +440,7 @@ func (d *DeepSeekModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -492,7 +492,7 @@ type deepseekBalanceResponse struct { // calling GET /user/balance with the configured Bearer token. // The result map matches the shape used by the Moonshot driver, // so the UI can render it without provider-specific code. -func (d *DeepSeekModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (d *DeepSeekModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := d.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -504,7 +504,7 @@ func (d *DeepSeekModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), d.baseModel.URLSuffix.Balance) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -552,8 +552,8 @@ func (d *DeepSeekModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e }, nil } -func (d *DeepSeekModel) CheckConnection(apiConfig *APIConfig) error { - _, err := d.ListModels(apiConfig) +func (d *DeepSeekModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := d.ListModels(ctx, apiConfig) if err != nil { return err } @@ -561,42 +561,42 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (d *DeepSeekModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (d *DeepSeekModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DeepSeekModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (d *DeepSeekModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DeepSeekModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (d *DeepSeekModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (d *DeepSeekModel) ParseFile(ctx context.Context, 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()) } -func (d *DeepSeekModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (d *DeepSeekModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DeepSeekModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (d *DeepSeekModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 aab13f72c6..bab79a2ea7 100644 --- a/internal/entity/models/deepseek_test.go +++ b/internal/entity/models/deepseek_test.go @@ -18,6 +18,7 @@ func newDeepSeekForTest(baseURL string) *DeepSeekModel { } func TestDeepSeekChatWithMessagesSupportsToolCalls(t *testing.T) { + ctx := t.Context() var requestBody map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/chat/completions" { @@ -62,6 +63,7 @@ func TestDeepSeekChatWithMessagesSupportsToolCalls(t *testing.T) { apiKey := "test-key" toolChoice := "auto" resp, err := newDeepSeekForTest(srv.URL).ChatWithMessages( + ctx, "deepseek-chat", []Message{ {Role: "user", Content: "what is marigold"}, @@ -106,6 +108,7 @@ func TestDeepSeekChatWithMessagesSupportsToolCalls(t *testing.T) { } func TestDeepSeekChatWithMessagesForwardsToolHistory(t *testing.T) { + ctx := t.Context() var requestBody map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { @@ -123,6 +126,7 @@ func TestDeepSeekChatWithMessagesForwardsToolHistory(t *testing.T) { apiKey := "test-key" _, err := newDeepSeekForTest(srv.URL).ChatWithMessages( + ctx, "deepseek-chat", []Message{ { diff --git a/internal/entity/models/dummy.go b/internal/entity/models/dummy.go index 86a123ae20..4cc08b57ea 100644 --- a/internal/entity/models/dummy.go +++ b/internal/entity/models/dummy.go @@ -17,6 +17,7 @@ package models import ( + "context" "fmt" "ragflow/internal/common" ) @@ -45,69 +46,69 @@ 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (d *DummyModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DummyModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (d *DummyModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } -func (d *DummyModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (d *DummyModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *DummyModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (d *DummyModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } -func (d *DummyModel) CheckConnection(apiConfig *APIConfig) error { +func (d *DummyModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("no such method") } // Rerank calculates similarity scores between query and documents -func (d *DummyModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (d *DummyModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (d *DummyModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DummyModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (d *DummyModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *DummyModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (d *DummyModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (d *DummyModel) ParseFile(ctx context.Context, 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()) } -func (d *DummyModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (d *DummyModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", d.Name()) } -func (d *DummyModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (d *DummyModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 df05c7c1ec..3710da496d 100644 --- a/internal/entity/models/fishaudio.go +++ b/internal/entity/models/fishaudio.go @@ -54,24 +54,24 @@ func (f *FishAudioModel) Name() string { return "Fish Audio" } -func (f *FishAudioModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (f *FishAudioModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FishAudioModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (f *FishAudioModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (f *FishAudioModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (f *FishAudioModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -137,7 +137,7 @@ func (f *FishAudioModel) TranscribeAudio(modelName *string, file *string, apiCon } // request - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, &body) @@ -177,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FishAudioModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (f *FishAudioModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -215,7 +215,7 @@ func (f *FishAudioModel) AudioSpeech(modelName *string, audioContent *string, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -245,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FishAudioModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -279,7 +279,7 @@ func (f *FishAudioModel) AudioSpeechWithSender(modelName *string, audioContent * } // Build Request - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -326,16 +326,16 @@ func (f *FishAudioModel) AudioSpeechWithSender(modelName *string, audioContent * } // OCRFile OCR file -func (f *FishAudioModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (f *FishAudioModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (f *FishAudioModel) ParseFile(ctx context.Context, 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()) } -func (f *FishAudioModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (f *FishAudioModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -346,7 +346,7 @@ func (f *FishAudioModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, } url := fmt.Sprintf("%s/%s", resolvedBaseURL, f.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -397,7 +397,7 @@ func (f *FishAudioModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(modelList), nil } -func (f *FishAudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (f *FishAudioModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -409,7 +409,7 @@ func (f *FishAudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, url := fmt.Sprintf("%s/wallet/self/api-credit", strings.TrimSuffix(baseURL, "/")) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -442,15 +442,15 @@ func (f *FishAudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, return result, nil } -func (f *FishAudioModel) CheckConnection(apiConfig *APIConfig) error { - _, err := f.ListModels(apiConfig) +func (f *FishAudioModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := f.ListModels(ctx, apiConfig) return err } -func (f *FishAudioModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (f *FishAudioModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } -func (f *FishAudioModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (f *FishAudioModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } diff --git a/internal/entity/models/fishaudio_test.go b/internal/entity/models/fishaudio_test.go index 123f2d303f..18763327b7 100644 --- a/internal/entity/models/fishaudio_test.go +++ b/internal/entity/models/fishaudio_test.go @@ -28,6 +28,7 @@ func newFishAudioForListModelsTest(baseURL string) *FishAudioModel { } func TestFishAudioListModels(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -44,7 +45,7 @@ func TestFishAudioListModels(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newFishAudioForListModelsTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newFishAudioForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -61,12 +62,14 @@ func TestFishAudioListModels(t *testing.T) { } func TestFishAudioListModelsRequiresAPIKey(t *testing.T) { - if _, err := newFishAudioForListModelsTest("http://unused").ListModels(&APIConfig{}); err == nil { + ctx := t.Context() + if _, err := newFishAudioForListModelsTest("http://unused").ListModels(ctx, &APIConfig{}); err == nil { t.Fatal("ListModels: expected error for missing api key, got nil") } } func TestFishAudioListModelsRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = io.WriteString(w, `{"error":"unauthorized"}`) @@ -74,7 +77,7 @@ func TestFishAudioListModelsRejectsHTTPError(t *testing.T) { defer srv.Close() apiKey := "bad-key" - if _, err := newFishAudioForListModelsTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}); err == nil { + if _, err := newFishAudioForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}); err == nil { t.Fatal("ListModels: expected error for HTTP 401, got nil") } } diff --git a/internal/entity/models/funasr.go b/internal/entity/models/funasr.go index be81620b01..e6c7e805c7 100644 --- a/internal/entity/models/funasr.go +++ b/internal/entity/models/funasr.go @@ -53,23 +53,23 @@ func (f *FunASR) Name() string { return "funasr" } -func (f *FunASR) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (f *FunASR) ChatWithMessages(ctx context.Context, 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 *FunASR) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FunASR) ChatStreamlyWithSender(ctx context.Context, 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 *FunASR) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (f *FunASR) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("%s no such method", f.Name()) } -func (f *FunASR) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (f *FunASR) Rerank(ctx context.Context, 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()) } -func (f *FunASR) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -147,7 +147,7 @@ func (f *FunASR) TranscribeAudio(modelName *string, file *string, apiConfig *API } // build request - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, &body) @@ -186,27 +186,27 @@ func (f *FunASR) TranscribeAudio(modelName *string, file *string, apiConfig *API return &ASRResponse{Text: result.Text}, nil } -func (f *FunASR) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FunASR) TranscribeAudioWithSender(ctx context.Context, 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()) } -func (f *FunASR) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (f *FunASR) AudioSpeech(ctx context.Context, 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 *FunASR) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FunASR) AudioSpeechWithSender(ctx context.Context, 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()) } -func (f *FunASR) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (f *FunASR) OCRFile(ctx context.Context, 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()) } -func (f *FunASR) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (f *FunASR) ParseFile(ctx context.Context, 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()) } -func (f *FunASR) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (f *FunASR) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -218,7 +218,7 @@ func (f *FunASR) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { } url := fmt.Sprintf("%s/%s", resolvedBaseURL, f.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -258,19 +258,19 @@ func (f *FunASR) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { return models, nil } -func (f *FunASR) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (f *FunASR) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", f.Name()) } -func (f *FunASR) CheckConnection(apiConfig *APIConfig) error { - _, err := f.ListModels(apiConfig) +func (f *FunASR) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := f.ListModels(ctx, apiConfig) return err } -func (f *FunASR) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (f *FunASR) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", f.Name()) } -func (f *FunASR) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (f *FunASR) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 c46b9ade77..5805400bfa 100644 --- a/internal/entity/models/futurmix.go +++ b/internal/entity/models/futurmix.go @@ -146,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (f *FuturMixModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -165,7 +165,7 @@ func (f *FuturMixModel) ChatWithMessages(modelName string, messages []Message, a reqBody := buildFuturMixChatRequest(modelName, messages, false, chatModelConfig) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newFuturMixJSONRequest(ctx, "POST", endpoint, reqBody, apiKey) @@ -208,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FuturMixModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -235,7 +235,7 @@ func (f *FuturMixModel) ChatStreamlyWithSender(modelName string, messages []Mess reqBody := buildFuturMixChatRequest(modelName, messages, true, chatModelConfig) - req, err := newFuturMixJSONRequest(context.Background(), "POST", endpoint, reqBody, apiKey) + req, err := newFuturMixJSONRequest(ctx, "POST", endpoint, reqBody, apiKey) if err != nil { return err } @@ -286,64 +286,64 @@ 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (f *FuturMixModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (f *FuturMixModel) Rerank(ctx context.Context, 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()) } // ListModels is not documented as a public endpoint by FuturMix. -func (f *FuturMixModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (f *FuturMixModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } // CheckConnection is not exposed by the FuturMix API. -func (f *FuturMixModel) CheckConnection(apiConfig *APIConfig) error { +func (f *FuturMixModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("%s, no such method", f.Name()) } // Balance is not exposed by the FuturMix public API. -func (f *FuturMixModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (f *FuturMixModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } // TranscribeAudio is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (f *FuturMixModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FuturMixModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (f *FuturMixModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (f *FuturMixModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (f *FuturMixModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (f *FuturMixModel) ParseFile(ctx context.Context, 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()) } // ListTasks is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (f *FuturMixModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", f.Name()) } // ShowTask is not exposed by the FuturMix API per the docs. -func (f *FuturMixModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (f *FuturMixModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 58fa8d588b..c9c5400e8a 100644 --- a/internal/entity/models/gitee.go +++ b/internal/entity/models/gitee.go @@ -55,7 +55,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (g *GiteeModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -140,7 +140,7 @@ func (g *GiteeModel) ChatWithMessages(modelName string, messages []Message, apiC common.Info(fmt.Sprintf("GiteeAPI request body: %s", string(jsonData))) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -232,7 +232,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GiteeModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -316,7 +316,7 @@ func (g *GiteeModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -502,7 +502,7 @@ type giteeUsage struct { } // Embed embeds a list of texts into embeddings -func (g *GiteeModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (g *GiteeModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -536,7 +536,7 @@ func (g *GiteeModel) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -587,7 +587,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (g *GiteeModel) Rerank(ctx context.Context, 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 } @@ -626,7 +626,7 @@ func (g *GiteeModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -661,20 +661,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (g *GiteeModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GiteeModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (g *GiteeModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GiteeModel) AudioSpeechWithSender(ctx context.Context, 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()) } @@ -684,7 +684,7 @@ type giteeOCRResponse struct { } // OCRFile OCR file -func (g *GiteeModel) OCRFile(modelName *string, content []byte, imageURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (g *GiteeModel) OCRFile(ctx context.Context, 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 } @@ -730,7 +730,7 @@ func (g *GiteeModel) OCRFile(modelName *string, content []byte, imageURL *string writer.Close() - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, payload) @@ -781,7 +781,7 @@ type giteeURLs struct { } // ParseFile parse file -func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (g *GiteeModel) ParseFile(ctx context.Context, 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 } @@ -827,7 +827,7 @@ func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *s writer.Close() - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, payload) @@ -858,7 +858,7 @@ func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *s return nil, fmt.Errorf("failed to parse response: %w", err) } - _, err = g.getParseFile(&baseURL, apiConfig.ApiKey, &giteeParseFileResp.TaskID, 5*time.Second, 10) + _, err = g.getParseFile(ctx, &baseURL, apiConfig.ApiKey, &giteeParseFileResp.TaskID, 5*time.Second, 10) if err != nil { return nil, err } @@ -871,7 +871,7 @@ func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *s type giteeGetParseFileResponse struct { } -func (g *GiteeModel) getParseFile(baseURL *string, apiKey, taskID *string, timeOut time.Duration, count int) (*giteeGetParseFileResponse, error) { +func (g *GiteeModel) getParseFile(ctx context.Context, baseURL *string, apiKey, taskID *string, timeOut time.Duration, count int) (*giteeGetParseFileResponse, error) { url := fmt.Sprintf("%s/task/%s/status", strings.TrimSuffix(*baseURL, "/"), *taskID) reqBody := map[string]interface{}{} @@ -881,7 +881,7 @@ func (g *GiteeModel) getParseFile(baseURL *string, apiKey, taskID *string, timeO return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() for i := 0; i < count; i++ { @@ -927,7 +927,7 @@ func (g *GiteeModel) getParseFile(baseURL *string, apiKey, taskID *string, timeO return nil, nil } -func (g *GiteeModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (g *GiteeModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { resolvedBaseURL, err := g.baseModel.GetBaseURL(apiConfig) if err != nil { @@ -943,7 +943,7 @@ func (g *GiteeModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -977,7 +977,7 @@ func (g *GiteeModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return ParseListModel(modelList), nil } -func (g *GiteeModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (g *GiteeModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -996,7 +996,7 @@ func (g *GiteeModel) Balance(apiConfig *APIConfig) (map[string]interface{}, erro return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -1038,7 +1038,7 @@ func (g *GiteeModel) Balance(apiConfig *APIConfig) (map[string]interface{}, erro return response, nil } -func (g *GiteeModel) CheckConnection(apiConfig *APIConfig) error { +func (g *GiteeModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -1057,7 +1057,7 @@ func (g *GiteeModel) CheckConnection(apiConfig *APIConfig) error { return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -1116,7 +1116,7 @@ type giteeTaskURLs struct { Cancel string `json:"cancel"` } -func (g *GiteeModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (g *GiteeModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -1135,7 +1135,7 @@ func (g *GiteeModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -1177,7 +1177,7 @@ func (g *GiteeModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { return taskListResp, nil } -func (g *GiteeModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (g *GiteeModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -1196,7 +1196,7 @@ func (g *GiteeModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskRespons return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/gitee_test.go b/internal/entity/models/gitee_test.go index e8e96acfbf..6dfeb62620 100644 --- a/internal/entity/models/gitee_test.go +++ b/internal/entity/models/gitee_test.go @@ -76,6 +76,7 @@ func deepSeekAliasModelsForTest(t *testing.T) map[string]Model { } func TestGiteeListModelsMapsAllDeepSeekAliasesToModelMetadata(t *testing.T) { + ctx := t.Context() initProviderManagerWithGiteeForTest(t) aliasModels := deepSeekAliasModelsForTest(t) aliases := make([]string, 0, len(aliasModels)) @@ -111,7 +112,7 @@ func TestGiteeListModelsMapsAllDeepSeekAliasesToModelMetadata(t *testing.T) { })) defer srv.Close() - models, err := newGiteeForListModelsTest(srv.URL).ListModels(&APIConfig{}) + models, err := newGiteeForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -152,6 +153,7 @@ func TestGiteeListModelsMapsAllDeepSeekAliasesToModelMetadata(t *testing.T) { } func TestGiteeListModelsKeepsOwnedBySuffixAfterAliasMetadataLookup(t *testing.T) { + ctx := t.Context() initProviderManagerWithGiteeForTest(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -159,7 +161,7 @@ func TestGiteeListModelsKeepsOwnedBySuffixAfterAliasMetadataLookup(t *testing.T) })) defer srv.Close() - models, err := newGiteeForListModelsTest(srv.URL).ListModels(&APIConfig{}) + models, err := newGiteeForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -182,6 +184,7 @@ func TestGiteeListModelsKeepsOwnedBySuffixAfterAliasMetadataLookup(t *testing.T) } func TestGiteeListModelsIntegration(t *testing.T) { + ctx := t.Context() if common.GetEnv(common.EnvGiteeListModelsIntegration) != "1" { t.Skip("set GITEE_LIST_MODELS_INTEGRATION=1 to call the real Gitee models endpoint") } @@ -197,7 +200,7 @@ func TestGiteeListModelsIntegration(t *testing.T) { apiConfig.ApiKey = &apiKey } - models, err := newGiteeForListModelsTest(baseURL).ListModels(apiConfig) + models, err := newGiteeForListModelsTest(baseURL).ListModels(ctx, apiConfig) if err != nil { t.Fatalf("real Gitee ListModels: %v", err) } diff --git a/internal/entity/models/google.go b/internal/entity/models/google.go index 171f096ddf..eb01866175 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (g *GoogleModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -131,7 +131,6 @@ func (g *GoogleModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("messages is empty") } - ctx := context.Background() client, err := genai.NewClient(ctx, g.clientConfig(strings.TrimSpace(*apiConfig.ApiKey), apiConfig)) if err != nil { return nil, err @@ -193,7 +192,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GoogleModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -208,7 +207,6 @@ func (g *GoogleModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("sender is nil") } - ctx := context.Background() client, err := genai.NewClient(ctx, g.clientConfig(strings.TrimSpace(*apiConfig.ApiKey), apiConfig)) if err != nil { return err @@ -298,7 +296,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (g *GoogleModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -309,7 +307,7 @@ func (g *GoogleModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("texts is empty") } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() client, err := genai.NewClient(ctx, g.clientConfig(strings.TrimSpace(*apiConfig.ApiKey), apiConfig)) @@ -352,60 +350,60 @@ func (g *GoogleModel) Embed(modelName *string, texts []string, apiConfig *APICon return result, nil } -func (g *GoogleModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (g *GoogleModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } - return googleListModels(context.Background(), g.clientConfig(strings.TrimSpace(*apiConfig.ApiKey), apiConfig)) + return googleListModels(ctx, g.clientConfig(strings.TrimSpace(*apiConfig.ApiKey), apiConfig)) } -func (g *GoogleModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (g *GoogleModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } -func (g *GoogleModel) CheckConnection(apiConfig *APIConfig) error { - _, err := g.ListModels(apiConfig) +func (g *GoogleModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := g.ListModels(ctx, apiConfig) return err } // Rerank calculates similarity scores between query and documents -func (g *GoogleModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (g *GoogleModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (g *GoogleModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GoogleModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (g *GoogleModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GoogleModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (g *GoogleModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (g *GoogleModel) ParseFile(ctx context.Context, 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()) } -func (g *GoogleModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (g *GoogleModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GoogleModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (g *GoogleModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 f19cbb1ce1..d441ff0b30 100644 --- a/internal/entity/models/google_test.go +++ b/internal/entity/models/google_test.go @@ -26,6 +26,7 @@ func withGoogleListModelsStub(t *testing.T, fn func(context.Context, *genai.Clie } func TestGoogleModelListModelsRequiresAPIKey(t *testing.T) { + ctx := t.Context() model := &GoogleModel{} cases := []struct { name string @@ -61,7 +62,7 @@ func TestGoogleModelListModelsRequiresAPIKey(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - models, err := model.ListModels(tc.apiConfig) + models, err := model.ListModels(ctx, tc.apiConfig) if err == nil { t.Fatal("expected an API key error") } @@ -80,6 +81,7 @@ func TestGoogleModelListModelsRequiresAPIKey(t *testing.T) { } func TestGoogleModelListModelsReturnsModelNames(t *testing.T) { + ctx := t.Context() model := &GoogleModel{} apiKey := "test-api-key" configuredAPIKey := " " + apiKey + " " @@ -92,7 +94,7 @@ func TestGoogleModelListModelsReturnsModelNames(t *testing.T) { return expected, nil }) - models, err := model.ListModels(&APIConfig{ApiKey: &configuredAPIKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &configuredAPIKey}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -102,6 +104,7 @@ func TestGoogleModelListModelsReturnsModelNames(t *testing.T) { } func TestGoogleModelCheckConnectionUsesListModels(t *testing.T) { + ctx := t.Context() customBaseURL := "https://check-connection.example.test/google" model := NewGoogleModel(map[string]string{"default": customBaseURL}, URLSuffix{}) apiKey := "test-api-key" @@ -118,7 +121,7 @@ func TestGoogleModelCheckConnectionUsesListModels(t *testing.T) { return []ListModelResponse{{Name: "models/gemini-2.5-flash"}}, nil }) - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("expected no error, got %v", err) } if calls != 1 { @@ -127,6 +130,7 @@ func TestGoogleModelCheckConnectionUsesListModels(t *testing.T) { } func TestGoogleModelCheckConnectionRequiresAPIKey(t *testing.T) { + ctx := t.Context() model := &GoogleModel{} calls := 0 @@ -163,7 +167,7 @@ func TestGoogleModelCheckConnectionRequiresAPIKey(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := model.CheckConnection(tc.apiConfig) + err := model.CheckConnection(ctx, tc.apiConfig) if err == nil { t.Fatal("expected an API key error") } @@ -178,6 +182,7 @@ func TestGoogleModelCheckConnectionRequiresAPIKey(t *testing.T) { } func TestGoogleModelCheckConnectionReturnsListModelsError(t *testing.T) { + ctx := t.Context() model := &GoogleModel{} apiKey := "test-api-key" listErr := errors.New("list models failed") @@ -186,13 +191,14 @@ func TestGoogleModelCheckConnectionReturnsListModelsError(t *testing.T) { return nil, listErr }) - err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}) + err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}) if !errors.Is(err, listErr) { t.Fatalf("expected ListModels error %v, got %v", listErr, err) } } func TestGoogleModelChatStreamlyRequiresAPIKey(t *testing.T) { + ctx := t.Context() model := &GoogleModel{} messages := []Message{{Role: "user", Content: "hello"}} cases := []struct { @@ -207,7 +213,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, nil, func(*string, *string) error { + err := model.ChatStreamlyWithSender(ctx, "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 }) @@ -222,11 +228,12 @@ func TestGoogleModelChatStreamlyRequiresAPIKey(t *testing.T) { } func TestGoogleModelChatRequiresModelName(t *testing.T) { + ctx := t.Context() model := &GoogleModel{} apiKey := "test-api-key" messages := []Message{{Role: "user", Content: "hello"}} - response, err := model.ChatWithMessages("", messages, &APIConfig{ApiKey: &apiKey}, nil, nil) + response, err := model.ChatWithMessages(ctx, "", messages, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil { t.Fatal("expected a model name error") } @@ -237,7 +244,7 @@ func TestGoogleModelChatRequiresModelName(t *testing.T) { t.Fatalf("expected no response, got %v", response) } - err = model.ChatStreamlyWithSender("", messages, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { + err = model.ChatStreamlyWithSender(ctx, "", 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 +255,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, nil) + err = model.ChatStreamlyWithSender(ctx, "gemini-2.5-flash", messages, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil { t.Fatal("expected a sender error") } @@ -312,6 +319,7 @@ func TestGoogleModelListModelsPassesBaseURL(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() model := NewGoogleModel(tc.baseURL, URLSuffix{}) withGoogleListModelsStub(t, func(_ context.Context, config *genai.ClientConfig) ([]ListModelResponse, error) { if config.HTTPOptions.BaseURL != tc.expectedBaseURL { @@ -320,7 +328,7 @@ func TestGoogleModelListModelsPassesBaseURL(t *testing.T) { return []ListModelResponse{{Name: "models/gemini-2.5-flash"}}, nil }) - if _, err := model.ListModels(&APIConfig{ApiKey: &apiKey, Region: tc.region}); err != nil { + if _, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey, Region: tc.region}); err != nil { t.Fatalf("expected no error, got %v", err) } }) diff --git a/internal/entity/models/gpustack.go b/internal/entity/models/gpustack.go index d3dd772100..14edd508f7 100644 --- a/internal/entity/models/gpustack.go +++ b/internal/entity/models/gpustack.go @@ -52,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (g *GPUStackModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -104,7 +104,7 @@ func (g *GPUStackModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -164,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GPUStackModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -223,7 +223,7 @@ func (g *GPUStackModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -304,7 +304,7 @@ type gpustackModelsResponse struct { Data []ModelListItem `json:"data"` } -func (g *GPUStackModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (g *GPUStackModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -316,7 +316,7 @@ func (g *GPUStackModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, g.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -350,8 +350,8 @@ func (g *GPUStackModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return ParseListModel(ModelList{Models: parsed.Data}), nil } -func (g *GPUStackModel) CheckConnection(apiConfig *APIConfig) error { - _, err := g.ListModels(apiConfig) +func (g *GPUStackModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := g.ListModels(ctx, apiConfig) return err } @@ -368,6 +368,7 @@ type gpustackEmbeddingResponse struct { // Embed requests embedding vectors via GPUStack's v1-openai/embeddings endpoint. func (g *GPUStackModel) Embed( + ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, @@ -408,7 +409,7 @@ func (g *GPUStackModel) Embed( return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -469,42 +470,42 @@ func (g *GPUStackModel) Embed( return embeddings, nil } -func (g *GPUStackModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (g *GPUStackModel) Rerank(ctx context.Context, 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()) } -func (g *GPUStackModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (g *GPUStackModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (g *GPUStackModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GPUStackModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (g *GPUStackModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GPUStackModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (g *GPUStackModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (g *GPUStackModel) ParseFile(ctx context.Context, 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()) } -func (g *GPUStackModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (g *GPUStackModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GPUStackModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (g *GPUStackModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 cbbfbeeb17..4c3675a5f6 100644 --- a/internal/entity/models/gpustack_test.go +++ b/internal/entity/models/gpustack_test.go @@ -112,7 +112,9 @@ func TestGPUStackChatHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() resp, err := newGPUStackForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -143,7 +145,9 @@ func TestGPUStackChatExtractsReasoningContent(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() resp, err := newGPUStackForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -179,7 +183,9 @@ func TestGPUStackChatForwardsDocumentedFields(t *testing.T) { temp := 0.5 topP := 0.95 stop := []string{"END"} + ctx := t.Context() _, err := newGPUStackForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -192,7 +198,9 @@ func TestGPUStackChatForwardsDocumentedFields(t *testing.T) { } func TestGPUStackChatAllowsEmptyAPIKey(t *testing.T) { + ctx := t.Context() _, err := newGPUStackForTest("http://unused").ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, @@ -204,7 +212,9 @@ func TestGPUStackChatAllowsEmptyAPIKey(t *testing.T) { func TestGPUStackChatRequiresModelName(t *testing.T) { apiKey := "test-key" + ctx := t.Context() _, err := newGPUStackForTest("http://unused").ChatWithMessages( + ctx, "", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -216,7 +226,9 @@ func TestGPUStackChatRequiresModelName(t *testing.T) { func TestGPUStackChatRequiresMessages(t *testing.T) { apiKey := "test-key" + ctx := t.Context() _, err := newGPUStackForTest("http://unused").ChatWithMessages( + ctx, "qwen3-8b", nil, &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "messages is empty") { @@ -232,7 +244,9 @@ func TestGPUStackChatRejectsHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() _, err := newGPUStackForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -245,7 +259,9 @@ func TestGPUStackChatRejectsHTTPError(t *testing.T) { func TestGPUStackChatRequiresBaseURL(t *testing.T) { model := NewGPUStackModel(map[string]string{}, URLSuffix{Chat: "v1/chat/completions"}) apiKey := "test-key" + ctx := t.Context() _, err := model.ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -267,7 +283,9 @@ func TestGPUStackStreamHappyPath(t *testing.T) { apiKey := "test-key" var chunks []string var sawDone bool + ctx := t.Context() err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -305,7 +323,9 @@ func TestGPUStackStreamExtractsReasoningContent(t *testing.T) { apiKey := "test-key" var content, reasoning []string + ctx := t.Context() err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -333,7 +353,9 @@ func TestGPUStackStreamExtractsReasoningContent(t *testing.T) { func TestGPUStackStreamRejectsExplicitFalse(t *testing.T) { apiKey := "test-key" stream := false + ctx := t.Context() err := newGPUStackForTest("http://unused").ChatStreamlyWithSender( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -348,7 +370,9 @@ func TestGPUStackStreamRejectsExplicitFalse(t *testing.T) { func TestGPUStackStreamRequiresSender(t *testing.T) { apiKey := "test-key" + ctx := t.Context() err := newGPUStackForTest("http://unused").ChatStreamlyWithSender( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil, @@ -365,7 +389,9 @@ func TestGPUStackStreamFailsWithoutTerminal(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -384,7 +410,9 @@ func TestGPUStackStreamRejectsMalformedFrame(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -403,7 +431,9 @@ func TestGPUStackStreamSurfacesUpstreamError(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newGPUStackForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -415,6 +445,7 @@ func TestGPUStackStreamSurfacesUpstreamError(t *testing.T) { } func TestGPUStackListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method=%s want GET", r.Method) @@ -436,20 +467,21 @@ func TestGPUStackListModelsHappyPath(t *testing.T) { apiKey := "test-key" model := newGPUStackForTest(srv.URL) - models, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "qwen3-8b,qwen3-32b" { t.Errorf("models=%v", models) } - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestGPUStackListModelsAllowsEmptyAPIKey(t *testing.T) { - _, err := newGPUStackForTest("http://unused").ListModels(&APIConfig{}) + ctx := t.Context() + _, err := newGPUStackForTest("http://unused").ListModels(ctx, &APIConfig{}) if err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("self-hosted model should not require api key, got %v", err) } @@ -479,8 +511,9 @@ func TestGPUStackEmbedHappyPath(t *testing.T) { apiKey := "test-key" model := "bge-m3" + ctx := t.Context() vecs, err := newGPUStackForTest(srv.URL).Embed( - &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 512}, nil) + ctx, &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 512}, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -507,8 +540,9 @@ func TestGPUStackEmbedReordersByIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" + ctx := t.Context() vecs, err := newGPUStackForTest(srv.URL).Embed( - &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -529,7 +563,9 @@ func TestGPUStackEmbedEmptyInputShortCircuits(t *testing.T) { apiKey := "test-key" model := "bge-m3" - vecs, err := newGPUStackForTest(srv.URL).Embed(&model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx := t.Context() + vecs, err := newGPUStackForTest(srv.URL).Embed( + ctx, &model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed([]): %v", err) } @@ -540,8 +576,9 @@ func TestGPUStackEmbedEmptyInputShortCircuits(t *testing.T) { // TestGPUStackEmbedRequiresAPIKey rejects requests without an API key. func TestGPUStackEmbedAllowsEmptyAPIKey(t *testing.T) { + ctx := t.Context() model := "bge-m3" - _, err := newGPUStackForTest("http://unused").Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) + _, err := newGPUStackForTest("http://unused").Embed(ctx, &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) } @@ -561,7 +598,8 @@ func TestGPUStackEmbedRejectsDuplicateIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx := t.Context() + _, err := newGPUStackForTest(srv.URL).Embed(ctx, &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) } @@ -580,7 +618,8 @@ func TestGPUStackEmbedRejectsOutOfRangeIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx := t.Context() + _, err := newGPUStackForTest(srv.URL).Embed(ctx, &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) } @@ -599,7 +638,8 @@ func TestGPUStackEmbedRejectsMissingIndex(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx := t.Context() + _, err := newGPUStackForTest(srv.URL).Embed(ctx, &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) } @@ -618,7 +658,8 @@ func TestGPUStackEmbedRejectsEmptyVector(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx := t.Context() + _, err := newGPUStackForTest(srv.URL).Embed(ctx, &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) } @@ -637,28 +678,30 @@ func TestGPUStackEmbedRejectsMissingSlot(t *testing.T) { apiKey := "test-key" model := "bge-m3" - _, err := newGPUStackForTest(srv.URL).Embed(&model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx := t.Context() + _, err := newGPUStackForTest(srv.URL).Embed(ctx, &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) } } func TestGPUStackUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newGPUStackForTest("http://unused") model := "x" - if _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, &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") { + if _, err := m.Balance(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: %v", err) } - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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 8b034681b6..1052dba2c4 100644 --- a/internal/entity/models/groq.go +++ b/internal/entity/models/groq.go @@ -125,7 +125,7 @@ type groqChatResponse struct { FinishReason string `json:"finish_reason"` } -func (g *GroqModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (g *GroqModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -146,7 +146,7 @@ func (g *GroqModel) ChatWithMessages(modelName string, messages []Message, apiCo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -193,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GroqModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -221,7 +221,7 @@ func (g *GroqModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -292,7 +292,7 @@ type groqListModelsResponse struct { Error interface{} `json:"error"` } -func (g *GroqModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (g *GroqModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -302,7 +302,7 @@ func (g *GroqModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -337,24 +337,24 @@ func (g *GroqModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return ParseListModel(ModelList{Models: result.Data}), nil } -func (g *GroqModel) CheckConnection(apiConfig *APIConfig) error { - _, err := g.ListModels(apiConfig) +func (g *GroqModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := g.ListModels(ctx, apiConfig) return err } -func (g *GroqModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (g *GroqModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (g *GroqModel) Rerank(ctx context.Context, 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()) } -func (g *GroqModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (g *GroqModel) Balance(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (g *GroqModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -471,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GroqModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (g *GroqModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := g.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -535,22 +535,22 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (g *GroqModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (g *GroqModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (g *GroqModel) ParseFile(ctx context.Context, 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()) } -func (g *GroqModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (g *GroqModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", g.Name()) } -func (g *GroqModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (g *GroqModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 3811a35883..b6cc65d1f4 100644 --- a/internal/entity/models/groq_test.go +++ b/internal/entity/models/groq_test.go @@ -125,7 +125,9 @@ func TestGroqChatHappyPath(t *testing.T) { topP := 0.9 stop := []string{"END"} effort := "high" + ctx := t.Context() resp, err := newGroqForTest(srv.URL).ChatWithMessages( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -144,16 +146,18 @@ func TestGroqChatHappyPath(t *testing.T) { } func TestGroqChatRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newGroqForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newGroqForTest("http://unused").ChatWithMessages(ctx, "", []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 TestGroqChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newGroqForTest("http://unused").ChatWithMessages("llama-3.3-70b-versatile", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newGroqForTest("http://unused").ChatWithMessages(ctx, "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) } @@ -167,7 +171,9 @@ func TestGroqChatRejectsHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() _, err := newGroqForTest(srv.URL).ChatWithMessages( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -203,7 +209,9 @@ func TestGroqStreamHappyPath(t *testing.T) { var content []string var reasoning []string var sawDone bool + ctx := t.Context() err := newGroqForTest(srv.URL).ChatStreamlyWithSender( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -239,7 +247,9 @@ func TestGroqStreamHappyPath(t *testing.T) { func TestGroqStreamRejectsExplicitFalse(t *testing.T) { apiKey := "test-key" stream := false + ctx := t.Context() err := newGroqForTest("http://unused").ChatStreamlyWithSender( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -254,7 +264,9 @@ func TestGroqStreamRejectsExplicitFalse(t *testing.T) { func TestGroqStreamRequiresSender(t *testing.T) { apiKey := "test-key" + ctx := t.Context() err := newGroqForTest("http://unused").ChatStreamlyWithSender( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -275,7 +287,9 @@ func TestGroqStreamRejectsUnterminatedStream(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newGroqForTest(srv.URL).ChatStreamlyWithSender( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -289,6 +303,7 @@ func TestGroqStreamRejectsUnterminatedStream(t *testing.T) { } func TestGroqListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := newGroqServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s", r.Method) @@ -307,19 +322,20 @@ func TestGroqListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" model := newGroqForTest(srv.URL) - models, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "llama-3.3-70b-versatile,openai/gpt-oss-120b" { t.Errorf("models=%v", models) } - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestGroqBaseURLTrimsTrailingSlash(t *testing.T) { + ctx := t.Context() srv := newGroqServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -334,6 +350,7 @@ func TestGroqBaseURLTrimsTrailingSlash(t *testing.T) { apiKey := "test-key" _, err := newGroqForTest(srv.URL+"/").ChatWithMessages( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -346,6 +363,7 @@ func TestGroqBaseURLTrimsTrailingSlash(t *testing.T) { } func TestGroqUsesEmptyRegionCustomBaseURL(t *testing.T) { + ctx := t.Context() srv := newGroqServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -365,6 +383,7 @@ func TestGroqUsesEmptyRegionCustomBaseURL(t *testing.T) { URLSuffix{Chat: "chat/completions", Models: "models"}, ) _, err := model.ChatWithMessages( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: ®ion}, @@ -377,20 +396,21 @@ func TestGroqUsesEmptyRegionCustomBaseURL(t *testing.T) { } func TestGroqUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newGroqForTest("http://unused") - if _, err := m.Embed(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, 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") { + if _, err := m.Balance(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(ctx, 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") { + if _, err := m.ListTasks(ctx, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ListTasks error=%v", err) } } diff --git a/internal/entity/models/huaweicloud.go b/internal/entity/models/huaweicloud.go index 8cf75c9eb2..54eef1ce4d 100644 --- a/internal/entity/models/huaweicloud.go +++ b/internal/entity/models/huaweicloud.go @@ -163,7 +163,7 @@ func huaweiCloudApplyChatConfig(req map[string]any, modelName string, chatModelC } } -func (h *HuaweiCloudModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (h *HuaweiCloudModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -200,7 +200,7 @@ func (h *HuaweiCloudModel) ChatWithMessages(modelName string, messages []Message return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -264,7 +264,7 @@ func (h *HuaweiCloudModel) ChatWithMessages(modelName string, messages []Message }, nil } -func (h *HuaweiCloudModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HuaweiCloudModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -310,7 +310,7 @@ func (h *HuaweiCloudModel) ChatStreamlyWithSender(modelName string, messages []M return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -391,7 +391,7 @@ type huaweiCloudEmbeddingResponse struct { } `json:"data"` } -func (h *HuaweiCloudModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (h *HuaweiCloudModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -425,7 +425,7 @@ func (h *HuaweiCloudModel) Embed(modelName *string, texts []string, apiConfig *A return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -489,7 +489,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (h *HuaweiCloudModel) Rerank(ctx context.Context, 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 } @@ -526,7 +526,7 @@ func (h *HuaweiCloudModel) Rerank(modelName *string, query string, documents []s return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -582,31 +582,31 @@ 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (h *HuaweiCloudModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HuaweiCloudModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (h *HuaweiCloudModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HuaweiCloudModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (h *HuaweiCloudModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (h *HuaweiCloudModel) ParseFile(ctx context.Context, 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()) } -func (h *HuaweiCloudModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (h *HuaweiCloudModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -623,7 +623,7 @@ func (h *HuaweiCloudModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, strings.TrimPrefix(h.baseModel.URLSuffix.Models, "/")) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -659,19 +659,19 @@ func (h *HuaweiCloudModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse return ParseListModel(ModelList{Models: parsed.Data}), nil } -func (h *HuaweiCloudModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (h *HuaweiCloudModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) CheckConnection(apiConfig *APIConfig) error { - _, err := h.ListModels(apiConfig) +func (h *HuaweiCloudModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := h.ListModels(ctx, apiConfig) return err } -func (h *HuaweiCloudModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (h *HuaweiCloudModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuaweiCloudModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (h *HuaweiCloudModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 4c7e49dd04..1cce23edf7 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (h *HuggingFaceModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -120,7 +120,7 @@ func (h *HuggingFaceModel) ChatWithMessages(modelName string, messages []Message return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HuggingFaceModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -264,7 +264,7 @@ func (h *HuggingFaceModel) ChatStreamlyWithSender(modelName string, messages []M return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (h *HuggingFaceModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -356,7 +356,7 @@ func (h *HuggingFaceModel) Embed(modelName *string, texts []string, apiConfig *A } url := fmt.Sprintf("%s/%s/%s", resolvedBaseURL, h.baseModel.URLSuffix.Embedding, *modelName) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -398,39 +398,39 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (h *HuggingFaceModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (h *HuggingFaceModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HuggingFaceModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (h *HuggingFaceModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HuggingFaceModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (h *HuggingFaceModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (h *HuggingFaceModel) ParseFile(ctx context.Context, 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()) } -func (h *HuggingFaceModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (h *HuggingFaceModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -449,7 +449,7 @@ func (h *HuggingFaceModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -488,19 +488,19 @@ func (h *HuggingFaceModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse return ParseListModel(modelList), nil } -func (h *HuggingFaceModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (h *HuggingFaceModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } -func (h *HuggingFaceModel) CheckConnection(apiConfig *APIConfig) error { - _, err := h.ListModels(apiConfig) +func (h *HuggingFaceModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := h.ListModels(ctx, apiConfig) return err } -func (h *HuggingFaceModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (h *HuggingFaceModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HuggingFaceModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (h *HuggingFaceModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 c777706711..a1c8f7caca 100644 --- a/internal/entity/models/hunyuan.go +++ b/internal/entity/models/hunyuan.go @@ -51,7 +51,7 @@ func (h *HunyuanModel) Name() string { return "Tencent Hunyuan" } -func (h *HunyuanModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (h *HunyuanModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -100,7 +100,7 @@ func (h *HunyuanModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -161,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HunyuanModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -217,7 +217,7 @@ func (h *HunyuanModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -282,7 +282,7 @@ func (h *HunyuanModel) ChatStreamlyWithSender(modelName string, messages []Messa return nil } -func (h *HunyuanModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (h *HunyuanModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -294,7 +294,7 @@ func (h *HunyuanModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, h.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -329,12 +329,12 @@ func (h *HunyuanModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er return ParseListModel(modelList), nil } -func (h *HunyuanModel) CheckConnection(apiConfig *APIConfig) error { - _, err := h.ListModels(apiConfig) +func (h *HunyuanModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := h.ListModels(ctx, apiConfig) return err } -func (h *HunyuanModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (h *HunyuanModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := h.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -363,7 +363,7 @@ func (h *HunyuanModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -415,42 +415,42 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (h *HunyuanModel) Rerank(ctx context.Context, 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()) } -func (h *HunyuanModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (h *HunyuanModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (h *HunyuanModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HunyuanModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (h *HunyuanModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (h *HunyuanModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (h *HunyuanModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (h *HunyuanModel) ParseFile(ctx context.Context, 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()) } -func (h *HunyuanModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (h *HunyuanModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", h.Name()) } -func (h *HunyuanModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (h *HunyuanModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 fb31ff97c0..1c521c42c6 100644 --- a/internal/entity/models/hunyuan_test.go +++ b/internal/entity/models/hunyuan_test.go @@ -123,7 +123,9 @@ func TestHunyuanChatHappyPath(t *testing.T) { apiKey := "test-key" mt := 64 temp := 0.3 + ctx := t.Context() resp, err := newHunyuanForTest(srv.URL).ChatWithMessages( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -152,7 +154,9 @@ func TestHunyuanChatNoReasoning(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() resp, err := newHunyuanForTest(srv.URL).ChatWithMessages( + ctx, "hunyuan-lite", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -169,7 +173,9 @@ func TestHunyuanChatNoReasoning(t *testing.T) { } func TestHunyuanChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() _, err := newHunyuanForTest("http://unused").ChatWithMessages( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, @@ -180,8 +186,10 @@ func TestHunyuanChatRequiresAPIKey(t *testing.T) { } func TestHunyuanChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" _, err := newHunyuanForTest("http://unused").ChatWithMessages( + ctx, "hunyuan-pro", nil, &APIConfig{ApiKey: &apiKey}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "messages is empty") { @@ -197,7 +205,9 @@ func TestHunyuanChatPropagatesHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() _, err := newHunyuanForTest(srv.URL).ChatWithMessages( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -219,7 +229,9 @@ func TestHunyuanStreamHappyPath(t *testing.T) { apiKey := "test-key" var chunks []string var sawDone bool + ctx := t.Context() err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -257,7 +269,9 @@ func TestHunyuanStreamSplitsReasoning(t *testing.T) { apiKey := "test-key" var content, reasoning []string + ctx := t.Context() err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( + ctx, "hunyuan-standard-256K", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -287,7 +301,9 @@ func TestHunyuanStreamSplitsReasoning(t *testing.T) { func TestHunyuanStreamRejectsExplicitFalse(t *testing.T) { apiKey := "test-key" stream := false + ctx := t.Context() err := newHunyuanForTest("http://unused").ChatStreamlyWithSender( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -300,8 +316,10 @@ func TestHunyuanStreamRejectsExplicitFalse(t *testing.T) { } func TestHunyuanStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newHunyuanForTest("http://unused").ChatStreamlyWithSender( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) @@ -317,7 +335,9 @@ func TestHunyuanStreamFailsWithoutTerminal(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -335,7 +355,9 @@ func TestHunyuanStreamRejectsMalformedFrame(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -353,7 +375,9 @@ func TestHunyuanStreamSurfacesUpstreamError(t *testing.T) { defer srv.Close() apiKey := "test-key" + ctx := t.Context() err := newHunyuanForTest(srv.URL).ChatStreamlyWithSender( + ctx, "hunyuan-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -367,6 +391,7 @@ func TestHunyuanStreamSurfacesUpstreamError(t *testing.T) { } func TestHunyuanListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newHunyuanServer(t, http.MethodGet, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -379,7 +404,7 @@ func TestHunyuanListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newHunyuanForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newHunyuanForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -390,13 +415,15 @@ func TestHunyuanListModelsHappyPath(t *testing.T) { } func TestHunyuanListModelsRequiresAPIKey(t *testing.T) { - _, err := newHunyuanForTest("http://unused").ListModels(&APIConfig{}) + ctx := t.Context() + _, err := newHunyuanForTest("http://unused").ListModels(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } } func TestHunyuanCheckConnectionDelegatesToListModels(t *testing.T) { + ctx := t.Context() srv := newHunyuanServer(t, http.MethodGet, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{{"id": "hunyuan-pro"}}, @@ -405,12 +432,13 @@ func TestHunyuanCheckConnectionDelegatesToListModels(t *testing.T) { defer srv.Close() apiKey := "test-key" - if err := newHunyuanForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newHunyuanForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection: %v", err) } } func TestHunyuanCheckConnectionPropagatesError(t *testing.T) { + ctx := t.Context() srv := newHunyuanServer(t, http.MethodGet, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"bad key"}`)) @@ -418,23 +446,25 @@ func TestHunyuanCheckConnectionPropagatesError(t *testing.T) { defer srv.Close() apiKey := "test-key" - err := newHunyuanForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}) + err := newHunyuanForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } } func TestHunyuanBaseURLForRegionUnknown(t *testing.T) { + ctx := t.Context() m := newHunyuanForTest("http://unused") apiKey := "test-key" region := "missing" - _, err := m.ListModels(&APIConfig{ApiKey: &apiKey, Region: ®ion}) + _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey, Region: ®ion}) if err == nil || !strings.Contains(err.Error(), "no base URL configured") { t.Errorf("expected base-URL error, got %v", err) } } func TestHunyuanEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newHunyuanServer(t, http.MethodPost, "/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "hunyuan-embedding" { t.Errorf("model=%v", body["model"]) @@ -456,7 +486,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, nil) + embeddings, err := newHunyuanForTest(srv.URL).Embed(ctx, &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -466,41 +496,43 @@ func TestHunyuanEmbedHappyPath(t *testing.T) { } func TestHunyuanEmbedValidatesInputs(t *testing.T) { + ctx := t.Context() apiKey := "test-key" model := "hunyuan-embedding" - if embeddings, err := newHunyuanForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil || len(embeddings) != 0 { + if embeddings, err := newHunyuanForTest("http://unused").Embed(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := newHunyuanForTest("http://unused").Embed(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := newHunyuanForTest("http://unused").Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newHunyuanForTest("http://unused").Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newHunyuanForTest("http://unused").Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newHunyuanForTest("http://unused").Embed(ctx, &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) } } func TestHunyuanAudioOCRReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newHunyuanForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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 31b6a19a57..9a9a0cee4d 100644 --- a/internal/entity/models/jiekouai.go +++ b/internal/entity/models/jiekouai.go @@ -18,6 +18,7 @@ package models import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -61,7 +62,7 @@ func validateJieKouAIModelName(modelName *string) (string, error) { return strings.TrimSpace(*modelName), nil } -func (j *JieKouAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (j *JieKouAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -196,7 +197,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (j *JieKouAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -332,7 +333,7 @@ func (j *JieKouAIModel) ChatStreamlyWithSender(modelName string, messages []Mess return nil } -func (j *JieKouAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (j *JieKouAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -411,7 +412,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (j *JieKouAIModel) Rerank(ctx context.Context, 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 } @@ -494,31 +495,31 @@ 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (j *JieKouAIModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (j *JieKouAIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (j *JieKouAIModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (j *JieKouAIModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (j *JieKouAIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (j *JieKouAIModel) ParseFile(ctx context.Context, 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()) } -func (j *JieKouAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (j *JieKouAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -574,19 +575,19 @@ func (j *JieKouAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return ParseListModel(ModelList{Models: result.Data}), nil } -func (j *JieKouAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (j *JieKouAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JieKouAIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := j.ListModels(apiConfig) +func (j *JieKouAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := j.ListModels(ctx, apiConfig) return err } -func (j *JieKouAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (j *JieKouAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", j.Name()) } -func (j *JieKouAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (j *JieKouAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 588dcfc15d..c87243a474 100644 --- a/internal/entity/models/jiekouai_test.go +++ b/internal/entity/models/jiekouai_test.go @@ -89,7 +89,8 @@ func TestJieKouAIChatForcesNonStreaming(t *testing.T) { apiKey := "test-key" stream := true thinking := true - resp, err := newJieKouAIForTest(srv.URL).ChatWithMessages( + ctx := t.Context() + resp, err := newJieKouAIForTest(srv.URL).ChatWithMessages(ctx, " gpt-5 ", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -131,7 +132,8 @@ func TestJieKouAIStreamForcesStreaming(t *testing.T) { apiKey := "test-key" stream := false var content, reasoning []string - err := newJieKouAIForTest(srv.URL).ChatStreamlyWithSender( + ctx := t.Context() + err := newJieKouAIForTest(srv.URL).ChatStreamlyWithSender(ctx, "gpt-5", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -159,6 +161,7 @@ func TestJieKouAIStreamForcesStreaming(t *testing.T) { } func TestJieKouAIListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newJieKouAIServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -176,7 +179,7 @@ func TestJieKouAIListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newJieKouAIForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newJieKouAIForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -186,6 +189,7 @@ func TestJieKouAIListModelsHappyPath(t *testing.T) { } func TestJieKouAIListModelsRejectsMalformedResponse(t *testing.T) { + ctx := t.Context() apiKey := "test-key" for name, response := range map[string]interface{}{ "missing data": map[string]interface{}{"object": "list"}, @@ -197,7 +201,7 @@ func TestJieKouAIListModelsRejectsMalformedResponse(t *testing.T) { }) defer srv.Close() - if _, err := newJieKouAIForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}); err == nil { + if _, err := newJieKouAIForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}); err == nil { t.Fatal("expected malformed response error") } }) @@ -205,6 +209,7 @@ func TestJieKouAIListModelsRejectsMalformedResponse(t *testing.T) { } func TestJieKouAIEmbedSendsValidatedRequest(t *testing.T) { + ctx := t.Context() srv := newJieKouAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/openai/v1/embeddings" { t.Errorf("path=%s, want /openai/v1/embeddings", r.URL.Path) @@ -227,7 +232,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, nil) + embeddings, err := newJieKouAIForTest(srv.URL).Embed(ctx, &model, []string{"hello"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -237,6 +242,7 @@ func TestJieKouAIEmbedSendsValidatedRequest(t *testing.T) { } func TestJieKouAIRerankHandlesNilConfig(t *testing.T) { + ctx := t.Context() srv := newJieKouAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/openai/v1/rerank" { t.Errorf("path=%s, want /openai/v1/rerank", r.URL.Path) @@ -261,7 +267,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, nil) + resp, err := newJieKouAIForTest(srv.URL).Rerank(ctx, &model, " question ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -271,6 +277,7 @@ func TestJieKouAIRerankHandlesNilConfig(t *testing.T) { } func TestJieKouAIValidatesInputs(t *testing.T) { + ctx := t.Context() apiKey := "test-key" emptyKey := " " model := "gpt-5" @@ -284,7 +291,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, nil) + _, err := newJieKouAIForTest("http://unused").ChatWithMessages(ctx, "gpt-5", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -292,7 +299,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, nil) + _, err := newJieKouAIForTest("http://unused").ChatWithMessages(ctx, " ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -300,21 +307,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, nil, send) + return newJieKouAIForTest("http://unused").ChatStreamlyWithSender(ctx, "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, nil) + return newJieKouAIForTest("http://unused").ChatStreamlyWithSender(ctx, "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, nil) + _, err := newJieKouAIForTest("http://unused").Embed(ctx, nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -322,7 +329,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "embed api key", run: func() error { - _, err := newJieKouAIForTest("http://unused").Embed(&model, []string{"x"}, nil, nil, nil) + _, err := newJieKouAIForTest("http://unused").Embed(ctx, &model, []string{"x"}, nil, nil, nil) return err }, want: "api key is required", @@ -330,7 +337,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, nil) + _, err := newJieKouAIForTest("http://unused").Rerank(ctx, nil, "q", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -338,7 +345,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, nil) + _, err := newJieKouAIForTest("http://unused").Rerank(ctx, &model, "q", []string{"doc"}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -346,7 +353,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "rerank query", run: func() error { - _, err := newJieKouAIForTest("http://unused").Rerank(&model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newJieKouAIForTest("http://unused").Rerank(ctx, &model, " ", []string{"doc"}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "query is required", @@ -354,7 +361,7 @@ func TestJieKouAIValidatesInputs(t *testing.T) { { name: "models api key", run: func() error { - _, err := newJieKouAIForTest("http://unused").ListModels(&APIConfig{}) + _, err := newJieKouAIForTest("http://unused").ListModels(ctx, &APIConfig{}) return err }, want: "api key is required", diff --git a/internal/entity/models/jina.go b/internal/entity/models/jina.go index d68a0bf595..fe711a7e97 100644 --- a/internal/entity/models/jina.go +++ b/internal/entity/models/jina.go @@ -55,7 +55,7 @@ func (j *JinaModel) Name() string { return "jina" } -func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (j *JinaModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -100,7 +100,7 @@ func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiCo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -160,12 +160,12 @@ func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiCo }, nil } -func (j *JinaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (j *JinaModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (j *JinaModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := j.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -239,7 +239,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (j *JinaModel) Rerank(ctx context.Context, 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 } @@ -317,7 +317,7 @@ func (j *JinaModel) Rerank(modelName *string, query string, documents []string, return &rerankResponse, nil } -func (j *JinaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (j *JinaModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { resolvedBaseURL, err := j.baseModel.GetBaseURL(apiConfig) if err != nil { @@ -366,47 +366,47 @@ func (j *JinaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return ParseListModel(ModelList{Models: models}), nil } -func (j *JinaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (j *JinaModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } -func (j *JinaModel) CheckConnection(apiConfig *APIConfig) error { - _, err := j.ListModels(apiConfig) +func (j *JinaModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := j.ListModels(ctx, apiConfig) return err } // TranscribeAudio transcribe audio -func (j *JinaModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (j *JinaModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (j *JinaModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (j *JinaModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (j *JinaModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (j *JinaModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (j *JinaModel) ParseFile(ctx context.Context, 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()) } -func (j *JinaModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (j *JinaModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", j.Name()) } -func (j *JinaModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (j *JinaModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 26389771d8..c34f9a93ed 100644 --- a/internal/entity/models/jina_test.go +++ b/internal/entity/models/jina_test.go @@ -51,6 +51,7 @@ func newJinaForTest(baseURL string) *JinaModel { } func TestJinaChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newJinaServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "jina-vlm" { t.Errorf("expected model=jina-vlm, got %v", body["model"]) @@ -77,7 +78,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, nil) + resp, err := j.ChatWithMessages(ctx, "jina-vlm", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -96,6 +97,7 @@ func TestJinaChatSupportsToolCalls(t *testing.T) { } func TestJinaChatPropagatesConfig(t *testing.T) { + ctx := t.Context() srv := newJinaServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["max_tokens"] != float64(128) { t.Errorf("max_tokens=%v want 128", body["max_tokens"]) @@ -122,7 +124,7 @@ func TestJinaChatPropagatesConfig(t *testing.T) { temperature := 0.2 topP := 0.8 stop := []string{"END"} - _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "ping"}}, + _, err := j.ChatWithMessages(ctx, "jina-vlm", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &maxTokens, Temperature: &temperature, TopP: &topP, Stop: &stop}, nil, @@ -180,7 +182,8 @@ 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, nil) + ctx := t.Context() + _, err := j.ChatWithMessages(ctx, 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) } @@ -189,6 +192,7 @@ func TestJinaChatValidation(t *testing.T) { } func TestJinaChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newJinaServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"detail":"invalid api key"}`)) @@ -197,13 +201,14 @@ 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, nil) + _, err := j.ChatWithMessages(ctx, "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) } } func TestJinaChatRejectsMalformedResponse(t *testing.T) { + ctx := t.Context() srv := newJinaServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"choices": []map[string]interface{}{}}) }) @@ -211,17 +216,18 @@ 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, nil) + _, err := j.ChatWithMessages(ctx, "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) } } func TestJinaChatRejectsUnknownRegion(t *testing.T) { + ctx := t.Context() j := newJinaForTest("http://unused") apiKey := "test-key" region := "eu" - _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, + _, err := j.ChatWithMessages(ctx, "jina-vlm", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "no base URL configured for region") { @@ -230,6 +236,7 @@ func TestJinaChatRejectsUnknownRegion(t *testing.T) { } func TestJinaChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { + ctx := t.Context() srv := newJinaServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{"message": map[string]interface{}{"content": "ok"}}}, @@ -240,7 +247,7 @@ func TestJinaChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { j := newJinaForTest(srv.URL) apiKey := "test-key" emptyRegion := "" - _, err := j.ChatWithMessages("jina-vlm", []Message{{Role: "user", Content: "x"}}, + _, err := j.ChatWithMessages(ctx, "jina-vlm", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil, nil, ) if err != nil { diff --git a/internal/entity/models/llm.go b/internal/entity/models/llm.go index 8194fd54c1..985e3c917c 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, nil) + resp, err := m.inner.ModelDriver.ChatWithMessages(ctx, *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) } @@ -313,7 +313,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, internalMessage, m.inner.APIConfig, chatCfg, nil, sender); err != nil { + if err := m.inner.ModelDriver.ChatStreamlyWithSender(ctx, *m.inner.ModelName, internalMessage, m.inner.APIConfig, chatCfg, nil, sender); err != nil { _ = sw.Send(nil, err) return } diff --git a/internal/entity/models/llm_test.go b/internal/entity/models/llm_test.go index 5bd22d87d4..b94bbfc957 100644 --- a/internal/entity/models/llm_test.go +++ b/internal/entity/models/llm_test.go @@ -250,7 +250,7 @@ type streamSentinelDriver struct { *captureToolDriver } -func (d *streamSentinelDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *APIConfig, _ *ChatConfig, _ *common.ModelUsage, sender func(*string, *string) error) error { +func (d *streamSentinelDriver) ChatStreamlyWithSender(ctx context.Context, _ string, _ []Message, _ *APIConfig, _ *ChatConfig, _ *common.ModelUsage, sender func(*string, *string) error) error { answer := "answer" if err := sender(&answer, nil); err != nil { return err @@ -265,11 +265,11 @@ 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (d *captureToolDriver) ChatWithMessages(ctx context.Context, _ string, _ []Message, _ *APIConfig, cfg *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { d.lastConfig = cfg return d.resp, nil } -func (d *captureToolDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *APIConfig, cfg *ChatConfig, _ *common.ModelUsage, sender func(*string, *string) error) error { +func (d *captureToolDriver) ChatStreamlyWithSender(ctx context.Context, _ string, _ []Message, _ *APIConfig, cfg *ChatConfig, _ *common.ModelUsage, sender func(*string, *string) error) error { d.lastConfig = cfg if d.resp == nil { return nil @@ -284,40 +284,40 @@ func (d *captureToolDriver) ChatStreamlyWithSender(_ string, _ []Message, _ *API } return nil } -func (d *captureToolDriver) Embed(_ *string, _ []string, _ *APIConfig, _ *EmbeddingConfig, _ *common.ModelUsage) ([]EmbeddingData, error) { +func (d *captureToolDriver) Embed(ctx context.Context, _ *string, _ []string, _ *APIConfig, _ *EmbeddingConfig, _ *common.ModelUsage) ([]EmbeddingData, error) { return nil, nil } -func (d *captureToolDriver) Rerank(_ *string, _ string, _ []string, _ *APIConfig, _ *RerankConfig, _ *common.ModelUsage) (*RerankResponse, error) { +func (d *captureToolDriver) Rerank(ctx context.Context, _ *string, _ string, _ []string, _ *APIConfig, _ *RerankConfig, _ *common.ModelUsage) (*RerankResponse, error) { return nil, nil } -func (d *captureToolDriver) TranscribeAudio(_ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ *common.ModelUsage) (*ASRResponse, error) { +func (d *captureToolDriver) TranscribeAudio(ctx context.Context, _ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ *common.ModelUsage) (*ASRResponse, error) { return nil, nil } -func (d *captureToolDriver) TranscribeAudioWithSender(_ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { +func (d *captureToolDriver) TranscribeAudioWithSender(ctx context.Context, _ *string, _ *string, _ *APIConfig, _ *ASRConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { return nil } -func (d *captureToolDriver) AudioSpeech(_ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ *common.ModelUsage) (*TTSResponse, error) { +func (d *captureToolDriver) AudioSpeech(ctx context.Context, _ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ *common.ModelUsage) (*TTSResponse, error) { return nil, nil } -func (d *captureToolDriver) AudioSpeechWithSender(_ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { +func (d *captureToolDriver) AudioSpeechWithSender(ctx context.Context, _ *string, _ *string, _ *APIConfig, _ *TTSConfig, _ *common.ModelUsage, _ func(*string, *string) error) error { return nil } -func (d *captureToolDriver) OCRFile(_ *string, _ []byte, _ *string, _ *APIConfig, _ *OCRConfig, _ *common.ModelUsage) (*OCRFileResponse, error) { +func (d *captureToolDriver) OCRFile(ctx context.Context, _ *string, _ []byte, _ *string, _ *APIConfig, _ *OCRConfig, _ *common.ModelUsage) (*OCRFileResponse, error) { return nil, nil } -func (d *captureToolDriver) ParseFile(_ *string, _ []byte, _ *string, _ *APIConfig, _ *ParseFileConfig, _ *common.ModelUsage) (*ParseFileResponse, error) { +func (d *captureToolDriver) ParseFile(ctx context.Context, _ *string, _ []byte, _ *string, _ *APIConfig, _ *ParseFileConfig, _ *common.ModelUsage) (*ParseFileResponse, error) { return nil, nil } -func (d *captureToolDriver) ListModels(_ *APIConfig) ([]ListModelResponse, error) { +func (d *captureToolDriver) ListModels(ctx context.Context, _ *APIConfig) ([]ListModelResponse, error) { return nil, nil } -func (d *captureToolDriver) Balance(_ *APIConfig) (map[string]interface{}, error) { +func (d *captureToolDriver) Balance(ctx context.Context, _ *APIConfig) (map[string]interface{}, error) { return nil, nil } -func (d *captureToolDriver) CheckConnection(_ *APIConfig) error { return nil } -func (d *captureToolDriver) ListTasks(_ *APIConfig) ([]ListTaskStatus, error) { +func (d *captureToolDriver) CheckConnection(ctx context.Context, _ *APIConfig) error { return nil } +func (d *captureToolDriver) ListTasks(ctx context.Context, _ *APIConfig) ([]ListTaskStatus, error) { return nil, nil } -func (d *captureToolDriver) ShowTask(_ string, _ *APIConfig) (*TaskResponse, error) { +func (d *captureToolDriver) ShowTask(ctx context.Context, _ string, _ *APIConfig) (*TaskResponse, error) { return nil, nil } diff --git a/internal/entity/models/lmstudio.go b/internal/entity/models/lmstudio.go index 393f605f56..30b3c1bfc7 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (l *LmStudioModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -130,7 +130,7 @@ func (l *LmStudioModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LmStudioModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -280,7 +280,7 @@ func (l *LmStudioModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (l *LmStudioModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -391,7 +391,7 @@ func (l *LmStudioModel) Embed(modelName *string, texts []string, apiConfig *APIC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -435,40 +435,40 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (l *LmStudioModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (l *LmStudioModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LmStudioModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (l *LmStudioModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LmStudioModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (l *LmStudioModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (l *LmStudioModel) ParseFile(ctx context.Context, 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()) } // ListModels list supported models -func (l *LmStudioModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (l *LmStudioModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -494,7 +494,7 @@ func (l *LmStudioModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -535,20 +535,20 @@ func (l *LmStudioModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return ParseListModel(modelList), nil } -func (l *LmStudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (l *LmStudioModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection verifies that the configured LM Studio base URL is reachable -func (l *LmStudioModel) CheckConnection(apiConfig *APIConfig) error { - _, err := l.ListModels(apiConfig) +func (l *LmStudioModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := l.ListModels(ctx, apiConfig) return err } -func (l *LmStudioModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (l *LmStudioModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LmStudioModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (l *LmStudioModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 3f06c94dc6..ccd8d8c5f8 100644 --- a/internal/entity/models/localai.go +++ b/internal/entity/models/localai.go @@ -81,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (l *LocalAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -129,7 +129,7 @@ func (l *LocalAIModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -195,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LocalAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -370,7 +370,7 @@ type localAIEmbeddingResponse struct { Object string `json:"object"` } -func (l *LocalAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (l *LocalAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -401,7 +401,7 @@ func (l *LocalAIModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -472,7 +472,7 @@ type localAIRerankResponse struct { } `json:"results"` } -func (l *LocalAIModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (l *LocalAIModel) Rerank(ctx context.Context, 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 } @@ -506,7 +506,7 @@ func (l *LocalAIModel) Rerank(modelName *string, query string, documents []strin return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -553,7 +553,7 @@ func (l *LocalAIModel) Rerank(modelName *string, query string, documents []strin return rerankResponse, nil } -func (l *LocalAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (l *LocalAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -564,7 +564,7 @@ func (l *LocalAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), l.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -604,48 +604,48 @@ func (l *LocalAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er return ParseListModel(modelList), nil } -func (l *LocalAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (l *LocalAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } -func (l *LocalAIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := l.ListModels(apiConfig) +func (l *LocalAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := l.ListModels(ctx, apiConfig) if err != nil { return err } return nil } -func (l *LocalAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (l *LocalAIModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LocalAIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (l *LocalAIModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LocalAIModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (l *LocalAIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (l *LocalAIModel) ParseFile(ctx context.Context, 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()) } -func (l *LocalAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (l *LocalAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LocalAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (l *LocalAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 63db762e2e..cd573d51d0 100644 --- a/internal/entity/models/localai_test.go +++ b/internal/entity/models/localai_test.go @@ -43,6 +43,7 @@ func TestLocalAIName(t *testing.T) { } func TestLocalAIStreamCancelsOnIdle(t *testing.T) { + ctx := t.Context() // The server emits one valid chunk and then stalls. Without the // watchdog, scanner.Scan() would hang forever. With the watchdog // at 200ms, it must return a clear "stream idle" error in well @@ -78,7 +79,7 @@ func TestLocalAIStreamCancelsOnIdle(t *testing.T) { l := newLocalAIForTest(srv.URL) var got []string var mu sync.Mutex - err := l.ChatStreamlyWithSender("gpt-4", + err := l.ChatStreamlyWithSender(ctx, "gpt-4", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, func(content *string, _ *string) error { @@ -106,6 +107,7 @@ func TestLocalAIStreamCancelsOnIdle(t *testing.T) { } func TestLocalAIStreamCompletesWithoutTriggeringWatchdog(t *testing.T) { + ctx := t.Context() // Sanity check: a fast, complete stream should not trip the // watchdog even with a moderately tight idle window. withLocalAIIdleTimeout(t, 500*time.Millisecond) @@ -128,7 +130,7 @@ func TestLocalAIStreamCompletesWithoutTriggeringWatchdog(t *testing.T) { l := newLocalAIForTest(srv.URL) var chunks []string - err := l.ChatStreamlyWithSender("gpt-4", + err := l.ChatStreamlyWithSender(ctx, "gpt-4", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, func(content *string, _ *string) error { @@ -147,8 +149,9 @@ func TestLocalAIStreamCompletesWithoutTriggeringWatchdog(t *testing.T) { } func TestLocalAIStreamRequiresSender(t *testing.T) { + ctx := t.Context() l := newLocalAIForTest("http://unused") - err := l.ChatStreamlyWithSender("gpt-4", + err := l.ChatStreamlyWithSender(ctx, "gpt-4", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { @@ -157,11 +160,12 @@ func TestLocalAIStreamRequiresSender(t *testing.T) { } func TestLocalAIChatMissingBaseURLFailsClearly(t *testing.T) { + ctx := t.Context() // LocalAI has no public default; resolveBaseURL must fail with a // helpful message when neither the requested region nor "default" // is configured. l := NewLocalAIModel(map[string]string{}, URLSuffix{Chat: "chat/completions"}) - _, err := l.ChatWithMessages("gpt-4", + _, err := l.ChatWithMessages(ctx, "gpt-4", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, ) @@ -171,6 +175,7 @@ func TestLocalAIChatMissingBaseURLFailsClearly(t *testing.T) { } func TestLocalAIChatOmitsAuthHeaderWhenKeyEmpty(t *testing.T) { + ctx := t.Context() // Optional-auth contract: LocalAI accepts an empty key, so the // driver must NOT send a "Bearer " header in that case. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -182,7 +187,7 @@ func TestLocalAIChatOmitsAuthHeaderWhenKeyEmpty(t *testing.T) { defer srv.Close() l := newLocalAIForTest(srv.URL) - resp, err := l.ChatWithMessages("gpt-4", + resp, err := l.ChatWithMessages(ctx, "gpt-4", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, ) @@ -195,6 +200,7 @@ func TestLocalAIChatOmitsAuthHeaderWhenKeyEmpty(t *testing.T) { } func TestLocalAIChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { + ctx := t.Context() // And conversely: when a tenant has put LocalAI behind an auth // proxy with a token, the driver does send the Bearer header. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -207,7 +213,7 @@ func TestLocalAIChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { l := newLocalAIForTest(srv.URL) key := "secret" - _, err := l.ChatWithMessages("gpt-4", + _, err := l.ChatWithMessages(ctx, "gpt-4", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil, ) @@ -217,14 +223,16 @@ func TestLocalAIChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { } func TestLocalAIBalanceReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() l := newLocalAIForTest("http://unused") - _, err := l.Balance(&APIConfig{}) + _, err := l.Balance(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: expected 'no such method', got %v", err) } } func TestLocalAIEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/embeddings" { t.Errorf("path=%s", r.URL.Path) @@ -238,7 +246,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, nil) + vecs, err := l.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -251,6 +259,7 @@ func TestLocalAIEmbedHappyPath(t *testing.T) { } func TestLocalAIEmbedRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() // CodeRabbit caught that a response repeating data[*].index would // silently overwrite the earlier vector. Verify the driver fails // loudly instead. @@ -263,13 +272,14 @@ func TestLocalAIEmbedRejectsDuplicateIndex(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := l.Embed(ctx, &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) } } func TestLocalAIEmbedRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"data":[{"embedding":[1],"index":7}]}`) })) @@ -277,13 +287,14 @@ func TestLocalAIEmbedRejectsOutOfRangeIndex(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := l.Embed(ctx, &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) } } func TestLocalAIEmbedRejectsMissingSlot(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"data":[{"embedding":[1],"index":0}]}`) })) @@ -291,13 +302,14 @@ func TestLocalAIEmbedRejectsMissingSlot(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - _, err := l.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := l.Embed(ctx, &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) } } func TestLocalAIEmbedEmptyInputShortCircuits(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { t.Error("Embed([]) made an unexpected HTTP call") })) @@ -305,7 +317,7 @@ func TestLocalAIEmbedEmptyInputShortCircuits(t *testing.T) { l := newLocalAIForTest(srv.URL) model := "text-embedding-ada-002" - vecs, err := l.Embed(&model, []string{}, &APIConfig{}, nil, nil) + vecs, err := l.Embed(ctx, &model, []string{}, &APIConfig{}, nil, nil) if err != nil || len(vecs) != 0 { t.Errorf("Embed([])=(%v,%v) want ([],nil)", vecs, err) } @@ -360,6 +372,7 @@ func TestExtractLocalAIReasoning(t *testing.T) { // when proxied through OpenAI-shim). The driver must surface it on // ChatResponse.ReasonContent. func TestLocalAIChatExtractsReasoningContent(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"choices":[{"message":{ "role":"assistant", @@ -370,7 +383,7 @@ func TestLocalAIChatExtractsReasoningContent(t *testing.T) { defer srv.Close() l := newLocalAIForTest(srv.URL) - resp, err := l.ChatWithMessages("kimi-k2.6", + resp, err := l.ChatWithMessages(ctx, "kimi-k2.6", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{}, nil, nil, ) @@ -388,6 +401,7 @@ func TestLocalAIChatExtractsReasoningContent(t *testing.T) { // Non-streaming chat that uses message.thinking (Qwen3 via Ollama-shim // inside LocalAI). The driver must surface it on ReasonContent too. func TestLocalAIChatExtractsThinking(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"choices":[{"message":{ "role":"assistant", @@ -398,7 +412,7 @@ func TestLocalAIChatExtractsThinking(t *testing.T) { defer srv.Close() l := newLocalAIForTest(srv.URL) - resp, err := l.ChatWithMessages("qwen3-32b", + resp, err := l.ChatWithMessages(ctx, "qwen3-32b", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{}, nil, nil, ) @@ -414,6 +428,7 @@ func TestLocalAIChatExtractsThinking(t *testing.T) { // non-reasoning model) must produce empty ReasonContent without // crashing or erroring. func TestLocalAIChatHandlesAbsentReasoning(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"choices":[{"message":{ "role":"assistant","content":"hello" @@ -422,7 +437,7 @@ func TestLocalAIChatHandlesAbsentReasoning(t *testing.T) { defer srv.Close() l := newLocalAIForTest(srv.URL) - resp, err := l.ChatWithMessages("llama-3-8b-instruct", + resp, err := l.ChatWithMessages(ctx, "llama-3-8b-instruct", []Message{{Role: "user", Content: "hi"}}, &APIConfig{}, nil, nil, ) @@ -441,6 +456,7 @@ func TestLocalAIChatHandlesAbsentReasoning(t *testing.T) { // chunks and delta.content chunks (kimi-k2.6, o-series shape). // Reasoning must reach the sender's 2nd arg, content the 1st. func TestLocalAIStreamExtractsReasoningContentDelta(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) @@ -456,7 +472,7 @@ func TestLocalAIStreamExtractsReasoningContentDelta(t *testing.T) { l := newLocalAIForTest(srv.URL) var content, reasoning []string - err := l.ChatStreamlyWithSender("kimi-k2.6", + err := l.ChatStreamlyWithSender(ctx, "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, func(c *string, r *string) error { @@ -486,6 +502,7 @@ func TestLocalAIStreamExtractsReasoningContentDelta(t *testing.T) { // Streaming chat where the upstream uses delta.thinking (Qwen3 shape). // The same handler must work. func TestLocalAIStreamExtractsThinkingDelta(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) @@ -499,7 +516,7 @@ func TestLocalAIStreamExtractsThinkingDelta(t *testing.T) { l := newLocalAIForTest(srv.URL) var got []string - err := l.ChatStreamlyWithSender("qwen3-32b", + err := l.ChatStreamlyWithSender(ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, func(c *string, r *string) error { @@ -524,6 +541,7 @@ func TestLocalAIStreamExtractsThinkingDelta(t *testing.T) { // Request-side: ChatConfig.Effort must flow into request body as // reasoning_effort. func TestLocalAIChatPropagatesReasoningEffort(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, err := io.ReadAll(r.Body) @@ -541,7 +559,7 @@ func TestLocalAIChatPropagatesReasoningEffort(t *testing.T) { l := newLocalAIForTest(srv.URL) effort := "high" - _, err := l.ChatWithMessages("kimi-k2.6", + _, err := l.ChatWithMessages(ctx, "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Effort: &effort}, nil) if err != nil { @@ -558,6 +576,7 @@ func TestLocalAIChatPropagatesReasoningEffort(t *testing.T) { // Request-side: ChatConfig.Thinking must flow into request body as // enable_thinking (Qwen3-style). func TestLocalAIChatPropagatesEnableThinking(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, err := io.ReadAll(r.Body) @@ -575,7 +594,7 @@ func TestLocalAIChatPropagatesEnableThinking(t *testing.T) { l := newLocalAIForTest(srv.URL) think := true - _, err := l.ChatWithMessages("qwen3-32b", + _, err := l.ChatWithMessages(ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Thinking: &think}, nil) if err != nil { @@ -588,6 +607,7 @@ func TestLocalAIChatPropagatesEnableThinking(t *testing.T) { // Stream request also propagates the reasoning params. func TestLocalAIStreamPropagatesReasoningParams(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, err := io.ReadAll(r.Body) @@ -610,7 +630,7 @@ func TestLocalAIStreamPropagatesReasoningParams(t *testing.T) { l := newLocalAIForTest(srv.URL) effort := "medium" think := true - err := l.ChatStreamlyWithSender("kimi-k2.6", + err := l.ChatStreamlyWithSender(ctx, "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Effort: &effort, Thinking: &think}, nil, func(*string, *string) error { return nil }, diff --git a/internal/entity/models/longcat.go b/internal/entity/models/longcat.go index 856b6c81ca..5aac51be6c 100644 --- a/internal/entity/models/longcat.go +++ b/internal/entity/models/longcat.go @@ -52,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (l *LongCatModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -99,7 +99,7 @@ func (l *LongCatModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -167,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LongCatModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -224,7 +224,7 @@ func (l *LongCatModel) ChatStreamlyWithSender(modelName string, messages []Messa } // SSE streams are long-lived. - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -309,7 +309,7 @@ type longCatListModelsResponse struct { const longCatMaxListModelsResponseBytes = 1 << 20 -func (l *LongCatModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (l *LongCatModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := l.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -321,7 +321,7 @@ func (l *LongCatModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, l.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -362,56 +362,56 @@ func (l *LongCatModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er return ParseListModel(ModelList{Models: result.Data}), nil } -func (l *LongCatModel) CheckConnection(apiConfig *APIConfig) error { - _, err := l.ListModels(apiConfig) +func (l *LongCatModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := l.ListModels(ctx, apiConfig) return err } -func (l *LongCatModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (l *LongCatModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (l *LongCatModel) Rerank(ctx context.Context, 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()) } // Balance is not exposed by the LongCat API. -func (l *LongCatModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (l *LongCatModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } // TranscribeAudio (ASR) is not exposed by the LongCat API. -func (l *LongCatModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (l *LongCatModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LongCatModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (l *LongCatModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (l *LongCatModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (l *LongCatModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (l *LongCatModel) ParseFile(ctx context.Context, 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()) } -func (l *LongCatModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (l *LongCatModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", l.Name()) } -func (l *LongCatModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (l *LongCatModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 c397f4a7ac..3d2ffafbf4 100644 --- a/internal/entity/models/longcat_test.go +++ b/internal/entity/models/longcat_test.go @@ -100,6 +100,7 @@ func TestLongCatNewModelWithCustomDefaultTransport(t *testing.T) { } func TestLongCatChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newLongCatServer(t, "/openai/v1/chat/completions", func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "LongCat-Flash-Chat" { t.Errorf("model=%v", body["model"]) @@ -117,7 +118,7 @@ func TestLongCatChatHappyPath(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("LongCat-Flash-Chat", + resp, err := m.ChatWithMessages(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -135,6 +136,7 @@ func TestLongCatChatHappyPath(t *testing.T) { } func TestLongCatChatExtractsReasoningContent(t *testing.T) { + ctx := t.Context() // LongCat-Flash-Thinking returns the chain-of-thought in // message.reasoning_content (OpenAI o-series shape). Live-probed // against api.longcat.chat; the fixture mimics the actual response @@ -157,7 +159,7 @@ func TestLongCatChatExtractsReasoningContent(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("LongCat-Flash-Thinking", + resp, err := m.ChatWithMessages(ctx, "LongCat-Flash-Thinking", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -180,6 +182,7 @@ func TestLongCatChatExtractsReasoningContent(t *testing.T) { // top_p — anything else is undocumented and must not be sent, since // the maintainer specifically flagged this on PR #14809. func TestLongCatChatDropsUndocumentedFields(t *testing.T) { + ctx := t.Context() srv := newLongCatServer(t, "/openai/v1/chat/completions", func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { for _, k := range []string{"stop", "reasoning_effort", "response_format", "tools", "tool_choice", "presence_penalty", "frequency_penalty", "n", "logprobs"} { if _, present := body[k]; present { @@ -207,7 +210,7 @@ func TestLongCatChatDropsUndocumentedFields(t *testing.T) { topP := 0.9 stop := []string{"END"} effort := "high" - _, err := m.ChatWithMessages("LongCat-Flash-Chat", + _, err := m.ChatWithMessages(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, // Deliberately pass Stop/Effort to prove they are filtered out. @@ -220,8 +223,9 @@ func TestLongCatChatDropsUndocumentedFields(t *testing.T) { } func TestLongCatChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") - _, err := m.ChatWithMessages("LongCat-Flash-Chat", + _, err := m.ChatWithMessages(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, ) @@ -231,15 +235,17 @@ func TestLongCatChatRequiresAPIKey(t *testing.T) { } func TestLongCatChatRequiresMessages(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("LongCat-Flash-Chat", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := m.ChatWithMessages(ctx, "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) } } func TestLongCatChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newLongCatServer(t, "/openai/v1/chat/completions", func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) @@ -248,7 +254,7 @@ func TestLongCatChatRejectsHTTPError(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" - _, err := m.ChatWithMessages("LongCat-Flash-Chat", + _, err := m.ChatWithMessages(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { @@ -257,6 +263,7 @@ func TestLongCatChatRejectsHTTPError(t *testing.T) { } func TestLongCatStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newLongCatSSEServer(t, "/openai/v1/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant"}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"content":"Hello"}}]}`+"\n"+ @@ -269,7 +276,7 @@ func TestLongCatStreamHappyPath(t *testing.T) { apiKey := "test-key" var chunks []string var sawDone bool - err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { @@ -295,6 +302,7 @@ func TestLongCatStreamHappyPath(t *testing.T) { } func TestLongCatStreamExtractsReasoningContent(t *testing.T) { + ctx := t.Context() // Fixture matches the shape captured live from // LongCat-Flash-Thinking against api.longcat.chat: deltas // interleave reasoning_content and content within the stream. @@ -310,7 +318,7 @@ func TestLongCatStreamExtractsReasoningContent(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" var content, reasoning []string - err := m.ChatStreamlyWithSender("LongCat-Flash-Thinking", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Thinking", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { @@ -337,10 +345,11 @@ func TestLongCatStreamExtractsReasoningContent(t *testing.T) { } func TestLongCatStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") apiKey := "test-key" stream := false - err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, @@ -352,9 +361,10 @@ func TestLongCatStreamRejectsExplicitFalse(t *testing.T) { } func TestLongCatStreamRequiresSender(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") apiKey := "test-key" - err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { @@ -363,6 +373,7 @@ func TestLongCatStreamRequiresSender(t *testing.T) { } func TestLongCatStreamFailsWithoutTerminal(t *testing.T) { + ctx := t.Context() srv := newLongCatSSEServer(t, "/openai/v1/chat/completions", `data: {"choices":[{"delta":{"content":"half"}}]}`+"\n", ) @@ -370,7 +381,7 @@ func TestLongCatStreamFailsWithoutTerminal(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" - err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) @@ -383,6 +394,7 @@ func TestLongCatStreamFailsWithoutTerminal(t *testing.T) { // which masked truncated or corrupted streams. The driver must now // fail hard with a "longcat: invalid SSE event" wrapper. func TestLongCatStreamRejectsMalformedFrame(t *testing.T) { + ctx := t.Context() srv := newLongCatSSEServer(t, "/openai/v1/chat/completions", `data: {"choices":[{"delta":{"content":"ok"}}]}`+"\n"+ `data: {this is not valid json}`+"\n", @@ -391,7 +403,7 @@ func TestLongCatStreamRejectsMalformedFrame(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" - err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) @@ -404,6 +416,7 @@ func TestLongCatStreamRejectsMalformedFrame(t *testing.T) { // the "no choices" continue and leave the caller with a generic // truncation error. The driver must surface the upstream error verbatim. func TestLongCatStreamSurfacesUpstreamError(t *testing.T) { + ctx := t.Context() srv := newLongCatSSEServer(t, "/openai/v1/chat/completions", `data: {"choices":[{"delta":{"content":"partial "}}]}`+"\n"+ `data: {"error":{"message":"rate limit exceeded","type":"rate_limit_error"}}`+"\n", @@ -412,7 +425,7 @@ func TestLongCatStreamSurfacesUpstreamError(t *testing.T) { m := newLongCatForTest(srv.URL) apiKey := "test-key" - err := m.ChatStreamlyWithSender("LongCat-Flash-Chat", + err := m.ChatStreamlyWithSender(ctx, "LongCat-Flash-Chat", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) @@ -425,6 +438,7 @@ func TestLongCatStreamSurfacesUpstreamError(t *testing.T) { } func TestLongCatListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() var requests int srv := newLongCatServer(t, "/openai/v1/models", func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { requests++ @@ -463,19 +477,20 @@ func TestLongCatListModelsAndCheckConnection(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newLongCatForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newLongCatForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if got := joinModelNames(models, ","); got != "LongCat-Flash-Chat,LongCat-Flash-Thinking-2601" { t.Errorf("models=%q", got) } - if err := newLongCatForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newLongCatForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestLongCatListModelsRejectsInvalidResponses(t *testing.T) { + ctx := t.Context() for name, response := range map[string]string{ "missing data": `{}`, "null data": `{"data":null}`, @@ -488,7 +503,7 @@ func TestLongCatListModelsRejectsInvalidResponses(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newLongCatForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := newLongCatForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil { t.Fatalf("expected error") } @@ -497,17 +512,18 @@ func TestLongCatListModelsRejectsInvalidResponses(t *testing.T) { } func TestLongCatListModelsRequiresAPIKey(t *testing.T) { + ctx := t.Context() for name, cfg := range map[string]*APIConfig{ "nil config": nil, "nil key": {}, "empty key": {ApiKey: new(string)}, } { t.Run(name, func(t *testing.T) { - _, err := newLongCatForTest("http://unused").ListModels(cfg) + _, err := newLongCatForTest("http://unused").ListModels(ctx, cfg) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } - err = newLongCatForTest("http://unused").CheckConnection(cfg) + err = newLongCatForTest("http://unused").CheckConnection(ctx, cfg) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("CheckConnection expected api-key error, got %v", err) } @@ -516,41 +532,45 @@ func TestLongCatListModelsRequiresAPIKey(t *testing.T) { } func TestLongCatEmbedReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") model := "x" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) + _, err := m.Embed(ctx, &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) } } func TestLongCatRerankReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") model := "x" - _, err := m.Rerank(&model, "q", []string{"a"}, &APIConfig{}, &RerankConfig{TopN: 1}, nil) + _, err := m.Rerank(ctx, &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) } } func TestLongCatBalanceReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") - _, err := m.Balance(&APIConfig{}) + _, err := m.Balance(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: want 'no such method', got %v", err) } } func TestLongCatAudioOCRReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newLongCatForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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 bafe664e77..d4c39a2583 100644 --- a/internal/entity/models/mineru.go +++ b/internal/entity/models/mineru.go @@ -48,51 +48,51 @@ func (m *MinerUModel) Name() string { return "mineru.net" } -func (m *MinerUModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (m *MinerUModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinerUModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (m *MinerUModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (m *MinerUModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (m *MinerUModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinerUModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (m *MinerUModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinerUModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (m *MinerUModel) OCRFile(ctx context.Context, 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 *MinerUModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (m *MinerUModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (m *MinerUModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerUModel) CheckConnection(apiConfig *APIConfig) error { +func (m *MinerUModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("%s no such method", m.Name()) } @@ -105,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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (m *MinerUModel) ParseFile(ctx context.Context, 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 } @@ -137,7 +137,7 @@ func (m *MinerUModel) ParseFile(modelName *string, content []byte, documentURL * return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData)) @@ -177,7 +177,7 @@ func (m *MinerUModel) ParseFile(modelName *string, content []byte, documentURL * }, nil } -func (m *MinerUModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (m *MinerUModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } @@ -196,7 +196,7 @@ type mineruTaskQueryResponse struct { Msg string `json:"msg"` } -func (m *MinerUModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (m *MinerUModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -208,7 +208,7 @@ func (m *MinerUModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskRespon } apiURL := fmt.Sprintf("%s/api/%s/%s", resolvedBaseURL, m.baseModel.URLSuffix.DocumentParse, taskID) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) diff --git a/internal/entity/models/mineru_local.go b/internal/entity/models/mineru_local.go index 125fce4ebe..f478c7eeb8 100644 --- a/internal/entity/models/mineru_local.go +++ b/internal/entity/models/mineru_local.go @@ -50,55 +50,55 @@ func (m *MinerULocalModel) Name() string { return "mineru" } -func (m *MinerULocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (m *MinerULocalModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinerULocalModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (m *MinerULocalModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (m *MinerULocalModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (m *MinerULocalModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinerULocalModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (m *MinerULocalModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinerULocalModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (m *MinerULocalModel) OCRFile(ctx context.Context, 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 *MinerULocalModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (m *MinerULocalModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (m *MinerULocalModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) CheckConnection(apiConfig *APIConfig) error { +func (m *MinerULocalModel) CheckConnection(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (m *MinerULocalModel) ParseFile(ctx context.Context, 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 } @@ -135,7 +135,7 @@ func (m *MinerULocalModel) ParseFile(modelName *string, content []byte, document return nil, fmt.Errorf("failed to close multipart writer: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", apiURL, &body) @@ -187,11 +187,11 @@ func (m *MinerULocalModel) ParseFile(modelName *string, content []byte, document }, nil } -func (m *MinerULocalModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (m *MinerULocalModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", m.Name()) } -func (m *MinerULocalModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (m *MinerULocalModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -205,7 +205,7 @@ func (m *MinerULocalModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskR return nil, err } url := fmt.Sprintf("%s/%s/%s/result", resolvedBaseURL, m.baseModel.URLSuffix.Task, taskID) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go index e2f8db1e80..052b0812e5 100644 --- a/internal/entity/models/minimax.go +++ b/internal/entity/models/minimax.go @@ -60,7 +60,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (m *MinimaxModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -141,7 +141,7 @@ func (m *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -224,7 +224,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinimaxModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -312,7 +312,7 @@ func (m *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -392,11 +392,11 @@ 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (m *MinimaxModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } -func (m *MinimaxModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (m *MinimaxModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -408,7 +408,7 @@ func (m *MinimaxModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er } url := fmt.Sprintf("%s/%s", resolvedBaseURL, m.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -449,31 +449,31 @@ func (m *MinimaxModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er return models, nil } -func (m *MinimaxModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (m *MinimaxModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MinimaxModel) CheckConnection(apiConfig *APIConfig) error { - _, err := m.ListModels(apiConfig) +func (m *MinimaxModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := m.ListModels(ctx, apiConfig) return err } // Rerank calculates similarity scores between query and documents -func (m *MinimaxModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (m *MinimaxModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (m *MinimaxModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinimaxModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (m *MinimaxModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -508,7 +508,7 @@ func (m *MinimaxModel) AudioSpeech(modelName *string, audioContent *string, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -563,7 +563,7 @@ func (m *MinimaxModel) AudioSpeech(modelName *string, audioContent *string, apiC }, nil } -func (m *MinimaxModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MinimaxModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -609,7 +609,7 @@ func (m *MinimaxModel) AudioSpeechWithSender(modelName *string, audioContent *st return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -658,19 +658,19 @@ 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (m *MinimaxModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (m *MinimaxModel) ParseFile(ctx context.Context, 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()) } -func (m *MinimaxModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (m *MinimaxModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MinimaxModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (m *MinimaxModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 df24d993f0..fc922d2d39 100644 --- a/internal/entity/models/minimax_test.go +++ b/internal/entity/models/minimax_test.go @@ -85,6 +85,7 @@ func TestMinimaxNewInstancePreservesConfig(t *testing.T) { } func TestMinimaxChatForcesNonStreaming(t *testing.T) { + ctx := t.Context() srv := newMinimaxServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -113,6 +114,7 @@ func TestMinimaxChatForcesNonStreaming(t *testing.T) { stream := true thinking := true resp, err := newMinimaxForTest(srv.URL).ChatWithMessages( + ctx, " MiniMax-M3 ", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -131,6 +133,7 @@ func TestMinimaxChatForcesNonStreaming(t *testing.T) { } func TestMinimaxChatRejectsEmptyChoices(t *testing.T) { + ctx := t.Context() srv := newMinimaxServer(t, func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"choices": []map[string]interface{}{}}) }) @@ -138,6 +141,7 @@ func TestMinimaxChatRejectsEmptyChoices(t *testing.T) { apiKey := "test-key" _, err := newMinimaxForTest(srv.URL).ChatWithMessages( + ctx, "MiniMax-M3", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -150,6 +154,7 @@ func TestMinimaxChatRejectsEmptyChoices(t *testing.T) { } func TestMinimaxStreamForcesStreaming(t *testing.T) { + ctx := t.Context() srv := newMinimaxServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -177,6 +182,7 @@ func TestMinimaxStreamForcesStreaming(t *testing.T) { stream := false var content, reasoning []string err := newMinimaxForTest(srv.URL).ChatStreamlyWithSender( + ctx, "MiniMax-M3", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -204,6 +210,7 @@ func TestMinimaxStreamForcesStreaming(t *testing.T) { } func TestMinimaxStreamAcceptsNilConfig(t *testing.T) { + ctx := t.Context() srv := newMinimaxServer(t, func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { if body["stream"] != true { t.Errorf("stream=%v, want true", body["stream"]) @@ -215,6 +222,7 @@ func TestMinimaxStreamAcceptsNilConfig(t *testing.T) { apiKey := "test-key" err := newMinimaxForTest(srv.URL).ChatStreamlyWithSender( + ctx, "MiniMax-M3", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -228,6 +236,7 @@ func TestMinimaxStreamAcceptsNilConfig(t *testing.T) { } func TestMinimaxListModelsUsesBodylessGet(t *testing.T) { + ctx := t.Context() srv := newMinimaxServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -245,7 +254,7 @@ func TestMinimaxListModelsUsesBodylessGet(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newMinimaxForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newMinimaxForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -255,6 +264,7 @@ func TestMinimaxListModelsUsesBodylessGet(t *testing.T) { } func TestMinimaxListModelsRejectsMalformedResponse(t *testing.T) { + ctx := t.Context() apiKey := "test-key" for name, response := range map[string]interface{}{ "missing data": map[string]interface{}{"object": "list"}, @@ -266,7 +276,7 @@ func TestMinimaxListModelsRejectsMalformedResponse(t *testing.T) { }) defer srv.Close() - if _, err := newMinimaxForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}); err == nil { + if _, err := newMinimaxForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}); err == nil { t.Fatal("expected malformed response error") } }) @@ -274,6 +284,7 @@ func TestMinimaxListModelsRejectsMalformedResponse(t *testing.T) { } func TestMinimaxCheckConnectionUsesListModels(t *testing.T) { + ctx := t.Context() srv := newMinimaxServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -288,12 +299,13 @@ func TestMinimaxCheckConnectionUsesListModels(t *testing.T) { defer srv.Close() apiKey := "test-key" - if err := newMinimaxForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newMinimaxForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestMinimaxValidatesInputs(t *testing.T) { + ctx := t.Context() apiKey := "test-key" emptyKey := " " send := func(*string, *string) error { return nil } @@ -306,7 +318,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, nil) + _, err := newMinimaxForTest("http://unused").ChatWithMessages(ctx, "MiniMax-M3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -314,7 +326,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, nil) + _, err := newMinimaxForTest("http://unused").ChatWithMessages(ctx, " ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -322,28 +334,28 @@ 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, nil, send) + return newMinimaxForTest("http://unused").ChatStreamlyWithSender(ctx, "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, nil, send) + return newMinimaxForTest("http://unused").ChatStreamlyWithSender(ctx, " ", []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, nil) + return newMinimaxForTest("http://unused").ChatStreamlyWithSender(ctx, "MiniMax-M3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) }, want: "sender is required", }, { name: "models api key", run: func() error { - _, err := newMinimaxForTest("http://unused").ListModels(&APIConfig{}) + _, err := newMinimaxForTest("http://unused").ListModels(ctx, &APIConfig{}) return err }, want: "api key is required", diff --git a/internal/entity/models/mistral.go b/internal/entity/models/mistral.go index d9197a3723..88eadbabf6 100644 --- a/internal/entity/models/mistral.go +++ b/internal/entity/models/mistral.go @@ -55,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (m *MistralModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -108,7 +108,7 @@ func (m *MistralModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -208,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MistralModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -266,7 +266,7 @@ func (m *MistralModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -343,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (m *MistralModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -376,7 +376,7 @@ func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -432,7 +432,7 @@ func (m *MistralModel) Embed(modelName *string, texts []string, apiConfig *APICo } // ListModels returns the list of model ids visible to the API key. -func (m *MistralModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (m *MistralModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -444,7 +444,7 @@ func (m *MistralModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -482,13 +482,13 @@ func (m *MistralModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er } // Balance is not exposed by the Mistral API, so this returns "no such method". -func (m *MistralModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (m *MistralModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } // CheckConnection runs a lightweight ListModels call to verify the API key. -func (m *MistralModel) CheckConnection(apiConfig *APIConfig) error { - _, err := m.ListModels(apiConfig) +func (m *MistralModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := m.ListModels(ctx, apiConfig) if err != nil { return err } @@ -496,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (m *MistralModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (m *MistralModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MistralModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (m *MistralModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MistralModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (m *MistralModel) OCRFile(ctx context.Context, 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 } @@ -556,7 +556,7 @@ func (m *MistralModel) OCRFile(modelName *string, content []byte, urls *string, return nil, fmt.Errorf("failed to marshal json payload: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + ctx, cancel := context.WithTimeout(ctx, 120*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -606,14 +606,14 @@ 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (m *MistralModel) ParseFile(ctx context.Context, 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()) } -func (m *MistralModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (m *MistralModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MistralModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (m *MistralModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 9d15590b4c..38759ec581 100644 --- a/internal/entity/models/mistral_test.go +++ b/internal/entity/models/mistral_test.go @@ -65,6 +65,7 @@ func TestMistralName(t *testing.T) { } func TestMistralChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "mistral-large-latest" { t.Errorf("expected model=mistral-large-latest, got %v", body["model"]) @@ -87,7 +88,7 @@ func TestMistralChatHappyPath(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("mistral-large-latest", []Message{ + resp, err := m.ChatWithMessages(ctx, "mistral-large-latest", []Message{ {Role: "user", Content: "ping"}, }, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -102,6 +103,7 @@ func TestMistralChatHappyPath(t *testing.T) { } func TestMistralChatPropagatesConfig(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["max_tokens"] != float64(64) { t.Errorf("max_tokens=%v want 64", body["max_tokens"]) @@ -128,7 +130,7 @@ func TestMistralChatPropagatesConfig(t *testing.T) { temp := 0.3 topP := 0.9 stop := []string{"END"} - _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "ping"}}, + _, err := m.ChatWithMessages(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop}, nil, @@ -139,28 +141,31 @@ func TestMistralChatPropagatesConfig(t *testing.T) { } func TestMistralChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") - _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) + _, err := m.ChatWithMessages(ctx, "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, nil) + _, err = m.ChatWithMessages(ctx, "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) } } func TestMistralChatRequiresMessages(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") apiKey := "test-key" - _, err := m.ChatWithMessages("mistral-large-latest", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := m.ChatWithMessages(ctx, "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) } } func TestMistralChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) @@ -169,13 +174,14 @@ 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, nil) + _, err := m.ChatWithMessages(ctx, "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) } } func TestMistralChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { + ctx := t.Context() // Empty *Region pointer must fall back to the "default" entry, not // be treated as an explicit "" region (which would miss the lookup). srv := newMistralServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { @@ -188,7 +194,7 @@ func TestMistralChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" emptyRegion := "" - _, err := m.ChatWithMessages("mistral-large-latest", + _, err := m.ChatWithMessages(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}, nil, nil, ) @@ -198,6 +204,7 @@ func TestMistralChatFallsBackToDefaultOnEmptyRegion(t *testing.T) { } func TestMistralListModelsFallsBackToDefaultOnEmptyRegion(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"data": []map[string]interface{}{{"id": "x"}}}) }) @@ -206,15 +213,16 @@ func TestMistralListModelsFallsBackToDefaultOnEmptyRegion(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" emptyRegion := "" - if _, err := m.ListModels(&APIConfig{ApiKey: &apiKey, Region: &emptyRegion}); err != nil { + if _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey, Region: &emptyRegion}); err != nil { t.Errorf("empty Region: expected fallback to default, got %v", err) } } func TestMistralStreamRequiresSender(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") apiKey := "test-key" - err := m.ChatStreamlyWithSender("mistral-large-latest", + err := m.ChatStreamlyWithSender(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { @@ -223,10 +231,11 @@ func TestMistralStreamRequiresSender(t *testing.T) { } func TestMistralChatRejectsUnknownRegion(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") apiKey := "test-key" region := "eu" - _, err := m.ChatWithMessages("mistral-large-latest", []Message{{Role: "user", Content: "x"}}, + _, err := m.ChatWithMessages(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey, Region: ®ion}, nil, nil, ) if err == nil || !strings.Contains(err.Error(), "no base URL configured for region") { @@ -235,6 +244,7 @@ func TestMistralChatRejectsUnknownRegion(t *testing.T) { } func TestMistralStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -262,7 +272,7 @@ func TestMistralStreamHappyPath(t *testing.T) { apiKey := "test-key" var chunks []string var sawDone int32 - err := m.ChatStreamlyWithSender("mistral-large-latest", + err := m.ChatStreamlyWithSender(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, _ *string) error { @@ -289,10 +299,11 @@ func TestMistralStreamHappyPath(t *testing.T) { } func TestMistralStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") apiKey := "test-key" stream := false - err := m.ChatStreamlyWithSender("mistral-large-latest", + err := m.ChatStreamlyWithSender(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, @@ -305,6 +316,7 @@ func TestMistralStreamRejectsExplicitFalse(t *testing.T) { } func TestMistralStreamFailsWithoutTerminal(t *testing.T) { + ctx := t.Context() // Body closes before [DONE] or a finish_reason -> driver must complain // instead of pretending the stream finished cleanly. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -315,7 +327,7 @@ func TestMistralStreamFailsWithoutTerminal(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - err := m.ChatStreamlyWithSender("mistral-large-latest", + err := m.ChatStreamlyWithSender(ctx, "mistral-large-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }, @@ -326,6 +338,7 @@ func TestMistralStreamFailsWithoutTerminal(t *testing.T) { } func TestMistralListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -339,7 +352,7 @@ func TestMistralListModelsHappyPath(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - ids, err := m.ListModels(&APIConfig{ApiKey: &apiKey}) + ids, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -349,13 +362,15 @@ func TestMistralListModelsHappyPath(t *testing.T) { } func TestMistralListModelsRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") - if _, err := m.ListModels(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := m.ListModels(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } } func TestMistralCheckConnectionDelegatesToListModels(t *testing.T) { + ctx := t.Context() // 200 -> CheckConnection succeeds; 401 -> CheckConnection propagates. okSrv := newMistralServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"data": []map[string]interface{}{{"id": "x"}}}) @@ -368,33 +383,36 @@ func TestMistralCheckConnectionDelegatesToListModels(t *testing.T) { apiKey := "test-key" mOK := newMistralForTest(okSrv.URL) - if err := mOK.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := mOK.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection(ok): %v", err) } mFail := newMistralForTest(failSrv.URL) - if err := mFail.CheckConnection(&APIConfig{ApiKey: &apiKey}); err == nil { + if err := mFail.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err == nil { t.Error("CheckConnection(fail): expected error, got nil") } } func TestMistralBalanceReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") - _, err := m.Balance(&APIConfig{}) + _, err := m.Balance(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: expected 'no such method', got %v", err) } } func TestMistralRerankReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") q := "mistral-large-latest" - _, err := m.Rerank(&q, "what is rag?", []string{"a", "b"}, &APIConfig{}, &RerankConfig{TopN: 2}, nil) + _, err := m.Rerank(ctx, &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) } } func TestMistralUnsupportedDefaultsReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") modelName := "mistral-large-latest" @@ -403,29 +421,29 @@ func TestMistralUnsupportedDefaultsReturnNoSuchMethod(t *testing.T) { call func() error }{ {"TranscribeAudio", func() error { - _, err := m.TranscribeAudio(&modelName, nil, &APIConfig{}, nil, nil) + _, err := m.TranscribeAudio(ctx, &modelName, nil, &APIConfig{}, nil, nil) return err }}, {"TranscribeAudioWithSender", func() error { - return m.TranscribeAudioWithSender(&modelName, nil, &APIConfig{}, nil, nil, nil) + return m.TranscribeAudioWithSender(ctx, &modelName, nil, &APIConfig{}, nil, nil, nil) }}, {"AudioSpeech", func() error { - _, err := m.AudioSpeech(&modelName, nil, &APIConfig{}, nil, nil) + _, err := m.AudioSpeech(ctx, &modelName, nil, &APIConfig{}, nil, nil) return err }}, {"AudioSpeechWithSender", func() error { - return m.AudioSpeechWithSender(&modelName, nil, &APIConfig{}, nil, nil, nil) + return m.AudioSpeechWithSender(ctx, &modelName, nil, &APIConfig{}, nil, nil, nil) }}, {"ParseFile", func() error { - _, err := m.ParseFile(&modelName, nil, nil, &APIConfig{}, nil, nil) + _, err := m.ParseFile(ctx, &modelName, nil, nil, &APIConfig{}, nil, nil) return err }}, {"ListTasks", func() error { - _, err := m.ListTasks(&APIConfig{}) + _, err := m.ListTasks(ctx, &APIConfig{}) return err }}, {"ShowTask", func() error { - _, err := m.ShowTask("task-id", &APIConfig{}) + _, err := m.ShowTask(ctx, "task-id", &APIConfig{}) return err }}, } @@ -438,6 +456,7 @@ func TestMistralUnsupportedDefaultsReturnNoSuchMethod(t *testing.T) { } func TestMistralEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "mistral-embed" { t.Errorf("model=%v want mistral-embed", body["model"]) @@ -459,7 +478,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, nil) + vecs, err := m.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -472,6 +491,7 @@ func TestMistralEmbedHappyPath(t *testing.T) { } func TestMistralEmbedReordersByIndex(t *testing.T) { + ctx := t.Context() // Upstream returns the three vectors in shuffled order. The driver // must reorder them so the slot at position i corresponds to input i. srv := newMistralServer(t, "/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { @@ -488,7 +508,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, nil) + vecs, err := m.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -500,6 +520,7 @@ func TestMistralEmbedReordersByIndex(t *testing.T) { } func TestMistralEmbedEmptyInputShortCircuits(t *testing.T) { + ctx := t.Context() // Empty input must NOT make an HTTP call; the test fails the request // rather than the assertion if it does. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -511,7 +532,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, nil) + vecs, err := m.Embed(ctx, &model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed([]): %v", err) } @@ -521,29 +542,32 @@ func TestMistralEmbedEmptyInputShortCircuits(t *testing.T) { } func TestMistralEmbedRequiresAPIKey(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") model := "mistral-embed" - _, err := m.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) + _, err := m.Embed(ctx, &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) } } func TestMistralEmbedRequiresModelName(t *testing.T) { + ctx := t.Context() m := newMistralForTest("http://unused") apiKey := "test-key" - _, err := m.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := m.Embed(ctx, 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, nil) + _, err = m.Embed(ctx, &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) } } func TestMistralEmbedRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() // A malformed upstream that repeats data[*].index would silently // overwrite the earlier vector; the driver must fail loudly instead. srv := newMistralServer(t, "/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { @@ -559,13 +583,14 @@ 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, nil) + _, err := m.Embed(ctx, &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) } } func TestMistralEmbedRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -578,13 +603,14 @@ 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, nil) + _, err := m.Embed(ctx, &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) } } func TestMistralEmbedRejectsMissingSlot(t *testing.T) { + ctx := t.Context() // Upstream returns only one of the two requested embeddings. srv := newMistralServer(t, "/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ @@ -598,13 +624,14 @@ 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, nil) + _, err := m.Embed(ctx, &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) } } func TestMistralEmbedRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) @@ -614,7 +641,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, nil) + _, err := m.Embed(ctx, &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) } @@ -625,6 +652,7 @@ func TestMistralEmbedRejectsHTTPError(t *testing.T) { // Regression net: the existing string-content path stays green for every // non-reasoning Mistral model. func TestMistralChatHandlesStringContent(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{"message": map[string]interface{}{ @@ -637,7 +665,7 @@ func TestMistralChatHandlesStringContent(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("ministral-3b-latest", + resp, err := m.ChatWithMessages(ctx, "ministral-3b-latest", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -659,6 +687,7 @@ func TestMistralChatHandlesStringContent(t *testing.T) { // from api.mistral.ai/v1/chat/completions against magistral-medium-latest // with the prompt "When do two trains meet?". func TestMistralChatExtractsReasoningFromStructuredContent(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{"message": map[string]interface{}{ @@ -681,7 +710,7 @@ func TestMistralChatExtractsReasoningFromStructuredContent(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("magistral-medium-latest", + resp, err := m.ChatWithMessages(ctx, "magistral-medium-latest", []Message{{Role: "user", Content: "When do they meet?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -700,6 +729,7 @@ func TestMistralChatExtractsReasoningFromStructuredContent(t *testing.T) { // magistral with a trivial answer that needed no reasoning returns the // structured shape with only a `text` part. ReasonContent must be empty. func TestMistralChatHandlesStructuredContentWithoutThinking(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{"message": map[string]interface{}{ @@ -714,7 +744,7 @@ func TestMistralChatHandlesStructuredContentWithoutThinking(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("magistral-small-latest", + resp, err := m.ChatWithMessages(ctx, "magistral-small-latest", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -732,6 +762,7 @@ func TestMistralChatHandlesStructuredContentWithoutThinking(t *testing.T) { // the parser forward-compatible with new Mistral content variants // (audio chunks, citations, etc.) that ragflow doesn't surface yet. func TestMistralChatIgnoresUnknownContentPartTypes(t *testing.T) { + ctx := t.Context() srv := newMistralServer(t, "/chat/completions", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{"message": map[string]interface{}{ @@ -748,7 +779,7 @@ func TestMistralChatIgnoresUnknownContentPartTypes(t *testing.T) { m := newMistralForTest(srv.URL) apiKey := "test-key" - resp, err := m.ChatWithMessages("magistral-small-latest", + resp, err := m.ChatWithMessages(ctx, "magistral-small-latest", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { diff --git a/internal/entity/models/modelscope.go b/internal/entity/models/modelscope.go index fc9bd6c99c..3a3c533ad4 100644 --- a/internal/entity/models/modelscope.go +++ b/internal/entity/models/modelscope.go @@ -141,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (m *ModelScopeModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -163,7 +163,7 @@ func (m *ModelScopeModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -211,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *ModelScopeModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -331,41 +331,41 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(modelName string, messages []Me return sender(&endOfStream, nil) } -func (m *ModelScopeModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (m *ModelScopeModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (m *ModelScopeModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (m *ModelScopeModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *ModelScopeModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (m *ModelScopeModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *ModelScopeModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (m *ModelScopeModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (m *ModelScopeModel) ParseFile(ctx context.Context, 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()) } // ListModels returns the model IDs exposed by ModelScope's OpenAI-compatible // /v1/models endpoint. -func (m *ModelScopeModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (m *ModelScopeModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -377,7 +377,7 @@ func (m *ModelScopeModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = normalizeModelScopeBaseURL(baseURL) url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -412,19 +412,19 @@ func (m *ModelScopeModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(ModelList{Models: result.Data}), nil } -func (m *ModelScopeModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (m *ModelScopeModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) CheckConnection(apiConfig *APIConfig) error { - _, err := m.ListModels(apiConfig) +func (m *ModelScopeModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := m.ListModels(ctx, apiConfig) return err } -func (m *ModelScopeModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (m *ModelScopeModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *ModelScopeModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (m *ModelScopeModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 1be310552d..704909b6f1 100644 --- a/internal/entity/models/modelscope_test.go +++ b/internal/entity/models/modelscope_test.go @@ -94,6 +94,7 @@ func TestModelScopeNewModelWithCustomDefaultTransport(t *testing.T) { } func TestModelScopeChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/chat/completions" { @@ -120,7 +121,7 @@ func TestModelScopeChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) m := newModelScopeForTest(srv.URL) maxTokens := 32 temp := 0.2 - resp, err := m.ChatWithMessages("Qwen/Qwen2.5-7B-Instruct", + resp, err := m.ChatWithMessages(ctx, "Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "ping"}}, &APIConfig{}, &ChatConfig{MaxTokens: &maxTokens, Temperature: &temp}, nil) @@ -145,6 +146,7 @@ func TestModelScopeChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) } func TestModelScopeChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "Bearer ms-test" { t.Errorf("Authorization=%q, want Bearer ms-test", got) @@ -155,7 +157,7 @@ func TestModelScopeChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { m := newModelScopeForTest(srv.URL + "/v1") key := "ms-test" - _, err := m.ChatWithMessages("Qwen/Qwen2.5-7B-Instruct", + _, err := m.ChatWithMessages(ctx, "Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil) if err != nil { @@ -164,6 +166,7 @@ func TestModelScopeChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { } func TestModelScopeChatExtractsReasoningFields(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"choices":[{"message":{ "content":"12", @@ -173,7 +176,7 @@ func TestModelScopeChatExtractsReasoningFields(t *testing.T) { defer srv.Close() m := newModelScopeForTest(srv.URL) - resp, err := m.ChatWithMessages("Qwen/Qwen3-8B", + resp, err := m.ChatWithMessages(ctx, "Qwen/Qwen3-8B", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{}, nil, nil) if err != nil { @@ -185,6 +188,7 @@ func TestModelScopeChatExtractsReasoningFields(t *testing.T) { } func TestModelScopeStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -218,7 +222,7 @@ func TestModelScopeStreamHappyPath(t *testing.T) { var content []string var reasoning []string var sawDone bool - err := m.ChatStreamlyWithSender("Qwen/Qwen2.5-7B-Instruct", + err := m.ChatStreamlyWithSender(ctx, "Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "hi"}}, &APIConfig{}, nil, nil, func(c *string, r *string) error { @@ -248,9 +252,10 @@ func TestModelScopeStreamHappyPath(t *testing.T) { } func TestModelScopeStreamRejectsFalseStreamConfig(t *testing.T) { + ctx := t.Context() m := newModelScopeForTest("http://unused") stream := false - err := m.ChatStreamlyWithSender("Qwen/Qwen2.5-7B-Instruct", + err := m.ChatStreamlyWithSender(ctx, "Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Stream: &stream}, @@ -262,6 +267,7 @@ func TestModelScopeStreamRejectsFalseStreamConfig(t *testing.T) { } func TestModelScopeStreamCancelsOnIdle(t *testing.T) { + ctx := t.Context() withModelScopeIdleTimeout(t, 200*time.Millisecond) hold := make(chan struct{}) @@ -281,7 +287,7 @@ func TestModelScopeStreamCancelsOnIdle(t *testing.T) { t.Cleanup(func() { close(hold) }) m := newModelScopeForTest(srv.URL) - err := m.ChatStreamlyWithSender("Qwen/Qwen2.5-7B-Instruct", + err := m.ChatStreamlyWithSender(ctx, "Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, func(*string, *string) error { return nil }) @@ -291,6 +297,7 @@ func TestModelScopeStreamCancelsOnIdle(t *testing.T) { } func TestModelScopeListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/models" { t.Errorf("path=%s, want /v1/models", r.URL.Path) @@ -305,21 +312,22 @@ func TestModelScopeListModelsAndCheckConnection(t *testing.T) { m := newModelScopeForTest(srv.URL) key := "ms-test" apiConfig := &APIConfig{ApiKey: &key} - models, err := m.ListModels(apiConfig) + models, err := m.ListModels(ctx, apiConfig) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "Qwen/Qwen2.5-7B-Instruct,Qwen/Qwen3-8B" { t.Errorf("models=%v", models) } - if err := m.CheckConnection(apiConfig); err != nil { + if err := m.CheckConnection(ctx, apiConfig); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestModelScopeMissingBaseURLFailsClearly(t *testing.T) { + ctx := t.Context() m := NewModelScopeModel(map[string]string{}, URLSuffix{Chat: "v1/chat/completions"}) - _, err := m.ChatWithMessages("Qwen/Qwen2.5-7B-Instruct", + _, err := m.ChatWithMessages(ctx, "Qwen/Qwen2.5-7B-Instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "base URL") { @@ -328,31 +336,32 @@ func TestModelScopeMissingBaseURLFailsClearly(t *testing.T) { } func TestModelScopeUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newModelScopeForTest("http://unused") model := "Qwen/Qwen2.5-7B-Instruct" - if _, err := m.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, &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") { + if _, err := m.Balance(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.TranscribeAudioWithSender(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.AudioSpeechWithSender(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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 3e789d1a0c..c6b7a1f22f 100644 --- a/internal/entity/models/moonshot.go +++ b/internal/entity/models/moonshot.go @@ -58,7 +58,7 @@ func validateMoonshotModelName(modelName string) (string, error) { return strings.TrimSpace(modelName), nil } -func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (m *MoonshotModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -143,7 +143,7 @@ func (m *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -228,7 +228,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MoonshotModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -320,7 +320,7 @@ func (m *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -407,11 +407,11 @@ 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (m *MoonshotModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } -func (m *MoonshotModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (m *MoonshotModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -423,7 +423,7 @@ func (m *MoonshotModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e } url := fmt.Sprintf("%s/%s", resolvedBaseURL, m.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -464,7 +464,7 @@ func (m *MoonshotModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return models, nil } -func (m *MoonshotModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (m *MoonshotModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := m.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -476,7 +476,7 @@ func (m *MoonshotModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e } url := fmt.Sprintf("%s/%s", baseURL, m.baseModel.URLSuffix.Balance) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -522,8 +522,8 @@ func (m *MoonshotModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e return response, nil } -func (m *MoonshotModel) CheckConnection(apiConfig *APIConfig) error { - _, err := m.ListModels(apiConfig) +func (m *MoonshotModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := m.ListModels(ctx, apiConfig) if err != nil { return err } @@ -531,42 +531,42 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (m *MoonshotModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (m *MoonshotModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MoonshotModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (m *MoonshotModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (m *MoonshotModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (m *MoonshotModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (m *MoonshotModel) ParseFile(ctx context.Context, 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()) } -func (m *MoonshotModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (m *MoonshotModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", m.Name()) } -func (m *MoonshotModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (m *MoonshotModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 cf48d520ca..bb6b563bc7 100644 --- a/internal/entity/models/moonshot_test.go +++ b/internal/entity/models/moonshot_test.go @@ -85,6 +85,7 @@ func TestMoonshotNewInstancePreservesConfig(t *testing.T) { } func TestMoonshotChatForcesNonStreaming(t *testing.T) { + ctx := t.Context() srv := newMoonshotServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -113,6 +114,7 @@ func TestMoonshotChatForcesNonStreaming(t *testing.T) { stream := true thinking := true resp, err := newMoonshotForTest(srv.URL).ChatWithMessages( + ctx, " kimi-k2.6 ", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -130,6 +132,7 @@ func TestMoonshotChatForcesNonStreaming(t *testing.T) { } func TestMoonshotChatSupportsTools(t *testing.T) { + ctx := t.Context() srv := newMoonshotServer(t, func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { tools, ok := body["tools"].([]interface{}) if !ok || len(tools) != 1 { @@ -164,6 +167,7 @@ func TestMoonshotChatSupportsTools(t *testing.T) { }, }} resp, err := newMoonshotForTest(srv.URL).ChatWithMessages( + ctx, "kimi-k2.6", []Message{{Role: "user", Content: "北京今天天气怎么样?"}}, &APIConfig{ApiKey: &apiKey}, @@ -181,6 +185,7 @@ func TestMoonshotChatSupportsTools(t *testing.T) { } func TestMoonshotStreamForcesStreaming(t *testing.T) { + ctx := t.Context() srv := newMoonshotServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -209,6 +214,7 @@ func TestMoonshotStreamForcesStreaming(t *testing.T) { var content, reasoning []string var sawDone bool err := newMoonshotForTest(srv.URL).ChatStreamlyWithSender( + ctx, "kimi-k2.6", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -243,6 +249,7 @@ func TestMoonshotStreamForcesStreaming(t *testing.T) { } func TestMoonshotStreamDoesNotSendDoneAfterScannerError(t *testing.T) { + ctx := t.Context() srv := newMoonshotServer(t, func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { if body["stream"] != true { t.Errorf("stream=%v, want true", body["stream"]) @@ -255,6 +262,7 @@ func TestMoonshotStreamDoesNotSendDoneAfterScannerError(t *testing.T) { apiKey := "test-key" var sawDone bool err := newMoonshotForTest(srv.URL).ChatStreamlyWithSender( + ctx, "kimi-k2.6", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -276,6 +284,7 @@ func TestMoonshotStreamDoesNotSendDoneAfterScannerError(t *testing.T) { } func TestMoonshotListModelsUsesBodylessGet(t *testing.T) { + ctx := t.Context() srv := newMoonshotServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -293,7 +302,7 @@ func TestMoonshotListModelsUsesBodylessGet(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newMoonshotForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newMoonshotForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -303,6 +312,7 @@ func TestMoonshotListModelsUsesBodylessGet(t *testing.T) { } func TestMoonshotBalanceUsesBodylessGet(t *testing.T) { + ctx := t.Context() srv := newMoonshotServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -323,7 +333,7 @@ func TestMoonshotBalanceUsesBodylessGet(t *testing.T) { defer srv.Close() apiKey := "test-key" - balance, err := newMoonshotForTest(srv.URL).Balance(&APIConfig{ApiKey: &apiKey}) + balance, err := newMoonshotForTest(srv.URL).Balance(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("Balance: %v", err) } @@ -336,6 +346,7 @@ func TestMoonshotBalanceUsesBodylessGet(t *testing.T) { } func TestMoonshotRejectsMalformedResponses(t *testing.T) { + ctx := t.Context() apiKey := "test-key" tests := []struct { name string @@ -346,7 +357,7 @@ func TestMoonshotRejectsMalformedResponses(t *testing.T) { name: "models missing data", response: map[string]interface{}{"object": "list"}, run: func(m *MoonshotModel) error { - _, err := m.ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) return err }, }, @@ -356,7 +367,7 @@ func TestMoonshotRejectsMalformedResponses(t *testing.T) { "data": []map[string]string{{"id": ""}}, }, run: func(m *MoonshotModel) error { - _, err := m.ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) return err }, }, @@ -366,7 +377,7 @@ func TestMoonshotRejectsMalformedResponses(t *testing.T) { "data": map[string]float64{"cash_balance": 3}, }, run: func(m *MoonshotModel) error { - _, err := m.Balance(&APIConfig{ApiKey: &apiKey}) + _, err := m.Balance(ctx, &APIConfig{ApiKey: &apiKey}) return err }, }, @@ -387,6 +398,7 @@ func TestMoonshotRejectsMalformedResponses(t *testing.T) { } func TestMoonshotValidatesInputs(t *testing.T) { + ctx := t.Context() apiKey := "test-key" emptyKey := " " send := func(*string, *string) error { return nil } @@ -399,7 +411,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, nil) + _, err := newMoonshotForTest("http://unused").ChatWithMessages(ctx, "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &emptyKey}, nil, nil) return err }, want: "api key is required", @@ -407,7 +419,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, nil) + _, err := newMoonshotForTest("http://unused").ChatWithMessages(ctx, " ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err }, want: "model name is required", @@ -415,28 +427,28 @@ 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, nil, send) + return newMoonshotForTest("http://unused").ChatStreamlyWithSender(ctx, "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, nil, send) + return newMoonshotForTest("http://unused").ChatStreamlyWithSender(ctx, " ", []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, nil) + return newMoonshotForTest("http://unused").ChatStreamlyWithSender(ctx, "kimi-k2.6", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) }, want: "sender is required", }, { name: "models api key", run: func() error { - _, err := newMoonshotForTest("http://unused").ListModels(&APIConfig{}) + _, err := newMoonshotForTest("http://unused").ListModels(ctx, &APIConfig{}) return err }, want: "api key is required", @@ -444,7 +456,7 @@ func TestMoonshotValidatesInputs(t *testing.T) { { name: "balance api key", run: func() error { - _, err := newMoonshotForTest("http://unused").Balance(nil) + _, err := newMoonshotForTest("http://unused").Balance(ctx, &APIConfig{}) return err }, want: "api key is required", diff --git a/internal/entity/models/n1n.go b/internal/entity/models/n1n.go index 7cc0f7e0a8..42c43dce05 100644 --- a/internal/entity/models/n1n.go +++ b/internal/entity/models/n1n.go @@ -159,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (n *N1NModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -178,7 +178,7 @@ func (n *N1NModel) ChatWithMessages(modelName string, messages []Message, apiCon reqBody := buildN1NChatRequest(modelName, messages, false, chatModelConfig) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newN1NJSONRequest(ctx, "POST", endpoint, reqBody, apiKey) @@ -224,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *N1NModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -251,7 +251,7 @@ func (n *N1NModel) ChatStreamlyWithSender(modelName string, messages []Message, reqBody := buildN1NChatRequest(modelName, messages, true, chatModelConfig) - req, err := newN1NJSONRequest(context.Background(), "POST", endpoint, reqBody, apiKey) + req, err := newN1NJSONRequest(ctx, "POST", endpoint, reqBody, apiKey) if err != nil { return err } @@ -322,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (n *N1NModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -348,7 +348,7 @@ func (n *N1NModel) Embed(modelName *string, texts []string, apiConfig *APIConfig reqBody.Dimensions = embeddingConfig.Dimension } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newN1NJSONRequest(ctx, "POST", endpoint, reqBody, apiKey) @@ -416,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (n *N1NModel) Rerank(ctx context.Context, 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 } @@ -443,7 +443,7 @@ func (n *N1NModel) Rerank(modelName *string, query string, documents []string, a reqBody.TopN = rerankConfig.TopN } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newN1NJSONRequest(ctx, "POST", endpoint, reqBody, apiKey) @@ -497,7 +497,7 @@ type n1nModelCatalogResponse struct { } // ListModels returns the live n1n.ai model catalog -func (n *N1NModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (n *N1NModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -508,7 +508,7 @@ func (n *N1NModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := newN1NJSONRequest(ctx, "GET", endpoint, nil, apiKey) @@ -539,48 +539,48 @@ func (n *N1NModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) } // CheckConnection verifies the API key -func (n *N1NModel) CheckConnection(apiConfig *APIConfig) error { - _, err := n.ListModels(apiConfig) +func (n *N1NModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := n.ListModels(ctx, apiConfig) return err } -func (n *N1NModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (n *N1NModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } // TranscribeAudio: n1n.ai exposes /v1/audio/transcriptions -func (n *N1NModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (n *N1NModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *N1NModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (n *N1NModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *N1NModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (n *N1NModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (n *N1NModel) ParseFile(ctx context.Context, 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()) } // ListTasks: n1n.ai has /v1/contents/generations/tasks -func (n *N1NModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (n *N1NModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *N1NModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (n *N1NModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 3617924121..547a43fd08 100644 --- a/internal/entity/models/novita.go +++ b/internal/entity/models/novita.go @@ -188,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (n *NovitaModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -249,7 +249,7 @@ func (n *NovitaModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -332,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *NovitaModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -392,7 +392,7 @@ func (n *NovitaModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -493,7 +493,7 @@ func (n *NovitaModel) ChatStreamlyWithSender(modelName string, messages []Messag } // ListModels returns the list of model ids visible to the API key. -func (n *NovitaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (n *NovitaModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -505,7 +505,7 @@ func (n *NovitaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, n.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -542,8 +542,8 @@ func (n *NovitaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err } // CheckConnection runs a lightweight ListModels call to verify the API key. -func (n *NovitaModel) CheckConnection(apiConfig *APIConfig) error { - _, err := n.ListModels(apiConfig) +func (n *NovitaModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := n.ListModels(ctx, apiConfig) return err } @@ -562,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (n *NovitaModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -595,7 +595,7 @@ func (n *NovitaModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -666,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (n *NovitaModel) Rerank(ctx context.Context, 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 } @@ -705,7 +705,7 @@ func (n *NovitaModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -756,7 +756,7 @@ func (n *NovitaModel) Rerank(modelName *string, query string, documents []string } // Balance Get remaining credit -func (n *NovitaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (n *NovitaModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -826,36 +826,36 @@ 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (n *NovitaModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *NovitaModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (n *NovitaModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *NovitaModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (n *NovitaModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (n *NovitaModel) ParseFile(ctx context.Context, 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()) } -func (n *NovitaModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (n *NovitaModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *NovitaModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (n *NovitaModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 e0ad544d86..b1a2eda284 100644 --- a/internal/entity/models/novita_test.go +++ b/internal/entity/models/novita_test.go @@ -255,6 +255,7 @@ func TestNovitaName(t *testing.T) { } func TestNovitaChatPureText(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -266,6 +267,7 @@ func TestNovitaChatPureText(t *testing.T) { apiKey := "test-key" resp, err := newNovitaForTest(srv.URL).ChatWithMessages( + ctx, "meta-llama/llama-3.3-70b-instruct", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -278,6 +280,7 @@ func TestNovitaChatPureText(t *testing.T) { } func TestNovitaChatExtractsThinkTags(t *testing.T) { + ctx := t.Context() // qwen3-style response: ... embedded in content. // Driver must split it into Answer + ReasonContent. srv := newNovitaServer(t, "/openai/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { @@ -294,6 +297,7 @@ func TestNovitaChatExtractsThinkTags(t *testing.T) { apiKey := "test-key" resp, err := newNovitaForTest(srv.URL).ChatWithMessages( + ctx, "qwen/qwen3-30b-a3b-fp8", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -314,6 +318,7 @@ func TestNovitaChatExtractsThinkTags(t *testing.T) { // to ChatResponse.ReasonContent. Live-confirmed against // api.novita.ai/openai/v1/chat/completions with deepseek/deepseek-v3.1. func TestNovitaChatExtractsReasoningContentField(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -329,6 +334,7 @@ func TestNovitaChatExtractsReasoningContentField(t *testing.T) { apiKey := "test-key" resp, err := newNovitaForTest(srv.URL).ChatWithMessages( + ctx, "deepseek/deepseek-v3.1", []Message{{Role: "user", Content: "2+2?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -350,6 +356,7 @@ func TestNovitaChatExtractsReasoningContentField(t *testing.T) { // (not delta.content with tags). The driver must forward // those chunks via the sender's second arg. func TestNovitaStreamExtractsDeltaReasoningContent(t *testing.T) { + ctx := t.Context() srv := newNovitaSSEServer(t, "/openai/v1/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"step 1. "}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"reasoning_content":"step 2."}}]}`+"\n"+ @@ -361,6 +368,7 @@ func TestNovitaStreamExtractsDeltaReasoningContent(t *testing.T) { apiKey := "test-key" var content, reasoning []string err := newNovitaForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-v3.1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -393,6 +401,7 @@ func TestNovitaStreamExtractsDeltaReasoningContent(t *testing.T) { // so a tenant can switch a deepseek-v3.1 / glm-4.5 / qwen3 deployment // out of its default thinking mode without prompt-level hacks. func TestNovitaChatPropagatesEnableThinking(t *testing.T) { + ctx := t.Context() cases := []struct { name string value bool @@ -421,6 +430,7 @@ func TestNovitaChatPropagatesEnableThinking(t *testing.T) { apiKey := "test-key" thinking := tc.value _, err := newNovitaForTest(srv.URL).ChatWithMessages( + ctx, "qwen/qwen3-30b-a3b-fp8", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -436,6 +446,7 @@ func TestNovitaChatPropagatesEnableThinking(t *testing.T) { // silently flip behavior for downstream proxies that distinguish // "field absent" from "field present with default". Leave it out. func TestNovitaChatOmitsEnableThinkingWhenUnset(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if _, present := body["enable_thinking"]; present { t.Errorf("enable_thinking must be absent when Thinking unset, got %v", body["enable_thinking"]) @@ -449,7 +460,7 @@ func TestNovitaChatOmitsEnableThinkingWhenUnset(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newNovitaForTest(srv.URL).ChatWithMessages("m", + _, err := newNovitaForTest(srv.URL).ChatWithMessages(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, nil) // no Thinking @@ -462,6 +473,7 @@ func TestNovitaChatOmitsEnableThinkingWhenUnset(t *testing.T) { // case for ChatStreamlyWithSender so callers get the same toggle // regardless of streaming mode. func TestNovitaStreamPropagatesEnableThinking(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) @@ -477,6 +489,7 @@ func TestNovitaStreamPropagatesEnableThinking(t *testing.T) { apiKey := "test-key" thinking := false err := newNovitaForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-v3.1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -492,7 +505,8 @@ func TestNovitaStreamPropagatesEnableThinking(t *testing.T) { } func TestNovitaChatRequiresAPIKey(t *testing.T) { - _, err := newNovitaForTest("http://unused").ChatWithMessages("m", + ctx := t.Context() + _, err := newNovitaForTest("http://unused").ChatWithMessages(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("got %v", err) @@ -500,8 +514,9 @@ func TestNovitaChatRequiresAPIKey(t *testing.T) { } func TestNovitaChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newNovitaForTest("http://unused").ChatWithMessages("m", nil, + _, err := newNovitaForTest("http://unused").ChatWithMessages(ctx, "m", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "messages is empty") { t.Errorf("got %v", err) @@ -509,6 +524,7 @@ func TestNovitaChatRequiresMessages(t *testing.T) { } func TestNovitaChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"detail":"unauthorized"}`)) @@ -516,7 +532,7 @@ func TestNovitaChatRejectsHTTPError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newNovitaForTest(srv.URL).ChatWithMessages("m", + _, err := newNovitaForTest(srv.URL).ChatWithMessages(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "401") { @@ -528,6 +544,7 @@ func TestNovitaChatRejectsHTTPError(t *testing.T) { // delta.content must surface reasoning chunks through the sender's // second arg, and visible content through the first. func TestNovitaStreamSplitsThinkTags(t *testing.T) { + ctx := t.Context() // Simulate the realistic case where tags span deltas — split // "" across two chunks, and split "" too. srv := newNovitaSSEServer(t, "/openai/v1/chat/completions", @@ -546,6 +563,7 @@ func TestNovitaStreamSplitsThinkTags(t *testing.T) { apiKey := "test-key" var content, reasoning []string err := newNovitaForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen/qwen3-30b-a3b-fp8", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -577,6 +595,7 @@ func TestNovitaStreamSplitsThinkTags(t *testing.T) { // Streaming for a non-reasoning model that emits only content chunks // must continue to work unchanged. func TestNovitaStreamPureContent(t *testing.T) { + ctx := t.Context() srv := newNovitaSSEServer(t, "/openai/v1/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant"}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"content":"Hello "}}]}`+"\n"+ @@ -588,7 +607,7 @@ func TestNovitaStreamPureContent(t *testing.T) { apiKey := "test-key" var chunks []string var sawDone bool - err := newNovitaForTest(srv.URL).ChatStreamlyWithSender("meta-llama/llama-3.3-70b-instruct", + err := newNovitaForTest(srv.URL).ChatStreamlyWithSender(ctx, "meta-llama/llama-3.3-70b-instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, _ *string) error { @@ -614,8 +633,9 @@ func TestNovitaStreamPureContent(t *testing.T) { } func TestNovitaStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - err := newNovitaForTest("http://unused").ChatStreamlyWithSender("m", + err := newNovitaForTest("http://unused").ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) if err == nil || !strings.Contains(err.Error(), "sender is required") { @@ -624,9 +644,10 @@ func TestNovitaStreamRequiresSender(t *testing.T) { } func TestNovitaStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() apiKey := "test-key" stream := false - err := newNovitaForTest("http://unused").ChatStreamlyWithSender("m", + err := newNovitaForTest("http://unused").ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Stream: &stream}, @@ -638,6 +659,7 @@ func TestNovitaStreamRejectsExplicitFalse(t *testing.T) { } func TestNovitaListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -650,7 +672,7 @@ func TestNovitaListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - ids, err := newNovitaForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + ids, err := newNovitaForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -660,18 +682,20 @@ func TestNovitaListModelsHappyPath(t *testing.T) { } func TestNovitaCheckConnection(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"data": []map[string]interface{}{{"id": "x"}}}) }) defer srv.Close() apiKey := "test-key" - if err := newNovitaForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newNovitaForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection: %v", err) } } func TestNovitaRerankHappyPathReordersByIndex(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/rerank", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "baai/bge-reranker-v2-m3" { t.Errorf("model=%v", body["model"]) @@ -692,7 +716,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, nil) + resp, err := newNovitaForTest(srv.URL).Rerank(ctx, &model, "what is rag", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -708,6 +732,7 @@ func TestNovitaRerankHappyPathReordersByIndex(t *testing.T) { } func TestNovitaRerankRespectsTopNConfig(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/rerank", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["top_n"] != float64(2) { t.Errorf("top_n=%v, want 2", body["top_n"]) @@ -718,15 +743,16 @@ 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}, nil); err != nil { + if _, err := newNovitaForTest(srv.URL).Rerank(ctx, &model, "q", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, nil); err != nil { t.Fatalf("Rerank: %v", err) } } func TestNovitaRerankEmptyDocumentsShortCircuits(t *testing.T) { + ctx := t.Context() apiKey := "test-key" model := "x" - resp, err := newNovitaForTest("http://unused").Rerank(&model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + resp, err := newNovitaForTest("http://unused").Rerank(ctx, &model, "q", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("expected nil error for empty docs, got %v", err) } @@ -736,22 +762,25 @@ func TestNovitaRerankEmptyDocumentsShortCircuits(t *testing.T) { } func TestNovitaRerankRequiresApiKey(t *testing.T) { + ctx := t.Context() model := "x" - _, err := newNovitaForTest("http://unused").Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) + _, err := newNovitaForTest("http://unused").Rerank(ctx, &model, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("got %v", err) } } func TestNovitaRerankRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newNovitaForTest("http://unused").Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newNovitaForTest("http://unused").Rerank(ctx, 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) } } func TestNovitaRerankRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/rerank", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _, _ = io.WriteString(w, `{"results":[{"index":5,"relevance_score":0.5}]}`) }) @@ -759,13 +788,14 @@ func TestNovitaRerankRejectsOutOfRangeIndex(t *testing.T) { apiKey := "test-key" model := "x" - _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newNovitaForTest(srv.URL).Rerank(ctx, &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) } } func TestNovitaRerankRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() srv := newNovitaServer(t, "/openai/v1/rerank", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _, _ = io.WriteString(w, `{"results":[{"index":0,"relevance_score":0.9},{"index":0,"relevance_score":0.5}]}`) }) @@ -773,13 +803,14 @@ func TestNovitaRerankRejectsDuplicateIndex(t *testing.T) { apiKey := "test-key" model := "x" - _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newNovitaForTest(srv.URL).Rerank(ctx, &model, "q", []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate") { t.Errorf("got %v", err) } } func TestNovitaRerankSurfacesHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = io.WriteString(w, `{"error":"bad key"}`) @@ -788,46 +819,49 @@ func TestNovitaRerankSurfacesHTTPError(t *testing.T) { apiKey := "test-key" model := "x" - _, err := newNovitaForTest(srv.URL).Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newNovitaForTest(srv.URL).Rerank(ctx, &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) } } func TestNovitaRerankRejectsMissingRerankSuffix(t *testing.T) { + ctx := t.Context() apiKey := "test-key" model := "x" driver := NewNovitaModel( map[string]string{"default": "http://unused"}, URLSuffix{Chat: "openai/v1/chat/completions"}, ) - _, err := driver.Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := driver.Rerank(ctx, &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) } } func TestNovitaBalanceReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() // Balance IS implemented (makes HTTP call), not a "no such method" stub. // With dummy key it should reach the HTTP call stage, not fail at APIConfigCheck. apiKey := "test-key" - _, err := newNovitaForTest("http://unused").Balance(&APIConfig{ApiKey: &apiKey}) + _, err := newNovitaForTest("http://unused").Balance(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || strings.Contains(err.Error(), "api key is required") { t.Errorf("expected non-api-key error (e.g. connection refused), got %v", err) } } func TestNovitaAudioOCRReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := "x" apiKey := "test-key" v := newNovitaForTest("http://unused") - if _, err := v.TranscribeAudio(&m, &m, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := v.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := v.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := v.OCRFile(ctx, &m, nil, &m, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("OCRFile: %v", err) } } @@ -839,6 +873,7 @@ func TestNovitaAudioOCRReturnNoSuchMethod(t *testing.T) { // slash. baseURLForRegion now trims the trailing "/" so all three // endpoint builders (Chat, Stream, ListModels) emit clean paths. func TestNovitaBaseURLTrimsTrailingSlash(t *testing.T) { + ctx := t.Context() cases := []struct { name string path string @@ -854,7 +889,7 @@ func TestNovitaBaseURLTrimsTrailingSlash(t *testing.T) { method: http.MethodPost, urlSuffix: URLSuffix{Chat: "openai/v1/chat/completions"}, invoke: func(n *NovitaModel, apiKey string) error { - _, err := n.ChatWithMessages("m", + _, err := n.ChatWithMessages(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) return err @@ -867,7 +902,7 @@ func TestNovitaBaseURLTrimsTrailingSlash(t *testing.T) { method: http.MethodGet, urlSuffix: URLSuffix{Models: "openai/v1/models"}, invoke: func(n *NovitaModel, apiKey string) error { - _, err := n.ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := n.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) return err }, respBody: `{"data":[]}`, @@ -878,7 +913,7 @@ func TestNovitaBaseURLTrimsTrailingSlash(t *testing.T) { method: http.MethodPost, urlSuffix: URLSuffix{Chat: "openai/v1/chat/completions"}, invoke: func(n *NovitaModel, apiKey string) error { - return n.ChatStreamlyWithSender("m", + return n.ChatStreamlyWithSender(ctx, "m", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(*string, *string) error { return nil }) diff --git a/internal/entity/models/nvidia.go b/internal/entity/models/nvidia.go index f699924cc6..d40923a5dd 100644 --- a/internal/entity/models/nvidia.go +++ b/internal/entity/models/nvidia.go @@ -51,7 +51,7 @@ func (n NvidiaModel) Name() string { return "nvidia" } -func (n *NvidiaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (n *NvidiaModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -114,7 +114,7 @@ func (n *NvidiaModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -184,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *NvidiaModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -252,7 +252,7 @@ func (n *NvidiaModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -324,7 +324,7 @@ type nvidiaEmbeddingResponse struct { } `json:"data"` } -func (n NvidiaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (n NvidiaModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -364,7 +364,7 @@ func (n NvidiaModel) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -441,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (n NvidiaModel) Rerank(ctx context.Context, 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 } @@ -487,7 +487,7 @@ func (n NvidiaModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -533,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (n *NvidiaModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *NvidiaModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (n *NvidiaModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (n *NvidiaModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (n *NvidiaModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (n *NvidiaModel) ParseFile(ctx context.Context, 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()) } @@ -564,7 +564,7 @@ func (n *NvidiaModel) ParseFile(modelName *string, content []byte, url *string, // and returns the list of available model ids. The endpoint is // OpenAI-compatible, so the parsing follows the same shape used by // the moonshot, xai, and openai drivers. -func (n NvidiaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (n NvidiaModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := n.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -580,7 +580,7 @@ func (n NvidiaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro url := fmt.Sprintf("%s/%s", baseURL, n.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -617,7 +617,7 @@ func (n NvidiaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return ParseListModel(modelList), nil } -func (n NvidiaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (n NvidiaModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } @@ -625,15 +625,15 @@ func (n NvidiaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, erro // is reachable and that the API key is accepted, by issuing a // lightweight ListModels call. Mirrors the pattern used by the xai, // moonshot, deepseek, aliyun, and gitee drivers. -func (n NvidiaModel) CheckConnection(apiConfig *APIConfig) error { - _, err := n.ListModels(apiConfig) +func (n NvidiaModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := n.ListModels(ctx, apiConfig) return err } -func (n *NvidiaModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (n *NvidiaModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", n.Name()) } -func (n *NvidiaModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (n *NvidiaModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 10edee00dc..ad3f95ddfb 100644 --- a/internal/entity/models/nvidia_rerank_test.go +++ b/internal/entity/models/nvidia_rerank_test.go @@ -52,6 +52,7 @@ func newNvidiaModelForTest(baseURL string) *NvidiaModel { } func TestNvidiaRerankHappyPath(t *testing.T) { + ctx := t.Context() srv := newNvidiaRerankServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "nvidia/nv-rerankqa-mistral-4b-v3" { t.Errorf("expected model=nvidia/nv-rerankqa-mistral-4b-v3, got %v", body["model"]) @@ -86,6 +87,7 @@ func TestNvidiaRerankHappyPath(t *testing.T) { apiKey := "test-key" modelName := "nvidia/nv-rerankqa-mistral-4b-v3" resp, err := model.Rerank( + ctx, &modelName, "What is RAPTOR?", []string{"doc-zero", "doc-one", "doc-two"}, @@ -108,6 +110,7 @@ func TestNvidiaRerankHappyPath(t *testing.T) { } func TestNvidiaRerankTopNClamp(t *testing.T) { + ctx := t.Context() srv := newNvidiaRerankServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["top_n"] != float64(2) { t.Errorf("expected top_n clamp to RerankConfig.TopN=2, got %v", body["top_n"]) @@ -120,6 +123,7 @@ func TestNvidiaRerankTopNClamp(t *testing.T) { apiKey := "test-key" modelName := "nvidia/nv-rerankqa-mistral-4b-v3" if _, err := model.Rerank( + ctx, &modelName, "q", []string{"a", "b", "c", "d"}, &APIConfig{ApiKey: &apiKey}, @@ -131,10 +135,11 @@ func TestNvidiaRerankTopNClamp(t *testing.T) { } func TestNvidiaRerankEmptyDocuments(t *testing.T) { + ctx := t.Context() 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{}, nil) + resp, err := model.Rerank(ctx, &modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err != nil { t.Fatalf("expected nil error for empty documents, got %v", err) } @@ -144,24 +149,27 @@ func TestNvidiaRerankEmptyDocuments(t *testing.T) { } func TestNvidiaRerankRequiresAPIKey(t *testing.T) { + ctx := t.Context() model := newNvidiaModelForTest("http://unused") modelName := "nvidia/nv-rerankqa-mistral-4b-v3" - _, err := model.Rerank(&modelName, "q", []string{"a"}, &APIConfig{}, &RerankConfig{}, nil) + _, err := model.Rerank(ctx, &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) } } func TestNvidiaRerankRequiresModelName(t *testing.T) { + ctx := t.Context() model := newNvidiaModelForTest("http://unused") apiKey := "test-key" - _, err := model.Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) + _, err := model.Rerank(ctx, 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) } } func TestNvidiaRerankRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newNvidiaRerankServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) @@ -171,13 +179,14 @@ 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{}, nil) + _, err := model.Rerank(ctx, &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) } } func TestNvidiaRerankRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newNvidiaRerankServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "rankings": []map[string]interface{}{ @@ -190,7 +199,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{}, nil) + _, err := model.Rerank(ctx, &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 e04e02f89c..b917e8fafd 100644 --- a/internal/entity/models/ollama.go +++ b/internal/entity/models/ollama.go @@ -73,7 +73,7 @@ func (o *OllamaModel) Name() string { return "Ollama" } -func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (o *OllamaModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if len(messages) == 0 { return nil, fmt.Errorf("message is nil") } @@ -144,7 +144,7 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -195,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OllamaModel) ChatStreamlyWithSender(ctx context.Context, 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") } @@ -265,7 +265,7 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -330,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (o *OllamaModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -370,7 +370,7 @@ func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -427,39 +427,39 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (o *OllamaModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (o *OllamaModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OllamaModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (o *OllamaModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OllamaModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (o *OllamaModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (o *OllamaModel) ParseFile(ctx context.Context, 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()) } -func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (o *OllamaModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { baseURL, err := o.baseModel.GetBaseURL(apiConfig) if err != nil { @@ -471,7 +471,7 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), o.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -526,20 +526,20 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return ParseListModel(modelList), nil } -func (o *OllamaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (o *OllamaModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection verifies that the configured Ollama base URL is reachable -func (o *OllamaModel) CheckConnection(apiConfig *APIConfig) error { - _, err := o.ListModels(apiConfig) +func (o *OllamaModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := o.ListModels(ctx, apiConfig) return err } -func (o *OllamaModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (o *OllamaModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } -func (o *OllamaModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (o *OllamaModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } diff --git a/internal/entity/models/ollama_test.go b/internal/entity/models/ollama_test.go index cdfc2066de..daf65d9e69 100644 --- a/internal/entity/models/ollama_test.go +++ b/internal/entity/models/ollama_test.go @@ -28,6 +28,7 @@ func newOllamaForListModelsTest(baseURL string) *OllamaModel { } func TestOllamaListModels(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -40,7 +41,7 @@ func TestOllamaListModels(t *testing.T) { })) defer srv.Close() - models, err := newOllamaForListModelsTest(srv.URL).ListModels(&APIConfig{}) + models, err := newOllamaForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -53,13 +54,14 @@ func TestOllamaListModels(t *testing.T) { } func TestOllamaListModelsFallsBackToModelField(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Some entries may carry only the "model" field; it should be used as the name. _, _ = io.WriteString(w, `{"models":[{"model":"phi3:mini"},{"name":""},{"name":" "}]}`) })) defer srv.Close() - models, err := newOllamaForListModelsTest(srv.URL).ListModels(&APIConfig{}) + models, err := newOllamaForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -72,20 +74,22 @@ func TestOllamaListModelsFallsBackToModelField(t *testing.T) { } func TestOllamaListModelsRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = io.WriteString(w, `boom`) })) defer srv.Close() - if _, err := newOllamaForListModelsTest(srv.URL).ListModels(&APIConfig{}); err == nil { + if _, err := newOllamaForListModelsTest(srv.URL).ListModels(ctx, &APIConfig{}); err == nil { t.Fatal("ListModels: expected error for HTTP 500, got nil") } } func TestOllamaListModelsRequiresBaseURL(t *testing.T) { + ctx := t.Context() m := NewOllamaModel(map[string]string{}, URLSuffix{Models: "api/tags"}) - if _, err := m.ListModels(&APIConfig{}); err == nil { + if _, err := m.ListModels(ctx, &APIConfig{}); err == nil { t.Fatal("ListModels: expected error for missing base URL, got nil") } } diff --git a/internal/entity/models/openai.go b/internal/entity/models/openai.go index 22f47f8d91..b3769c0d2d 100644 --- a/internal/entity/models/openai.go +++ b/internal/entity/models/openai.go @@ -58,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (o *OpenAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -139,7 +139,7 @@ func (o *OpenAIModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -230,7 +230,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OpenAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -320,7 +320,7 @@ func (o *OpenAIModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -455,7 +455,7 @@ type openaiUsage struct { TotalTokens int `json:"total_tokens"` } -func (o *OpenAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (o *OpenAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -488,7 +488,7 @@ func (o *OpenAIModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -531,7 +531,7 @@ func (o *OpenAIModel) Embed(modelName *string, texts []string, apiConfig *APICon } // ListModels returns the list of model ids visible to the API key. -func (o *OpenAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (o *OpenAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -543,7 +543,7 @@ func (o *OpenAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, o.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -582,13 +582,13 @@ func (o *OpenAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err } // Balance is not exposed by the OpenAI API, so this returns "no such method". -func (o *OpenAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (o *OpenAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection runs a lightweight ListModels call to verify the API key. -func (o *OpenAIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := o.ListModels(apiConfig) +func (o *OpenAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := o.ListModels(ctx, apiConfig) if err != nil { return err } @@ -597,13 +597,13 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (o *OpenAIModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) +func (o *OpenAIModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, responseFormat, err := o.newOpenAIASRRequest(ctx, modelName, file, apiConfig, asrConfig, false) @@ -631,12 +631,12 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OpenAIModel) TranscribeAudioWithSender(ctx context.Context, 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") } - req, responseFormat, err := o.newOpenAIASRRequest(context.Background(), modelName, file, apiConfig, asrConfig, true) + req, responseFormat, err := o.newOpenAIASRRequest(ctx, modelName, file, apiConfig, asrConfig, true) if err != nil { return err } @@ -723,8 +723,8 @@ func decodeOpenAIASRResponse(respBody []byte, responseFormat string) (*ASRRespon } // AudioSpeech convert text to audio -func (o *OpenAIModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) +func (o *OpenAIModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, _, err := o.newOpenAITTSRequest(ctx, modelName, audioContent, apiConfig, ttsConfig, false) @@ -750,12 +750,12 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OpenAIModel) AudioSpeechWithSender(ctx context.Context, 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") } - req, streamFormat, err := o.newOpenAITTSRequest(context.Background(), modelName, audioContent, apiConfig, ttsConfig, true) + req, streamFormat, err := o.newOpenAITTSRequest(ctx, modelName, audioContent, apiConfig, ttsConfig, true) if err != nil { return err } @@ -1014,20 +1014,20 @@ 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (o *OpenAIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (o *OpenAIModel) ParseFile(ctx context.Context, 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()) } -func (o *OpenAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (o *OpenAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } -func (o *OpenAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (o *OpenAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 b7c0ceb4a3..a717c29810 100644 --- a/internal/entity/models/openai_test.go +++ b/internal/entity/models/openai_test.go @@ -68,6 +68,7 @@ func TestOpenAIConfigAdvertisedAudioModelsHaveSuffixes(t *testing.T) { } func TestOpenAITranscribeAudioPostsMultipartToAudioEndpoint(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -126,6 +127,7 @@ func TestOpenAITranscribeAudioPostsMultipartToAudioEndpoint(t *testing.T) { apiKey := "test-key" model := "whisper-1" resp, err := newOpenAIForTest(srv.URL).TranscribeAudio( + ctx, &model, &audioPath, &APIConfig{ApiKey: &apiKey}, @@ -144,6 +146,7 @@ func TestOpenAITranscribeAudioPostsMultipartToAudioEndpoint(t *testing.T) { } func TestOpenAITranscribeAudioWithSenderStreamsDeltas(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -189,6 +192,7 @@ func TestOpenAITranscribeAudioWithSenderStreamsDeltas(t *testing.T) { model := "gpt-4o-mini-transcribe" var chunks []string err := newOpenAIForTest(srv.URL).TranscribeAudioWithSender( + ctx, &model, &audioPath, &APIConfig{ApiKey: &apiKey}, @@ -210,6 +214,7 @@ func TestOpenAITranscribeAudioWithSenderStreamsDeltas(t *testing.T) { } func TestOpenAIAudioSpeechPostsJSONToAudioEndpoint(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method=%s, want POST", r.Method) @@ -254,6 +259,7 @@ func TestOpenAIAudioSpeechPostsJSONToAudioEndpoint(t *testing.T) { model := "tts-1" input := "hello" resp, err := newOpenAIForTest(srv.URL).AudioSpeech( + ctx, &model, &input, &APIConfig{ApiKey: &apiKey}, @@ -278,8 +284,10 @@ func TestOpenAIAudioSpeechRequiresVoice(t *testing.T) { apiKey := "test-key" model := "tts-1" input := "hello" + ctx := t.Context() _, err := newOpenAIForTest("http://unused").AudioSpeech( + ctx, &model, &input, &APIConfig{ApiKey: &apiKey}, @@ -292,11 +300,13 @@ func TestOpenAIAudioSpeechRequiresVoice(t *testing.T) { } func TestOpenAIAudioSpeechRejectsNonStringVoice(t *testing.T) { + ctx := t.Context() apiKey := "test-key" model := "tts-1" input := "hello" _, err := newOpenAIForTest("http://unused").AudioSpeech( + ctx, &model, &input, &APIConfig{ApiKey: &apiKey}, @@ -347,9 +357,11 @@ func TestOpenAIAudioSpeechWithSenderStreamsRawAudio(t *testing.T) { apiKey := "test-key" model := "tts-1" input := "hello" + ctx := t.Context() var chunks []string err := newOpenAIForTest(srv.URL).AudioSpeechWithSender( + ctx, &model, &input, &APIConfig{ApiKey: &apiKey}, @@ -371,6 +383,7 @@ func TestOpenAIAudioSpeechWithSenderStreamsRawAudio(t *testing.T) { } func TestOpenAIAudioSpeechWithSenderStreamsSSEAudioDeltas(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Accept"); got != "text/event-stream" { t.Errorf("Accept=%q, want text/event-stream", got) @@ -397,6 +410,7 @@ func TestOpenAIAudioSpeechWithSenderStreamsSSEAudioDeltas(t *testing.T) { input := "hello" var chunks []string err := newOpenAIForTest(srv.URL).AudioSpeechWithSender( + ctx, &model, &input, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/openrouter.go b/internal/entity/models/openrouter.go index 638aa6cd26..965fce3819 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (o *OpenRouterModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -118,7 +118,7 @@ func (o *OpenRouterModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OpenRouterModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -267,7 +267,7 @@ func (o *OpenRouterModel) ChatStreamlyWithSender(modelName string, messages []Me return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -353,7 +353,7 @@ type openrouterUsage struct { TotalTokens int `json:"total_tokens"` } -func (o *OpenRouterModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (o *OpenRouterModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -385,7 +385,7 @@ func (o *OpenRouterModel) Embed(modelName *string, texts []string, apiConfig *AP return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -448,7 +448,7 @@ type OpenRouterRerankResponse struct { } `json:"results"` } -func (o *OpenRouterModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (o *OpenRouterModel) Rerank(ctx context.Context, 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 } @@ -480,7 +480,7 @@ func (o *OpenRouterModel) Rerank(modelName *string, query string, documents []st } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(resolvedBaseURL, "/"), o.baseModel.URLSuffix.Rerank) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (o *OpenRouterModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -591,7 +591,7 @@ func (o *OpenRouterModel) TranscribeAudio(modelName *string, file *string, apiCo return nil, err } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(resolvedBaseURL, "/"), o.baseModel.URLSuffix.ASR) - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OpenRouterModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (o *OpenRouterModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -664,7 +664,7 @@ func (o *OpenRouterModel) AudioSpeech(modelName *string, audioContent *string, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -693,21 +693,21 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OpenRouterModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (o *OpenRouterModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (o *OpenRouterModel) ParseFile(ctx context.Context, 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()) } -func (o *OpenRouterModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (o *OpenRouterModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -726,7 +726,7 @@ func (o *OpenRouterModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -765,7 +765,7 @@ func (o *OpenRouterModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(modelList), nil } -func (o *OpenRouterModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (o *OpenRouterModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -776,7 +776,7 @@ func (o *OpenRouterModel) Balance(apiConfig *APIConfig) (map[string]interface{}, } url := fmt.Sprintf("%s/%s", baseURL, o.baseModel.URLSuffix.Balance) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -823,15 +823,15 @@ func (o *OpenRouterModel) Balance(apiConfig *APIConfig) (map[string]interface{}, }, nil } -func (o *OpenRouterModel) CheckConnection(apiConfig *APIConfig) error { - _, err := o.Balance(apiConfig) +func (o *OpenRouterModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := o.Balance(ctx, apiConfig) return err } -func (o *OpenRouterModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (o *OpenRouterModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", o.Name()) } -func (o *OpenRouterModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (o *OpenRouterModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 3aef619eb7..e3c51f695a 100644 --- a/internal/entity/models/openrouter_test.go +++ b/internal/entity/models/openrouter_test.go @@ -77,6 +77,7 @@ func TestOpenRouterAudioFormatUsesConfiguredValue(t *testing.T) { } func TestOpenRouterTranscribeAudioHappyPath(t *testing.T) { + ctx := t.Context() audio := []byte("RIFF test audio") srv := newOpenRouterServer(t, "/audio/transcriptions", func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodPost { @@ -120,6 +121,7 @@ func TestOpenRouterTranscribeAudioHappyPath(t *testing.T) { modelName := "openai/whisper-large-v3" file := writeOpenRouterAudioFile(t, "sample.wav", audio) resp, err := newOpenRouterForTest(srv.URL).TranscribeAudio( + ctx, &modelName, &file, &APIConfig{ApiKey: &apiKey}, @@ -141,6 +143,7 @@ func TestOpenRouterTranscribeAudioHappyPath(t *testing.T) { } func TestOpenRouterTranscribeAudioInfersFormat(t *testing.T) { + ctx := t.Context() srv := newOpenRouterServer(t, "/audio/transcriptions", func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { inputAudio, ok := body["input_audio"].(map[string]interface{}) if !ok { @@ -157,13 +160,14 @@ 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, nil) + _, err := newOpenRouterForTest(srv.URL).TranscribeAudio(ctx, &modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("TranscribeAudio: %v", err) } } func TestOpenRouterTranscribeAudioValidatesInputs(t *testing.T) { + ctx := t.Context() modelName := "openai/whisper-large-v3" apiKey := "test-key" file := "sample.wav" @@ -182,7 +186,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, nil) + _, err := newOpenRouterForTest("http://unused").TranscribeAudio(ctx, 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) } @@ -191,18 +195,20 @@ func TestOpenRouterTranscribeAudioValidatesInputs(t *testing.T) { } func TestOpenRouterTranscribeAudioValidatesASRSuffix(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "openai/whisper-large-v3" file := "sample.wav" model := NewOpenRouterModel(map[string]string{"default": "http://unused"}, URLSuffix{}) - _, err := model.TranscribeAudio(&modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := model.TranscribeAudio(ctx, &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) } } func TestOpenRouterTranscribeAudioHTTPError(t *testing.T) { + ctx := t.Context() srv := newOpenRouterServer(t, "/audio/transcriptions", func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) }) @@ -211,7 +217,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, nil) + _, err := newOpenRouterForTest(srv.URL).TranscribeAudio(ctx, &modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil) if err == nil || !strings.Contains(err.Error(), "OpenRouter ASR API error") || !strings.Contains(err.Error(), "401 Unauthorized") || @@ -225,6 +231,7 @@ func TestOpenRouterTranscribeAudioHTTPError(t *testing.T) { // and reasoning is requested via OpenRouter's standard `reasoning` object rather // than the non-standard `thinking` key that the API silently ignores. func TestOpenRouterChatStreamlyRequest(t *testing.T) { + ctx := t.Context() var gotPath string var gotBody map[string]interface{} srv := newOpenRouterServer(t, "/chat/completions", func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { @@ -242,6 +249,7 @@ func TestOpenRouterChatStreamlyRequest(t *testing.T) { var reasoning strings.Builder // "qwen_max" would have triggered the removed async-routing branch (empty suffix). err := newOpenRouterForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen_max", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/orcarouter.go b/internal/entity/models/orcarouter.go index 27d2d185f1..67e022e4ea 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (o *OrcaRouterModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -106,7 +106,7 @@ func (o *OrcaRouterModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OrcaRouterModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -226,7 +226,7 @@ func (o *OrcaRouterModel) ChatStreamlyWithSender(modelName string, messages []Me return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (o *OrcaRouterModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (o *OrcaRouterModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (o *OrcaRouterModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OrcaRouterModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (o *OrcaRouterModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -347,7 +347,7 @@ func (o *OrcaRouterModel) AudioSpeech(modelName *string, audioContent *string, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -376,19 +376,19 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (o *OrcaRouterModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (o *OrcaRouterModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (o *OrcaRouterModel) ParseFile(ctx context.Context, 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()) } -func (o *OrcaRouterModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (o *OrcaRouterModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := o.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -406,7 +406,7 @@ func (o *OrcaRouterModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -445,19 +445,19 @@ func (o *OrcaRouterModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(modelList), nil } -func (o *OrcaRouterModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (o *OrcaRouterModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) CheckConnection(apiConfig *APIConfig) error { - _, err := o.ListModels(apiConfig) +func (o *OrcaRouterModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := o.ListModels(ctx, apiConfig) return err } -func (o *OrcaRouterModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (o *OrcaRouterModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", o.Name()) } -func (o *OrcaRouterModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (o *OrcaRouterModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 51a11cf39e..77e6c664a7 100644 --- a/internal/entity/models/paddleocr.go +++ b/internal/entity/models/paddleocr.go @@ -53,35 +53,35 @@ func (p *PaddleOCRModel) Name() string { return "paddle_ocr.net" } -func (p *PaddleOCRModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (p *PaddleOCRModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PaddleOCRModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (p *PaddleOCRModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (p *PaddleOCRModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (p *PaddleOCRModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PaddleOCRModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (p *PaddleOCRModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PaddleOCRModel) AudioSpeechWithSender(ctx context.Context, 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()) } @@ -116,7 +116,7 @@ type paddleJsonlLine struct { } `json:"result"` } -func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (p *PaddleOCRModel) OCRFile(ctx context.Context, 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 } @@ -140,7 +140,7 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str // One generous deadline bounds the whole OCR operation (submit + poll + // result download), so the poll loop below can no longer spin forever. - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() var req *http.Request @@ -319,26 +319,26 @@ 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (p *PaddleOCRModel) ParseFile(ctx context.Context, 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()) } -func (p *PaddleOCRModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (p *PaddleOCRModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (p *PaddleOCRModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) CheckConnection(apiConfig *APIConfig) error { +func (p *PaddleOCRModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (p *PaddleOCRModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PaddleOCRModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (p *PaddleOCRModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 8e78554677..d14cf9bf46 100644 --- a/internal/entity/models/paddleocr_local.go +++ b/internal/entity/models/paddleocr_local.go @@ -50,35 +50,35 @@ func (p *PaddleOCRLocalModel) Name() string { return "paddleocr" } -func (p *PaddleOCRLocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (p *PaddleOCRLocalModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PaddleOCRLocalModel) ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (p *PaddleOCRLocalModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (p *PaddleOCRLocalModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (p *PaddleOCRLocalModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PaddleOCRLocalModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (p *PaddleOCRLocalModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PaddleOCRLocalModel) AudioSpeechWithSender(ctx context.Context, 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()) } @@ -98,7 +98,7 @@ type paddleLocalOCRResponse struct { } `json:"result"` } -func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (p *PaddleOCRLocalModel) OCRFile(ctx context.Context, 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") } @@ -133,7 +133,7 @@ func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL return nil, fmt.Errorf("failed to marshal local PaddleOCR request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -185,26 +185,26 @@ func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL }, nil } -func (p *PaddleOCRLocalModel) ParseFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (p *PaddleOCRLocalModel) ParseFile(ctx context.Context, 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()) } -func (p *PaddleOCRLocalModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (p *PaddleOCRLocalModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (p *PaddleOCRLocalModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) CheckConnection(apiConfig *APIConfig) error { +func (p *PaddleOCRLocalModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (p *PaddleOCRLocalModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", p.Name()) } -func (p *PaddleOCRLocalModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (p *PaddleOCRLocalModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 6f10d9c074..601f72fb39 100644 --- a/internal/entity/models/perplexity.go +++ b/internal/entity/models/perplexity.go @@ -114,7 +114,7 @@ type perplexityChatResponse struct { FinishReason string `json:"finish_reason"` } -func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (p *PerplexityModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -135,7 +135,7 @@ func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -181,7 +181,7 @@ func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, }, nil } -func (p *PerplexityModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PerplexityModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -212,7 +212,7 @@ func (p *PerplexityModel) ChatStreamlyWithSender(modelName string, messages []Me // ResponseHeaderTimeout caps the initial header wait. This context // also caps the body-read phase so a stalled SSE stream cannot hold // the caller's goroutine and connection indefinitely. - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -284,7 +284,7 @@ type perplexityModelListResponse struct { Data []ModelListItem `json:"data"` } -func (p *PerplexityModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (p *PerplexityModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -296,7 +296,7 @@ func (p *PerplexityModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, p.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -333,8 +333,8 @@ func (p *PerplexityModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(ModelList{Models: bare}), nil } -func (p *PerplexityModel) CheckConnection(apiConfig *APIConfig) error { - _, err := p.ListModels(apiConfig) +func (p *PerplexityModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := p.ListModels(ctx, apiConfig) return err } @@ -350,7 +350,7 @@ type perplexityEmbeddingResponse struct { Error interface{} `json:"error"` } -func (p *PerplexityModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (p *PerplexityModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -382,7 +382,7 @@ func (p *PerplexityModel) Embed(modelName *string, texts []string, apiConfig *AP return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -424,42 +424,42 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (p *PerplexityModel) Rerank(ctx context.Context, 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 *PerplexityModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (p *PerplexityModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (p *PerplexityModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PerplexityModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (p *PerplexityModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PerplexityModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (p *PerplexityModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (p *PerplexityModel) ParseFile(ctx context.Context, 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()) } -func (p *PerplexityModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (p *PerplexityModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PerplexityModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (p *PerplexityModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 81030a4624..77cfca0804 100644 --- a/internal/entity/models/perplexity_test.go +++ b/internal/entity/models/perplexity_test.go @@ -60,6 +60,7 @@ func TestPerplexityFactory(t *testing.T) { } func TestPerplexityChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/v1/sonar" { t.Errorf("path=%s", r.URL.Path) @@ -91,6 +92,7 @@ func TestPerplexityChatHappyPath(t *testing.T) { stop := []string{"END"} effort := "high" resp, err := newPerplexityForTest(srv.URL).ChatWithMessages( + ctx, "sonar-reasoning-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -109,6 +111,7 @@ func TestPerplexityChatHappyPath(t *testing.T) { } func TestPerplexityChatSkipsReasoningEffortForNonReasoningModel(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "sonar" { t.Errorf("model=%v", body["model"]) @@ -129,6 +132,7 @@ func TestPerplexityChatSkipsReasoningEffortForNonReasoningModel(t *testing.T) { apiKey := "test-key" effort := "high" resp, err := newPerplexityForTest(srv.URL).ChatWithMessages( + ctx, "sonar", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -144,21 +148,24 @@ func TestPerplexityChatSkipsReasoningEffortForNonReasoningModel(t *testing.T) { } func TestPerplexityChatRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newPerplexityForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newPerplexityForTest("http://unused").ChatWithMessages(ctx, "", []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, nil) + ctx := t.Context() + _, err := newPerplexityForTest("http://unused").ChatWithMessages(ctx, "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) } } func TestPerplexityStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/v1/sonar" { t.Errorf("path=%s", r.URL.Path) @@ -182,6 +189,7 @@ func TestPerplexityStreamHappyPath(t *testing.T) { var content []string var reasoning []string err := newPerplexityForTest(srv.URL).ChatStreamlyWithSender( + ctx, "sonar", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -207,6 +215,7 @@ func TestPerplexityStreamHappyPath(t *testing.T) { } func TestPerplexityStreamStopsOnDoneMarker(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") _, _ = io.WriteString(w, @@ -219,6 +228,7 @@ func TestPerplexityStreamStopsOnDoneMarker(t *testing.T) { apiKey := "test-key" var chunks []string err := newPerplexityForTest(srv.URL).ChatStreamlyWithSender( + ctx, "sonar", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -238,6 +248,7 @@ func TestPerplexityStreamStopsOnDoneMarker(t *testing.T) { } func TestPerplexityListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s", r.Method) @@ -257,19 +268,20 @@ func TestPerplexityListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" model := newPerplexityForTest(srv.URL) - models, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "sonar,sonar-pro,pplx-embed-v1-0.6b" { t.Errorf("models=%v", models) } - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestPerplexityListModelsAcceptsBareArray(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode([]map[string]interface{}{ {"id": "sonar"}, @@ -279,7 +291,7 @@ func TestPerplexityListModelsAcceptsBareArray(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newPerplexityForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newPerplexityForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -289,6 +301,7 @@ func TestPerplexityListModelsAcceptsBareArray(t *testing.T) { } func TestPerplexityEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newPerplexityServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/v1/embeddings" { t.Errorf("path=%s", r.URL.Path) @@ -315,6 +328,7 @@ func TestPerplexityEmbedHappyPath(t *testing.T) { apiKey := "test-key" modelName := "pplx-embed-v1-0.6b" out, err := newPerplexityForTest(srv.URL).Embed( + ctx, &modelName, []string{"hello", "world"}, &APIConfig{ApiKey: &apiKey}, @@ -336,9 +350,10 @@ func TestPerplexityEmbedHappyPath(t *testing.T) { } func TestPerplexityEmbedEmptyTextsReturnsEmpty(t *testing.T) { + ctx := t.Context() modelName := "pplx-embed-v1-0.6b" apiKey := "test-key" - out, err := newPerplexityForTest("http://unused").Embed(&modelName, nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + out, err := newPerplexityForTest("http://unused").Embed(ctx, &modelName, nil, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -348,19 +363,21 @@ func TestPerplexityEmbedEmptyTextsReturnsEmpty(t *testing.T) { } func TestPerplexityEmbedRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newPerplexityForTest("http://unused").Embed(nil, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newPerplexityForTest("http://unused").Embed(ctx, 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) } } func TestPerplexityUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newPerplexityForTest("http://unused") - if _, err := m.Rerank(nil, "", nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, 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") { + if _, err := m.Balance(ctx, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance error=%v", err) } } diff --git a/internal/entity/models/ppio.go b/internal/entity/models/ppio.go index aa3a6d590b..62dc1528db 100644 --- a/internal/entity/models/ppio.go +++ b/internal/entity/models/ppio.go @@ -112,7 +112,7 @@ type ppioChatResponse struct { FinishReason string `json:"finish_reason"` } -func (p *PPIOModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (p *PPIOModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -133,7 +133,7 @@ func (p *PPIOModel) ChatWithMessages(modelName string, messages []Message, apiCo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -180,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PPIOModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -208,7 +208,7 @@ func (p *PPIOModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -279,7 +279,7 @@ type ppioListModelsResponse struct { Error interface{} `json:"error"` } -func (p *PPIOModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (p *PPIOModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := p.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -289,7 +289,7 @@ func (p *PPIOModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -324,51 +324,51 @@ func (p *PPIOModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return ParseListModel(ModelList{Models: result.Data}), nil } -func (p *PPIOModel) CheckConnection(apiConfig *APIConfig) error { - _, err := p.ListModels(apiConfig) +func (p *PPIOModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := p.ListModels(ctx, apiConfig) return err } -func (p *PPIOModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (p *PPIOModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (p *PPIOModel) Rerank(ctx context.Context, 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 *PPIOModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (p *PPIOModel) Balance(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (p *PPIOModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PPIOModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (p *PPIOModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (p *PPIOModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (p *PPIOModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (p *PPIOModel) ParseFile(ctx context.Context, 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()) } -func (p *PPIOModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (p *PPIOModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", p.Name()) } -func (p *PPIOModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (p *PPIOModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 9c8f6921bf..3dede3b64b 100644 --- a/internal/entity/models/ppio_test.go +++ b/internal/entity/models/ppio_test.go @@ -75,6 +75,7 @@ func TestPPIONewModelWithCustomDefaultTransport(t *testing.T) { } func TestPPIOChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -130,6 +131,7 @@ func TestPPIOChatHappyPath(t *testing.T) { stop := []string{"END"} effort := "high" resp, err := newPPIOForTest(srv.URL).ChatWithMessages( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -148,6 +150,7 @@ func TestPPIOChatHappyPath(t *testing.T) { } func TestPPIOChatUsesReasoningFallback(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -162,6 +165,7 @@ func TestPPIOChatUsesReasoningFallback(t *testing.T) { apiKey := "test-key" resp, err := newPPIOForTest(srv.URL).ChatWithMessages( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -177,35 +181,39 @@ func TestPPIOChatUsesReasoningFallback(t *testing.T) { } func TestPPIOChatRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newPPIOForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newPPIOForTest("http://unused").ChatWithMessages(ctx, "", []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 TestPPIOChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newPPIOForTest("http://unused").ChatWithMessages("deepseek/deepseek-r1", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newPPIOForTest("http://unused").ChatWithMessages(ctx, "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) } } func TestPPIOChatSurfacesHTTPError(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { http.Error(w, "bad key", http.StatusUnauthorized) }) defer srv.Close() apiKey := "test-key" - _, err := newPPIOForTest(srv.URL).ChatWithMessages("deepseek/deepseek-r1", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newPPIOForTest(srv.URL).ChatWithMessages(ctx, "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) } } func TestPPIOChatRejectsProviderError(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "error": map[string]interface{}{"message": "invalid model"}, @@ -214,13 +222,14 @@ 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, nil) + _, err := newPPIOForTest(srv.URL).ChatWithMessages(ctx, "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) } } func TestPPIOStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -245,6 +254,7 @@ func TestPPIOStreamHappyPath(t *testing.T) { var content []string var reasoning []string err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -273,6 +283,7 @@ func TestPPIOStreamHappyPath(t *testing.T) { } func TestPPIOStreamSurfacesHTTPError(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { http.Error(w, "bad key", http.StatusUnauthorized) }) @@ -280,6 +291,7 @@ func TestPPIOStreamSurfacesHTTPError(t *testing.T) { apiKey := "test-key" err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -291,6 +303,7 @@ func TestPPIOStreamSurfacesHTTPError(t *testing.T) { } func TestPPIOStreamStopsOnSenderError(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") _, _ = io.WriteString(w, `data: {"choices":[{"delta":{"content":"partial"}}]}`+"\n") @@ -299,6 +312,7 @@ func TestPPIOStreamStopsOnSenderError(t *testing.T) { apiKey := "test-key" err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -310,9 +324,11 @@ func TestPPIOStreamStopsOnSenderError(t *testing.T) { } func TestPPIOStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() apiKey := "test-key" stream := false err := newPPIOForTest("http://unused").ChatStreamlyWithSender( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -326,8 +342,10 @@ func TestPPIOStreamRejectsExplicitFalse(t *testing.T) { } func TestPPIOStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newPPIOForTest("http://unused").ChatStreamlyWithSender( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil, @@ -338,6 +356,7 @@ func TestPPIOStreamRequiresSender(t *testing.T) { } func TestPPIOStreamRequiresTerminalEvent(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") _, _ = io.WriteString(w, `data: {"choices":[{"delta":{"content":"partial"}}]}`+"\n") @@ -346,6 +365,7 @@ func TestPPIOStreamRequiresTerminalEvent(t *testing.T) { apiKey := "test-key" err := newPPIOForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek/deepseek-r1", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -357,6 +377,7 @@ func TestPPIOStreamRequiresTerminalEvent(t *testing.T) { } func TestPPIOListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s", r.Method) @@ -375,26 +396,28 @@ func TestPPIOListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" model := newPPIOForTest(srv.URL) - models, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "deepseek/deepseek-r1,qwen/qwen-2.5-72b-instruct" { t.Errorf("models=%v", models) } - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestPPIOListModelsRequiresAPIKey(t *testing.T) { - _, err := newPPIOForTest("http://unused").ListModels(&APIConfig{}) + ctx := t.Context() + _, err := newPPIOForTest("http://unused").ListModels(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } } func TestPPIOListModelsRejectsProviderError(t *testing.T) { + ctx := t.Context() srv := newPPIOServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "error": map[string]interface{}{"message": "unauthorized"}, @@ -403,7 +426,7 @@ func TestPPIOListModelsRejectsProviderError(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newPPIOForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := newPPIOForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "upstream error") { t.Errorf("expected upstream error, got %v", err) } @@ -473,38 +496,39 @@ func TestPPIOMissingRegionBaseURL(t *testing.T) { } func TestPPIOUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newPPIOForTest("http://unused") - if _, err := m.Embed(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, 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") { + if _, err := m.Balance(ctx, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance error=%v", err) } - if _, err := m.TranscribeAudio(nil, nil, nil, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.TranscribeAudioWithSender(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := m.AudioSpeechWithSender(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ParseFile(ctx, 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") { + if _, err := m.ListTasks(ctx, nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ListTasks error=%v", err) } - if _, err := m.ShowTask("", nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.ShowTask(ctx, "", nil); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ShowTask error=%v", err) } } diff --git a/internal/entity/models/qiniu.go b/internal/entity/models/qiniu.go index 6f6e16cfe4..bb0f648d7a 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (q *QiniuModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := q.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -187,7 +187,7 @@ func (q *QiniuModel) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -260,7 +260,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (q *QiniuModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -308,7 +308,7 @@ func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -385,39 +385,39 @@ func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message return nil } -func (q *QiniuModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (q *QiniuModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (q *QiniuModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (q *QiniuModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (q *QiniuModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (q *QiniuModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (q *QiniuModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (q *QiniuModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (q *QiniuModel) ParseFile(ctx context.Context, 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()) } -func (q *QiniuModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (q *QiniuModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := q.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -429,7 +429,7 @@ func (q *QiniuModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro baseURL := strings.TrimSuffix(resolvedBaseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, q.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -464,22 +464,22 @@ func (q *QiniuModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return ParseListModel(modelList), nil } -func (q *QiniuModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (q *QiniuModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) CheckConnection(apiConfig *APIConfig) error { - _, err := q.ListModels(apiConfig) +func (q *QiniuModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := q.ListModels(ctx, apiConfig) if err != nil { return err } return nil } -func (q *QiniuModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (q *QiniuModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } -func (q *QiniuModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (q *QiniuModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", q.Name()) } diff --git a/internal/entity/models/qiniu_test.go b/internal/entity/models/qiniu_test.go index a0436a927f..0181d913ab 100644 --- a/internal/entity/models/qiniu_test.go +++ b/internal/entity/models/qiniu_test.go @@ -36,6 +36,7 @@ func TestQiniuToolCalls(t *testing.T) { } func TestQiniuChatStreamRejectsTruncatedResponse(t *testing.T) { + ctx := t.Context() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"choices":[{"delta":{"content":"partial"}}]}` + "\n\n")) @@ -44,13 +45,14 @@ func TestQiniuChatStreamRejectsTruncatedResponse(t *testing.T) { apiKey := "test-key" model := NewQiniuModel(map[string]string{"default": server.URL}, URLSuffix{Chat: "chat/completions"}) - err := model.ChatStreamlyWithSender("model", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, nil, func(_, _ *string) error { return nil }) + err := model.ChatStreamlyWithSender(ctx, "model", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, nil, func(_, _ *string) error { return nil }) if err == nil || !strings.Contains(err.Error(), "stream ended before [DONE] or finish_reason") { t.Fatalf("error = %v", err) } } func TestQiniuCheckConnectionUsesListModels(t *testing.T) { + ctx := t.Context() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method = %s, want GET", r.Method) @@ -67,12 +69,13 @@ func TestQiniuCheckConnectionUsesListModels(t *testing.T) { apiKey := "test-key" model := NewQiniuModel(map[string]string{"default": server.URL}, URLSuffix{Models: "models"}) - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection() error = %v", err) } } func TestQiniuCheckConnectionPropagatesListModelsError(t *testing.T) { + ctx := t.Context() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, `{"message":"invalid api key"}`, http.StatusUnauthorized) })) @@ -80,7 +83,7 @@ func TestQiniuCheckConnectionPropagatesListModelsError(t *testing.T) { apiKey := "invalid-key" model := NewQiniuModel(map[string]string{"default": server.URL}, URLSuffix{Models: "models"}) - err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}) + err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "status 401") { t.Fatalf("CheckConnection() error = %v, want status 401", err) } diff --git a/internal/entity/models/ragcon.go b/internal/entity/models/ragcon.go index be1e6cdfeb..ddf666f4cc 100644 --- a/internal/entity/models/ragcon.go +++ b/internal/entity/models/ragcon.go @@ -127,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (r *RAGconModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -148,7 +148,7 @@ func (r *RAGconModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -223,7 +223,7 @@ func (r *RAGconModel) ChatWithMessages(modelName string, messages []Message, api return chatResponse, nil } -func (r *RAGconModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (r *RAGconModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -250,7 +250,7 @@ func (r *RAGconModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -349,7 +349,7 @@ type ragconEmbeddingResponse struct { } `json:"data"` } -func (r *RAGconModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (r *RAGconModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -380,7 +380,7 @@ func (r *RAGconModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -417,7 +417,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (r *RAGconModel) Rerank(ctx context.Context, 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 } @@ -452,7 +452,7 @@ func (r *RAGconModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -497,7 +497,7 @@ func (r *RAGconModel) Rerank(modelName *string, query string, documents []string } // ListModels returns the model ids visible to the configured API key. -func (r *RAGconModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (r *RAGconModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -509,7 +509,7 @@ func (r *RAGconModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, r.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -541,13 +541,13 @@ func (r *RAGconModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return ParseListModel(modelList), nil } -func (r *RAGconModel) CheckConnection(apiConfig *APIConfig) error { - _, err := r.ListModels(apiConfig) +func (r *RAGconModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := r.ListModels(ctx, apiConfig) return err } // Balance is not exposed by RAGcon's LiteLLM proxy. -func (r *RAGconModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (r *RAGconModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } @@ -623,8 +623,8 @@ 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) +func (r *RAGconModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, responseFormat, err := r.newASRRequest(ctx, modelName, file, apiConfig, asrConfig, false) @@ -650,12 +650,12 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (r *RAGconModel) TranscribeAudioWithSender(ctx context.Context, 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") } - req, responseFormat, err := r.newASRRequest(context.Background(), modelName, file, apiConfig, asrConfig, true) + req, responseFormat, err := r.newASRRequest(ctx, modelName, file, apiConfig, asrConfig, true) if err != nil { return err } @@ -786,8 +786,8 @@ 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) +func (r *RAGconModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, _, err := r.newTTSRequest(ctx, modelName, audioContent, apiConfig, ttsConfig, false) @@ -812,12 +812,12 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (r *RAGconModel) AudioSpeechWithSender(ctx context.Context, 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") } - req, streamFormat, err := r.newTTSRequest(context.Background(), modelName, audioContent, apiConfig, ttsConfig, true) + req, streamFormat, err := r.newTTSRequest(ctx, modelName, audioContent, apiConfig, ttsConfig, true) if err != nil { return err } @@ -843,19 +843,19 @@ 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (r *RAGconModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (r *RAGconModel) ParseFile(ctx context.Context, 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()) } -func (r *RAGconModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (r *RAGconModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *RAGconModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (r *RAGconModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 e30a23465c..04b9414008 100644 --- a/internal/entity/models/ragcon_test.go +++ b/internal/entity/models/ragcon_test.go @@ -82,6 +82,8 @@ func TestRAGconFactory(t *testing.T) { } func TestRAGconChatHappyPath(t *testing.T) { + ctx := t.Context() + srv := newRAGconServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s, want /chat/completions", r.URL.Path) @@ -110,6 +112,7 @@ func TestRAGconChatHappyPath(t *testing.T) { apiKey := "test-key" resp, err := newRAGconForTest(srv.URL).ChatWithMessages( + ctx, "llama-4-maverick", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -130,7 +133,9 @@ func TestRAGconChatHappyPath(t *testing.T) { } func TestRAGconChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() _, err := newRAGconForTest("http://unused").ChatWithMessages( + ctx, "llama-4-maverick", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, @@ -142,8 +147,10 @@ func TestRAGconChatRequiresAPIKey(t *testing.T) { } func TestRAGconChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" _, err := newRAGconForTest("http://unused").ChatWithMessages( + ctx, "llama-4-maverick", []Message{}, &APIConfig{ApiKey: &apiKey}, @@ -155,6 +162,7 @@ func TestRAGconChatRequiresMessages(t *testing.T) { } func TestRAGconChatSurfacesHTTPError(t *testing.T) { + ctx := t.Context() srv := newRAGconServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = io.WriteString(w, `{"error":"invalid key"}`) @@ -163,6 +171,7 @@ func TestRAGconChatSurfacesHTTPError(t *testing.T) { apiKey := "test-key" _, err := newRAGconForTest(srv.URL).ChatWithMessages( + ctx, "llama-4-maverick", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -174,6 +183,7 @@ func TestRAGconChatSurfacesHTTPError(t *testing.T) { } func TestRAGconStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newRAGconServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -195,6 +205,7 @@ func TestRAGconStreamHappyPath(t *testing.T) { var content, reasoning []string var sawDone bool err := newRAGconForTest(srv.URL).ChatStreamlyWithSender( + ctx, "llama-4-maverick", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -228,8 +239,10 @@ func TestRAGconStreamHappyPath(t *testing.T) { } func TestRAGconStreamRejectsNilSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newRAGconForTest("http://unused").ChatStreamlyWithSender( + ctx, "llama-4-maverick", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -243,6 +256,7 @@ func TestRAGconStreamRejectsNilSender(t *testing.T) { } func TestRAGconEmbed(t *testing.T) { + ctx := t.Context() srv := newRAGconServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/embeddings" { t.Errorf("path=%s, want /embeddings", r.URL.Path) @@ -258,7 +272,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, nil) + embeddings, err := newRAGconForTest(srv.URL).Embed(ctx, &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -268,6 +282,7 @@ func TestRAGconEmbed(t *testing.T) { } func TestRAGconRerank(t *testing.T) { + ctx := t.Context() srv := newRAGconServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/rerank" { t.Errorf("path=%s, want /rerank", r.URL.Path) @@ -286,7 +301,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}, nil) + resp, err := newRAGconForTest(srv.URL).Rerank(ctx, &model, "q", []string{"doc0", "doc1"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 2}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -296,6 +311,7 @@ func TestRAGconRerank(t *testing.T) { } func TestRAGconListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := newRAGconServer(t, func(t *testing.T, r *http.Request, _ map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/models" { t.Errorf("path=%s, want /models", r.URL.Path) @@ -306,19 +322,20 @@ func TestRAGconListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" cfg := &APIConfig{ApiKey: &apiKey} - models, err := newRAGconForTest(srv.URL).ListModels(cfg) + models, err := newRAGconForTest(srv.URL).ListModels(ctx, cfg) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "llama-4-maverick,gpt-oss-120b" { t.Errorf("models=%v", models) } - if err := newRAGconForTest(srv.URL).CheckConnection(cfg); err != nil { + if err := newRAGconForTest(srv.URL).CheckConnection(ctx, cfg); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestRAGconTranscribeAudioPostsMultipart(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/audio/transcriptions" { t.Errorf("path=%s, want /audio/transcriptions", r.URL.Path) @@ -352,7 +369,7 @@ func TestRAGconTranscribeAudioPostsMultipart(t *testing.T) { apiKey := "test-key" model := "whisper-1" - resp, err := newRAGconForTest(srv.URL).TranscribeAudio(&model, &audioPath, &APIConfig{ApiKey: &apiKey}, nil, nil) + resp, err := newRAGconForTest(srv.URL).TranscribeAudio(ctx, &model, &audioPath, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("TranscribeAudio: %v", err) } @@ -362,6 +379,7 @@ func TestRAGconTranscribeAudioPostsMultipart(t *testing.T) { } func TestRAGconAudioSpeechPostsJSON(t *testing.T) { + ctx := t.Context() srv := newRAGconServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/audio/speech" { t.Errorf("path=%s, want /audio/speech", r.URL.Path) @@ -376,7 +394,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"}}, nil) + resp, err := newRAGconForTest(srv.URL).AudioSpeech(ctx, &model, &text, &APIConfig{ApiKey: &apiKey}, &TTSConfig{Params: map[string]interface{}{"voice": "alloy"}}, nil) if err != nil { t.Fatalf("AudioSpeech: %v", err) } @@ -386,22 +404,23 @@ func TestRAGconAudioSpeechPostsJSON(t *testing.T) { } func TestRAGconUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() r := newRAGconForTest("http://unused") model := "llama-4-maverick" - if _, err := r.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := r.Balance(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := r.OCRFile(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := r.ParseFile(ctx, &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") { + if _, err := r.ListTasks(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ListTasks: expected no such method, got %v", err) } - if _, err := r.ShowTask("t1", &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := r.ShowTask(ctx, "t1", &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("ShowTask: expected no such method, got %v", err) } } diff --git a/internal/entity/models/reasoning_family_provider_test.go b/internal/entity/models/reasoning_family_provider_test.go index 2687429925..37a9b21aaf 100644 --- a/internal/entity/models/reasoning_family_provider_test.go +++ b/internal/entity/models/reasoning_family_provider_test.go @@ -47,6 +47,7 @@ func newReasoningFamilyChatServer(t *testing.T, handler func(t *testing.T, body } func TestGiteeChatExtractsQwenThinkingFromInlineContent(t *testing.T) { + ctx := t.Context() srv := newReasoningFamilyChatServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "qwen3-8b" { t.Errorf("model=%v, want qwen3-8b", body["model"]) @@ -72,6 +73,7 @@ func TestGiteeChatExtractsQwenThinkingFromInlineContent(t *testing.T) { map[string]string{"default": srv.URL}, URLSuffix{Chat: "chat/completions"}, ).ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -85,6 +87,7 @@ func TestGiteeChatExtractsQwenThinkingFromInlineContent(t *testing.T) { } func TestSiliconflowChatExtractsProviderPrefixedQwenThinkingFromInlineContent(t *testing.T) { + ctx := t.Context() srv := newReasoningFamilyChatServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "qwen/qwen3-8b" { t.Errorf("model=%v, want qwen/qwen3-8b", body["model"]) @@ -110,6 +113,7 @@ func TestSiliconflowChatExtractsProviderPrefixedQwenThinkingFromInlineContent(t map[string]string{"default": srv.URL}, URLSuffix{Chat: "chat/completions"}, ).ChatWithMessages( + ctx, "qwen/qwen3-8b", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/replicate.go b/internal/entity/models/replicate.go index b208bd4c8c..22f7be9327 100644 --- a/internal/entity/models/replicate.go +++ b/internal/entity/models/replicate.go @@ -325,7 +325,7 @@ func (r *ReplicateModel) waitForPrediction(ctx context.Context, prediction *repl } } -func (r *ReplicateModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (r *ReplicateModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -341,7 +341,7 @@ func (r *ReplicateModel) ChatWithMessages(modelName string, messages []Message, return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() prediction, err := r.createPrediction(ctx, url, version, replicateInputFromMessages(messages, chatModelConfig), false, *apiConfig.ApiKey, true) @@ -364,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (r *ReplicateModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -387,12 +387,12 @@ func (r *ReplicateModel) ChatStreamlyWithSender(modelName string, messages []Mes return err } - prediction, err := r.createPrediction(context.Background(), url, version, replicateInputFromMessages(messages, chatModelConfig), true, *apiConfig.ApiKey, false) + prediction, err := r.createPrediction(ctx, url, version, replicateInputFromMessages(messages, chatModelConfig), true, *apiConfig.ApiKey, false) if err != nil { return err } if prediction.URLs.Stream == "" { - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() prediction, err = r.waitForPrediction(ctx, prediction, *apiConfig.ApiKey) if err != nil { @@ -411,11 +411,11 @@ func (r *ReplicateModel) ChatStreamlyWithSender(modelName string, messages []Mes return sender(&endOfStream, nil) } - return r.readPredictionStream(prediction.URLs.Stream, *apiConfig.ApiKey, sender) + return r.readPredictionStream(ctx, prediction.URLs.Stream, *apiConfig.ApiKey, sender) } -func (r *ReplicateModel) readPredictionStream(url string, apiKey string, sender func(*string, *string) error) error { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) +func (r *ReplicateModel) readPredictionStream(ctx context.Context, url string, apiKey string, sender func(*string, *string) error) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -499,7 +499,7 @@ func dispatchReplicateSSEEvent(event replicateSSEEvent, sender func(*string, *st } } -func (r *ReplicateModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (r *ReplicateModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -509,7 +509,7 @@ func (r *ReplicateModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -567,8 +567,8 @@ func (r *ReplicateModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(modelList), nil } -func (r *ReplicateModel) CheckConnection(apiConfig *APIConfig) error { - _, err := r.ListModels(apiConfig) +func (r *ReplicateModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := r.ListModels(ctx, apiConfig) return err } @@ -674,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (r *ReplicateModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := r.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -696,7 +696,7 @@ func (r *ReplicateModel) Embed(modelName *string, texts []string, apiConfig *API return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() prediction, err := r.createPrediction(ctx, url, version, input, false, *apiConfig.ApiKey, true) @@ -765,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (r *ReplicateModel) Rerank(ctx context.Context, 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 } @@ -787,7 +787,7 @@ func (r *ReplicateModel) Rerank(modelName *string, query string, documents []str return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() prediction, err := r.createPrediction(ctx, url, version, input, false, *apiConfig.ApiKey, true) @@ -829,38 +829,38 @@ func (r *ReplicateModel) Rerank(modelName *string, query string, documents []str return &RerankResponse{Data: results}, nil } -func (r *ReplicateModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (r *ReplicateModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (r *ReplicateModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (r *ReplicateModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (r *ReplicateModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (r *ReplicateModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (r *ReplicateModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (r *ReplicateModel) ParseFile(ctx context.Context, 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()) } -func (r *ReplicateModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (r *ReplicateModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", r.Name()) } -func (r *ReplicateModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (r *ReplicateModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 020a4fb91a..489b86b33d 100644 --- a/internal/entity/models/replicate_test.go +++ b/internal/entity/models/replicate_test.go @@ -122,7 +122,9 @@ func TestReplicateOfficialChatHappyPath(t *testing.T) { apiKey := "test-key" maxTokens := 128 stop := []string{"END"} + ctx := t.Context() resp, err := newReplicateForTest(srv.URL).ChatWithMessages( + ctx, "meta/meta-llama-3-70b-instruct", []Message{{Role: "system", Content: "be helpful"}, {Role: "user", Content: "hello"}}, &APIConfig{ApiKey: &apiKey}, @@ -161,9 +163,10 @@ func TestReplicateCommunityChatUsesVersionEndpoint(t *testing.T) { }) })) defer srv.Close() - + ctx := t.Context() apiKey := "test-key" resp, err := newReplicateForTest(srv.URL).ChatWithMessages( + ctx, version, []Message{{Role: "user", Content: "hello"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -201,9 +204,10 @@ func TestReplicateChatPollsUntilSucceeded(t *testing.T) { } })) defer srv.Close() - + ctx := t.Context() apiKey := "test-key" resp, err := newReplicateForTest(srv.URL).ChatWithMessages( + ctx, "meta/meta-llama-3-70b-instruct", []Message{{Role: "user", Content: "hello"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -256,10 +260,11 @@ func TestReplicateStreamHappyPath(t *testing.T) { }) })) defer apiSrv.Close() - + ctx := t.Context() apiKey := "test-key" var chunks []string err := newReplicateForTest(apiSrv.URL).ChatStreamlyWithSender( + ctx, "meta/meta-llama-3-70b-instruct", []Message{{Role: "user", Content: "hello"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -278,6 +283,7 @@ func TestReplicateStreamHappyPath(t *testing.T) { } func TestReplicateListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/models" { t.Errorf("path=%s", r.URL.Path) @@ -296,28 +302,29 @@ func TestReplicateListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" model := newReplicateForTest(srv.URL) - models, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "meta/meta-llama-3-70b-instruct,replicate/hello-world" { t.Errorf("models=%v", models) } - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestReplicateUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newReplicateForTest("http://unused") 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, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := m.Rerank(ctx, 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" - if _, err := m.Balance(&APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Balance(ctx, &APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance error=%v", err) } } diff --git a/internal/entity/models/siliconflow.go b/internal/entity/models/siliconflow.go index cb6196743f..424a027982 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (s *SiliconflowModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -132,7 +132,7 @@ func (s *SiliconflowModel) ChatWithMessages(modelName string, messages []Message return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -213,7 +213,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (s *SiliconflowModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -275,7 +275,7 @@ func (s *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []M return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -380,7 +380,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (s *SiliconflowModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -417,7 +417,7 @@ func (s *SiliconflowModel) Embed(modelName *string, texts []string, apiConfig *A return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -462,7 +462,7 @@ func (s *SiliconflowModel) Embed(modelName *string, texts []string, apiConfig *A return embeddings, nil } -func (s *SiliconflowModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (s *SiliconflowModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -481,7 +481,7 @@ func (s *SiliconflowModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -526,7 +526,7 @@ type siliconflowBalanceResponse struct { } `json:"data"` } -func (s *SiliconflowModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (s *SiliconflowModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -538,7 +538,7 @@ func (s *SiliconflowModel) Balance(apiConfig *APIConfig) (map[string]interface{} url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), s.baseModel.URLSuffix.Balance) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -595,8 +595,8 @@ func (s *SiliconflowModel) Balance(apiConfig *APIConfig) (map[string]interface{} }, nil } -func (s *SiliconflowModel) CheckConnection(apiConfig *APIConfig) error { - _, err := s.ListModels(apiConfig) +func (s *SiliconflowModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := s.ListModels(ctx, apiConfig) if err != nil { return err } @@ -630,7 +630,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (s *SiliconflowModel) Rerank(ctx context.Context, 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 } @@ -667,7 +667,7 @@ func (s *SiliconflowModel) Rerank(modelName *string, query string, documents []s } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(resolvedBaseURL, "/"), s.baseModel.URLSuffix.Rerank) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData))) @@ -709,7 +709,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (s *SiliconflowModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -790,7 +790,7 @@ func (s *SiliconflowModel) TranscribeAudio(modelName *string, file *string, apiC } // build request - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, &body) @@ -830,12 +830,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (s *SiliconflowModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (s *SiliconflowModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -870,7 +870,7 @@ func (s *SiliconflowModel) AudioSpeech(modelName *string, audioContent *string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -899,7 +899,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (s *SiliconflowModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -934,7 +934,7 @@ func (s *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -979,19 +979,19 @@ func (s *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent } // OCRFile OCR file -func (s *SiliconflowModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (s *SiliconflowModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (s *SiliconflowModel) ParseFile(ctx context.Context, 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()) } -func (s *SiliconflowModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (s *SiliconflowModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } -func (s *SiliconflowModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (s *SiliconflowModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } diff --git a/internal/entity/models/siliconflow_test.go b/internal/entity/models/siliconflow_test.go index 28bb872478..d40863f1b6 100644 --- a/internal/entity/models/siliconflow_test.go +++ b/internal/entity/models/siliconflow_test.go @@ -42,7 +42,8 @@ func TestSiliconflowChatRejectsMissingContent(t *testing.T) { apiKey := "test-key" model := NewSiliconflowModel(map[string]string{"default": server.URL}, URLSuffix{Chat: "chat/completions"}) - if _, err := model.ChatWithMessages("model", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil { + ctx := t.Context() + if _, err := model.ChatWithMessages(ctx, "model", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil); err == nil { t.Fatal("expected missing content error") } } diff --git a/internal/entity/models/sse_scanner_buffer_test.go b/internal/entity/models/sse_scanner_buffer_test.go index e2aee7de24..89a2b1410b 100644 --- a/internal/entity/models/sse_scanner_buffer_test.go +++ b/internal/entity/models/sse_scanner_buffer_test.go @@ -1,6 +1,7 @@ package models import ( + "context" "io" "net/http" "net/http/httptest" @@ -13,7 +14,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error + ChatStreamlyWithSender(ctx context.Context, 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 @@ -63,6 +64,7 @@ func TestChatStreamLargeChunkNotTruncated(t *testing.T) { {"Jiekou.AI", build(func(b map[string]string, s URLSuffix) chatStreamer { return NewJieKouAIModel(b, s) })}, } + ctx := t.Context() for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { @@ -72,6 +74,7 @@ func TestChatStreamLargeChunkNotTruncated(t *testing.T) { apiKey := "test-key" var got strings.Builder err := tc.build(srv.URL).ChatStreamlyWithSender( + ctx, "test-model", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/stepfun.go b/internal/entity/models/stepfun.go index 7b8f1004a8..512e435aae 100644 --- a/internal/entity/models/stepfun.go +++ b/internal/entity/models/stepfun.go @@ -53,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (s *StepFunModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -103,7 +103,7 @@ func (s *StepFunModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -162,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (s *StepFunModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -220,7 +220,7 @@ func (s *StepFunModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -285,12 +285,12 @@ 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (s *StepFunModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } // ListModels returns the list of model ids visible to the API key. -func (s *StepFunModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (s *StepFunModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -302,7 +302,7 @@ func (s *StepFunModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, s.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -340,13 +340,13 @@ func (s *StepFunModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er } // Balance is not exposed by the StepFun API, so this returns "no such method". -func (s *StepFunModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (s *StepFunModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection runs a lightweight ListModels call to verify the API key. -func (s *StepFunModel) CheckConnection(apiConfig *APIConfig) error { - _, err := s.ListModels(apiConfig) +func (s *StepFunModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := s.ListModels(ctx, apiConfig) if err != nil { return err } @@ -355,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (s *StepFunModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (s *StepFunModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (s *StepFunModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (s *StepFunModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := s.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -431,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (s *StepFunModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -513,19 +513,19 @@ 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (s *StepFunModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (s *StepFunModel) ParseFile(ctx context.Context, 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()) } -func (s *StepFunModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (s *StepFunModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", s.Name()) } -func (s *StepFunModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (s *StepFunModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 2b2097970d..d41e3bbcfc 100644 --- a/internal/entity/models/timeout_test.go +++ b/internal/entity/models/timeout_test.go @@ -59,6 +59,7 @@ func newTimeoutTestGroq(baseURL string) *GroqModel { // nonStreamCallTimeout (the old 120s-wall footgun, just relocated), the // context would cancel mid-stream and this test would fail. func TestStreamNotTruncatedByNonStreamTimeout(t *testing.T) { + ctx := t.Context() // Stream emits for ~240ms, far past the 60ms non-stream deadline but well // inside the 10s stream deadline. withTestTimeouts(t, 60*time.Millisecond, 10*time.Second) @@ -86,6 +87,7 @@ func TestStreamNotTruncatedByNonStreamTimeout(t *testing.T) { apiKey := "test-key" var got strings.Builder err := newTimeoutTestGroq(srv.URL).ChatStreamlyWithSender( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -116,6 +118,7 @@ func TestStreamNotTruncatedByNonStreamTimeout(t *testing.T) { // watchdog so a broken timeout surfaces as a direct test failure // rather than relying on the package's global test timeout. func TestNonStreamHonorsShortDeadline(t *testing.T) { + ctx := t.Context() withTestTimeouts(t, 100*time.Millisecond, 10*time.Second) var hits atomic.Int32 @@ -130,6 +133,7 @@ func TestNonStreamHonorsShortDeadline(t *testing.T) { done := make(chan error, 1) go func() { _, err := newTimeoutTestGroq(srv.URL).ChatWithMessages( + ctx, "llama-3.3-70b-versatile", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/togetherai.go b/internal/entity/models/togetherai.go index e137f543d4..cf880f8412 100644 --- a/internal/entity/models/togetherai.go +++ b/internal/entity/models/togetherai.go @@ -127,7 +127,7 @@ type togetherAIChatResponse struct { FinishReason string `json:"finish_reason"` } -func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (t *TogetherAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -148,7 +148,7 @@ func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -194,7 +194,7 @@ func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, }, nil } -func (t *TogetherAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TogetherAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -222,7 +222,7 @@ func (t *TogetherAIModel) ChatStreamlyWithSender(modelName string, messages []Me return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -289,7 +289,7 @@ type togetherAIModelInfo struct { ID string `json:"id"` } -func (t *TogetherAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (t *TogetherAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -301,7 +301,7 @@ func (t *TogetherAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, t.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -333,8 +333,8 @@ func (t *TogetherAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(ModelList{Models: result}), nil } -func (t *TogetherAIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := t.ListModels(apiConfig) +func (t *TogetherAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := t.ListModels(ctx, apiConfig) return err } @@ -350,7 +350,7 @@ type togetherAIEmbeddingResponse struct { Object string `json:"object"` } -func (t *TogetherAIModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (t *TogetherAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -383,7 +383,7 @@ func (t *TogetherAIModel) Embed(modelName *string, texts []string, apiConfig *AP return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -438,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (t *TogetherAIModel) Rerank(ctx context.Context, 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 } @@ -516,11 +516,11 @@ func (t *TogetherAIModel) Rerank(modelName *string, query string, documents []st return &rerankResponse, nil } -func (t *TogetherAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (t *TogetherAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TogetherAIModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (t *TogetherAIModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -623,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TogetherAIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (t *TogetherAIModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -687,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TogetherAIModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -773,18 +773,18 @@ func (t *TogetherAIModel) AudioSpeechWithSender(modelName *string, audioContent return nil } -func (t *TogetherAIModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (t *TogetherAIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (t *TogetherAIModel) ParseFile(ctx context.Context, 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()) } -func (t *TogetherAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (t *TogetherAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TogetherAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (t *TogetherAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 f9f5a7b79d..6ea82d6717 100644 --- a/internal/entity/models/togetherai_test.go +++ b/internal/entity/models/togetherai_test.go @@ -60,6 +60,7 @@ func TestTogetherAIFactory(t *testing.T) { } func TestTogetherAIChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newTogetherAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -91,6 +92,7 @@ func TestTogetherAIChatHappyPath(t *testing.T) { stop := []string{"END"} effort := "high" resp, err := newTogetherAIForTest(srv.URL).ChatWithMessages( + ctx, "openai/gpt-oss-20b", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -109,6 +111,7 @@ func TestTogetherAIChatHappyPath(t *testing.T) { } func TestTogetherAIChatForwardsReasoningEnabled(t *testing.T) { + ctx := t.Context() srv := newTogetherAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "Qwen/Qwen3.5-9B" { t.Errorf("model=%v", body["model"]) @@ -136,6 +139,7 @@ func TestTogetherAIChatForwardsReasoningEnabled(t *testing.T) { apiKey := "test-key" thinking := false resp, err := newTogetherAIForTest(srv.URL).ChatWithMessages( + ctx, "Qwen/Qwen3.5-9B", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -151,14 +155,16 @@ func TestTogetherAIChatForwardsReasoningEnabled(t *testing.T) { } func TestTogetherAIChatRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newTogetherAIForTest("http://unused").ChatWithMessages("", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newTogetherAIForTest("http://unused").ChatWithMessages(ctx, "", []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 TestTogetherAIStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newTogetherAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.URL.Path != "/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -182,6 +188,7 @@ func TestTogetherAIStreamHappyPath(t *testing.T) { var content []string var reasoning []string err := newTogetherAIForTest(srv.URL).ChatStreamlyWithSender( + ctx, "meta-llama/Llama-3.3-70B-Instruct-Turbo", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -207,6 +214,7 @@ func TestTogetherAIStreamHappyPath(t *testing.T) { } func TestTogetherAIStreamStopsOnRootFinishReason(t *testing.T) { + ctx := t.Context() srv := newTogetherAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") _, _ = io.WriteString(w, @@ -218,6 +226,7 @@ func TestTogetherAIStreamStopsOnRootFinishReason(t *testing.T) { apiKey := "test-key" var chunks []string err := newTogetherAIForTest(srv.URL).ChatStreamlyWithSender( + ctx, "meta-llama/Llama-3.3-70B-Instruct-Turbo", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -237,6 +246,7 @@ func TestTogetherAIStreamStopsOnRootFinishReason(t *testing.T) { } func TestTogetherAIListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := newTogetherAIServer(t, func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s", r.Method) @@ -253,28 +263,29 @@ func TestTogetherAIListModelsAndCheckConnection(t *testing.T) { apiKey := "test-key" model := newTogetherAIForTest(srv.URL) - models, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "openai/gpt-oss-20b,meta-llama/Llama-3.3-70B-Instruct-Turbo" { t.Errorf("models=%v", models) } - if err := model.CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := model.CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestTogetherAIUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newTogetherAIForTest("http://unused") 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, nil); err != nil { + if _, err := m.Rerank(ctx, 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" - if _, err := m.Balance(&APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Balance(ctx, &APIConfig{ApiKey: &apiKey}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance error=%v", err) } } diff --git a/internal/entity/models/tokenhub.go b/internal/entity/models/tokenhub.go index a01b17d342..19cf73933d 100644 --- a/internal/entity/models/tokenhub.go +++ b/internal/entity/models/tokenhub.go @@ -59,7 +59,7 @@ func validateTokenHubChatRequest(modelName string, messages []Message) error { return nil } -func (t *TokenHubModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (t *TokenHubModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -113,7 +113,7 @@ func (t *TokenHubModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -184,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TokenHubModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -249,7 +249,7 @@ func (t *TokenHubModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -315,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (t *TokenHubModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -343,7 +343,7 @@ func (t *TokenHubModel) Embed(modelName *string, texts []string, apiConfig *APIC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -395,35 +395,35 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (t *TokenHubModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (t *TokenHubModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TokenHubModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (t *TokenHubModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TokenHubModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (t *TokenHubModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (t *TokenHubModel) ParseFile(ctx context.Context, 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()) } -func (t *TokenHubModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (t *TokenHubModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -434,7 +434,7 @@ func (t *TokenHubModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e } url := fmt.Sprintf("%s/%s", resolvedBaseURL, t.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -490,19 +490,19 @@ func (t *TokenHubModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, e return ParseListModel(modelList), nil } -func (t *TokenHubModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (t *TokenHubModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) CheckConnection(apiConfig *APIConfig) error { - _, err := t.ListModels(apiConfig) +func (t *TokenHubModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := t.ListModels(ctx, apiConfig) return err } -func (t *TokenHubModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (t *TokenHubModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s no such method", t.Name()) } -func (t *TokenHubModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (t *TokenHubModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 ccdf0f8fd0..e91068d580 100644 --- a/internal/entity/models/tokenhub_test.go +++ b/internal/entity/models/tokenhub_test.go @@ -98,6 +98,7 @@ func TestTokenHubFactory(t *testing.T) { } func TestTokenHubChatWithMessagesForcesNonStreaming(t *testing.T) { + ctx := t.Context() srv := newTokenHubServer(t, http.MethodPost, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["stream"] != false { t.Errorf("stream=%v, want false", body["stream"]) @@ -116,6 +117,7 @@ func TestTokenHubChatWithMessagesForcesNonStreaming(t *testing.T) { apiKey := "test-key" stream := true resp, err := newTokenHubForTest(srv.URL).ChatWithMessages( + ctx, "gpt-4o-mini", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -134,21 +136,24 @@ 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, nil) + ctx := t.Context() + _, err := newTokenHubForTest("http://unused").ChatWithMessages(ctx, "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) } } func TestTokenHubChatRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - _, err := newTokenHubForTest("http://unused").ChatWithMessages(" ", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newTokenHubForTest("http://unused").ChatWithMessages(ctx, " ", []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) } } func TestTokenHubStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newTokenHubSSEServer(t, "/chat/completions", strings.Join([]string{ `data: {"choices":[{"delta":{"reasoning_content":"thinking"}}]}`, `data: {"choices":[{"delta":{"content":"hello"}}]}`, @@ -161,6 +166,7 @@ func TestTokenHubStreamHappyPath(t *testing.T) { var content []string var reasoning []string err := newTokenHubForTest(srv.URL).ChatStreamlyWithSender( + ctx, "gpt-4o-mini", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -188,9 +194,11 @@ func TestTokenHubStreamHappyPath(t *testing.T) { } func TestTokenHubStreamRejectsFalseStreamConfig(t *testing.T) { + ctx := t.Context() apiKey := "test-key" stream := false err := newTokenHubForTest("http://unused").ChatStreamlyWithSender( + ctx, "gpt-4o-mini", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -204,8 +212,10 @@ func TestTokenHubStreamRejectsFalseStreamConfig(t *testing.T) { } func TestTokenHubStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newTokenHubForTest("http://unused").ChatStreamlyWithSender( + ctx, "gpt-4o-mini", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -219,7 +229,9 @@ func TestTokenHubStreamRequiresSender(t *testing.T) { } func TestTokenHubStreamRequiresAPIKey(t *testing.T) { + ctx := t.Context() err := newTokenHubForTest("http://unused").ChatStreamlyWithSender( + ctx, "gpt-4o-mini", []Message{{Role: "user", Content: "ping"}}, &APIConfig{}, @@ -233,8 +245,10 @@ func TestTokenHubStreamRequiresAPIKey(t *testing.T) { } func TestTokenHubStreamRequiresModelName(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newTokenHubForTest("http://unused").ChatStreamlyWithSender( + ctx, " ", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -248,6 +262,7 @@ func TestTokenHubStreamRequiresModelName(t *testing.T) { } func TestTokenHubEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newTokenHubServer(t, http.MethodPost, "/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "text-embedding-3-small" { t.Errorf("model=%v", body["model"]) @@ -269,7 +284,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, nil) + embeddings, err := newTokenHubForTest(srv.URL).Embed(ctx, &model, []string{"a", "b"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -279,20 +294,22 @@ func TestTokenHubEmbedHappyPath(t *testing.T) { } func TestTokenHubEmbedValidatesInputs(t *testing.T) { + ctx := t.Context() apiKey := "test-key" - if embeddings, err := newTokenHubForTest("http://unused").Embed(nil, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil || len(embeddings) != 0 { + if embeddings, err := newTokenHubForTest("http://unused").Embed(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := newTokenHubForTest("http://unused").Embed(ctx, 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, nil); err == nil || !strings.Contains(err.Error(), "api key is required") { + if _, err := newTokenHubForTest("http://unused").Embed(ctx, &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) } } func TestTokenHubListModelsHappyPathSkipsMalformedItems(t *testing.T) { + ctx := t.Context() srv := newTokenHubServer(t, http.MethodGet, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []interface{}{ @@ -306,7 +323,7 @@ func TestTokenHubListModelsHappyPathSkipsMalformedItems(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newTokenHubForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newTokenHubForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -317,7 +334,8 @@ func TestTokenHubListModelsHappyPathSkipsMalformedItems(t *testing.T) { } func TestTokenHubListModelsValidatesResponseAndAPIKey(t *testing.T) { - if _, err := newTokenHubForTest("http://unused").ListModels(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "api key is required") { + ctx := t.Context() + if _, err := newTokenHubForTest("http://unused").ListModels(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "api key is required") { t.Fatalf("expected api-key error, got %v", err) } @@ -327,7 +345,7 @@ func TestTokenHubListModelsValidatesResponseAndAPIKey(t *testing.T) { defer srv.Close() apiKey := "test-key" - _, err := newTokenHubForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := newTokenHubForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "invalid models list format") { t.Fatalf("expected invalid-format error, got %v", err) } diff --git a/internal/entity/models/tokenpony.go b/internal/entity/models/tokenpony.go index 8c2fcead5e..63f304c755 100644 --- a/internal/entity/models/tokenpony.go +++ b/internal/entity/models/tokenpony.go @@ -52,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (t *TokenPonyModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -101,7 +101,7 @@ func (t *TokenPonyModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -162,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TokenPonyModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -218,7 +218,7 @@ func (t *TokenPonyModel) ChatStreamlyWithSender(modelName string, messages []Mes return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -284,7 +284,7 @@ func (t *TokenPonyModel) ChatStreamlyWithSender(modelName string, messages []Mes return nil } -func (t *TokenPonyModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (t *TokenPonyModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := t.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -296,7 +296,7 @@ func (t *TokenPonyModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, t.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -332,51 +332,51 @@ func (t *TokenPonyModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, } // CheckConnection verifies the API key by calling ListModels. -func (t *TokenPonyModel) CheckConnection(apiConfig *APIConfig) error { - _, err := t.ListModels(apiConfig) +func (t *TokenPonyModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := t.ListModels(ctx, apiConfig) return err } -func (t *TokenPonyModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (t *TokenPonyModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (t *TokenPonyModel) Rerank(ctx context.Context, 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 *TokenPonyModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (t *TokenPonyModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (t *TokenPonyModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TokenPonyModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (t *TokenPonyModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (t *TokenPonyModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (t *TokenPonyModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (t *TokenPonyModel) ParseFile(ctx context.Context, 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()) } -func (t *TokenPonyModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (t *TokenPonyModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", t.Name()) } -func (t *TokenPonyModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (t *TokenPonyModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 5f1094f40a..d8b3674faf 100644 --- a/internal/entity/models/tokenpony_test.go +++ b/internal/entity/models/tokenpony_test.go @@ -92,6 +92,7 @@ func TestTokenPonyFactory(t *testing.T) { } func TestTokenPonyChatHappyPath(t *testing.T) { + ctx := t.Context() srv := newTokenPonyServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "qwen3-32b" { t.Errorf("model=%v", body["model"]) @@ -120,6 +121,7 @@ func TestTokenPonyChatHappyPath(t *testing.T) { mt := 64 temp := 0.3 resp, err := newTokenPonyForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -138,6 +140,7 @@ func TestTokenPonyChatHappyPath(t *testing.T) { } func TestTokenPonyChatNoReasoning(t *testing.T) { + ctx := t.Context() srv := newTokenPonyServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -149,6 +152,7 @@ func TestTokenPonyChatNoReasoning(t *testing.T) { apiKey := "test-key" resp, err := newTokenPonyForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-8b", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -164,7 +168,9 @@ func TestTokenPonyChatNoReasoning(t *testing.T) { } func TestTokenPonyChatRequiresAPIKey(t *testing.T) { + ctx := t.Context() _, err := newTokenPonyForTest("http://unused").ChatWithMessages( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) @@ -174,15 +180,17 @@ func TestTokenPonyChatRequiresAPIKey(t *testing.T) { } func TestTokenPonyChatRequiresMessages(t *testing.T) { + ctx := t.Context() apiKey := "test-key" _, err := newTokenPonyForTest("http://unused").ChatWithMessages( - "qwen3-32b", nil, &APIConfig{ApiKey: &apiKey}, nil, nil) + ctx, "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) } } func TestTokenPonyChatPropagatesHTTPError(t *testing.T) { + ctx := t.Context() srv := newTokenPonyServer(t, "/chat/completions", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"bad key"}`)) @@ -191,6 +199,7 @@ func TestTokenPonyChatPropagatesHTTPError(t *testing.T) { apiKey := "test-key" _, err := newTokenPonyForTest(srv.URL).ChatWithMessages( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) @@ -200,6 +209,7 @@ func TestTokenPonyChatPropagatesHTTPError(t *testing.T) { } func TestTokenPonyStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newTokenPonySSEServer(t, "/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant"}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"content":"Hello"}}]}`+"\n"+ @@ -212,6 +222,7 @@ func TestTokenPonyStreamHappyPath(t *testing.T) { var chunks []string var sawDone bool err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -238,6 +249,7 @@ func TestTokenPonyStreamHappyPath(t *testing.T) { } func TestTokenPonyStreamSplitsReasoning(t *testing.T) { + ctx := t.Context() srv := newTokenPonySSEServer(t, "/chat/completions", `data: {"choices":[{"index":0,"delta":{"role":"assistant"}}]}`+"\n"+ `data: {"choices":[{"index":0,"delta":{"reasoning_content":"step 1. "}}]}`+"\n"+ @@ -250,6 +262,7 @@ func TestTokenPonyStreamSplitsReasoning(t *testing.T) { apiKey := "test-key" var content, reasoning []string err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( + ctx, "deepseek-r1-0528", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -277,9 +290,11 @@ func TestTokenPonyStreamSplitsReasoning(t *testing.T) { } func TestTokenPonyStreamRejectsExplicitFalse(t *testing.T) { + ctx := t.Context() apiKey := "test-key" stream := false err := newTokenPonyForTest("http://unused").ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, @@ -292,8 +307,10 @@ func TestTokenPonyStreamRejectsExplicitFalse(t *testing.T) { } func TestTokenPonyStreamRequiresSender(t *testing.T) { + ctx := t.Context() apiKey := "test-key" err := newTokenPonyForTest("http://unused").ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, nil) @@ -303,6 +320,7 @@ func TestTokenPonyStreamRequiresSender(t *testing.T) { } func TestTokenPonyStreamFailsWithoutTerminal(t *testing.T) { + ctx := t.Context() srv := newTokenPonySSEServer(t, "/chat/completions", `data: {"choices":[{"delta":{"content":"half"}}]}`+"\n", ) @@ -310,6 +328,7 @@ func TestTokenPonyStreamFailsWithoutTerminal(t *testing.T) { apiKey := "test-key" err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -320,6 +339,7 @@ func TestTokenPonyStreamFailsWithoutTerminal(t *testing.T) { } func TestTokenPonyStreamRejectsMalformedFrame(t *testing.T) { + ctx := t.Context() srv := newTokenPonySSEServer(t, "/chat/completions", `data: {"choices":[{"delta":{"content":"ok"}}]}`+"\n"+ `data: {oops not json}`+"\n", @@ -328,6 +348,7 @@ func TestTokenPonyStreamRejectsMalformedFrame(t *testing.T) { apiKey := "test-key" err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -344,8 +365,10 @@ func TestTokenPonyStreamSurfacesUpstreamError(t *testing.T) { ) defer srv.Close() + ctx := t.Context() apiKey := "test-key" err := newTokenPonyForTest(srv.URL).ChatStreamlyWithSender( + ctx, "qwen3-32b", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, @@ -359,6 +382,7 @@ func TestTokenPonyStreamSurfacesUpstreamError(t *testing.T) { } func TestTokenPonyListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newTokenPonyServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -371,7 +395,7 @@ func TestTokenPonyListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newTokenPonyForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newTokenPonyForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -382,13 +406,15 @@ func TestTokenPonyListModelsHappyPath(t *testing.T) { } func TestTokenPonyListModelsRequiresAPIKey(t *testing.T) { - _, err := newTokenPonyForTest("http://unused").ListModels(&APIConfig{}) + ctx := t.Context() + _, err := newTokenPonyForTest("http://unused").ListModels(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Errorf("expected api-key error, got %v", err) } } func TestTokenPonyCheckConnectionDelegatesToListModels(t *testing.T) { + ctx := t.Context() srv := newTokenPonyServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{{"id": "qwen3-32b"}}, @@ -397,12 +423,13 @@ func TestTokenPonyCheckConnectionDelegatesToListModels(t *testing.T) { defer srv.Close() apiKey := "test-key" - if err := newTokenPonyForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newTokenPonyForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Errorf("CheckConnection: %v", err) } } func TestTokenPonyCheckConnectionPropagatesError(t *testing.T) { + ctx := t.Context() srv := newTokenPonyServer(t, "/models", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"bad key"}`)) @@ -410,40 +437,43 @@ func TestTokenPonyCheckConnectionPropagatesError(t *testing.T) { defer srv.Close() apiKey := "test-key" - err := newTokenPonyForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}) + err := newTokenPonyForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "401") { t.Errorf("expected 401 propagated, got %v", err) } } func TestTokenPonyBaseURLForRegionUnknown(t *testing.T) { + ctx := t.Context() m := newTokenPonyForTest("http://unused") apiKey := "test-key" region := "missing" - _, err := m.ListModels(&APIConfig{ApiKey: &apiKey, Region: ®ion}) + _, err := m.ListModels(ctx, &APIConfig{ApiKey: &apiKey, Region: ®ion}) if err == nil || !strings.Contains(err.Error(), "no base URL configured") { t.Errorf("expected base-URL error, got %v", err) } } func TestTokenPonyEmbedReturnsNoSuchMethod(t *testing.T) { + ctx := t.Context() model := "x" - _, err := newTokenPonyForTest("http://unused").Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) + _, err := newTokenPonyForTest("http://unused").Embed(ctx, &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) } } func TestTokenPonyAudioOCRReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() m := newTokenPonyForTest("http://unused") model := "x" - if _, err := m.TranscribeAudio(&model, &model, &APIConfig{}, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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/tool_call_test.go b/internal/entity/models/tool_call_test.go index 6bedbd911c..c13a536dbb 100644 --- a/internal/entity/models/tool_call_test.go +++ b/internal/entity/models/tool_call_test.go @@ -24,6 +24,7 @@ import ( ) func testNonStreamingToolCall(t *testing.T, modelName, path string, newDriver func(string) ModelDriver) { + ctx := t.Context() t.Helper() var requestBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -39,6 +40,7 @@ func testNonStreamingToolCall(t *testing.T, modelName, path string, newDriver fu apiKey, toolChoice := "test-key", "required" response, err := newDriver(server.URL).ChatWithMessages( + ctx, modelName, []Message{ {Role: "assistant", ToolCalls: []map[string]interface{}{{"id": "call-1"}}}, @@ -65,6 +67,7 @@ func testNonStreamingToolCall(t *testing.T, modelName, path string, newDriver fu } func testStreamingToolCall(t *testing.T, modelName, path string, newDriver func(string) ModelDriver) { + ctx := t.Context() t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != path { @@ -86,6 +89,7 @@ data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ra ToolCallsResult: &[]map[string]interface{}{{"id": "stale"}}, } err := newDriver(server.URL).ChatStreamlyWithSender( + ctx, modelName, []Message{{Role: "user", Content: "find ragflow"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/entity/models/types.go b/internal/entity/models/types.go index 6987cfd8b6..a16864243a 100644 --- a/internal/entity/models/types.go +++ b/internal/entity/models/types.go @@ -1,6 +1,7 @@ package models import ( + "context" "encoding/json" "ragflow/internal/common" ) @@ -30,33 +31,33 @@ type ModelDriver interface { Name() string // ChatWithMessages sends multiple messages synchronously - ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) + ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error + ChatStreamlyWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) + Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) + Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) - TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error + TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) + TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) - AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error + AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) + AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) + OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) + ParseFile(ctx context.Context, modelName *string, content []byte, url *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig, modelUsage *common.ModelUsage) (*ParseFileResponse, error) // ListModels List supported models - ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) + ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) - Balance(apiConfig *APIConfig) (map[string]interface{}, error) + Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) - CheckConnection(apiConfig *APIConfig) error + CheckConnection(ctx context.Context, apiConfig *APIConfig) error - ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) + ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) - ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) + ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) } type ChatResponse struct { @@ -252,8 +253,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { - return r.ModelDriver.Rerank(r.ModelName, query, texts, apiConfig, rerankConfig, modelUsage) +func (r *RerankModel) Rerank(ctx context.Context, query string, texts []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { + return r.ModelDriver.Rerank(ctx, r.ModelName, query, texts, apiConfig, rerankConfig, modelUsage) } // ToolConfig bundles tool-calling configuration for a ChatModel. diff --git a/internal/entity/models/upstage.go b/internal/entity/models/upstage.go index de0751d4d1..c22d2a4a46 100644 --- a/internal/entity/models/upstage.go +++ b/internal/entity/models/upstage.go @@ -52,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (u *UpstageModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := u.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -105,7 +105,7 @@ func (u *UpstageModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -168,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (u *UpstageModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -230,7 +230,7 @@ func (u *UpstageModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(jsonData)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -312,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (u *UpstageModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := u.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -342,7 +342,7 @@ func (u *UpstageModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -398,7 +398,7 @@ func (u *UpstageModel) Embed(modelName *string, texts []string, apiConfig *APICo } // ListModels returns the list of model ids visible to the API key. -func (u *UpstageModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (u *UpstageModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := u.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -410,7 +410,7 @@ func (u *UpstageModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/%s", baseURL, u.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -448,13 +448,13 @@ func (u *UpstageModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er } // Balance is not exposed by the Upstage API, so this returns "no such method". -func (u *UpstageModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (u *UpstageModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection runs a lightweight ListModels call to verify the API key. -func (u *UpstageModel) CheckConnection(apiConfig *APIConfig) error { - _, err := u.ListModels(apiConfig) +func (u *UpstageModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := u.ListModels(ctx, apiConfig) if err != nil { return err } @@ -463,42 +463,42 @@ 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (u *UpstageModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (u *UpstageModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (u *UpstageModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (u *UpstageModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (u *UpstageModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (u *UpstageModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (u *UpstageModel) ParseFile(ctx context.Context, 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()) } -func (u *UpstageModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (u *UpstageModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", u.Name()) } -func (u *UpstageModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (u *UpstageModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 8e81cec111..86d5df3cd0 100644 --- a/internal/entity/models/upstage_test.go +++ b/internal/entity/models/upstage_test.go @@ -23,6 +23,7 @@ func newUpstageForTest(baseURL string) *UpstageModel { // ---------- reasoning_effort / reasoning field ---------- func TestUpstageChatPropagatesReasoningEffort(t *testing.T) { + ctx := t.Context() // Per https://console.upstage.ai/api/docs/for-agents/raw, Upstage // Solar models accept `reasoning_effort: minimal|low|medium|high`. // ChatConfig.Effort is the canonical carrier; this test asserts it @@ -38,7 +39,7 @@ func TestUpstageChatPropagatesReasoningEffort(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" effort := "high" - _, err := u.ChatWithMessages("solar-pro2", + _, err := u.ChatWithMessages(ctx, "solar-pro2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Effort: &effort}, nil) @@ -51,6 +52,7 @@ func TestUpstageChatPropagatesReasoningEffort(t *testing.T) { } func TestUpstageChatOmitsReasoningEffortWhenUnset(t *testing.T) { + ctx := t.Context() // If the caller does not opt in, the field must NOT be sent. Sending // "minimal" by default would silently change behavior for downstream // proxies that treat a present field differently from an absent one. @@ -64,7 +66,7 @@ func TestUpstageChatOmitsReasoningEffortWhenUnset(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" - _, err := u.ChatWithMessages("solar-pro2", + _, err := u.ChatWithMessages(ctx, "solar-pro2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{}, // no Effort @@ -79,6 +81,7 @@ func TestUpstageChatOmitsReasoningEffortWhenUnset(t *testing.T) { } func TestUpstageStreamPropagatesReasoningEffort(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) @@ -94,7 +97,7 @@ func TestUpstageStreamPropagatesReasoningEffort(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" effort := "medium" - err := u.ChatStreamlyWithSender("solar-pro2", + err := u.ChatStreamlyWithSender(ctx, "solar-pro2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{Effort: &effort}, @@ -110,6 +113,7 @@ func TestUpstageStreamPropagatesReasoningEffort(t *testing.T) { } func TestUpstageChatExtractsReasoningField(t *testing.T) { + ctx := t.Context() // Per the Upstage docs: when reasoning_effort is high|medium for // solar-pro3 (or high for solar-pro2), the response's // choices[0].message includes a `reasoning` field. The driver must @@ -124,7 +128,7 @@ func TestUpstageChatExtractsReasoningField(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" - resp, err := u.ChatWithMessages("solar-pro3", + resp, err := u.ChatWithMessages(ctx, "solar-pro3", []Message{{Role: "user", Content: "What is 15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -139,6 +143,7 @@ func TestUpstageChatExtractsReasoningField(t *testing.T) { } func TestUpstageChatHandlesAbsentReasoning(t *testing.T) { + ctx := t.Context() // Models without reasoning (solar-mini, syn-pro) or low-effort // requests return no `reasoning` field. The driver must leave // ReasonContent empty without crashing. @@ -149,7 +154,7 @@ func TestUpstageChatHandlesAbsentReasoning(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" - resp, err := u.ChatWithMessages("solar-mini", + resp, err := u.ChatWithMessages(ctx, "solar-mini", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { @@ -167,6 +172,7 @@ func TestUpstageChatHandlesAbsentReasoning(t *testing.T) { // https://console.upstage.ai/api/chat) round-trips through the request // body for both streaming and non-streaming paths. func TestUpstageRequestBodyMatchesSolarAPIShape(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) @@ -182,7 +188,7 @@ func TestUpstageRequestBodyMatchesSolarAPIShape(t *testing.T) { topP := 0.9 stop := []string{"END"} effort := "high" - _, err := u.ChatWithMessages("solar-pro2", + _, err := u.ChatWithMessages(ctx, "solar-pro2", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, &ChatConfig{MaxTokens: &mt, Temperature: &temp, TopP: &topP, Stop: &stop, Effort: &effort}, nil) @@ -215,6 +221,7 @@ func TestUpstageRequestBodyMatchesSolarAPIShape(t *testing.T) { // ---------- Embed: duplicate / out-of-range / reorder ---------- func TestUpstageEmbedRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() // A malformed upstream that repeats data[*].index would silently // overwrite the earlier vector; the driver must fail loudly instead. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -227,13 +234,14 @@ 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, nil) + _, err := u.Embed(ctx, &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) } } func TestUpstageEmbedRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"data":[{"embedding":[1],"index":7}]}`) })) @@ -242,13 +250,14 @@ 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, nil) + _, err := u.Embed(ctx, &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) } } func TestUpstageEmbedHappyPathReordersByIndex(t *testing.T) { + ctx := t.Context() // Upstream returns vectors in shuffled order; driver must realign. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"data":[ @@ -261,7 +270,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, nil) + vecs, err := u.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -281,6 +290,7 @@ func TestUpstageEmbedHappyPathReordersByIndex(t *testing.T) { // reasoning_effort=high — both fields appear, sometimes in the same // chunk and sometimes separately. func TestUpstageStreamExtractsReasoningDelta(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) @@ -299,7 +309,7 @@ func TestUpstageStreamExtractsReasoningDelta(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" var contentChunks, reasoningChunks []string - err := u.ChatStreamlyWithSender("solar-pro3", + err := u.ChatStreamlyWithSender(ctx, "solar-pro3", []Message{{Role: "user", Content: "What is 15% of 80?"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, reason *string) error { @@ -338,6 +348,7 @@ func TestUpstageStreamExtractsReasoningDelta(t *testing.T) { // present: reasoning is forwarded first so a UI consuming both can // render the chain-of-thought before the answer for that token. func TestUpstageStreamReasoningChunksArriveBeforeContent(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) @@ -354,7 +365,7 @@ func TestUpstageStreamReasoningChunksArriveBeforeContent(t *testing.T) { u := newUpstageForTest(srv.URL) apiKey := "test-key" var seq []string - err := u.ChatStreamlyWithSender("solar-pro3", + err := u.ChatStreamlyWithSender(ctx, "solar-pro3", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(content *string, reason *string) error { @@ -385,6 +396,7 @@ func TestUpstageStreamReasoningChunksArriveBeforeContent(t *testing.T) { // non-reasoning models (solar-mini, solar-pro2 with no reasoning_effort) // emit only delta.content. The driver must not regress on them. func TestUpstageStreamWithoutReasoningStillWorks(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) @@ -400,7 +412,7 @@ func TestUpstageStreamWithoutReasoningStillWorks(t *testing.T) { apiKey := "test-key" var content []string var reasonCalled bool - err := u.ChatStreamlyWithSender("solar-mini", + err := u.ChatStreamlyWithSender(ctx, "solar-mini", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil, func(c *string, r *string) error { diff --git a/internal/entity/models/vllm.go b/internal/entity/models/vllm.go index ff8792bca0..6c497a0405 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (v *VllmModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -130,7 +130,7 @@ func (v *VllmModel) ChatWithMessages(modelName string, messages []Message, apiCo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VllmModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -280,7 +280,7 @@ func (v *VllmModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (v *VllmModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -400,7 +400,7 @@ func (v *VllmModel) Embed(modelName *string, texts []string, apiConfig *APIConfi return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -444,7 +444,7 @@ func (v *VllmModel) Embed(modelName *string, texts []string, apiConfig *APIConfi return embeddings, nil } -func (v *VllmModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (v *VllmModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -470,7 +470,7 @@ func (v *VllmModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -511,13 +511,13 @@ func (v *VllmModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error return ParseListModel(modelList), nil } -func (v *VllmModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (v *VllmModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection verifies that the configured vLLM base URL is reachable -func (v *VllmModel) CheckConnection(apiConfig *APIConfig) error { - _, err := v.ListModels(apiConfig) +func (v *VllmModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := v.ListModels(ctx, apiConfig) return 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (v *VllmModel) Rerank(ctx context.Context, 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 } @@ -593,7 +593,7 @@ func (v *VllmModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -641,37 +641,37 @@ 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (v *VllmModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VllmModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (v *VllmModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VllmModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (v *VllmModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (v *VllmModel) ParseFile(ctx context.Context, 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()) } -func (v *VllmModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (v *VllmModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VllmModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (v *VllmModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 19203a1e76..d7d4f3db12 100644 --- a/internal/entity/models/vllm_rerank_test.go +++ b/internal/entity/models/vllm_rerank_test.go @@ -50,6 +50,7 @@ func newVllmModelForTest(baseURL string) *VllmModel { } func TestVllmRerankHappyPath(t *testing.T) { + ctx := t.Context() srv := newVllmRerankServer(t, "Bearer test-key", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "BAAI/bge-reranker-v2-m3" { t.Errorf("expected model=BAAI/bge-reranker-v2-m3, got %v", body["model"]) @@ -85,6 +86,7 @@ func TestVllmRerankHappyPath(t *testing.T) { apiKey := "test-key" modelName := "BAAI/bge-reranker-v2-m3" resp, err := model.Rerank( + ctx, &modelName, "What is RAPTOR?", []string{"doc-zero", "doc-one", "doc-two"}, @@ -107,6 +109,7 @@ func TestVllmRerankHappyPath(t *testing.T) { } func TestVllmRerankTopNClamp(t *testing.T) { + ctx := t.Context() srv := newVllmRerankServer(t, "Bearer test-key", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["top_n"] != float64(2) { t.Errorf("expected top_n clamp to RerankConfig.TopN=2, got %v", body["top_n"]) @@ -119,6 +122,7 @@ func TestVllmRerankTopNClamp(t *testing.T) { apiKey := "test-key" modelName := "BAAI/bge-reranker-v2-m3" if _, err := model.Rerank( + ctx, &modelName, "q", []string{"a", "b", "c", "d"}, &APIConfig{ApiKey: &apiKey}, @@ -130,10 +134,11 @@ func TestVllmRerankTopNClamp(t *testing.T) { } func TestVllmRerankEmptyDocuments(t *testing.T) { + ctx := t.Context() model := newVllmModelForTest("http://unused") apiKey := "test-key" modelName := "BAAI/bge-reranker-v2-m3" - resp, err := model.Rerank(&modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) + resp, err := model.Rerank(ctx, &modelName, "q", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err != nil { t.Fatalf("expected nil error for empty documents, got %v", err) } @@ -146,6 +151,7 @@ func TestVllmRerankEmptyDocuments(t *testing.T) { // no APIConfig.ApiKey is configured. This diverges from the NVIDIA driver // which requires an API key. func TestVllmRerankWithoutAPIKey(t *testing.T) { + ctx := t.Context() srv := newVllmRerankServer(t, "", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "results": []map[string]interface{}{ @@ -157,7 +163,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{}, nil) + resp, err := model.Rerank(ctx, &modelName, "q", []string{"a"}, &APIConfig{}, &RerankConfig{}, nil) if err != nil { t.Fatalf("Rerank failed without api key: %v", err) } @@ -167,15 +173,17 @@ func TestVllmRerankWithoutAPIKey(t *testing.T) { } func TestVllmRerankRequiresModelName(t *testing.T) { + ctx := t.Context() model := newVllmModelForTest("http://unused") apiKey := "test-key" - _, err := model.Rerank(nil, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) + _, err := model.Rerank(ctx, 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) } } func TestVllmRerankRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newVllmRerankServer(t, "Bearer test-key", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"boom"}`)) @@ -185,13 +193,14 @@ 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{}, nil) + _, err := model.Rerank(ctx, &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) } } func TestVllmRerankRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newVllmRerankServer(t, "Bearer test-key", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "results": []map[string]interface{}{ @@ -204,7 +213,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{}, nil) + _, err := model.Rerank(ctx, &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 1fb7b94e11..9920188792 100644 --- a/internal/entity/models/volcengine.go +++ b/internal/entity/models/volcengine.go @@ -73,7 +73,7 @@ func (v *VolcEngine) getAPIKey(apiConfig *APIConfig) string { } // ChatWithMessages sends multiple messages with roles and returns response -func (v *VolcEngine) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (v *VolcEngine) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -180,7 +180,7 @@ func (v *VolcEngine) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -264,7 +264,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VolcEngine) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -386,7 +386,7 @@ func (v *VolcEngine) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -544,7 +544,7 @@ type volcenginePromptTokensDetails struct { } // Embed embeds a list of texts into embeddings -func (v *VolcEngine) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (v *VolcEngine) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -591,7 +591,7 @@ func (v *VolcEngine) Embed(modelName *string, texts []string, apiConfig *APIConf parsed, err := func() (volcengineEmbeddingResponse, error) { var parsed volcengineEmbeddingResponse - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -636,35 +636,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (v *VolcEngine) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (v *VolcEngine) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VolcEngine) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (v *VolcEngine) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VolcEngine) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (v *VolcEngine) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (v *VolcEngine) ParseFile(ctx context.Context, 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()) } @@ -743,7 +743,7 @@ func inferVolcengineModelTypes(m volcengineModelData) []string { return types } -func (v *VolcEngine) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (v *VolcEngine) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -760,7 +760,7 @@ func (v *VolcEngine) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro url := fmt.Sprintf("%s/%s", strings.TrimSuffix(resolvedBaseURL, "/"), modelsSuffix) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -825,11 +825,11 @@ func (v *VolcEngine) ListModels(apiConfig *APIConfig) ([]ListModelResponse, erro return enrichedModels, nil } -func (v *VolcEngine) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (v *VolcEngine) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VolcEngine) CheckConnection(apiConfig *APIConfig) error { +func (v *VolcEngine) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -840,7 +840,7 @@ func (v *VolcEngine) CheckConnection(apiConfig *APIConfig) error { } url := fmt.Sprintf("%s/%s", resolvedBaseURL, v.baseModel.URLSuffix.Files) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -869,10 +869,10 @@ func (v *VolcEngine) CheckConnection(apiConfig *APIConfig) error { return nil } -func (v *VolcEngine) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (v *VolcEngine) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VolcEngine) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (v *VolcEngine) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } diff --git a/internal/entity/models/volcengine_test.go b/internal/entity/models/volcengine_test.go index c7870d4ad4..dbde616be5 100644 --- a/internal/entity/models/volcengine_test.go +++ b/internal/entity/models/volcengine_test.go @@ -55,6 +55,7 @@ func TestVolcEngineConfigDeclaresModelsSuffix(t *testing.T) { } func TestVolcEngineListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := newVolcEngineServer(t, func(t *testing.T, r *http.Request, w http.ResponseWriter) { if r.Method != http.MethodGet { t.Errorf("method=%s", r.Method) @@ -77,7 +78,7 @@ func TestVolcEngineListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newVolcEngineForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newVolcEngineForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -87,23 +88,25 @@ func TestVolcEngineListModelsHappyPath(t *testing.T) { } func TestVolcEngineListModelsRejectsProviderError(t *testing.T) { + ctx := t.Context() srv := newVolcEngineServer(t, func(t *testing.T, r *http.Request, w http.ResponseWriter) { http.Error(w, "bad key", http.StatusUnauthorized) }) defer srv.Close() apiKey := "test-key" - _, err := newVolcEngineForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := newVolcEngineForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "401 Unauthorized") || !strings.Contains(err.Error(), "bad key") { t.Fatalf("expected provider error with status and body, got %v", err) } } func TestVolcEngineListModelsRequiresModelsSuffix(t *testing.T) { + ctx := t.Context() apiKey := "test-key" model := NewVolcEngine(map[string]string{"default": "http://unused"}, URLSuffix{}) - _, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "models URL suffix is not configured") { t.Fatalf("expected missing models suffix error, got %v", err) } diff --git a/internal/entity/models/voyage.go b/internal/entity/models/voyage.go index 247f0dded3..25d1a86db2 100644 --- a/internal/entity/models/voyage.go +++ b/internal/entity/models/voyage.go @@ -63,7 +63,7 @@ type voyageEmbeddingResponse struct { Model string `json:"model"` } -func (v *VoyageModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (v *VoyageModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := v.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -96,7 +96,7 @@ func (v *VoyageModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -167,7 +167,7 @@ type voyageRerankResponse struct { Model string `json:"model"` } -func (v *VoyageModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (v *VoyageModel) Rerank(ctx context.Context, 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 } @@ -203,7 +203,7 @@ func (v *VoyageModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -253,59 +253,59 @@ func (v *VoyageModel) Rerank(modelName *string, query string, documents []string return rerankResponse, nil } -func (v *VoyageModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (v *VoyageModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) CheckConnection(apiConfig *APIConfig) error { +func (v *VoyageModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("%s, no such method", v.Name()) } // ChatWithMessages is not exposed by the Voyage AI API. -func (v *VoyageModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (v *VoyageModel) ChatWithMessages(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VoyageModel) ChatStreamlyWithSender(ctx context.Context, 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()) } // Balance is not exposed by the Voyage AI API. -func (v *VoyageModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (v *VoyageModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } // TranscribeAudio / AudioSpeech / OCRFile: Voyage does not host any of // these surfaces. -func (v *VoyageModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (v *VoyageModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VoyageModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (v *VoyageModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (v *VoyageModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (v *VoyageModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (v *VoyageModel) ParseFile(ctx context.Context, 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()) } -func (v *VoyageModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (v *VoyageModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", v.Name()) } -func (v *VoyageModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (v *VoyageModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 e4f978fd82..3b0b255ff5 100644 --- a/internal/entity/models/voyage_test.go +++ b/internal/entity/models/voyage_test.go @@ -66,6 +66,7 @@ func TestVoyageNewModelWithCustomDefaultTransport(t *testing.T) { } func TestVoyageEmbedHappyPath(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "voyage-3.5" { t.Errorf("model=%v", body["model"]) @@ -85,7 +86,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, nil) + vecs, err := v.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -102,6 +103,7 @@ func TestVoyageEmbedHappyPath(t *testing.T) { // "dimensions" returns "Argument 'dimensions' is not supported by our // API"), so this name matters and must not regress. func TestVoyageEmbedPropagatesOutputDimension(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if got, ok := body["output_dimension"].(float64); !ok || got != 256 { t.Errorf("output_dimension=%v want 256", body["output_dimension"]) @@ -120,7 +122,7 @@ func TestVoyageEmbedPropagatesOutputDimension(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "voyage-3.5" - _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, + _, err := v.Embed(ctx, &model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, &EmbeddingConfig{Dimension: 256}, nil) if err != nil { t.Fatalf("Embed: %v", err) @@ -131,6 +133,7 @@ func TestVoyageEmbedPropagatesOutputDimension(t *testing.T) { // would default the vector length, but only if we don't send the key // at all (sending output_dimension: 0 is a 400). func TestVoyageEmbedOmitsOutputDimensionWhenUnset(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if _, present := body["output_dimension"]; present { t.Errorf("output_dimension must be absent when Dimension is unset, got %v", body["output_dimension"]) @@ -144,13 +147,14 @@ 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, nil) + _, err := v.Embed(ctx, &model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } } func TestVoyageEmbedReordersByIndex(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -165,7 +169,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, nil) + vecs, err := v.Embed(ctx, &model, []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -177,6 +181,7 @@ func TestVoyageEmbedReordersByIndex(t *testing.T) { } func TestVoyageEmbedEmptyInputShortCircuits(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { t.Error("Embed([]) made an unexpected HTTP call") })) @@ -185,31 +190,34 @@ 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, nil) + vecs, err := v.Embed(ctx, &model, []string{}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil || len(vecs) != 0 { t.Errorf("Embed([])=(%v,%v)", vecs, err) } } func TestVoyageEmbedRequiresAPIKey(t *testing.T) { + ctx := t.Context() v := newVoyageForTest("http://unused") model := "voyage-3.5" - _, err := v.Embed(&model, []string{"a"}, &APIConfig{}, nil, nil) + _, err := v.Embed(ctx, &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) } } func TestVoyageEmbedRequiresModelName(t *testing.T) { + ctx := t.Context() v := newVoyageForTest("http://unused") apiKey := "test-key" - _, err := v.Embed(nil, []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := v.Embed(ctx, 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) } } func TestVoyageEmbedRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -223,13 +231,14 @@ 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, nil) + _, err := v.Embed(ctx, &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) } } func TestVoyageEmbedRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -242,13 +251,14 @@ 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, nil) + _, err := v.Embed(ctx, &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) } } func TestVoyageEmbedRejectsMissingSlot(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/embeddings", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -261,13 +271,14 @@ 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, nil) + _, err := v.Embed(ctx, &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) } } func TestVoyageRerankHappyPath(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/rerank", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { // Voyage's request key is top_k (not top_n). if body["top_k"] != float64(3) { @@ -291,7 +302,7 @@ func TestVoyageRerankHappyPath(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "rerank-2" - resp, err := v.Rerank(&model, "x", []string{"a", "b", "c"}, + resp, err := v.Rerank(ctx, &model, "x", []string{"a", "b", "c"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 3}, nil) if err != nil { t.Fatalf("Rerank: %v", err) @@ -308,6 +319,7 @@ func TestVoyageRerankHappyPath(t *testing.T) { } func TestVoyageRerankTopKDefaultsToLenDocuments(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/rerank", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["top_k"] != float64(4) { t.Errorf("top_k=%v want 4 (len(documents))", body["top_k"]) @@ -319,7 +331,7 @@ func TestVoyageRerankTopKDefaultsToLenDocuments(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "rerank-2" - _, err := v.Rerank(&model, "x", []string{"a", "b", "c", "d"}, + _, err := v.Rerank(ctx, &model, "x", []string{"a", "b", "c", "d"}, &APIConfig{ApiKey: &apiKey}, &RerankConfig{}, nil) if err != nil { t.Fatalf("Rerank: %v", err) @@ -327,10 +339,11 @@ func TestVoyageRerankTopKDefaultsToLenDocuments(t *testing.T) { } func TestVoyageRerankEmptyDocuments(t *testing.T) { + ctx := t.Context() v := newVoyageForTest("http://unused") apiKey := "test-key" model := "rerank-2" - resp, err := v.Rerank(&model, "x", nil, + resp, err := v.Rerank(ctx, &model, "x", nil, &APIConfig{ApiKey: &apiKey}, &RerankConfig{TopN: 0}, nil) if err != nil { t.Fatalf("Rerank: %v", err) @@ -341,6 +354,7 @@ func TestVoyageRerankEmptyDocuments(t *testing.T) { } func TestVoyageRerankRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newVoyageServer(t, "/v1/rerank", func(t *testing.T, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]interface{}{ @@ -353,7 +367,7 @@ func TestVoyageRerankRejectsOutOfRangeIndex(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "rerank-2" - _, err := v.Rerank(&model, "x", []string{"a", "b"}, + _, err := v.Rerank(ctx, &model, "x", []string{"a", "b"}, &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) @@ -361,6 +375,7 @@ func TestVoyageRerankRejectsOutOfRangeIndex(t *testing.T) { } func TestVoyageRerankRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() // A duplicate index would silently overwrite an earlier slot, which // is the same failure mode Embed already guards against. Make sure // Rerank fails loudly too. @@ -377,7 +392,7 @@ func TestVoyageRerankRejectsDuplicateIndex(t *testing.T) { v := newVoyageForTest(srv.URL) apiKey := "test-key" model := "rerank-2" - _, err := v.Rerank(&model, "x", []string{"a", "b"}, + _, err := v.Rerank(ctx, &model, "x", []string{"a", "b"}, &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) @@ -389,6 +404,7 @@ func TestVoyageRerankRejectsDuplicateIndex(t *testing.T) { // (e.g. `.../v1//embeddings`). Rerank already trims, so Embed must // trim too; CodeRabbit flagged the inconsistency. func TestVoyageEmbedTrimsTrailingSlashInBaseURL(t *testing.T) { + ctx := t.Context() var sawPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sawPath = r.URL.Path @@ -404,7 +420,7 @@ func TestVoyageEmbedTrimsTrailingSlashInBaseURL(t *testing.T) { ) apiKey := "test-key" model := "voyage-3.5" - if _, err := v.Embed(&model, []string{"x"}, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { + if _, err := v.Embed(ctx, &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 303e7e5d5c..a2c17417ec 100644 --- a/internal/entity/models/xai.go +++ b/internal/entity/models/xai.go @@ -63,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (x *XAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -119,7 +119,7 @@ func (x *XAIModel) ChatWithMessages(modelName string, messages []Message, apiCon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -190,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -249,7 +249,7 @@ func (x *XAIModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -326,12 +326,12 @@ 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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (x *XAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { return nil, fmt.Errorf("not implemented") } // ListModels returns the list of model ids visible to the API key. -func (x *XAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (x *XAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -347,7 +347,7 @@ func (x *XAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), modelsSuffix) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -387,13 +387,13 @@ func (x *XAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) } // Balance is not exposed by the xAI API, so this returns "no such method". -func (x *XAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (x *XAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method") } // CheckConnection runs a lightweight ListModels call to verify the API key. -func (x *XAIModel) CheckConnection(apiConfig *APIConfig) error { - _, err := x.ListModels(apiConfig) +func (x *XAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := x.ListModels(ctx, apiConfig) if err != nil { return err } @@ -401,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (x *XAIModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (x *XAIModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -535,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XAIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (x *XAIModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -602,24 +602,24 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XAIModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (x *XAIModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (x *XAIModel) ParseFile(ctx context.Context, 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()) } -func (x *XAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (x *XAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (x *XAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } diff --git a/internal/entity/models/xai_test.go b/internal/entity/models/xai_test.go index 44cc3fde04..25267917a2 100644 --- a/internal/entity/models/xai_test.go +++ b/internal/entity/models/xai_test.go @@ -43,6 +43,7 @@ func TestXAIConfigDeclaresModelsSuffix(t *testing.T) { } func TestXAIListModelsHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method=%s, want GET", r.Method) @@ -69,7 +70,7 @@ func TestXAIListModelsHappyPath(t *testing.T) { defer srv.Close() apiKey := "test-key" - models, err := newXAIForTest(srv.URL + "/").ListModels(&APIConfig{ApiKey: &apiKey}) + models, err := newXAIForTest(srv.URL+"/").ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err != nil { t.Fatalf("ListModels: %v", err) } @@ -79,26 +80,29 @@ func TestXAIListModelsHappyPath(t *testing.T) { } func TestXAIListModelsRequiresAPIKey(t *testing.T) { - _, err := newXAIForTest("http://unused").ListModels(&APIConfig{}) + ctx := t.Context() + _, err := newXAIForTest("http://unused").ListModels(ctx, &APIConfig{}) if err == nil || !strings.Contains(err.Error(), "api key is required") { t.Fatalf("expected api key error, got %v", err) } } func TestXAIListModelsRejectsProviderError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad key", http.StatusUnauthorized) })) defer srv.Close() apiKey := "test-key" - _, err := newXAIForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := newXAIForTest(srv.URL).ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "401") || !strings.Contains(err.Error(), "bad key") { t.Fatalf("expected provider error with status and body, got %v", err) } } func TestXAICheckConnectionDelegatesToListModels(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/models" { t.Errorf("path=%s, want /models", r.URL.Path) @@ -108,12 +112,13 @@ func TestXAICheckConnectionDelegatesToListModels(t *testing.T) { defer srv.Close() apiKey := "test-key" - if err := newXAIForTest(srv.URL).CheckConnection(&APIConfig{ApiKey: &apiKey}); err != nil { + if err := newXAIForTest(srv.URL).CheckConnection(ctx, &APIConfig{ApiKey: &apiKey}); err != nil { t.Fatalf("CheckConnection: %v", err) } } func TestXAIListModelsRequiresModelsSuffix(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("ListModels should reject a missing models suffix before sending a request") })) @@ -122,7 +127,7 @@ func TestXAIListModelsRequiresModelsSuffix(t *testing.T) { apiKey := "test-key" model := NewXAIModel(map[string]string{"default": srv.URL}, URLSuffix{}) - _, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + _, err := model.ListModels(ctx, &APIConfig{ApiKey: &apiKey}) if err == nil || !strings.Contains(err.Error(), "models URL suffix is not configured") { t.Fatalf("expected missing models suffix error, got %v", err) } diff --git a/internal/entity/models/xiaomi.go b/internal/entity/models/xiaomi.go index e522952983..fd00c286c9 100644 --- a/internal/entity/models/xiaomi.go +++ b/internal/entity/models/xiaomi.go @@ -53,7 +53,7 @@ func (x *XiaomiModel) Name() string { return "xiaomi" } -func (x *XiaomiModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (x *XiaomiModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -139,7 +139,7 @@ func (x *XiaomiModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -224,7 +224,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XiaomiModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -312,7 +312,7 @@ func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -387,16 +387,16 @@ func (x *XiaomiModel) ChatStreamlyWithSender(modelName string, messages []Messag return nil } -func (x *XiaomiModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (x *XiaomiModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (x *XiaomiModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) +func (x *XiaomiModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := x.newXiaomiASRRequest(ctx, modelName, file, apiConfig, asrConfig, false) @@ -430,12 +430,12 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XiaomiModel) TranscribeAudioWithSender(ctx context.Context, 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") } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := x.newXiaomiASRRequest(ctx, modelName, file, apiConfig, asrConfig, true) @@ -563,9 +563,6 @@ func (x *XiaomiModel) newXiaomiASRRequest(ctx context.Context, modelName *string } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(x.baseModel.URLSuffix.Chat, "/")) - if ctx == nil { - ctx = context.Background() - } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -641,8 +638,8 @@ 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) +func (x *XiaomiModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := x.newXiaomiTTSRequest(ctx, modelName, audioContent, apiConfig, ttsConfig, false) @@ -667,12 +664,12 @@ func (x *XiaomiModel) AudioSpeech(modelName *string, audioContent *string, apiCo return decodeXiaomiTTSResponse(body) } -func (x *XiaomiModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XiaomiModel) AudioSpeechWithSender(ctx context.Context, 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") } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := x.newXiaomiTTSRequest(ctx, modelName, audioContent, apiConfig, ttsConfig, true) @@ -760,9 +757,6 @@ func (x *XiaomiModel) newXiaomiTTSRequest(ctx context.Context, modelName *string } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(x.baseModel.URLSuffix.Chat, "/")) - if ctx == nil { - ctx = context.Background() - } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -840,23 +834,23 @@ 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (x *XiaomiModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (x *XiaomiModel) ParseFile(ctx context.Context, 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()) } -func (x *XiaomiModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (x *XiaomiModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } -func (x *XiaomiModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (x *XiaomiModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } -func (x *XiaomiModel) CheckConnection(apiConfig *APIConfig) error { +func (x *XiaomiModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -864,10 +858,10 @@ func (x *XiaomiModel) CheckConnection(apiConfig *APIConfig) error { return err } -func (x *XiaomiModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (x *XiaomiModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("no such method %s", x.Name()) } -func (x *XiaomiModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (x *XiaomiModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 03b1caca01..31637cd684 100644 --- a/internal/entity/models/xiaomi_test.go +++ b/internal/entity/models/xiaomi_test.go @@ -111,7 +111,8 @@ func TestXiaomiChatHappyPath(t *testing.T) { apiKey := "test-key" maxTokens := 1024 thinking := false - resp, err := newXiaomiForTest(srv.URL).ChatWithMessages( + ctx := t.Context() + resp, err := newXiaomiForTest(srv.URL).ChatWithMessages(ctx, "mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, @@ -129,6 +130,7 @@ func TestXiaomiChatHappyPath(t *testing.T) { } func TestXiaomiUsesEmptyRegionBaseURLOverride(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/v1/chat/completions", func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -145,7 +147,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, nil) + resp, err := m.ChatWithMessages(ctx, "mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -155,6 +157,7 @@ func TestXiaomiUsesEmptyRegionBaseURLOverride(t *testing.T) { } func TestXiaomiAPIConfigBaseURLOverridesRegionMap(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/override/chat", func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -172,7 +175,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, nil) + resp, err := m.ChatWithMessages(ctx, "mimo-v2.5-pro", []Message{{Role: "user", Content: "ping"}}, &APIConfig{ApiKey: &apiKey, BaseURL: &baseURL}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -182,6 +185,7 @@ func TestXiaomiAPIConfigBaseURLOverridesRegionMap(t *testing.T) { } func TestXiaomiChatExtractsReasoningContent(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/v1/chat/completions", func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "choices": []map[string]interface{}{{ @@ -195,7 +199,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, nil) + resp, err := newXiaomiForTest(srv.URL).ChatWithMessages(ctx, "mimo-v2.5-pro", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("ChatWithMessages: %v", err) } @@ -205,20 +209,22 @@ func TestXiaomiChatExtractsReasoningContent(t *testing.T) { } func TestXiaomiChatRequiresInputs(t *testing.T) { + ctx := t.Context() apiKey := "test-key" m := newXiaomiForTest("http://unused") - 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") { + if _, err := m.ChatWithMessages(ctx, "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, nil); err == nil || !strings.Contains(err.Error(), "model name is required") { + if _, err := m.ChatWithMessages(ctx, "", []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, nil); err == nil || !strings.Contains(err.Error(), "messages is empty") { + if _, err := m.ChatWithMessages(ctx, "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) } } func TestXiaomiChatRejectsHTTPError(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/v1/chat/completions", func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) _, _ = io.WriteString(w, `{"error":"unauthorized"}`) @@ -226,13 +232,14 @@ 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, nil) + _, err := newXiaomiForTest(srv.URL).ChatWithMessages(ctx, "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) } } func TestXiaomiStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/v1/chat/completions", func(t *testing.T, _ *http.Request, body map[string]interface{}, w http.ResponseWriter) { if body["stream"] != true { t.Errorf("stream=%v want true", body["stream"]) @@ -250,6 +257,7 @@ func TestXiaomiStreamHappyPath(t *testing.T) { var content, reasoning []string var sawDone bool err := newXiaomiForTest(srv.URL).ChatStreamlyWithSender( + ctx, "mimo-v2.5-pro", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -284,6 +292,7 @@ func TestXiaomiStreamHappyPath(t *testing.T) { } func TestXiaomiStreamHandlesCRLFFrames(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/v1/chat/completions", func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") _, _ = io.WriteString(w, @@ -296,6 +305,7 @@ func TestXiaomiStreamHandlesCRLFFrames(t *testing.T) { apiKey := "test-key" var content []string err := newXiaomiForTest(srv.URL).ChatStreamlyWithSender( + ctx, "mimo-v2.5-pro", []Message{{Role: "user", Content: "hi"}}, &APIConfig{ApiKey: &apiKey}, @@ -317,6 +327,7 @@ func TestXiaomiStreamHandlesCRLFFrames(t *testing.T) { } func TestXiaomiStreamRejectsMalformedFrame(t *testing.T) { + ctx := t.Context() srv := newXiaomiServer(t, "/v1/chat/completions", func(t *testing.T, _ *http.Request, _ map[string]interface{}, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") _, _ = io.WriteString(w, "data: {bad json}\n\n") @@ -325,37 +336,38 @@ 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, nil, func(*string, *string) error { return nil }) + err := newXiaomiForTest(srv.URL).ChatStreamlyWithSender(ctx, "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) } } func TestXiaomiUnsupportedMethods(t *testing.T) { + ctx := t.Context() m := newXiaomiForTest("http://unused") model := "mimo-v2.5-pro" apiKey := "test-key" cfg := &APIConfig{ApiKey: &apiKey} - if _, err := m.Embed(&model, []string{"x"}, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Embed(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.Rerank(ctx, &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. - if err := m.CheckConnection(cfg); err != nil { + if err := m.CheckConnection(ctx, cfg); err != nil { t.Errorf("CheckConnection: %v", err) } // TranscribeAudio IS implemented; with nil file it returns input validation error. - if _, err := m.TranscribeAudio(&model, nil, cfg, nil, nil); err == nil || !strings.Contains(err.Error(), "file is missing") { + if _, err := m.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "audio content is empty") { + if _, err := m.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := m.OCRFile(ctx, &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 79c256669b..3672545ac4 100644 --- a/internal/entity/models/xinference.go +++ b/internal/entity/models/xinference.go @@ -143,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (x *XinferenceModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -165,7 +165,7 @@ func (x *XinferenceModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -213,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XinferenceModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -342,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, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (x *XinferenceModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -377,7 +377,7 @@ func (x *XinferenceModel) Embed(modelName *string, texts []string, apiConfig *AP return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -450,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (x *XinferenceModel) Rerank(ctx context.Context, 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 } @@ -489,7 +489,7 @@ func (x *XinferenceModel) Rerank(modelName *string, query string, documents []st return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -541,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (x *XinferenceModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -650,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XinferenceModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (x *XinferenceModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -716,21 +716,21 @@ 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XinferenceModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (x *XinferenceModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (x *XinferenceModel) ParseFile(ctx context.Context, 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()) } // ListModels returns the model IDs exposed by Xinference's OpenAI-compatible // /v1/models endpoint. -func (x *XinferenceModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (x *XinferenceModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -742,7 +742,7 @@ func (x *XinferenceModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, baseURL = normalizeXinferenceBaseURL(baseURL) url := fmt.Sprintf("%s/%s", baseURL, x.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -777,19 +777,19 @@ func (x *XinferenceModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, return ParseListModel(ModelList{Models: result.Data}), nil } -func (x *XinferenceModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (x *XinferenceModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XinferenceModel) CheckConnection(apiConfig *APIConfig) error { - _, err := x.ListModels(apiConfig) +func (x *XinferenceModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { + _, err := x.ListModels(ctx, apiConfig) return err } -func (x *XinferenceModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (x *XinferenceModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XinferenceModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (x *XinferenceModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 4ff820c900..872c9564bc 100644 --- a/internal/entity/models/xinference_test.go +++ b/internal/entity/models/xinference_test.go @@ -66,6 +66,7 @@ func TestXinferenceFactoryRoute(t *testing.T) { } func TestXinferenceChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) { + ctx := t.Context() var seen map[string]interface{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/chat/completions" { @@ -90,7 +91,7 @@ func TestXinferenceChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) x := newXinferenceForTest(srv.URL) maxTokens := 32 temp := 0.2 - resp, err := x.ChatWithMessages("qwen2.5-instruct", + resp, err := x.ChatWithMessages(ctx, "qwen2.5-instruct", []Message{{Role: "user", Content: "ping"}}, &APIConfig{}, &ChatConfig{MaxTokens: &maxTokens, Temperature: &temp}, nil) @@ -112,6 +113,7 @@ func TestXinferenceChatHappyPathNormalizesBaseURLAndOmitsEmptyAuth(t *testing.T) } func TestXinferenceChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "Bearer sk-test" { t.Errorf("Authorization=%q, want Bearer sk-test", got) @@ -122,7 +124,7 @@ func TestXinferenceChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { x := newXinferenceForTest(srv.URL + "/v1") key := "sk-test" - _, err := x.ChatWithMessages("qwen2.5-instruct", + _, err := x.ChatWithMessages(ctx, "qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{ApiKey: &key}, nil, nil) if err != nil { @@ -131,6 +133,7 @@ func TestXinferenceChatSendsAuthHeaderWhenKeyProvided(t *testing.T) { } func TestXinferenceChatExtractsReasoningFields(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"choices":[{"message":{ "content":"12", @@ -140,7 +143,7 @@ func TestXinferenceChatExtractsReasoningFields(t *testing.T) { defer srv.Close() x := newXinferenceForTest(srv.URL) - resp, err := x.ChatWithMessages("qwen3", + resp, err := x.ChatWithMessages(ctx, "qwen3", []Message{{Role: "user", Content: "15% of 80?"}}, &APIConfig{}, nil, nil) if err != nil { @@ -152,6 +155,7 @@ func TestXinferenceChatExtractsReasoningFields(t *testing.T) { } func TestXinferenceStreamHappyPath(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/chat/completions" { t.Errorf("path=%s", r.URL.Path) @@ -176,7 +180,7 @@ func TestXinferenceStreamHappyPath(t *testing.T) { var content []string var reasoning []string var sawDone bool - err := x.ChatStreamlyWithSender("qwen2.5-instruct", + err := x.ChatStreamlyWithSender(ctx, "qwen2.5-instruct", []Message{{Role: "user", Content: "hi"}}, &APIConfig{}, nil, nil, func(c *string, r *string) error { @@ -206,9 +210,10 @@ func TestXinferenceStreamHappyPath(t *testing.T) { } func TestXinferenceStreamRejectsFalseStreamConfig(t *testing.T) { + ctx := t.Context() x := newXinferenceForTest("http://unused") stream := false - err := x.ChatStreamlyWithSender("qwen2.5-instruct", + err := x.ChatStreamlyWithSender(ctx, "qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, &ChatConfig{Stream: &stream}, @@ -220,6 +225,7 @@ func TestXinferenceStreamRejectsFalseStreamConfig(t *testing.T) { } func TestXinferenceStreamCancelsOnIdle(t *testing.T) { + ctx := t.Context() withXinferenceIdleTimeout(t, 200*time.Millisecond) hold := make(chan struct{}) @@ -239,7 +245,7 @@ func TestXinferenceStreamCancelsOnIdle(t *testing.T) { t.Cleanup(func() { close(hold) }) x := newXinferenceForTest(srv.URL) - err := x.ChatStreamlyWithSender("qwen2.5-instruct", + err := x.ChatStreamlyWithSender(ctx, "qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil, @@ -250,6 +256,7 @@ func TestXinferenceStreamCancelsOnIdle(t *testing.T) { } func TestXinferenceListModelsAndCheckConnection(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/models" { t.Errorf("path=%s, want /v1/models", r.URL.Path) @@ -264,14 +271,14 @@ func TestXinferenceListModelsAndCheckConnection(t *testing.T) { x := newXinferenceForTest(srv.URL) key := "sk-test" apiConfig := &APIConfig{ApiKey: &key} - models, err := x.ListModels(apiConfig) + models, err := x.ListModels(ctx, apiConfig) if err != nil { t.Fatalf("ListModels: %v", err) } if joinModelNames(models, ",") != "qwen2.5-instruct,custom-chat" { t.Errorf("models=%v", models) } - if err := x.CheckConnection(apiConfig); err != nil { + if err = x.CheckConnection(ctx, apiConfig); err != nil { t.Fatalf("CheckConnection: %v", err) } } @@ -306,6 +313,7 @@ func newXinferenceEmbedServer(t *testing.T, handler func(t *testing.T, body map[ } func TestXinferenceEmbedHappyPathAndOmitsEmptyAuth(t *testing.T) { + ctx := t.Context() srv := newXinferenceEmbedServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "bge-m3" { t.Errorf("model=%v, want bge-m3", body["model"]) @@ -322,7 +330,7 @@ func TestXinferenceEmbedHappyPathAndOmitsEmptyAuth(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - got, err := x.Embed(&model, []string{"hello", "world"}, &APIConfig{}, nil, nil) + got, err := x.Embed(ctx, &model, []string{"hello", "world"}, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("Embed: %v", err) } @@ -338,6 +346,7 @@ func TestXinferenceEmbedHappyPathAndOmitsEmptyAuth(t *testing.T) { } func TestXinferenceEmbedSendsAuthWhenKeyConfigured(t *testing.T) { + ctx := t.Context() gotAuth := "" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") @@ -348,7 +357,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, nil); err != nil { + if _, err := x.Embed(ctx, &model, []string{"x"}, &APIConfig{ApiKey: &key}, nil, nil); err != nil { t.Fatalf("Embed: %v", err) } if gotAuth != "Bearer sk-test" { @@ -357,6 +366,7 @@ func TestXinferenceEmbedSendsAuthWhenKeyConfigured(t *testing.T) { } func TestXinferenceEmbedNormalizesBaseURLWithV1Suffix(t *testing.T) { + ctx := t.Context() srv := newXinferenceEmbedServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _, _ = io.WriteString(w, `{"data":[{"index":0,"embedding":[0.1]}]}`) }) @@ -367,12 +377,13 @@ 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, nil); err != nil { + if _, err := x.Embed(ctx, &model, []string{"x"}, &APIConfig{}, nil, nil); err != nil { t.Fatalf("Embed: %v", err) } } func TestXinferenceEmbedForwardsDimension(t *testing.T) { + ctx := t.Context() srv := newXinferenceEmbedServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["dimensions"] != float64(384) { t.Errorf("dimensions=%v, want 384", body["dimensions"]) @@ -383,12 +394,13 @@ func TestXinferenceEmbedForwardsDimension(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - if _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, &EmbeddingConfig{Dimension: 384}, nil); err != nil { + if _, err := x.Embed(ctx, &model, []string{"x"}, &APIConfig{}, &EmbeddingConfig{Dimension: 384}, nil); err != nil { t.Fatalf("Embed: %v", err) } } func TestXinferenceEmbedRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() srv := newXinferenceEmbedServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _, _ = io.WriteString(w, `{"data":[{"index":0,"embedding":[0.1]},{"index":0,"embedding":[0.2]}]}`) }) @@ -396,13 +408,14 @@ func TestXinferenceEmbedRejectsDuplicateIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := x.Embed(ctx, &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) } } func TestXinferenceEmbedRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newXinferenceEmbedServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _, _ = io.WriteString(w, `{"data":[{"index":5,"embedding":[0.1]}]}`) }) @@ -410,13 +423,14 @@ func TestXinferenceEmbedRejectsOutOfRangeIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := x.Embed(ctx, &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) } } func TestXinferenceEmbedRejectsMissingIndex(t *testing.T) { + ctx := t.Context() srv := newXinferenceEmbedServer(t, func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { // Two inputs requested but only one returned — index 1 is missing. _, _ = io.WriteString(w, `{"data":[{"index":0,"embedding":[0.1]}]}`) @@ -425,16 +439,17 @@ func TestXinferenceEmbedRejectsMissingIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := x.Embed(ctx, &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) } } func TestXinferenceEmbedEmptyTextsShortCircuits(t *testing.T) { + ctx := t.Context() x := newXinferenceForTest("http://unused") model := "bge-m3" - got, err := x.Embed(&model, nil, &APIConfig{}, nil, nil) + got, err := x.Embed(ctx, &model, nil, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("expected nil error for empty inputs, got %v", err) } @@ -444,14 +459,16 @@ func TestXinferenceEmbedEmptyTextsShortCircuits(t *testing.T) { } func TestXinferenceEmbedRequiresModelName(t *testing.T) { + ctx := t.Context() x := newXinferenceForTest("http://unused") - _, err := x.Embed(nil, []string{"x"}, &APIConfig{}, nil, nil) + _, err := x.Embed(ctx, 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) } } func TestXinferenceEmbedSurfacesHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = io.WriteString(w, `{"error":"model not loaded"}`) @@ -460,27 +477,29 @@ func TestXinferenceEmbedSurfacesHTTPError(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-m3" - _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil) + _, err := x.Embed(ctx, &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) } } func TestXinferenceEmbedRejectsMissingEmbeddingSuffix(t *testing.T) { + ctx := t.Context() x := NewXinferenceModel( map[string]string{"default": "http://unused"}, URLSuffix{Chat: "v1/chat/completions"}, // no Embedding suffix ) model := "bge-m3" - _, err := x.Embed(&model, []string{"x"}, &APIConfig{}, nil, nil) + _, err := x.Embed(ctx, &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) } } func TestXinferenceMissingBaseURLFailsClearly(t *testing.T) { + ctx := t.Context() x := NewXinferenceModel(map[string]string{}, URLSuffix{Chat: "v1/chat/completions"}) - _, err := x.ChatWithMessages("qwen2.5-instruct", + _, err := x.ChatWithMessages(ctx, "qwen2.5-instruct", []Message{{Role: "user", Content: "x"}}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "base URL") { @@ -489,29 +508,30 @@ func TestXinferenceMissingBaseURLFailsClearly(t *testing.T) { } func TestXinferenceUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() x := newXinferenceForTest("http://unused") model := "qwen2.5-instruct" - if _, err := x.Balance(&APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := x.Balance(ctx, &APIConfig{}); err == nil || !strings.Contains(err.Error(), "no such method") { t.Errorf("Balance: expected no such method, got %v", err) } // 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, nil); err == nil || strings.Contains(err.Error(), "api key is required") { + if _, err := x.TranscribeAudio(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := x.TranscribeAudioWithSender(ctx, &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, nil); err == nil || strings.Contains(err.Error(), "api key is required") { + if _, err := x.AudioSpeech(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if err := x.AudioSpeechWithSender(ctx, &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, nil); err == nil || !strings.Contains(err.Error(), "no such method") { + if _, err := x.OCRFile(ctx, &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) } } @@ -546,6 +566,7 @@ func newXinferenceRerankServer(t *testing.T, expectedAuth string, handler func(t } func TestXinferenceRerankHappyPathReordersByIndex(t *testing.T) { + ctx := t.Context() srv := newXinferenceRerankServer(t, "", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if body["model"] != "bge-reranker-v2-m3" { t.Errorf("model=%v", body["model"]) @@ -568,7 +589,7 @@ func TestXinferenceRerankHappyPathReordersByIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - resp, err := x.Rerank(&model, "capital of France", + resp, err := x.Rerank(ctx, &model, "capital of France", []string{"Paris is the capital of France.", "Eiffel Tower.", "Berlin is the capital of Germany."}, &APIConfig{}, nil, nil, ) @@ -587,6 +608,7 @@ func TestXinferenceRerankHappyPathReordersByIndex(t *testing.T) { } func TestXinferenceRerankNormalizesV1BaseURL(t *testing.T) { + ctx := t.Context() srv := newXinferenceRerankServer(t, "Bearer test-key", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{"results": []map[string]interface{}{}}) }) @@ -598,13 +620,14 @@ func TestXinferenceRerankNormalizesV1BaseURL(t *testing.T) { ) apiKey := "test-key" model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := x.Rerank(ctx, &model, "q", []string{"a"}, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } } func TestXinferenceRerankRespectsTopNConfig(t *testing.T) { + ctx := t.Context() srv := newXinferenceRerankServer(t, "", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { if got := body["top_n"].(float64); got != 2 { t.Errorf("top_n=%v want 2", got) @@ -615,16 +638,17 @@ 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}, nil) + _, err := x.Rerank(ctx, &model, "q", []string{"a", "b", "c", "d"}, &APIConfig{}, &RerankConfig{TopN: 2}, nil) if err != nil { t.Fatalf("Rerank: %v", err) } } func TestXinferenceRerankEmptyDocumentsShortCircuits(t *testing.T) { + ctx := t.Context() x := newXinferenceForTest("http://unused") model := "bge-reranker-v2-m3" - resp, err := x.Rerank(&model, "q", nil, &APIConfig{}, nil, nil) + resp, err := x.Rerank(ctx, &model, "q", nil, &APIConfig{}, nil, nil) if err != nil { t.Fatalf("Rerank: %v", err) } @@ -634,14 +658,16 @@ func TestXinferenceRerankEmptyDocumentsShortCircuits(t *testing.T) { } func TestXinferenceRerankRequiresModelName(t *testing.T) { + ctx := t.Context() x := newXinferenceForTest("http://unused") - _, err := x.Rerank(nil, "q", []string{"a"}, &APIConfig{}, nil, nil) + _, err := x.Rerank(ctx, nil, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "model name is required") { t.Errorf("err=%v", err) } } func TestXinferenceRerankRejectsOutOfRangeIndex(t *testing.T) { + ctx := t.Context() srv := newXinferenceRerankServer(t, "", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "results": []map[string]interface{}{{"index": 5, "relevance_score": 0.1}}, @@ -651,13 +677,14 @@ func TestXinferenceRerankRejectsOutOfRangeIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := x.Rerank(ctx, &model, "q", []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "out of range") { t.Errorf("err=%v", err) } } func TestXinferenceRerankRejectsDuplicateIndex(t *testing.T) { + ctx := t.Context() srv := newXinferenceRerankServer(t, "", func(t *testing.T, body map[string]interface{}, w http.ResponseWriter) { _ = json.NewEncoder(w).Encode(map[string]interface{}{ "results": []map[string]interface{}{ @@ -670,13 +697,14 @@ func TestXinferenceRerankRejectsDuplicateIndex(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a", "b"}, &APIConfig{}, nil, nil) + _, err := x.Rerank(ctx, &model, "q", []string{"a", "b"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "duplicate") { t.Errorf("err=%v", err) } } func TestXinferenceRerankSurfacesHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"model not loaded"}`)) @@ -685,19 +713,20 @@ func TestXinferenceRerankSurfacesHTTPError(t *testing.T) { x := newXinferenceForTest(srv.URL) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) + _, err := x.Rerank(ctx, &model, "q", []string{"a"}, &APIConfig{}, nil, nil) if err == nil || !strings.Contains(err.Error(), "Xinference rerank API error") { t.Errorf("err=%v", err) } } func TestXinferenceRerankRejectsMissingRerankSuffix(t *testing.T) { + ctx := t.Context() x := NewXinferenceModel( map[string]string{"default": "http://unused"}, URLSuffix{Chat: "v1/chat/completions"}, ) model := "bge-reranker-v2-m3" - _, err := x.Rerank(&model, "q", []string{"a"}, &APIConfig{}, nil, nil) + _, err := x.Rerank(ctx, &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 80bbe35d78..b7a6104c35 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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (x *XunFeiModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -113,7 +113,7 @@ func (x *XunFeiModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request body: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -250,7 +250,7 @@ func (x *XunFeiModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -321,39 +321,39 @@ func (x *XunFeiModel) ChatStreamlyWithSender(modelName string, messages []Messag return nil } -func (x *XunFeiModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { +func (x *XunFeiModel) Embed(ctx context.Context, 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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (x *XunFeiModel) Rerank(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (x *XunFeiModel) TranscribeAudio(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XunFeiModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (x *XunFeiModel) AudioSpeech(ctx context.Context, 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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (x *XunFeiModel) AudioSpeechWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (x *XunFeiModel) OCRFile(ctx context.Context, 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (x *XunFeiModel) ParseFile(ctx context.Context, 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()) } -func (x *XunFeiModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (x *XunFeiModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := x.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -372,7 +372,7 @@ func (x *XunFeiModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) @@ -411,18 +411,18 @@ func (x *XunFeiModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, err return ParseListModel(modelList), nil } -func (x *XunFeiModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (x *XunFeiModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) CheckConnection(apiConfig *APIConfig) error { +func (x *XunFeiModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { return fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (x *XunFeiModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", x.Name()) } -func (x *XunFeiModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (x *XunFeiModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 a1ae98ee46..781d719f13 100644 --- a/internal/entity/models/xunfei_test.go +++ b/internal/entity/models/xunfei_test.go @@ -3,6 +3,7 @@ package models import "testing" func TestXunFeiUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { + ctx := t.Context() driver := NewXunFeiModel(map[string]string{"default": "http://unused"}, URLSuffix{}). NewInstance(map[string]string{"default": "http://unused"}) modelName := "spark" @@ -13,48 +14,48 @@ func TestXunFeiUnsupportedMethodsReturnNoSuchMethod(t *testing.T) { call func() error }{ {"Embed", func() error { - _, err := driver.Embed(&modelName, []string{text}, &APIConfig{}, nil, nil) + _, err := driver.Embed(ctx, &modelName, []string{text}, &APIConfig{}, nil, nil) return err }}, {"Rerank", func() error { - _, err := driver.Rerank(&modelName, text, []string{text}, &APIConfig{}, nil, nil) + _, err := driver.Rerank(ctx, &modelName, text, []string{text}, &APIConfig{}, nil, nil) return err }}, {"TranscribeAudio", func() error { - _, err := driver.TranscribeAudio(&modelName, &text, &APIConfig{}, nil, nil) + _, err := driver.TranscribeAudio(ctx, &modelName, &text, &APIConfig{}, nil, nil) return err }}, {"TranscribeAudioWithSender", func() error { - return driver.TranscribeAudioWithSender(&modelName, &text, &APIConfig{}, nil, nil, nil) + return driver.TranscribeAudioWithSender(ctx, &modelName, &text, &APIConfig{}, nil, nil, nil) }}, {"AudioSpeech", func() error { - _, err := driver.AudioSpeech(&modelName, &text, &APIConfig{}, nil, nil) + _, err := driver.AudioSpeech(ctx, &modelName, &text, &APIConfig{}, nil, nil) return err }}, {"AudioSpeechWithSender", func() error { - return driver.AudioSpeechWithSender(&modelName, &text, &APIConfig{}, nil, nil, nil) + return driver.AudioSpeechWithSender(ctx, &modelName, &text, &APIConfig{}, nil, nil, nil) }}, {"OCRFile", func() error { - _, err := driver.OCRFile(&modelName, nil, &text, &APIConfig{}, nil, nil) + _, err := driver.OCRFile(ctx, &modelName, nil, &text, &APIConfig{}, nil, nil) return err }}, {"ParseFile", func() error { - _, err := driver.ParseFile(&modelName, nil, &text, &APIConfig{}, nil, nil) + _, err := driver.ParseFile(ctx, &modelName, nil, &text, &APIConfig{}, nil, nil) return err }}, {"Balance", func() error { - _, err := driver.Balance(&APIConfig{}) + _, err := driver.Balance(ctx, &APIConfig{}) return err }}, {"CheckConnection", func() error { - return driver.CheckConnection(&APIConfig{}) + return driver.CheckConnection(ctx, &APIConfig{}) }}, {"ListTasks", func() error { - _, err := driver.ListTasks(&APIConfig{}) + _, err := driver.ListTasks(ctx, &APIConfig{}) return err }}, {"ShowTask", func() error { - _, err := driver.ShowTask("task-id", &APIConfig{}) + _, err := driver.ShowTask(ctx, "task-id", &APIConfig{}) return err }}, } diff --git a/internal/entity/models/zhipu-ai.go b/internal/entity/models/zhipu-ai.go index a40e7ab2a3..8f9b2ecfbd 100644 --- a/internal/entity/models/zhipu-ai.go +++ b/internal/entity/models/zhipu-ai.go @@ -87,7 +87,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, modelUsage *common.ModelUsage) (*ChatResponse, error) { +func (z *ZhipuAIModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -171,7 +171,7 @@ func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -247,7 +247,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (z *ZhipuAIModel) ChatStreamlyWithSender(ctx context.Context, 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 } @@ -337,7 +337,7 @@ func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -507,7 +507,7 @@ type zhipuUsage struct { } // 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) { +func (z *ZhipuAIModel) Embed(ctx context.Context, modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig, modelUsage *common.ModelUsage) ([]EmbeddingData, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -539,7 +539,7 @@ func (z *ZhipuAIModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -583,7 +583,7 @@ func (z *ZhipuAIModel) Embed(modelName *string, texts []string, apiConfig *APICo return embeddings, nil } -func (z *ZhipuAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, error) { +func (z *ZhipuAIModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -595,7 +595,7 @@ func (z *ZhipuAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er url := fmt.Sprintf("%s/%s", baseURL, z.baseModel.URLSuffix.Models) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -630,11 +630,11 @@ func (z *ZhipuAIModel) ListModels(apiConfig *APIConfig) ([]ListModelResponse, er return ParseListModel(modelList), nil } -func (z *ZhipuAIModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { +func (z *ZhipuAIModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("%s, no such method", z.Name()) } -func (z *ZhipuAIModel) CheckConnection(apiConfig *APIConfig) error { +func (z *ZhipuAIModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return err } @@ -646,7 +646,7 @@ func (z *ZhipuAIModel) CheckConnection(apiConfig *APIConfig) error { url := fmt.Sprintf("%s/%s", baseURL, z.baseModel.URLSuffix.Files) - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -710,7 +710,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, modelUsage *common.ModelUsage) (*RerankResponse, error) { +func (z *ZhipuAIModel) Rerank(ctx context.Context, 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 } @@ -748,7 +748,7 @@ func (z *ZhipuAIModel) Rerank(modelName *string, query string, documents []strin return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -792,7 +792,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, modelUsage *common.ModelUsage) (*ASRResponse, error) { +func (z *ZhipuAIModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -844,7 +844,7 @@ func (z *ZhipuAIModel) TranscribeAudio(modelName *string, file *string, apiConfi return nil, fmt.Errorf("failed to close multipart writer: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &body) @@ -919,12 +919,12 @@ func writeZhipuASRField(writer *multipart.Writer, key string, value interface{}) } } -func (z *ZhipuAIModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (z *ZhipuAIModel) TranscribeAudioWithSender(ctx context.Context, 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, modelUsage *common.ModelUsage) (*TTSResponse, error) { +func (z *ZhipuAIModel) AudioSpeech(ctx context.Context, modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, modelUsage *common.ModelUsage) (*TTSResponse, error) { if err := z.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } @@ -939,7 +939,7 @@ func (z *ZhipuAIModel) AudioSpeech(modelName *string, audioContent *string, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -966,7 +966,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, modelUsage *common.ModelUsage, sender func(*string, *string) error) error { +func (z *ZhipuAIModel) AudioSpeechWithSender(ctx context.Context, 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 } @@ -984,7 +984,7 @@ func (z *ZhipuAIModel) AudioSpeechWithSender(modelName *string, audioContent *st return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + ctx, cancel := context.WithTimeout(ctx, streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) @@ -1064,7 +1064,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, modelUsage *common.ModelUsage) (*OCRFileResponse, error) { +func (z *ZhipuAIModel) OCRFile(ctx context.Context, 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 } @@ -1108,7 +1108,7 @@ func (z *ZhipuAIModel) OCRFile(modelName *string, content []byte, fileURL *strin } url := fmt.Sprintf("%s/%s", baseURL, strings.TrimPrefix(z.baseModel.URLSuffix.OCR, "/")) - ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + ctx, cancel := context.WithTimeout(ctx, longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -1147,14 +1147,14 @@ 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, modelUsage *common.ModelUsage) (*ParseFileResponse, error) { +func (z *ZhipuAIModel) ParseFile(ctx context.Context, 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()) } -func (z *ZhipuAIModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { +func (z *ZhipuAIModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("%s, no such method", z.Name()) } -func (z *ZhipuAIModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { +func (z *ZhipuAIModel) ShowTask(ctx context.Context, taskID string, apiConfig *APIConfig) (*TaskResponse, 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 4e4c0fbafd..dad8fa7f46 100644 --- a/internal/entity/models/zhipu-ai_test.go +++ b/internal/entity/models/zhipu-ai_test.go @@ -9,6 +9,7 @@ import ( ) func TestZhipuAIOCRFileSendsLayoutParsingRequest(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-ocr" fileURL := "https://example.com/doc.png" @@ -52,7 +53,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, nil) + resp, err := model.OCRFile(ctx, &modelName, nil, &fileURL, &APIConfig{ApiKey: &apiKey}, nil, nil) if err != nil { t.Fatalf("OCRFile returned error: %v", err) } @@ -62,6 +63,7 @@ func TestZhipuAIOCRFileSendsLayoutParsingRequest(t *testing.T) { } func TestZhipuAIOCRFileEncodesContent(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-ocr" content := []byte("sample image bytes") @@ -81,12 +83,13 @@ 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, nil); err != nil { + if _, err := model.OCRFile(ctx, &modelName, content, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { t.Fatalf("OCRFile returned error: %v", err) } } func TestZhipuAIOCRFileDetectsPDFContent(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-ocr" content := []byte("%PDF-1.7 sample") @@ -106,12 +109,13 @@ 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, nil); err != nil { + if _, err := model.OCRFile(ctx, &modelName, content, nil, &APIConfig{ApiKey: &apiKey}, nil, nil); err != nil { t.Fatalf("OCRFile returned error: %v", err) } } func TestZhipuAIOCRFileValidation(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-ocr" fileURL := "https://example.com/doc.png" @@ -149,7 +153,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, nil) + _, err := model.OCRFile(ctx, 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 586f3dc116..b0ae7e685a 100644 --- a/internal/entity/models/zhipu_ai_test.go +++ b/internal/entity/models/zhipu_ai_test.go @@ -33,6 +33,7 @@ func writeZhipuAITestAudio(t *testing.T) string { } func TestZhipuAITranscribeAudio(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method=%s", r.Method) @@ -92,6 +93,7 @@ func TestZhipuAITranscribeAudio(t *testing.T) { modelName := "glm-asr-2512" file := writeZhipuAITestAudio(t) resp, err := newZhipuAIForTest(srv.URL).TranscribeAudio( + ctx, &modelName, &file, &APIConfig{ApiKey: &apiKey}, @@ -115,6 +117,7 @@ func TestZhipuAITranscribeAudio(t *testing.T) { } func TestZhipuAITranscribeAudioValidation(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-asr-2512" file := "speech.mp3" @@ -133,7 +136,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, nil) + _, err := newZhipuAIForTest("http://unused").TranscribeAudio(ctx, 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) } @@ -142,10 +145,12 @@ func TestZhipuAITranscribeAudioValidation(t *testing.T) { } func TestZhipuAITranscribeAudioRequiresASRSuffix(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-asr-2512" file := writeZhipuAITestAudio(t) _, err := NewZhipuAIModel(map[string]string{"default": "http://unused"}, URLSuffix{}).TranscribeAudio( + ctx, &modelName, &file, &APIConfig{ApiKey: &apiKey}, @@ -158,6 +163,7 @@ func TestZhipuAITranscribeAudioRequiresASRSuffix(t *testing.T) { } func TestZhipuAITranscribeAudioHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = io.WriteString(w, `{"error":"bad request"}`) @@ -167,13 +173,14 @@ 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, nil) + _, err := newZhipuAIForTest(srv.URL).TranscribeAudio(ctx, &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) } } func TestZhipuAIAudioSpeech(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method=%s", r.Method) @@ -221,6 +228,7 @@ func TestZhipuAIAudioSpeech(t *testing.T) { modelName := "glm-tts" content := "hello" resp, err := newZhipuAIForTest(srv.URL).AudioSpeech( + ctx, &modelName, &content, &APIConfig{ApiKey: &apiKey}, @@ -242,6 +250,7 @@ func TestZhipuAIAudioSpeech(t *testing.T) { } func TestZhipuAIAudioSpeechWithSender(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&body); err != nil { @@ -261,6 +270,7 @@ func TestZhipuAIAudioSpeechWithSender(t *testing.T) { content := "hello" var chunks []string err := newZhipuAIForTest(srv.URL).AudioSpeechWithSender( + ctx, &modelName, &content, &APIConfig{ApiKey: &apiKey}, @@ -285,6 +295,7 @@ func TestZhipuAIAudioSpeechWithSender(t *testing.T) { } func TestZhipuAIAudioSpeechValidation(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-tts" content := "hello" @@ -303,7 +314,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, nil) + _, err := newZhipuAIForTest("http://unused").AudioSpeech(ctx, 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) } @@ -312,10 +323,12 @@ func TestZhipuAIAudioSpeechValidation(t *testing.T) { } func TestZhipuAIAudioSpeechRequiresTTSSuffix(t *testing.T) { + ctx := t.Context() apiKey := "test-key" modelName := "glm-tts" content := "hello" _, err := NewZhipuAIModel(map[string]string{"default": "http://unused"}, URLSuffix{}).AudioSpeech( + ctx, &modelName, &content, &APIConfig{ApiKey: &apiKey}, @@ -328,6 +341,7 @@ func TestZhipuAIAudioSpeechRequiresTTSSuffix(t *testing.T) { } func TestZhipuAIAudioSpeechHTTPError(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = io.WriteString(w, `{"error":"bad request"}`) @@ -337,13 +351,14 @@ func TestZhipuAIAudioSpeechHTTPError(t *testing.T) { apiKey := "test-key" modelName := "glm-tts" content := "hello" - _, err := newZhipuAIForTest(srv.URL).AudioSpeech(&modelName, &content, &APIConfig{ApiKey: &apiKey}, nil, nil) + _, err := newZhipuAIForTest(srv.URL).AudioSpeech(ctx, &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) } } func TestZhipuAIChatStreamlyWithSenderCollectsToolCalls(t *testing.T) { + ctx := t.Context() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method=%s", r.Method) @@ -402,6 +417,7 @@ func TestZhipuAIChatStreamlyWithSenderCollectsToolCalls(t *testing.T) { map[string]string{"default": srv.URL}, URLSuffix{Chat: "chat/completions"}, ).ChatStreamlyWithSender( + ctx, "glm-4", []Message{{Role: "user", Content: "what is marigold"}}, &APIConfig{ApiKey: &apiKey}, diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 55d9ebbe4c..0f41486573 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -17,6 +17,7 @@ package handler import ( + "context" "fmt" "net/http" "ragflow/internal/common" @@ -37,10 +38,10 @@ type AuthHandler struct { // so the test suite can swap in a stub without spinning up the // full UserService (which requires a live Redis + JWT secret). type userTokenResolver interface { - GetUserByToken(authorization string) (*entity.User, common.ErrorCode, error) - GetUserByAPIToken(token string) (*entity.User, common.ErrorCode, error) - GetUserByBetaAPIToken(token string) (*entity.User, common.ErrorCode, error) - GetAPITokenByBeta(authorization string) (*entity.APIToken, error) + GetUserByToken(ctx context.Context, authorization string) (*entity.User, common.ErrorCode, error) + GetUserByAPIToken(ctx context.Context, token string) (*entity.User, common.ErrorCode, error) + GetUserByBetaAPIToken(ctx context.Context, token string) (*entity.User, common.ErrorCode, error) + GetAPITokenByBeta(ctx context.Context, authorization string) (*entity.APIToken, error) } // NewAuthHandler create auth handler @@ -69,6 +70,7 @@ func NewAuthHandler() *AuthHandler { // regular user token must keep working here too. func (h *AuthHandler) BetaAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + ctx := c.Request.Context() auth := c.GetHeader("Authorization") if auth == "" { if cookie, err := c.Cookie(oauthAuthCookie); err == nil { @@ -82,13 +84,13 @@ func (h *AuthHandler) BetaAuthMiddleware() gin.HandlerFunc { return } // AUTH_JWT - if u, code, err := h.userService.GetUserByToken(auth); err == nil && code == common.CodeSuccess { + if u, code, err := h.userService.GetUserByToken(ctx, auth); err == nil && code == common.CodeSuccess { c.Set("user", u) c.Next() return } // Then try a regular API token (non-beta public bot flow). - if u, code, err := h.userService.GetUserByAPIToken(auth); err == nil && code == common.CodeSuccess { + if u, code, err := h.userService.GetUserByAPIToken(ctx, auth); err == nil && code == common.CodeSuccess { c.Set("user", u) c.Set("auth_via_api_token", true) c.Next() @@ -101,9 +103,9 @@ func (h *AuthHandler) BetaAuthMiddleware() gin.HandlerFunc { // Mirrors the python // `APIToken.query(beta=token).dialog_id` lookup in // bot_api.py:agent_bot_logs. - if u, code, err := h.userService.GetUserByBetaAPIToken(auth); err == nil && code == common.CodeSuccess { + if u, code, err := h.userService.GetUserByBetaAPIToken(ctx, auth); err == nil && code == common.CodeSuccess { c.Set("user", u) - if tok, terr := h.userService.GetAPITokenByBeta(auth); terr == nil && tok != nil && tok.DialogID != nil { + if tok, terr := h.userService.GetAPITokenByBeta(ctx, auth); terr == nil && tok != nil && tok.DialogID != nil { // tok.DialogID is *string (nullable in the schema), but // downstream handlers (GetAgentbotLogs, GetAgentLogs) // read "agent_id" with agentID.(string) — they cannot @@ -126,6 +128,7 @@ func (h *AuthHandler) BetaAuthMiddleware() gin.HandlerFunc { // Validates that the user is authenticated and is a superuser (admin) func (h *AuthHandler) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + ctx := c.Request.Context() token := c.GetHeader("Authorization") if token == "" { common.ResponseWithHttpCodeData(c, http.StatusUnauthorized, 401, nil, "Missing Authorization header") @@ -136,9 +139,9 @@ func (h *AuthHandler) AuthMiddleware() gin.HandlerFunc { authViaAPIToken := false // Get user by access token - user, code, err := h.userService.GetUserByToken(token) + user, code, err := h.userService.GetUserByToken(ctx, token) if err != nil { - user, code, err = h.userService.GetUserByAPIToken(token) + user, code, err = h.userService.GetUserByAPIToken(ctx, token) if err != nil { common.ResponseWithHttpCodeData(c, http.StatusUnauthorized, code, nil, "Invalid access token") c.Abort() diff --git a/internal/handler/bot_test.go b/internal/handler/bot_test.go index 7e12236a58..c0664e1c6e 100644 --- a/internal/handler/bot_test.go +++ b/internal/handler/bot_test.go @@ -784,7 +784,7 @@ func TestBotRoutes_RequireAuth(t *testing.T) { func TestBotMiddleware_NonBearerRegularToken(t *testing.T) { gin.SetMode(gin.TestMode) stub := &stubUserTokenResolver{ - getUserByTokenFn: func(auth string) (*entity.User, common.ErrorCode, error) { + getUserByTokenFn: func(ctx context.Context, auth string) (*entity.User, common.ErrorCode, error) { if auth != "raw-access-token-abc" { t.Errorf("GetUserByToken called with %q, want raw-access-token-abc", auth) } @@ -820,36 +820,36 @@ func TestBotMiddleware_NonBearerRegularToken(t *testing.T) { // return safe defaults (CodeUnauthorized so the middleware // short-circuits to 401). type stubUserTokenResolver struct { - getUserByTokenFn func(authorization string) (*entity.User, common.ErrorCode, error) - getUserByAPITokenFn func(token string) (*entity.User, common.ErrorCode, error) - getUserByBetaAPITokenFn func(token string) (*entity.User, common.ErrorCode, error) - getAPITokenByBetaFn func(authorization string) (*entity.APIToken, error) + getUserByTokenFn func(ctx context.Context, authorization string) (*entity.User, common.ErrorCode, error) + getUserByAPITokenFn func(ctx context.Context, token string) (*entity.User, common.ErrorCode, error) + getUserByBetaAPITokenFn func(ctx context.Context, token string) (*entity.User, common.ErrorCode, error) + getAPITokenByBetaFn func(ctx context.Context, authorization string) (*entity.APIToken, error) } -func (s *stubUserTokenResolver) GetUserByToken(authorization string) (*entity.User, common.ErrorCode, error) { +func (s *stubUserTokenResolver) GetUserByToken(ctx context.Context, authorization string) (*entity.User, common.ErrorCode, error) { if s.getUserByTokenFn != nil { - return s.getUserByTokenFn(authorization) + return s.getUserByTokenFn(ctx, authorization) } return nil, common.CodeUnauthorized, errors.New("not stubbed") } -func (s *stubUserTokenResolver) GetUserByAPIToken(token string) (*entity.User, common.ErrorCode, error) { +func (s *stubUserTokenResolver) GetUserByAPIToken(ctx context.Context, token string) (*entity.User, common.ErrorCode, error) { if s.getUserByAPITokenFn != nil { - return s.getUserByAPITokenFn(token) + return s.getUserByAPITokenFn(ctx, token) } return nil, common.CodeUnauthorized, errors.New("not stubbed") } -func (s *stubUserTokenResolver) GetUserByBetaAPIToken(token string) (*entity.User, common.ErrorCode, error) { +func (s *stubUserTokenResolver) GetUserByBetaAPIToken(ctx context.Context, token string) (*entity.User, common.ErrorCode, error) { if s.getUserByBetaAPITokenFn != nil { - return s.getUserByBetaAPITokenFn(token) + return s.getUserByBetaAPITokenFn(ctx, token) } return nil, common.CodeUnauthorized, errors.New("not stubbed") } -func (s *stubUserTokenResolver) GetAPITokenByBeta(authorization string) (*entity.APIToken, error) { +func (s *stubUserTokenResolver) GetAPITokenByBeta(ctx context.Context, authorization string) (*entity.APIToken, error) { if s.getAPITokenByBetaFn != nil { - return s.getAPITokenByBetaFn(authorization) + return s.getAPITokenByBetaFn(ctx, authorization) } return nil, errors.New("not stubbed") } @@ -870,7 +870,7 @@ func (s *stubUserTokenResolver) GetAPITokenByBeta(authorization string) (*entity func TestBotRoutes_NoRegularAuthRequired(t *testing.T) { gin.SetMode(gin.TestMode) stub := &stubUserTokenResolver{ - getUserByTokenFn: func(auth string) (*entity.User, common.ErrorCode, error) { + getUserByTokenFn: func(ctx context.Context, auth string) (*entity.User, common.ErrorCode, error) { return &entity.User{ID: "u-regular"}, common.CodeSuccess, nil }, } @@ -955,7 +955,7 @@ func TestBotRoutes_NoRegularAuthRequired(t *testing.T) { func TestDownloadAttachment_Unauth(t *testing.T) { gin.SetMode(gin.TestMode) stub := &stubUserTokenResolver{ - getUserByTokenFn: func(auth string) (*entity.User, common.ErrorCode, error) { + getUserByTokenFn: func(ctx context.Context, auth string) (*entity.User, common.ErrorCode, error) { return nil, common.CodeUnauthorized, errors.New("invalid token") }, } @@ -968,13 +968,14 @@ func TestDownloadAttachment_Unauth(t *testing.T) { // resolve via GetUserByToken. Both branches must reject // with 401. g.Use(func(c *gin.Context) { + ctx := c.Request.Context() auth := c.GetHeader("Authorization") if auth == "" { common.ResponseWithCodeData(c, common.CodeUnauthorized, nil, "Authorization required") c.Abort() return } - if u, code, err := stub.GetUserByToken(auth); err != nil || code != common.CodeSuccess { + if u, code, err := stub.GetUserByToken(ctx, auth); err != nil || code != common.CodeSuccess { common.ResponseWithCodeData(c, common.CodeUnauthorized, nil, "Invalid auth credentials") c.Abort() return diff --git a/internal/handler/chat_audio.go b/internal/handler/chat_audio.go index 62000e6637..2197e51a5f 100644 --- a/internal/handler/chat_audio.go +++ b/internal/handler/chat_audio.go @@ -58,6 +58,7 @@ func (h *ChatHandler) ChatAudioSpeech(c *gin.Context) { return } + ctx := c.Request.Context() c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, chatAudioSpeechMaxBodyBytes) var req chatAudioSpeechRequest @@ -84,7 +85,7 @@ func (h *ChatHandler) ChatAudioSpeech(c *gin.Context) { if seg == "" { continue } - resp, err := driver.AudioSpeech(&modelName, &seg, apiConfig, &modelModule.TTSConfig{Format: "mp3"}, nil) + resp, err := driver.AudioSpeech(ctx, &modelName, &seg, apiConfig, &modelModule.TTSConfig{Format: "mp3"}, nil) if err != nil { common.Warn("chat TTS synthesis failed", zap.Int("segmentIndex", i), @@ -154,6 +155,7 @@ func (h *ChatHandler) ChatAudioTranscription(c *gin.Context) { return } + ctx := c.Request.Context() // Cap the body before any multipart parsing so an oversized upload is // rejected instead of being drained into memory or spooled to disk. c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, chatAudioUploadMaxBytes) @@ -234,7 +236,7 @@ func (h *ChatHandler) ChatAudioTranscription(c *gin.Context) { return nil } - if err = driver.TranscribeAudioWithSender(&modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil, sender); err != nil { + if err = driver.TranscribeAudioWithSender(ctx, &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 +254,7 @@ func (h *ChatHandler) ChatAudioTranscription(c *gin.Context) { return } - resp, err := driver.TranscribeAudio(&modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil) + resp, err := driver.TranscribeAudio(ctx, &modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil) if err != nil { common.ErrorWithCode(c, common.CodeServerError, err.Error()) return diff --git a/internal/handler/chat_recommendation.go b/internal/handler/chat_recommendation.go index b47ed6609e..5edfb868de 100644 --- a/internal/handler/chat_recommendation.go +++ b/internal/handler/chat_recommendation.go @@ -56,7 +56,8 @@ func (h *ChatHandler) Recommendation(c *gin.Context) { common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "question is required") return } - questions, err := service.GenerateRelatedQuestions(user.ID, req.Question, req.SearchID, h.searchSvc, h.tenantSvc, h.llm) + ctx := c.Request.Context() + questions, err := service.GenerateRelatedQuestions(ctx, user.ID, req.Question, req.SearchID, h.searchSvc, h.tenantSvc, h.llm) if err != nil { jsonInternalError(c, err) return diff --git a/internal/handler/chunk.go b/internal/handler/chunk.go index 5c9d61f4ed..d21d105c7b 100644 --- a/internal/handler/chunk.go +++ b/internal/handler/chunk.go @@ -16,6 +16,7 @@ package handler import ( + "context" "encoding/json" "errors" "fmt" @@ -39,7 +40,7 @@ type chunkService interface { UpdateChunk(req *service.UpdateChunkRequest, userID string) error RemoveChunks(req *service.RemoveChunksRequest, userID string) (int64, error) Parse(userID, datasetID string, req *service.ParseFileRequest) (map[string]interface{}, common.ErrorCode, error) - AddChunk(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) + AddChunk(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) StopParsing(userID, datasetID string, req service.StopParsingRequest) (*service.StopParsingResponse, common.ErrorCode, error) } @@ -698,6 +699,7 @@ func addChunkResponseMessage(code common.ErrorCode, err error) string { } func (h *ChunkHandler) AddChunk(c *gin.Context) { + ctx := c.Request.Context() user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { common.ErrorWithCode(c, errorCode, errorMessage) @@ -756,7 +758,7 @@ func (h *ChunkHandler) AddChunk(c *gin.Context) { ImageBase64: imageBase64, } - resp, err := h.chunkService.AddChunk(&req, userID) + resp, err := h.chunkService.AddChunk(ctx, &req, userID) if err != nil { var codedErr service.ErrorCoder if errors.As(err, &codedErr) { diff --git a/internal/handler/chunk_test.go b/internal/handler/chunk_test.go index 828a1abe3a..8a0516475b 100644 --- a/internal/handler/chunk_test.go +++ b/internal/handler/chunk_test.go @@ -1,6 +1,7 @@ package handler import ( + "context" "encoding/json" "errors" "net/http" @@ -20,7 +21,7 @@ import ( // Only the methods actually called by the test are set; others panic. type mockChunkSvc struct { retrievalTestFn func(req *service.RetrievalTestRequest, userID string) (*service.RetrievalTestResponse, error) - addChunkFn func(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) + addChunkFn func(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) listFn func(req *service.ListChunksRequest, userID string) (*service.ListChunksResponse, error) switchChunksFn func(userID, datasetID, documentID string, availableInt int, chunkIDs []string) error updateChunkFn func(req *service.UpdateChunkRequest, userID string) error @@ -82,9 +83,9 @@ func (m *mockChunkSvc) StopParsing(userID, datasetID string, req service.StopPar func (m *mockChunkSvc) Parse(string, string, *service.ParseFileRequest) (map[string]interface{}, common.ErrorCode, error) { panic("not implemented") } -func (m *mockChunkSvc) AddChunk(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { +func (m *mockChunkSvc) AddChunk(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { if m.addChunkFn != nil { - return m.addChunkFn(req, userID) + return m.addChunkFn(ctx, req, userID) } return &service.AddChunkResponse{Chunk: map[string]interface{}{"id": "chunk-1"}}, nil } @@ -641,7 +642,7 @@ func (e addChunkTestError) Code() common.ErrorCode { return e.code } func TestChunkHandlerAddChunkSuccess(t *testing.T) { mock := &mockChunkSvc{ - addChunkFn: func(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { + addChunkFn: func(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { if userID != "user1" { t.Fatalf("userID = %q, want user1", userID) } @@ -679,7 +680,7 @@ func TestChunkHandlerAddChunkSuccess(t *testing.T) { func TestChunkHandlerAddChunkPathIDsOverrideBody(t *testing.T) { mock := &mockChunkSvc{ - addChunkFn: func(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { + addChunkFn: func(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { if req.DatasetID != "kb1" || req.DocumentID != "doc1" { t.Fatalf("path IDs were not preserved: %#v", req) } @@ -710,7 +711,7 @@ func TestChunkHandlerAddChunkPathIDsOverrideBody(t *testing.T) { func TestChunkHandlerAddChunkCodedError(t *testing.T) { mock := &mockChunkSvc{ - addChunkFn: func(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { + addChunkFn: func(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { return nil, addChunkTestError{code: common.CodeDataError, msg: "`content` is required"} }, } @@ -761,7 +762,7 @@ func TestChunkHandlerAddChunkValidatesListFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mock := &mockChunkSvc{ - addChunkFn: func(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { + addChunkFn: func(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { t.Fatal("service should not be called for invalid request") return nil, nil }, @@ -792,7 +793,7 @@ func TestChunkHandlerAddChunkValidatesListFields(t *testing.T) { func TestChunkHandlerAddChunkHidesServerErrorDetails(t *testing.T) { mock := &mockChunkSvc{ - addChunkFn: func(req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { + addChunkFn: func(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) { return nil, addChunkTestError{code: common.CodeServerError, msg: "encode chunk embedding: provider secret"} }, } diff --git a/internal/handler/dataset.go b/internal/handler/dataset.go index 099d0985f2..1ebbd2b432 100644 --- a/internal/handler/dataset.go +++ b/internal/handler/dataset.go @@ -708,7 +708,8 @@ func (h *DatasetsHandler) CheckEmbedding(c *gin.Context) { return } - data, code, err := h.datasetsService.CheckEmbedding(userID, datasetID, &req) + ctx := c.Request.Context() + data, code, err := h.datasetsService.CheckEmbedding(ctx, userID, datasetID, &req) if err != nil { if code == common.CodeNotEffective { common.ResponseWithCodeData(c, code, data, err.Error()) diff --git a/internal/handler/providers.go b/internal/handler/providers.go index 53eca76eed..b24daaba75 100644 --- a/internal/handler/providers.go +++ b/internal/handler/providers.go @@ -190,6 +190,7 @@ func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { return } + ctx := c.Request.Context() var req CreateProviderInstanceRequest if err := c.ShouldBindJSON(&req); err != nil { common.ErrorWithCode(c, common.CodeBadRequest, err.Error()) @@ -211,7 +212,7 @@ func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { return } - _, err := h.modelProviderService.CreateProviderInstance(providerName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, userID, req.ModelInfo) + _, err := h.modelProviderService.CreateProviderInstance(ctx, providerName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, userID, req.ModelInfo) if err != nil { common.ErrorWithCode(c, common.CodeServerError, err.Error()) return @@ -276,9 +277,10 @@ func (h *ProviderHandler) ShowInstanceBalance(c *gin.Context) { } userID := c.GetString("user_id") + ctx := c.Request.Context() // Get tenant ID from user - balance, errorCode, err := h.modelProviderService.ShowInstanceBalance(providerName, instanceName, userID) + balance, errorCode, err := h.modelProviderService.ShowInstanceBalance(ctx, providerName, instanceName, userID) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -294,6 +296,7 @@ func (h *ProviderHandler) CheckConnection(c *gin.Context) { return } + ctx := c.Request.Context() var req service.CheckConnectionRequest if err := c.ShouldBindJSON(&req); err != nil { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, err.Error()) @@ -301,7 +304,7 @@ func (h *ProviderHandler) CheckConnection(c *gin.Context) { } userID := c.GetString("user_id") - errCode, err := h.modelProviderService.CheckConnection(providerName, req.APIKey, req.Region, req.BaseURL, req.InstanceID, userID, service.ListModelNames(req.ModelInfo)) + errCode, err := h.modelProviderService.CheckConnection(ctx, providerName, req.APIKey, req.Region, req.BaseURL, req.InstanceID, userID, service.ListModelNames(req.ModelInfo)) if err != nil { common.ErrorWithCode(c, errCode, err.Error()) return @@ -311,6 +314,7 @@ func (h *ProviderHandler) CheckConnection(c *gin.Context) { } func (h *ProviderHandler) CheckInstanceConnection(c *gin.Context) { + ctx := c.Request.Context() providerName := c.Param("provider_name") if providerName == "" { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required") @@ -336,7 +340,7 @@ func (h *ProviderHandler) CheckInstanceConnection(c *gin.Context) { baseURL, _ := instanceInfo["base_url"].(string) instanceID, _ := instanceInfo["id"].(string) - errorCode, err := h.modelProviderService.CheckConnection(providerName, apikey, region, baseURL, instanceID, userID, nil) + errorCode, err := h.modelProviderService.CheckConnection(ctx, providerName, apikey, region, baseURL, instanceID, userID, nil) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -346,6 +350,7 @@ func (h *ProviderHandler) CheckInstanceConnection(c *gin.Context) { } func (h *ProviderHandler) ListTasks(c *gin.Context) { + ctx := c.Request.Context() providerName := c.Param("provider_name") if providerName == "" { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required") @@ -361,7 +366,7 @@ func (h *ProviderHandler) ListTasks(c *gin.Context) { userID := c.GetString("user_id") // Get tenant ID from user - listTaskResponse, errorCode, err := h.modelProviderService.ListTasks(providerName, instanceName, userID) + listTaskResponse, errorCode, err := h.modelProviderService.ListTasks(ctx, providerName, instanceName, userID) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -371,6 +376,7 @@ func (h *ProviderHandler) ListTasks(c *gin.Context) { } func (h *ProviderHandler) ShowTask(c *gin.Context) { + ctx := c.Request.Context() providerName := c.Param("provider_name") if providerName == "" { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required") @@ -392,7 +398,7 @@ func (h *ProviderHandler) ShowTask(c *gin.Context) { userID := c.GetString("user_id") // Get tenant ID from user - taskResponse, errorCode, err := h.modelProviderService.ShowTask(providerName, instanceName, taskID, userID) + taskResponse, errorCode, err := h.modelProviderService.ShowTask(ctx, providerName, instanceName, taskID, userID) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -411,6 +417,7 @@ type AlterProviderInstanceRequest struct { } func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) { + ctx := c.Request.Context() providerName := c.Param("provider_name") if providerName == "" { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required") @@ -440,7 +447,7 @@ func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) { verify = *req.Verify } - code, err := h.modelProviderService.AlterProviderInstance(userID, providerName, instanceName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, req.ModelInfo, verify) + code, err := h.modelProviderService.AlterProviderInstance(ctx, userID, providerName, instanceName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, req.ModelInfo, verify) if err != nil { common.ErrorWithCode(c, code, err.Error()) return @@ -477,6 +484,7 @@ func (h *ProviderHandler) DropProviderInstance(c *gin.Context) { } func (h *ProviderHandler) ListInstanceModels(c *gin.Context) { + ctx := c.Request.Context() providerName := c.Param("provider_name") if providerName == "" { common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required") @@ -498,7 +506,7 @@ func (h *ProviderHandler) ListInstanceModels(c *gin.Context) { if keywords == "true" { // list supported models - modelList, err := h.modelProviderService.ListSupportedModels(providerName, instanceName, c.GetString("user_id")) + modelList, err := h.modelProviderService.ListSupportedModels(ctx, providerName, instanceName, c.GetString("user_id")) if err != nil { common.ErrorWithCode(c, common.CodeServerError, err.Error()) return @@ -694,6 +702,7 @@ type ChatToModelRequest struct { } func (h *ProviderHandler) ChatToModel(c *gin.Context) { + ctx := c.Request.Context() var req ChatToModelRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -799,6 +808,7 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { // Stream response using sender function (the best performance, no channel) errorCode, err := h.modelProviderService.ChatToModelStreamWithSender( + ctx, req.ProviderName, req.InstanceName, req.ModelName, @@ -830,6 +840,7 @@ func (h *ProviderHandler) ChatToModel(c *gin.Context) { messages[i] = models.Message{Role: role, Content: content} } response, errorCode, err = h.modelProviderService.ChatToModelWithMessages( + ctx, req.ProviderName, req.InstanceName, req.ModelName, @@ -864,6 +875,7 @@ type EmbedTextRequest struct { } func (h *ProviderHandler) EmbedText(c *gin.Context) { + ctx := c.Request.Context() var req EmbedTextRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -909,7 +921,7 @@ func (h *ProviderHandler) EmbedText(c *gin.Context) { var errorCode common.ErrorCode var err error - response, errorCode, err = h.modelProviderService.EmbedText(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Texts, &apiConfig, &embeddingConfig) + response, errorCode, err = h.modelProviderService.EmbedText(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Texts, &apiConfig, &embeddingConfig) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -929,6 +941,7 @@ type RerankDocumentRequest struct { } func (h *ProviderHandler) RerankDocument(c *gin.Context) { + ctx := c.Request.Context() var req RerankDocumentRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -974,7 +987,7 @@ func (h *ProviderHandler) RerankDocument(c *gin.Context) { var errorCode common.ErrorCode var err error - response, errorCode, err = h.modelProviderService.RerankDocument(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Query, req.Documents, &apiConfig, &rerankConfig) + response, errorCode, err = h.modelProviderService.RerankDocument(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Query, req.Documents, &apiConfig, &rerankConfig) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -995,6 +1008,7 @@ type TranscribeAudioRequest struct { } func (h *ProviderHandler) TranscribeAudio(c *gin.Context) { + ctx := c.Request.Context() var req TranscribeAudioRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -1070,7 +1084,7 @@ func (h *ProviderHandler) TranscribeAudio(c *gin.Context) { } // Stream response using sender function ( the best performance, no channel) - errorCode, err := h.modelProviderService.TranscribeAudioStream(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.File, &apiConfig, &asrConfig, sender) + errorCode, err := h.modelProviderService.TranscribeAudioStream(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.File, &apiConfig, &asrConfig, sender) if errorCode != common.CodeSuccess { c.SSEvent("error", err.Error()) } @@ -1082,7 +1096,7 @@ func (h *ProviderHandler) TranscribeAudio(c *gin.Context) { var errorCode common.ErrorCode var err error - response, errorCode, err = h.modelProviderService.TranscribeAudio(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.File, &apiConfig, &asrConfig) + response, errorCode, err = h.modelProviderService.TranscribeAudio(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.File, &apiConfig, &asrConfig) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -1102,6 +1116,7 @@ type AudioSpeechRequest struct { } func (h *ProviderHandler) AudioSpeech(c *gin.Context) { + ctx := c.Request.Context() var req AudioSpeechRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -1177,7 +1192,7 @@ func (h *ProviderHandler) AudioSpeech(c *gin.Context) { } // Stream response using sender function ( the best performance, no channel) - errorCode, err := h.modelProviderService.AudioSpeechStream(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Text, &apiConfig, &ttsConfig, sender) + errorCode, err := h.modelProviderService.AudioSpeechStream(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Text, &apiConfig, &ttsConfig, sender) if errorCode != common.CodeSuccess { c.SSEvent("error", err.Error()) } @@ -1189,7 +1204,7 @@ func (h *ProviderHandler) AudioSpeech(c *gin.Context) { var errorCode common.ErrorCode var err error - response, errorCode, err = h.modelProviderService.AudioSpeech(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Text, &apiConfig, &ttsConfig) + response, errorCode, err = h.modelProviderService.AudioSpeech(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Text, &apiConfig, &ttsConfig) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -1208,6 +1223,7 @@ type OCRFileRequest struct { } func (h *ProviderHandler) OCRFile(c *gin.Context) { + ctx := c.Request.Context() var req OCRFileRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -1251,7 +1267,7 @@ func (h *ProviderHandler) OCRFile(c *gin.Context) { var errorCode common.ErrorCode var err error - response, errorCode, err = h.modelProviderService.OCRFile(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Content, req.URL, &apiConfig, &OCRConfig) + response, errorCode, err = h.modelProviderService.OCRFile(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Content, req.URL, &apiConfig, &OCRConfig) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return @@ -1270,6 +1286,7 @@ type ParseFileRequest struct { } func (h *ProviderHandler) ParseFile(c *gin.Context) { + ctx := c.Request.Context() var req ParseFileRequest if err := c.ShouldBindJSON(&req); err != nil { println("JSON bind error: %v (type: %T)", err, err) @@ -1313,7 +1330,7 @@ func (h *ProviderHandler) ParseFile(c *gin.Context) { var errorCode common.ErrorCode var err error - response, errorCode, err = h.modelProviderService.ParseFile(req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Content, req.URL, &apiConfig, &parseFileConfig) + response, errorCode, err = h.modelProviderService.ParseFile(ctx, req.ProviderName, req.InstanceName, req.ModelName, req.ModelID, userID, req.Content, req.URL, &apiConfig, &parseFileConfig) if err != nil { common.ErrorWithCode(c, errorCode, err.Error()) return diff --git a/internal/handler/searchbot.go b/internal/handler/searchbot.go index 933b65ee12..1af77240d9 100644 --- a/internal/handler/searchbot.go +++ b/internal/handler/searchbot.go @@ -131,6 +131,7 @@ func (h *SearchBotHandler) SetAskService(svc *service.AskService) { h.askSvc = s // @Success 200 {object} map[string]interface{} // @Router /api/v1/searchbots/related_questions [post] func (h *SearchBotHandler) Handle(c *gin.Context) { + ctx := c.Request.Context() user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { common.ErrorWithCode(c, errorCode, errorMessage) @@ -148,7 +149,7 @@ func (h *SearchBotHandler) Handle(c *gin.Context) { return } - questions, err := service.GenerateRelatedQuestions(user.ID, req.Question, req.SearchID, h.searchSvc, h.tenantSvc, h.llm) + questions, err := service.GenerateRelatedQuestions(ctx, user.ID, req.Question, req.SearchID, h.searchSvc, h.tenantSvc, h.llm) if err != nil { common.Warn("searchbot related questions failed", zap.String("error", err.Error())) common.ResponseWithCodeData(c, common.CodeOperatingError, nil, "LLM call failed") @@ -378,10 +379,11 @@ func (h *SearchBotHandler) MindMap(c *gin.Context) { common.SuccessWithData(c, mindMap, "success") } -// SearchbotDetail returns the public share-page bootstrap payload for a +// SearchBotDetail returns the public share-page bootstrap payload for a // search app. The route is mounted under apiNoAuth but still requires a beta // token, matching Python's AUTH_BETA flow. -func (h *SearchBotHandler) SearchbotDetail(c *gin.Context) { +func (h *SearchBotHandler) SearchBotDetail(c *gin.Context) { + ctx := c.Request.Context() searchID := strings.TrimSpace(c.Query("search_id")) if searchID == "" { common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "search_id is required") @@ -389,7 +391,7 @@ func (h *SearchBotHandler) SearchbotDetail(c *gin.Context) { } userSvc := service.NewUserService() - user, code, err := userSvc.GetUserByBetaAPIToken(c.GetHeader("Authorization")) + user, code, err := userSvc.GetUserByBetaAPIToken(ctx, c.GetHeader("Authorization")) if err != nil { common.ResponseWithCodeData(c, code, nil, "Authentication error: API key is invalid!") return diff --git a/internal/handler/searchbot_detail_test.go b/internal/handler/searchbot_detail_test.go index 479db34c16..39a7db7883 100644 --- a/internal/handler/searchbot_detail_test.go +++ b/internal/handler/searchbot_detail_test.go @@ -58,7 +58,7 @@ func newSearchbotDetailRouter() *gin.Engine { gin.SetMode(gin.TestMode) h := NewSearchBotHandler(service.NewSearchService(), nil, nil, nil) r := gin.New() - r.GET("/api/v1/searchbots/detail", h.SearchbotDetail) + r.GET("/api/v1/searchbots/detail", h.SearchBotDetail) return r } diff --git a/internal/handler/user.go b/internal/handler/user.go index a2030e2a89..7985eadc1f 100644 --- a/internal/handler/user.go +++ b/internal/handler/user.go @@ -56,6 +56,7 @@ func NewUserHandler(userService *service.UserService) *UserHandler { // @Success 200 {object} map[string]interface{} // @Router /api/v1/users [post] func (h *UserHandler) Register(c *gin.Context) { + ctx := c.Request.Context() var req service.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { common.ResponseWithCodeData(c, common.CodeBadRequest, false, err.Error()) @@ -90,7 +91,7 @@ func (h *UserHandler) Register(c *gin.Context) { c.Header("Access-Control-Allow-Headers", "*") c.Header("Access-Control-Expose-Headers", "Authorization") - profile := h.userService.GetUserProfile(user) + profile := h.userService.GetUserProfile(ctx, user) common.SuccessWithData(c, profile, fmt.Sprintf("%s, welcome aboard!", req.Nickname)) } @@ -104,6 +105,7 @@ func (h *UserHandler) Register(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /api/v1/users/login [post] func (h *UserHandler) Login(c *gin.Context) { + ctx := c.Request.Context() startAt := time.Now() operationLog := &common.OperationLog{ EventTime: startAt, @@ -170,7 +172,7 @@ func (h *UserHandler) Login(c *gin.Context) { c.Header("Access-Control-Allow-Headers", "*") c.Header("Access-Control-Expose-Headers", "Authorization") - profile := h.userService.GetUserProfile(user) + profile := h.userService.GetUserProfile(ctx, user) common.SuccessWithData(c, profile, "Welcome back!") } @@ -184,6 +186,7 @@ func (h *UserHandler) Login(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /v1/user/login [post] func (h *UserHandler) LoginByEmail(c *gin.Context) { + ctx := c.Request.Context() startAt := time.Now() operationLog := &common.OperationLog{ EventTime: startAt, @@ -221,7 +224,7 @@ func (h *UserHandler) LoginByEmail(c *gin.Context) { return } - user, code, err := h.userService.LoginByEmail(&req) + user, code, err := h.userService.LoginByEmail(ctx, &req) if err != nil { common.ResponseWithCodeData(c, code, false, err.Error()) operationLog.ErrorCode = uint16(code) @@ -255,7 +258,7 @@ func (h *UserHandler) LoginByEmail(c *gin.Context) { c.Header("Access-Control-Allow-Headers", "*") c.Header("Access-Control-Expose-Headers", "Authorization") - profile := h.userService.GetUserProfile(user) + profile := h.userService.GetUserProfile(ctx, user) common.SuccessWithData(c, profile, "Welcome back!") } @@ -269,6 +272,7 @@ func (h *UserHandler) LoginByEmail(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /api/v1/users/{id} [get] func (h *UserHandler) GetUserByID(c *gin.Context) { + ctx := c.Request.Context() idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { @@ -276,7 +280,7 @@ func (h *UserHandler) GetUserByID(c *gin.Context) { return } - user, code, err := h.userService.GetUserByID(uint(id)) + user, code, err := h.userService.GetUserByID(ctx, uint(id)) if err != nil { common.ResponseWithCodeData(c, code, false, err.Error()) return @@ -295,6 +299,7 @@ func (h *UserHandler) GetUserByID(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /v1/user/logout [post] func (h *UserHandler) Logout(c *gin.Context) { + ctx := c.Request.Context() startAt := time.Now() operationLog := &common.OperationLog{ EventTime: startAt, @@ -335,7 +340,7 @@ func (h *UserHandler) Logout(c *gin.Context) { } // Get user by access token - user, code, err := h.userService.GetUserByToken(token) + user, code, err := h.userService.GetUserByToken(ctx, token) if err != nil { common.ResponseWithHttpCodeData(c, http.StatusUnauthorized, code, nil, "Invalid access token") c.Abort() @@ -369,6 +374,7 @@ func (h *UserHandler) Logout(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /v1/user/info [get] func (h *UserHandler) Info(c *gin.Context) { + ctx := c.Request.Context() user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { common.ErrorWithCode(c, errorCode, errorMessage) @@ -376,7 +382,7 @@ func (h *UserHandler) Info(c *gin.Context) { } // Get user profile - profile := h.userService.GetUserProfile(user) + profile := h.userService.GetUserProfile(ctx, user) common.SuccessWithData(c, profile, "success") } @@ -392,6 +398,7 @@ func (h *UserHandler) Info(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /api/v1/users/me [patch] func (h *UserHandler) Setting(c *gin.Context) { + ctx := c.Request.Context() user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { common.ErrorWithCode(c, errorCode, errorMessage) @@ -406,7 +413,7 @@ func (h *UserHandler) Setting(c *gin.Context) { } // Update user settings - code, err := h.userService.UpdateUserSettings(user, &req) + code, err := h.userService.UpdateUserSettings(ctx, user, &req) if err != nil { if code == common.CodeExceptionError { common.ResponseWithCodeData(c, common.CodeExceptionError, false, err.Error()) @@ -430,6 +437,7 @@ func (h *UserHandler) Setting(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /v1/user/setting/password [post] func (h *UserHandler) ChangePassword(c *gin.Context) { + ctx := c.Request.Context() user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { common.ErrorWithCode(c, errorCode, errorMessage) @@ -444,7 +452,7 @@ func (h *UserHandler) ChangePassword(c *gin.Context) { } // Change password - code, err := h.userService.ChangePassword(user, &req) + code, err := h.userService.ChangePassword(ctx, user, &req) if err != nil { common.ResponseWithCodeData(c, code, false, err.Error()) return @@ -482,6 +490,7 @@ func (h *UserHandler) GetLoginChannels(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /v1/user/set_tenant_info [post] func (h *UserHandler) SetTenantInfo(c *gin.Context) { + ctx := c.Request.Context() user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { common.ErrorWithCode(c, errorCode, errorMessage) @@ -531,7 +540,7 @@ func (h *UserHandler) SetTenantInfo(c *gin.Context) { req.TTSID = &value } - code, err := h.userService.SetTenantInfo(user.ID, &req) + code, err := h.userService.SetTenantInfo(ctx, user.ID, &req) if err != nil { common.ResponseWithCodeData(c, code, nil, err.Error()) return @@ -684,6 +693,7 @@ func (h *UserHandler) ForgotVerifyOTP(c *gin.Context) { // @Success 200 {object} map[string]interface{} // @Router /api/v1/auth/password/reset [post] func (h *UserHandler) ForgotResetPassword(c *gin.Context) { + ctx := c.Request.Context() var req service.ForgotResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { common.ResponseWithCodeData(c, common.CodeArgumentError, false, err.Error()) @@ -714,7 +724,7 @@ func (h *UserHandler) ForgotResetPassword(c *gin.Context) { // already in the Authorization header). Mirror the Python contract // `user.to_safe_dict(for_self=True)` by stripping those fields before // writing. PR #15290 review. - profile := h.userService.GetUserProfile(user) + profile := h.userService.GetUserProfile(ctx, user) delete(profile, "password") delete(profile, "access_token") common.SuccessWithData(c, profile, "Password reset successful. Logged in.") diff --git a/internal/ingestion/component/docx_vision_dispatch.go b/internal/ingestion/component/docx_vision_dispatch.go index 0cd81882f5..622bb968e5 100644 --- a/internal/ingestion/component/docx_vision_dispatch.go +++ b/internal/ingestion/component/docx_vision_dispatch.go @@ -27,6 +27,7 @@ package component import ( + "context" "fmt" "os" "path/filepath" @@ -71,6 +72,7 @@ var ( // It returns (result, handled, error). handled is true when the // dispatched result was modified. func maybeDispatchDOCXVision( + ctx context.Context, fileType utility.FileType, dispatched parserDispatchResult, inputs map[string]any, @@ -125,7 +127,7 @@ func maybeDispatchDOCXVision( } messages := buildVisionMessages(prompt, imageB64) - resp, err := visionChatInvoker(driver, modelName, messages, apiConfig) + resp, err := visionChatInvoker(ctx, driver, modelName, messages, apiConfig) if err != nil { return } @@ -290,11 +292,12 @@ func extractDOCXVisionAnswer(resp *modelModule.ChatResponse) string { } func defaultVisionChatInvoker( + ctx context.Context, driver modelModule.ModelDriver, modelName string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, ) (*modelModule.ChatResponse, error) { vision := true - return driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) + return driver.ChatWithMessages(ctx, modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) } diff --git a/internal/ingestion/component/markdown_vision_dispatch.go b/internal/ingestion/component/markdown_vision_dispatch.go index 88a3294da8..59f3793d6b 100644 --- a/internal/ingestion/component/markdown_vision_dispatch.go +++ b/internal/ingestion/component/markdown_vision_dispatch.go @@ -28,6 +28,7 @@ package component import ( + "context" "fmt" "strings" "sync" @@ -53,6 +54,7 @@ var ( // path produces JSON items with doc_type_kwd == "image" and an // "image" base64 field. It returns (result, handled, error). func maybeDispatchMarkdownVision( + ctx context.Context, fileType utility.FileType, dispatched parserDispatchResult, inputs map[string]any, @@ -122,7 +124,7 @@ func maybeDispatchMarkdownVision( } messages := buildVisionMessages(prompt, item.imageB64) - resp, err := visionChatInvoker(driver, modelName, messages, apiConfig) + resp, err := visionChatInvoker(ctx, driver, modelName, messages, apiConfig) if err != nil { return } diff --git a/internal/ingestion/component/media_dispatch.go b/internal/ingestion/component/media_dispatch.go index 09a4c9b378..5285220a82 100644 --- a/internal/ingestion/component/media_dispatch.go +++ b/internal/ingestion/component/media_dispatch.go @@ -52,6 +52,7 @@ import ( // Video dispatch: IMAGE2TEXT vision chat --- func maybeDispatchVideo( + ctx context.Context, fileType utility.FileType, filename string, binary []byte, @@ -94,7 +95,7 @@ func maybeDispatchVideo( }, }} vision := true - resp, err := driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) + resp, err := driver.ChatWithMessages(ctx, modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) if err != nil { return parserDispatchResult{}, true, fmt.Errorf("Parser: video describe: %w", err) @@ -124,6 +125,7 @@ func maybeDispatchVideo( // 4. Returns combined text func maybeDispatchImage( + ctx context.Context, fileType utility.FileType, filename string, binary []byte, @@ -221,7 +223,7 @@ func maybeDispatchImage( }, }} vision := true - resp, err := driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) + resp, err := driver.ChatWithMessages(ctx, modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) if err != nil { if ocrText != "" { return parserDispatchResult{ @@ -262,6 +264,7 @@ func maybeDispatchImage( // - Returns the transcription as text func maybeDispatchAudio( + ctx context.Context, fileType utility.FileType, filename string, binary []byte, @@ -294,7 +297,7 @@ func maybeDispatchAudio( } defer os.Remove(tmpFile) - resp, err := driver.TranscribeAudio(&modelName, &tmpFile, apiConfig, nil, nil) + resp, err := driver.TranscribeAudio(ctx, &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.go b/internal/ingestion/component/parser.go index 432c8f3939..9b8e24cdf9 100644 --- a/internal/ingestion/component/parser.go +++ b/internal/ingestion/component/parser.go @@ -377,7 +377,7 @@ func (c *ParserComponent) Invoke(ctx context.Context, inputs map[string]any) (ma } } - dispatched, handledVision, visionErr := maybeDispatchPDFVision(fileTypeExt, filename, binary, inputs, c.Setups) + dispatched, handledVision, visionErr := maybeDispatchPDFVision(ctx, fileTypeExt, filename, binary, inputs, c.Setups) if visionErr != nil { return nil, visionErr } @@ -386,7 +386,7 @@ func (c *ParserComponent) Invoke(ctx context.Context, inputs map[string]any) (ma if !handledVision { // Video dispatch: IMAGE2TEXT vision chat. // Mirrors Python's _video(). - dispatched, handledMedia, visionErr = maybeDispatchVideo(fileTypeExt, filename, binary, inputs, c.Setups) + dispatched, handledMedia, visionErr = maybeDispatchVideo(ctx, fileTypeExt, filename, binary, inputs, c.Setups) if visionErr != nil { return nil, visionErr } @@ -395,7 +395,7 @@ func (c *ParserComponent) Invoke(ctx context.Context, inputs map[string]any) (ma if !handledVision && !handledMedia { // Image/Picture dispatch: OCR + IMAGE2TEXT vision describe. // Mirrors Python's rag/app/picture.py:chunk() image branch. - dispatched, handledImage, visionErr = maybeDispatchImage(fileTypeExt, filename, binary, inputs, c.Setups) + dispatched, handledImage, visionErr = maybeDispatchImage(ctx, fileTypeExt, filename, binary, inputs, c.Setups) if visionErr != nil { return nil, visionErr } @@ -404,25 +404,25 @@ func (c *ParserComponent) Invoke(ctx context.Context, inputs map[string]any) (ma if !handledVision && !handledMedia && !handledImage { // Audio dispatch: SPEECH2TEXT transcription. // Mirrors Python's rag/app/audio.py:chunk(). - dispatched, handledAudio, visionErr = maybeDispatchAudio(fileTypeExt, filename, binary, inputs, c.Setups) + dispatched, handledAudio, visionErr = maybeDispatchAudio(ctx, fileTypeExt, filename, binary, inputs, c.Setups) if visionErr != nil { return nil, visionErr } } if !handledVision && !handledMedia && !handledImage && !handledAudio { - dispatched = dispatchParse(fileTypeExt, filename, binary, c.Setups) + dispatched = dispatchParse(ctx, fileTypeExt, filename, binary, c.Setups) dispatched = hydrateEmptyDispatchPayload(dispatched, binary) // DOCX vision figure enhancement: enrich the markdown // with LLM-generated descriptions of embedded images. // Mirrors Python's vision_figure_parser_docx_wrapper_naive. - dispatched, _, _ = maybeDispatchDOCXVision(fileTypeExt, dispatched, inputs, c.Setups) + dispatched, _, _ = maybeDispatchDOCXVision(ctx, fileTypeExt, dispatched, inputs, c.Setups) // Markdown vision figure enhancement: enrich parsed // markdown JSON items with LLM-generated descriptions of // referenced images (![alt](url)). Mirrors Python's // enhance_media_sections_with_vision in _markdown. - dispatched, _, _ = maybeDispatchMarkdownVision(fileTypeExt, dispatched, inputs) + dispatched, _, _ = maybeDispatchMarkdownVision(ctx, fileTypeExt, dispatched, inputs) } // Known/supported families must fail loudly when dispatch or // parsing breaks. Only unknown families keep the raw-text fallback. diff --git a/internal/ingestion/component/parser_dispatch.go b/internal/ingestion/component/parser_dispatch.go index e3a0ded7fb..981eb87f9b 100644 --- a/internal/ingestion/component/parser_dispatch.go +++ b/internal/ingestion/component/parser_dispatch.go @@ -24,6 +24,7 @@ package component import ( + "context" "fmt" "maps" "strings" @@ -138,7 +139,7 @@ func resolveOutputFormat(family string, setups map[string]schema.ParserSetup, al // re-reading setups. lib_type is no longer threaded through: the // Python dispatcher picks a single backend per family and the Go // constructors mirror that. -func dispatchParse(fileType utility.FileType, filename string, data []byte, setups map[string]schema.ParserSetup) parserDispatchResult { +func dispatchParse(ctx context.Context, fileType utility.FileType, filename string, data []byte, setups map[string]schema.ParserSetup) parserDispatchResult { if fileType == utility.FileTypeOTHER { // Unknown / unset family. The component treats the bytes // as text pages; splitIntoPages handles it. We return no @@ -159,7 +160,7 @@ func dispatchParse(fileType utility.FileType, filename string, data []byte, setu } configureParserFromSetups(p, fileType, setups) - res := p.ParseWithResult(filename, data) + res := p.ParseWithResult(ctx, filename, data) if res.Err != nil { return parserDispatchResult{Err: fmt.Errorf("Parser: %q: %w", fileType, res.Err)} } diff --git a/internal/ingestion/component/parser_dispatch_pdf_vision_cgo_test.go b/internal/ingestion/component/parser_dispatch_pdf_vision_cgo_test.go index 52874131e1..b697bec671 100644 --- a/internal/ingestion/component/parser_dispatch_pdf_vision_cgo_test.go +++ b/internal/ingestion/component/parser_dispatch_pdf_vision_cgo_test.go @@ -35,7 +35,7 @@ func TestDispatch_PDFVisionJSON_RealPDFFixture(t *testing.T) { } var callCount atomic.Int32 - pdfVisionChatInvoker = func(_ modelModule.ModelDriver, modelName string, messages []modelModule.Message, _ *modelModule.APIConfig) (*modelModule.ChatResponse, error) { + pdfVisionChatInvoker = func(ctx context.Context, _ modelModule.ModelDriver, modelName string, messages []modelModule.Message, _ *modelModule.APIConfig) (*modelModule.ChatResponse, error) { if modelName != "fixture-vlm" { t.Fatalf("modelName = %q, want fixture-vlm", modelName) } diff --git a/internal/ingestion/component/parser_dispatch_test.go b/internal/ingestion/component/parser_dispatch_test.go index 350383e09d..197ad40692 100644 --- a/internal/ingestion/component/parser_dispatch_test.go +++ b/internal/ingestion/component/parser_dispatch_test.go @@ -430,7 +430,7 @@ func TestDispatch_PDFVisionJSON_UsesTenantAwareModel(t *testing.T) { } return nil, "resolved-vlm", nil, nil } - pdfVisionChatInvoker = func(_ modelModule.ModelDriver, modelName string, messages []modelModule.Message, _ *modelModule.APIConfig) (*modelModule.ChatResponse, error) { + pdfVisionChatInvoker = func(ctx context.Context, _ modelModule.ModelDriver, modelName string, messages []modelModule.Message, _ *modelModule.APIConfig) (*modelModule.ChatResponse, error) { if modelName != "resolved-vlm" { return nil, fmt.Errorf("modelName = %q, want resolved-vlm", modelName) } @@ -508,7 +508,7 @@ func TestDispatch_PDFVisionJSON_PreservesEmptyPages(t *testing.T) { return nil, "resolved-vlm", nil, nil } call := 0 - pdfVisionChatInvoker = func(_ modelModule.ModelDriver, _ string, _ []modelModule.Message, _ *modelModule.APIConfig) (*modelModule.ChatResponse, error) { + pdfVisionChatInvoker = func(ctx context.Context, _ modelModule.ModelDriver, _ string, _ []modelModule.Message, _ *modelModule.APIConfig) (*modelModule.ChatResponse, error) { call++ answer := "" if call == 1 { @@ -595,49 +595,49 @@ 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, usage *common.ModelUsage) (*modelModule.ChatResponse, error) { +func (d *mineruTestDriver) ChatWithMessages(ctx context.Context, 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, usage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *mineruTestDriver) ChatStreamlyWithSender(ctx context.Context, 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, usage *common.ModelUsage) ([]modelModule.EmbeddingData, error) { +func (d *mineruTestDriver) Embed(ctx context.Context, 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, usage *common.ModelUsage) (*modelModule.RerankResponse, error) { +func (d *mineruTestDriver) Rerank(ctx context.Context, 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, usage *common.ModelUsage) (*modelModule.ASRResponse, error) { +func (d *mineruTestDriver) TranscribeAudio(ctx context.Context, 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, usage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *mineruTestDriver) TranscribeAudioWithSender(ctx context.Context, 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, usage *common.ModelUsage) (*modelModule.TTSResponse, error) { +func (d *mineruTestDriver) AudioSpeech(ctx context.Context, 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, usage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *mineruTestDriver) AudioSpeechWithSender(ctx context.Context, 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, usage *common.ModelUsage) (*modelModule.OCRFileResponse, error) { +func (d *mineruTestDriver) OCRFile(ctx context.Context, 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, usage *common.ModelUsage) (*modelModule.ParseFileResponse, error) { +func (d *mineruTestDriver) ParseFile(ctx context.Context, 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) { +func (d *mineruTestDriver) ListModels(ctx context.Context, apiConfig *modelModule.APIConfig) ([]modelModule.ListModelResponse, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) Balance(apiConfig *modelModule.APIConfig) (map[string]interface{}, error) { +func (d *mineruTestDriver) Balance(ctx context.Context, apiConfig *modelModule.APIConfig) (map[string]interface{}, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) CheckConnection(apiConfig *modelModule.APIConfig) error { +func (d *mineruTestDriver) CheckConnection(ctx context.Context, apiConfig *modelModule.APIConfig) error { return fmt.Errorf("not implemented") } -func (d *mineruTestDriver) ListTasks(apiConfig *modelModule.APIConfig) ([]modelModule.ListTaskStatus, error) { +func (d *mineruTestDriver) ListTasks(ctx context.Context, apiConfig *modelModule.APIConfig) ([]modelModule.ListTaskStatus, error) { return nil, fmt.Errorf("not implemented") } -func (d *mineruTestDriver) ShowTask(taskID string, apiConfig *modelModule.APIConfig) (*modelModule.TaskResponse, error) { +func (d *mineruTestDriver) ShowTask(ctx context.Context, taskID string, apiConfig *modelModule.APIConfig) (*modelModule.TaskResponse, error) { return nil, fmt.Errorf("not implemented") } diff --git a/internal/ingestion/component/pdf_vision_dispatch.go b/internal/ingestion/component/pdf_vision_dispatch.go index b47d1f8db8..8354a833c9 100644 --- a/internal/ingestion/component/pdf_vision_dispatch.go +++ b/internal/ingestion/component/pdf_vision_dispatch.go @@ -43,6 +43,7 @@ var ( ) func maybeDispatchPDFVision( + ctx context.Context, fileType utility.FileType, filename string, binary []byte, @@ -86,7 +87,7 @@ func maybeDispatchPDFVision( return parserDispatchResult{}, true, fmt.Errorf( `Parser: pdf parse_method %q requires tenant_id to resolve IMAGE2TEXT model`, modelID) } - res, err := dispatchPDFVision(filename, binary, tenantID, modelID, setup) + res, err := dispatchPDFVision(ctx, filename, binary, tenantID, modelID, setup) if err != nil { return parserDispatchResult{}, true, err } @@ -373,6 +374,7 @@ func isNamedPDFParseMethod(raw string) bool { } func dispatchPDFVision( + ctx context.Context, filename string, binary []byte, tenantID string, @@ -396,7 +398,7 @@ func dispatchPDFVision( markdownParts := make([]string, 0, len(renderedPages)) for _, page := range renderedPages { prompt := renderPDFVisionPrompt(promptTemplate, page.PageNumber) - resp, err := pdfVisionChatInvoker(driver, resolvedModelName, buildPDFVisionMessages(prompt, page.ImageURL), apiConfig) + resp, err := pdfVisionChatInvoker(ctx, driver, resolvedModelName, buildPDFVisionMessages(prompt, page.ImageURL), apiConfig) if err != nil { return parserDispatchResult{}, fmt.Errorf("Parser: pdf vision page %d: %w", page.PageNumber, err) } @@ -472,13 +474,14 @@ func defaultPDFVisionModelResolver( } func defaultPDFVisionChatInvoker( + ctx context.Context, driver modelModule.ModelDriver, modelName string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, ) (*modelModule.ChatResponse, error) { vision := true - return driver.ChatWithMessages(modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) + return driver.ChatWithMessages(ctx, modelName, messages, apiConfig, &modelModule.ChatConfig{Vision: &vision}, nil) } func loadPDFVisionPrompt(name string) (string, error) { diff --git a/internal/ingestion/component/tokenizer.go b/internal/ingestion/component/tokenizer.go index bc6e9488a3..dcae6b5ea9 100644 --- a/internal/ingestion/component/tokenizer.go +++ b/internal/ingestion/component/tokenizer.go @@ -153,7 +153,7 @@ type EmbeddingResult struct { // Embedder is the testability seam for the embedding branch. type Embedder interface { MaxTokens() int - Encode(texts []string) ([]EmbeddingResult, error) + Encode(ctx context.Context, texts []string) ([]EmbeddingResult, error) } // EmbedderResolver resolves the embedder for one tokenizer invocation. @@ -487,7 +487,7 @@ func encodeWithTimeout(ctx context.Context, embedder Embedder, texts []string) ( encErr error ) timeoutErr := runtime.WithTimeout(ctx, tokenizerTimeout(), func(timeoutCtx context.Context) error { - results, encErr = embedder.Encode(texts) + results, encErr = embedder.Encode(ctx, texts) return encErr }) if timeoutErr != nil { diff --git a/internal/ingestion/component/tokenizer_unit_test.go b/internal/ingestion/component/tokenizer_unit_test.go index d106979896..403799740f 100644 --- a/internal/ingestion/component/tokenizer_unit_test.go +++ b/internal/ingestion/component/tokenizer_unit_test.go @@ -53,7 +53,7 @@ func (s *stubEmbedder) MaxTokens() int { return s.maxTokens } -func (s *stubEmbedder) Encode(texts []string) ([]EmbeddingResult, error) { +func (s *stubEmbedder) Encode(ctx context.Context, texts []string) ([]EmbeddingResult, error) { s.calls.Add(1) copied := append([]string(nil), texts...) s.callInputs = append(s.callInputs, copied) diff --git a/internal/ingestion/pipeline/template_integration_test.go b/internal/ingestion/pipeline/template_integration_test.go index 7dc5215b7a..0b25a3d0a5 100644 --- a/internal/ingestion/pipeline/template_integration_test.go +++ b/internal/ingestion/pipeline/template_integration_test.go @@ -46,7 +46,7 @@ type fixedEmbedder struct{} func (fixedEmbedder) MaxTokens() int { return 0 } -func (fixedEmbedder) Encode(texts []string) ([]componentpkg.EmbeddingResult, error) { +func (fixedEmbedder) Encode(ctx context.Context, texts []string) ([]componentpkg.EmbeddingResult, error) { out := make([]componentpkg.EmbeddingResult, 0, len(texts)) for _, text := range texts { out = append(out, componentpkg.EmbeddingResult{ diff --git a/internal/ingestion/task/embedder.go b/internal/ingestion/task/embedder.go index 99c9423049..80ca34f202 100644 --- a/internal/ingestion/task/embedder.go +++ b/internal/ingestion/task/embedder.go @@ -17,6 +17,7 @@ package task import ( + "context" "fmt" "strings" @@ -37,12 +38,12 @@ func (e *embedder) MaxTokens() int { return e.model.MaxTokens } -func (e *embedder) Encode(texts []string) ([]componentpkg.EmbeddingResult, error) { +func (e *embedder) Encode(ctx context.Context, texts []string) ([]componentpkg.EmbeddingResult, error) { if e.model.ModelDriver == nil { 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, nil) + embeds, err := e.model.ModelDriver.Embed(ctx, 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 61618c9156..df2803e789 100644 --- a/internal/ingestion/task/embedder_test.go +++ b/internal/ingestion/task/embedder_test.go @@ -17,6 +17,7 @@ package task import ( + "context" "ragflow/internal/common" "testing" @@ -74,7 +75,7 @@ type stubDriver struct { capturedTexts []string } -func (d *stubDriver) Embed(modelName *string, texts []string, apiConfig *models.APIConfig, embeddingConfig *models.EmbeddingConfig, usage *common.ModelUsage) ([]models.EmbeddingData, error) { +func (d *stubDriver) Embed(ctx context.Context, 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 { @@ -88,46 +89,48 @@ 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, usage *common.ModelUsage) (*models.ChatResponse, error) { +func (d *stubDriver) ChatWithMessages(ctx context.Context, 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, usage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *stubDriver) ChatStreamlyWithSender(ctx context.Context, 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, usage *common.ModelUsage) (*models.RerankResponse, error) { +func (d *stubDriver) Rerank(ctx context.Context, 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, usage *common.ModelUsage) (*models.ASRResponse, error) { +func (d *stubDriver) TranscribeAudio(ctx context.Context, 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, usage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *stubDriver) TranscribeAudioWithSender(ctx context.Context, 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, usage *common.ModelUsage) (*models.TTSResponse, error) { +func (d *stubDriver) AudioSpeech(ctx context.Context, 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, usage *common.ModelUsage, sender func(*string, *string) error) error { +func (d *stubDriver) AudioSpeechWithSender(ctx context.Context, 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, usage *common.ModelUsage) (*models.OCRFileResponse, error) { +func (d *stubDriver) OCRFile(ctx context.Context, 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, usage *common.ModelUsage) (*models.ParseFileResponse, error) { +func (d *stubDriver) ParseFile(ctx context.Context, 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) { +func (d *stubDriver) ListModels(ctx context.Context, apiConfig *models.APIConfig) ([]models.ListModelResponse, error) { return nil, nil } -func (d *stubDriver) Balance(apiConfig *models.APIConfig) (map[string]interface{}, error) { +func (d *stubDriver) Balance(ctx context.Context, apiConfig *models.APIConfig) (map[string]interface{}, error) { return nil, nil } -func (d *stubDriver) CheckConnection(apiConfig *models.APIConfig) error { return nil } -func (d *stubDriver) ListTasks(apiConfig *models.APIConfig) ([]models.ListTaskStatus, error) { +func (d *stubDriver) CheckConnection(ctx context.Context, apiConfig *models.APIConfig) error { + return nil +} +func (d *stubDriver) ListTasks(ctx context.Context, apiConfig *models.APIConfig) ([]models.ListTaskStatus, error) { return nil, nil } -func (d *stubDriver) ShowTask(taskID string, apiConfig *models.APIConfig) (*models.TaskResponse, error) { +func (d *stubDriver) ShowTask(ctx context.Context, taskID string, apiConfig *models.APIConfig) (*models.TaskResponse, error) { return nil, nil } -func (d *stubDriver) ToolCall(name string, arguments map[string]interface{}) (string, error) { +func (d *stubDriver) ToolCall(ctx context.Context, name string, arguments map[string]interface{}) (string, error) { return "", nil } diff --git a/internal/parser/parser/audio_parser.go b/internal/parser/parser/audio_parser.go index 495c9f1b99..fab1c63cf3 100644 --- a/internal/parser/parser/audio_parser.go +++ b/internal/parser/parser/audio_parser.go @@ -22,6 +22,7 @@ package parser import ( + "context" "fmt" "path/filepath" "strings" @@ -70,7 +71,7 @@ func (p *AudioParser) ConfigureFromSetup(setup map[string]any) { // file extension against the audio extension whitelist. The actual // speech-to-text transcription happens via maybeDispatchAudio at the // component layer (mirrors Python's LLMBundle.transcription call). -func (p *AudioParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *AudioParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { ext := strings.ToLower(filepath.Ext(filename)) if len(ext) > 1 && ext[0] == '.' { ext = ext[1:] diff --git a/internal/parser/parser/audio_parser_test.go b/internal/parser/parser/audio_parser_test.go index 0910b9ad3a..19f9dbdba4 100644 --- a/internal/parser/parser/audio_parser_test.go +++ b/internal/parser/parser/audio_parser_test.go @@ -22,6 +22,7 @@ import ( ) func TestAudioParser_ValidExtension(t *testing.T) { + ctx := t.Context() p := NewAudioParser() p.ConfigureFromSetup(map[string]any{ "output_format": "text", @@ -30,7 +31,7 @@ func TestAudioParser_ValidExtension(t *testing.T) { }, }) - result := p.ParseWithResult("test.mp3", []byte{1, 2, 3, 4}) + result := p.ParseWithResult(ctx, "test.mp3", []byte{1, 2, 3, 4}) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } @@ -43,9 +44,10 @@ func TestAudioParser_ValidExtension(t *testing.T) { } func TestAudioParser_AllExtensions(t *testing.T) { + ctx := t.Context() for _, ext := range []string{"da", "wave", "wav", "mp3", "aac", "flac", "ogg", "aiff", "au", "midi", "wma", "realaudio", "vqf", "oggvorbis", "ape"} { p := NewAudioParser() - result := p.ParseWithResult("audio."+ext, []byte{}) + result := p.ParseWithResult(ctx, "audio."+ext, []byte{}) if result.Err != nil { t.Errorf("extension .%s rejected: %v", ext, result.Err) } @@ -53,8 +55,9 @@ func TestAudioParser_AllExtensions(t *testing.T) { } func TestAudioParser_InvalidExtension(t *testing.T) { + ctx := t.Context() p := NewAudioParser() - result := p.ParseWithResult("notaudio.txt", []byte{}) + result := p.ParseWithResult(ctx, "notaudio.txt", []byte{}) if result.Err == nil { t.Fatal("expected error for .txt extension") } @@ -76,19 +79,21 @@ func TestAudioParser_ConfigureVLM(t *testing.T) { } func TestAudioParser_ConfigureNilSafe(t *testing.T) { + ctx := t.Context() p := NewAudioParser() p.ConfigureFromSetup(nil) p.ConfigureFromSetup(map[string]any{}) // Should not panic. - result := p.ParseWithResult("test.wav", []byte{}) + result := p.ParseWithResult(ctx, "test.wav", []byte{}) if result.Err != nil { t.Errorf("unexpected error: %v", result.Err) } } func TestAudioParser_DefaultOutputFormat(t *testing.T) { + ctx := t.Context() p := NewAudioParser() - result := p.ParseWithResult("test.flac", []byte{}) + result := p.ParseWithResult(ctx, "test.flac", []byte{}) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } diff --git a/internal/parser/parser/csv_parser.go b/internal/parser/parser/csv_parser.go index aa527fc5ec..c37e5d88f6 100644 --- a/internal/parser/parser/csv_parser.go +++ b/internal/parser/parser/csv_parser.go @@ -31,6 +31,7 @@ package parser import ( + "context" "encoding/csv" "fmt" "html" @@ -113,7 +114,7 @@ func (p *CSVParser) ConfigureFromSetup(setup map[string]any) { // Python's RAGFlowExcelParser.html(). // When TCADP parse_method is configured, the file is dispatched to // the Tencent Cloud Document Parsing API. -func (p *CSVParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *CSVParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { method := normalizeXLSXParseMethod(p.ParseMethod) switch method { case "tcadp": diff --git a/internal/parser/parser/doc_parser.go b/internal/parser/parser/doc_parser.go index f0ef78435b..2aabc61b6a 100644 --- a/internal/parser/parser/doc_parser.go +++ b/internal/parser/parser/doc_parser.go @@ -19,6 +19,7 @@ package parser import ( + "context" "fmt" officeOxide "github.com/yfedoseev/office_oxide/go" @@ -39,7 +40,7 @@ func (p *DOCParser) String() string { // the Go side uses office_oxide which supports DOC via PlainText. // OutputFormat="text" — the python side falls back to text for // legacy DOC files since structured extraction is unreliable. -func (p *DOCParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *DOCParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { doc, err := officeOxide.OpenFromBytes(data, "doc") if err != nil { return ParseResult{Err: fmt.Errorf("doc open: %w", err)} diff --git a/internal/parser/parser/docx_parser.go b/internal/parser/parser/docx_parser.go index 11781f729a..7e3619ebea 100644 --- a/internal/parser/parser/docx_parser.go +++ b/internal/parser/parser/docx_parser.go @@ -19,6 +19,7 @@ package parser import ( + "context" "encoding/base64" "encoding/json" "fmt" @@ -66,7 +67,7 @@ func (p *DOCXParser) ConfigureFromSetup(setup map[string]any) { // // JSON path mirrors python parser.py:_docx() output_format == "json". // Markdown path mirrors python naive.py: Docx() → naive_merge_docx(). -func (p *DOCXParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *DOCXParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { doc, err := officeOxide.OpenFromBytes(data, "docx") if err != nil { return ParseResult{Err: fmt.Errorf("docx open: %w", err)} diff --git a/internal/parser/parser/docx_parser_cgo_test.go b/internal/parser/parser/docx_parser_cgo_test.go index 8eeb0cfca6..65ff846f7f 100644 --- a/internal/parser/parser/docx_parser_cgo_test.go +++ b/internal/parser/parser/docx_parser_cgo_test.go @@ -9,10 +9,11 @@ import ( ) func TestDOCXParser_ParseWithResult_JSON(t *testing.T) { + ctx := t.Context() p := NewDOCXParser() p.outputFormat = "json" data := minimalDOCX(t, "Hello from JSON path") - res := p.ParseWithResult("sample.docx", data) + res := p.ParseWithResult(ctx, "sample.docx", data) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -33,6 +34,7 @@ func TestDOCXParser_ParseWithResult_JSON(t *testing.T) { } func TestDOCXParser_ConfigureFromSetup_JSON(t *testing.T) { + ctx := t.Context() p := NewDOCXParser() p.ConfigureFromSetup(map[string]any{"output_format": "json"}) if p.outputFormat != "json" { @@ -43,7 +45,7 @@ func TestDOCXParser_ConfigureFromSetup_JSON(t *testing.T) { } // Full round-trip: json config → json output data := minimalDOCX(t, "Config test") - res := p.ParseWithResult("sample.docx", data) + res := p.ParseWithResult(ctx, "sample.docx", data) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -56,13 +58,14 @@ func TestDOCXParser_ConfigureFromSetup_JSON(t *testing.T) { } func TestDOCXParser_ConfigureFromSetup_Markdown(t *testing.T) { + ctx := t.Context() p := NewDOCXParser() p.ConfigureFromSetup(map[string]any{"output_format": "markdown"}) if p.outputFormat != "markdown" { t.Fatalf("After ConfigureFromSetup, outputFormat = %q, want %q", p.outputFormat, "markdown") } data := minimalDOCX(t, "Config md test") - res := p.ParseWithResult("sample.docx", data) + res := p.ParseWithResult(ctx, "sample.docx", data) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -75,9 +78,10 @@ func TestDOCXParser_ConfigureFromSetup_Markdown(t *testing.T) { } func TestDOCXParser_ParseWithResult_CGOMinimalDocument(t *testing.T) { + ctx := t.Context() p := NewDOCXParser() data := minimalDOCX(t, "Hello from DOCX parser") - res := p.ParseWithResult("sample.docx", data) + res := p.ParseWithResult(ctx, "sample.docx", data) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } diff --git a/internal/parser/parser/email_parser.go b/internal/parser/parser/email_parser.go index 2aea280337..6952225710 100644 --- a/internal/parser/parser/email_parser.go +++ b/internal/parser/parser/email_parser.go @@ -18,6 +18,7 @@ package parser import ( "bytes" + "context" "encoding/base64" "fmt" "io" @@ -73,7 +74,7 @@ func (p *EmailParser) ConfigureFromSetup(setup map[string]any) { } } -func (p *EmailParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *EmailParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { ext := strings.ToLower(filepath.Ext(filename)) if ext == ".msg" { return ParseResult{ diff --git a/internal/parser/parser/email_parser_test.go b/internal/parser/parser/email_parser_test.go index 7ad32196f9..610f9fc3ba 100644 --- a/internal/parser/parser/email_parser_test.go +++ b/internal/parser/parser/email_parser_test.go @@ -23,6 +23,7 @@ import ( ) func TestEmailParser_EmlJSON(t *testing.T) { + ctx := t.Context() raw := strings.Join([]string{ "From: sender@example.com", "To: recipient@example.com", @@ -41,7 +42,7 @@ func TestEmailParser_EmlJSON(t *testing.T) { "fields": []string{"from", "to", "cc", "date", "subject", "body", "metadata"}, }) - result := p.ParseWithResult("test.eml", []byte(raw)) + result := p.ParseWithResult(ctx, "test.eml", []byte(raw)) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } @@ -78,6 +79,7 @@ func TestEmailParser_EmlJSON(t *testing.T) { } func TestEmailParser_EmlText(t *testing.T) { + ctx := t.Context() raw := strings.Join([]string{ "From: sender@test.com", "To: recipient@test.com", @@ -93,7 +95,7 @@ func TestEmailParser_EmlText(t *testing.T) { "fields": []string{"from", "to", "subject", "body"}, }) - result := p.ParseWithResult("test.eml", []byte(raw)) + result := p.ParseWithResult(ctx, "test.eml", []byte(raw)) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } @@ -109,8 +111,9 @@ func TestEmailParser_EmlText(t *testing.T) { } func TestEmailParser_MsgNotSupported(t *testing.T) { + ctx := t.Context() p := NewEmailParser() - result := p.ParseWithResult("test.msg", []byte{}) + result := p.ParseWithResult(ctx, "test.msg", []byte{}) if result.Err == nil { t.Fatal("expected error for .msg file") } @@ -120,6 +123,7 @@ func TestEmailParser_MsgNotSupported(t *testing.T) { } func TestEmailParser_Base64Attachment(t *testing.T) { + ctx := t.Context() attachmentContent := "Hello! This is the decoded content of the attachment." encoded := base64.StdEncoding.EncodeToString([]byte(attachmentContent)) // Simulate MIME line-wrapping (typically 76 chars per line). @@ -153,7 +157,7 @@ func TestEmailParser_Base64Attachment(t *testing.T) { "fields": []string{"from", "body", "attachments"}, }) - result := p.ParseWithResult("test.eml", []byte(raw)) + result := p.ParseWithResult(ctx, "test.eml", []byte(raw)) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } @@ -179,6 +183,7 @@ func TestEmailParser_Base64Attachment(t *testing.T) { } func TestEmailParser_Base64AttachmentInMixedMultipart(t *testing.T) { + ctx := t.Context() // Simulates the original test email structure: // multipart/mixed → multipart/alternative (text/plain + text/html) + base64 attachment innerBoundary := "inneralt" @@ -223,7 +228,7 @@ func TestEmailParser_Base64AttachmentInMixedMultipart(t *testing.T) { "fields": []string{"from", "body", "attachments"}, }) - result := p.ParseWithResult("test.eml", []byte(raw)) + result := p.ParseWithResult(ctx, "test.eml", []byte(raw)) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } @@ -249,6 +254,7 @@ func TestEmailParser_Base64AttachmentInMixedMultipart(t *testing.T) { } func TestEmailParser_Multipart(t *testing.T) { + ctx := t.Context() boundary := "boundary123" raw := strings.Join([]string{ "From: multipart@test.com", @@ -273,7 +279,7 @@ func TestEmailParser_Multipart(t *testing.T) { "fields": []string{"from", "body"}, }) - result := p.ParseWithResult("test.eml", []byte(raw)) + result := p.ParseWithResult(ctx, "test.eml", []byte(raw)) if result.Err != nil { t.Fatalf("unexpected error: %v", result.Err) } diff --git a/internal/parser/parser/epub_parser.go b/internal/parser/parser/epub_parser.go index 081c1a81c4..29d2c5eebe 100644 --- a/internal/parser/parser/epub_parser.go +++ b/internal/parser/parser/epub_parser.go @@ -31,6 +31,7 @@ package parser import ( "archive/zip" "bytes" + "context" "encoding/xml" "fmt" "io" @@ -53,7 +54,7 @@ func (p *EPUBParser) String() string { // ParseWithResult implements ParseResultProducer. It extracts XHTML // content from the EPUB spine and emits one JSON item per spine entry // with {text, doc_type_kwd:"text"}. -func (p *EPUBParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *EPUBParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { if len(data) == 0 { return ParseResult{ OutputFormat: "json", diff --git a/internal/parser/parser/html_parser.go b/internal/parser/parser/html_parser.go index b73020ed0f..315195838e 100644 --- a/internal/parser/parser/html_parser.go +++ b/internal/parser/parser/html_parser.go @@ -18,6 +18,7 @@ package parser import ( "bytes" + "context" "fmt" "strings" @@ -47,7 +48,7 @@ func (p *HTMLParser) String() string { // (bold / links / images) is intentionally NOT surfaced as a // separate ck_type — the python HtmlParser collapses inline // formatting into the parent block's text. -func (p *HTMLParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *HTMLParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { doc, err := html.Parse(bytes.NewReader(data)) if err != nil { return ParseResult{Err: fmt.Errorf("html parse: %w", err)} diff --git a/internal/parser/parser/json_parser.go b/internal/parser/parser/json_parser.go index fe03f9ef7b..9f0481aa9c 100644 --- a/internal/parser/parser/json_parser.go +++ b/internal/parser/parser/json_parser.go @@ -32,6 +32,7 @@ package parser import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "strings" @@ -51,7 +52,7 @@ func (p *JSONParser) String() string { // ParseWithResult implements ParseResultProducer. It detects the JSON // shape (array, single object, or line-delimited) and emits one JSON // item per logical record, with {text, doc_type_kwd:"text"}. -func (p *JSONParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *JSONParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { text := string(bytes.TrimSpace(data)) if text == "" { return ParseResult{ diff --git a/internal/parser/parser/markdown_parser.go b/internal/parser/parser/markdown_parser.go index 482c136b8f..55ce6b5b51 100644 --- a/internal/parser/parser/markdown_parser.go +++ b/internal/parser/parser/markdown_parser.go @@ -89,7 +89,7 @@ func (p *MarkdownParser) ConfigureFromSetup(setup map[string]any) { // data is resolved and the item carries `doc_type_kwd: "image"` with // the base64-encoded image payload. The legacy debug-print path has // been removed; callers consume ParseResult directly. -func (p *MarkdownParser) ParseWithResult(filename string, data []byte) ParseResult { +func (p *MarkdownParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult { doc := markdownNew().Parse(data) rawText := string(data) diff --git a/internal/parser/parser/markdown_parser_test.go b/internal/parser/parser/markdown_parser_test.go index d0db085a97..b0c18c71ec 100644 --- a/internal/parser/parser/markdown_parser_test.go +++ b/internal/parser/parser/markdown_parser_test.go @@ -8,12 +8,13 @@ import ( ) func TestMarkdownParser_ParseWithResult_Basic(t *testing.T) { + ctx := t.Context() p, err := NewMarkdownParser(GoMarkdown) if err != nil { t.Fatalf("NewMarkdownParser: %v", err) } md := "# Hello\n\nThis is a paragraph.\n\n* List item 1\n* List item 2\n\n```go\nfunc main() {}\n```\n" - res := p.ParseWithResult("test.md", []byte(md)) + res := p.ParseWithResult(ctx, "test.md", []byte(md)) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -33,8 +34,9 @@ func TestMarkdownParser_ParseWithResult_Basic(t *testing.T) { } func TestMarkdownParser_ParseWithResult_EmptyInput(t *testing.T) { + ctx := t.Context() p, _ := NewMarkdownParser(GoMarkdown) - res := p.ParseWithResult("empty.md", []byte("")) + res := p.ParseWithResult(ctx, "empty.md", []byte("")) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -44,13 +46,14 @@ func TestMarkdownParser_ParseWithResult_EmptyInput(t *testing.T) { } func TestMarkdownParser_ParseWithResult_ImageDataURI(t *testing.T) { + ctx := t.Context() p, _ := NewMarkdownParser(GoMarkdown) // 1×1 pixel transparent PNG encoded as data URI pixelPNG := make([]byte, 68) // minimal 1x1 PNG header pixelB64 := base64.StdEncoding.EncodeToString([]byte("fake-png-data")) md := "Some text with an image\n![test](data:image/png;base64," + pixelB64 + ")\n" _ = pixelPNG - res := p.ParseWithResult("test.md", []byte(md)) + res := p.ParseWithResult(ctx, "test.md", []byte(md)) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -71,9 +74,10 @@ func TestMarkdownParser_ParseWithResult_ImageDataURI(t *testing.T) { } func TestMarkdownParser_ParseWithResult_NoImage(t *testing.T) { + ctx := t.Context() p, _ := NewMarkdownParser(GoMarkdown) md := "# Title\n\nJust some text, no images here.\n\nMore text." - res := p.ParseWithResult("test.md", []byte(md)) + res := p.ParseWithResult(ctx, "test.md", []byte(md)) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } diff --git a/internal/parser/parser/parse_result.go b/internal/parser/parser/parse_result.go index 21a3c019ea..6dc642cdf8 100644 --- a/internal/parser/parser/parse_result.go +++ b/internal/parser/parser/parse_result.go @@ -31,6 +31,8 @@ package parser +import "context" + // ParseResult is the structured return value of a successful parse. // Exactly one of the payload fields (JSON / Markdown / Text / HTML) // is populated on success, matching the Python contract — see @@ -84,5 +86,5 @@ type ParseResult struct { // ParseResultProducer is the parser package's single structured-output // contract. Every parser returned by GetParser must implement it. type ParseResultProducer interface { - ParseWithResult(filename string, data []byte) ParseResult + ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult } diff --git a/internal/parser/parser/parse_result_test.go b/internal/parser/parser/parse_result_test.go index 36d39adf34..ffb3707755 100644 --- a/internal/parser/parser/parse_result_test.go +++ b/internal/parser/parser/parse_result_test.go @@ -130,9 +130,10 @@ func (s sentinelErr) Error() string { return string(s) } // tiny: 1 heading, 1 paragraph, 1 unordered list item, no nested // formatting. func TestMarkdownParser_ParseWithResult(t *testing.T) { + ctx := t.Context() p, _ := NewMarkdownParser(GoMarkdown) src := []byte("# Title\n\nFirst paragraph.\n\n- Item one\n") - res := p.ParseWithResult("doc.md", src) + res := p.ParseWithResult(ctx, "doc.md", src) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } diff --git a/internal/parser/parser/parse_with_result_test.go b/internal/parser/parser/parse_with_result_test.go index d38670f83c..2c523a6361 100644 --- a/internal/parser/parser/parse_with_result_test.go +++ b/internal/parser/parser/parse_with_result_test.go @@ -45,7 +45,8 @@ import ( func TestTextParser_ParseWithResult_ParaSplit(t *testing.T) { p := NewTextParser() src := []byte("First paragraph.\n\nSecond paragraph.\n\nThird.") - res := p.ParseWithResult("doc.txt", src) + ctx := t.Context() + res := p.ParseWithResult(ctx, "doc.txt", src) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -71,8 +72,9 @@ func TestTextParser_ParseWithResult_ParaSplit(t *testing.T) { // sees a non-nil JSON slice. Mirrors the MarkdownParser convention // at markdown_parser.go:71-76. func TestTextParser_ParseWithResult_Empty(t *testing.T) { + ctx := t.Context() p := NewTextParser() - res := p.ParseWithResult("empty.txt", []byte{}) + res := p.ParseWithResult(ctx, "empty.txt", []byte{}) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -85,9 +87,10 @@ func TestTextParser_ParseWithResult_Empty(t *testing.T) { // maxItemBytes boundary behaviour. A single paragraph longer // than 8192 bytes is sliced at the nearest line boundary. func TestTextParser_ParseWithResult_LongParagraphSlicing(t *testing.T) { + ctx := t.Context() p := NewTextParser() long := strings.Repeat("a", 9000) - res := p.ParseWithResult("long.txt", []byte(long)) + res := p.ParseWithResult(ctx, "long.txt", []byte(long)) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -105,9 +108,10 @@ func TestTextParser_ParseWithResult_LongParagraphSlicing(t *testing.T) { // validation rule. Invalid bytes produce an error in the result // (matching the python TxtParser's behaviour). func TestTextParser_ParseWithResult_InvalidUTF8(t *testing.T) { + ctx := t.Context() p := NewTextParser() bad := []byte{0xff, 0xfe, 0xfd} - res := p.ParseWithResult("bad.txt", bad) + res := p.ParseWithResult(ctx, "bad.txt", bad) if res.Err == nil { t.Fatal("want error for invalid UTF-8, got nil") } @@ -117,13 +121,14 @@ func TestTextParser_ParseWithResult_InvalidUTF8(t *testing.T) { // Three block elements (heading, paragraph, list) yield three // items with the python-compatible ck_type vocabulary. func TestHTMLParser_ParseWithResult_BlockSplit(t *testing.T) { + ctx := t.Context() p := NewHTMLParser() src := []byte(`

Title

First paragraph.

`) - res := p.ParseWithResult("doc.html", src) + res := p.ParseWithResult(ctx, "doc.html", src) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } @@ -157,6 +162,7 @@ func TestHTMLParser_ParseWithResult_BlockSplit(t *testing.T) { // rule that