From 73ecb7f90ba10c43e7f4f654c0e55c6b0eb5fa1a Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Jul 2025 21:06:09 +0200 Subject: [PATCH] chore: drop assistants endpoint (#5926) Signed-off-by: Ettore Di Giacinto --- core/cli/run.go | 2 - core/config/application_config.go | 9 +- core/http/app.go | 7 - core/http/endpoints/openai/assistant.go | 522 ------------------- core/http/endpoints/openai/assistant_test.go | 460 ---------------- core/http/endpoints/openai/files.go | 194 ------- core/http/endpoints/openai/files_test.go | 301 ----------- core/http/routes/openai.go | 32 -- core/schema/openai.go | 32 -- pkg/utils/config.go | 42 -- 10 files changed, 1 insertion(+), 1600 deletions(-) delete mode 100644 core/http/endpoints/openai/assistant.go delete mode 100644 core/http/endpoints/openai/assistant_test.go delete mode 100644 core/http/endpoints/openai/files.go delete mode 100644 core/http/endpoints/openai/files_test.go delete mode 100644 pkg/utils/config.go diff --git a/core/cli/run.go b/core/cli/run.go index 47e765dd8..377185a21 100644 --- a/core/cli/run.go +++ b/core/cli/run.go @@ -25,7 +25,6 @@ type RunCMD struct { ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"` GeneratedContentPath string `env:"LOCALAI_GENERATED_CONTENT_PATH,GENERATED_CONTENT_PATH" type:"path" default:"/tmp/generated/content" help:"Location for generated content (e.g. images, audio, videos)" group:"storage"` UploadPath string `env:"LOCALAI_UPLOAD_PATH,UPLOAD_PATH" type:"path" default:"/tmp/localai/upload" help:"Path to store uploads from files api" group:"storage"` - ConfigPath string `env:"LOCALAI_CONFIG_PATH,CONFIG_PATH" default:"/tmp/localai/config" group:"storage"` LocalaiConfigDir string `env:"LOCALAI_CONFIG_DIR" type:"path" default:"${basepath}/configuration" help:"Directory for dynamic loading of certain configuration files (currently api_keys.json and external_backends.json)" group:"storage"` LocalaiConfigDirPollInterval time.Duration `env:"LOCALAI_CONFIG_DIR_POLL_INTERVAL" help:"Typically the config path picks up changes automatically, but if your system has broken fsnotify events, set this to an interval to poll the LocalAI Config Dir (example: 1m)" group:"storage"` // The alias on this option is there to preserve functionality with the old `--config-file` parameter @@ -88,7 +87,6 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error { config.WithDebug(zerolog.GlobalLevel() <= zerolog.DebugLevel), config.WithGeneratedContentDir(r.GeneratedContentPath), config.WithUploadDir(r.UploadPath), - config.WithConfigsDir(r.ConfigPath), config.WithDynamicConfigDir(r.LocalaiConfigDir), config.WithDynamicConfigDirPollInterval(r.LocalaiConfigDirPollInterval), config.WithF16(r.F16), diff --git a/core/config/application_config.go b/core/config/application_config.go index 4f5f878d1..ab2526d26 100644 --- a/core/config/application_config.go +++ b/core/config/application_config.go @@ -21,8 +21,7 @@ type ApplicationConfig struct { Debug bool GeneratedContentDir string - ConfigsDir string - UploadDir string + UploadDir string DynamicConfigsDir string DynamicConfigsDirPollInterval time.Duration @@ -302,12 +301,6 @@ func WithUploadDir(uploadDir string) AppOption { } } -func WithConfigsDir(configsDir string) AppOption { - return func(o *ApplicationConfig) { - o.ConfigsDir = configsDir - } -} - func WithDynamicConfigDir(dynamicConfigsDir string) AppOption { return func(o *ApplicationConfig) { o.DynamicConfigsDir = dynamicConfigsDir diff --git a/core/http/app.go b/core/http/app.go index aba00ff7e..11b3ae573 100644 --- a/core/http/app.go +++ b/core/http/app.go @@ -10,10 +10,8 @@ import ( "github.com/dave-gray101/v2keyauth" "github.com/gofiber/websocket/v2" - "github.com/mudler/LocalAI/pkg/utils" "github.com/mudler/LocalAI/core/http/endpoints/localai" - "github.com/mudler/LocalAI/core/http/endpoints/openai" "github.com/mudler/LocalAI/core/http/middleware" "github.com/mudler/LocalAI/core/http/routes" @@ -199,11 +197,6 @@ func API(application *application.Application) (*fiber.App, error) { router.Use(csrf.New()) } - // Load config jsons - utils.LoadConfig(application.ApplicationConfig().UploadDir, openai.UploadedFilesFile, &openai.UploadedFiles) - utils.LoadConfig(application.ApplicationConfig().ConfigsDir, openai.AssistantsConfigFile, &openai.Assistants) - utils.LoadConfig(application.ApplicationConfig().ConfigsDir, openai.AssistantsFileConfigFile, &openai.AssistantFiles) - galleryService := services.NewGalleryService(application.ApplicationConfig(), application.ModelLoader()) err = galleryService.Start(application.ApplicationConfig().Context, application.BackendLoader()) if err != nil { diff --git a/core/http/endpoints/openai/assistant.go b/core/http/endpoints/openai/assistant.go deleted file mode 100644 index 1d83066a5..000000000 --- a/core/http/endpoints/openai/assistant.go +++ /dev/null @@ -1,522 +0,0 @@ -package openai - -import ( - "fmt" - "net/http" - "sort" - "strconv" - "strings" - "sync/atomic" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/microcosm-cc/bluemonday" - "github.com/mudler/LocalAI/core/config" - "github.com/mudler/LocalAI/core/schema" - "github.com/mudler/LocalAI/core/services" - model "github.com/mudler/LocalAI/pkg/model" - "github.com/mudler/LocalAI/pkg/utils" - "github.com/rs/zerolog/log" -) - -// ToolType defines a type for tool options -type ToolType string - -const ( - CodeInterpreter ToolType = "code_interpreter" - Retrieval ToolType = "retrieval" - Function ToolType = "function" - - MaxCharacterInstructions = 32768 - MaxCharacterDescription = 512 - MaxCharacterName = 256 - MaxToolsSize = 128 - MaxFileIdSize = 20 - MaxCharacterMetadataKey = 64 - MaxCharacterMetadataValue = 512 -) - -type Tool struct { - Type ToolType `json:"type"` -} - -// Assistant represents the structure of an assistant object from the OpenAI API. -type Assistant struct { - ID string `json:"id"` // The unique identifier of the assistant. - Object string `json:"object"` // Object type, which is "assistant". - Created int64 `json:"created"` // The time at which the assistant was created. - Model string `json:"model"` // The model ID used by the assistant. - Name string `json:"name,omitempty"` // The name of the assistant. - Description string `json:"description,omitempty"` // The description of the assistant. - Instructions string `json:"instructions,omitempty"` // The system instructions that the assistant uses. - Tools []Tool `json:"tools,omitempty"` // A list of tools enabled on the assistant. - FileIDs []string `json:"file_ids,omitempty"` // A list of file IDs attached to this assistant. - Metadata map[string]string `json:"metadata,omitempty"` // Set of key-value pairs attached to the assistant. -} - -var ( - Assistants = []Assistant{} // better to return empty array instead of "null" - AssistantsConfigFile = "assistants.json" -) - -type AssistantRequest struct { - Model string `json:"model"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Instructions string `json:"instructions,omitempty"` - Tools []Tool `json:"tools,omitempty"` - FileIDs []string `json:"file_ids,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// CreateAssistantEndpoint is the OpenAI Assistant API endpoint https://platform.openai.com/docs/api-reference/assistants/createAssistant -// @Summary Create an assistant with a model and instructions. -// @Param request body AssistantRequest true "query params" -// @Success 200 {object} Assistant "Response" -// @Router /v1/assistants [post] -func CreateAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - request := new(AssistantRequest) - if err := c.BodyParser(request); err != nil { - log.Warn().AnErr("Unable to parse AssistantRequest", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"}) - } - - if !modelExists(cl, ml, request.Model) { - log.Warn().Msgf("Model: %s was not found in list of models.", request.Model) - return c.Status(fiber.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Model %q not found", request.Model))) - } - - if request.Tools == nil { - request.Tools = []Tool{} - } - - if request.FileIDs == nil { - request.FileIDs = []string{} - } - - if request.Metadata == nil { - request.Metadata = make(map[string]string) - } - - id := "asst_" + strconv.FormatInt(generateRandomID(), 10) - - assistant := Assistant{ - ID: id, - Object: "assistant", - Created: time.Now().Unix(), - Model: request.Model, - Name: request.Name, - Description: request.Description, - Instructions: request.Instructions, - Tools: request.Tools, - FileIDs: request.FileIDs, - Metadata: request.Metadata, - } - - Assistants = append(Assistants, assistant) - utils.SaveConfig(appConfig.ConfigsDir, AssistantsConfigFile, Assistants) - return c.Status(fiber.StatusOK).JSON(assistant) - } -} - -var currentId int64 = 0 - -func generateRandomID() int64 { - atomic.AddInt64(¤tId, 1) - return currentId -} - -// ListAssistantsEndpoint is the OpenAI Assistant API endpoint to list assistents https://platform.openai.com/docs/api-reference/assistants/listAssistants -// @Summary List available assistents -// @Param limit query int false "Limit the number of assistants returned" -// @Param order query string false "Order of assistants returned" -// @Param after query string false "Return assistants created after the given ID" -// @Param before query string false "Return assistants created before the given ID" -// @Success 200 {object} []Assistant "Response" -// @Router /v1/assistants [get] -func ListAssistantsEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - // Because we're altering the existing assistants list we should just duplicate it for now. - returnAssistants := Assistants - // Parse query parameters - limitQuery := c.Query("limit", "20") - orderQuery := c.Query("order", "desc") - afterQuery := c.Query("after") - beforeQuery := c.Query("before") - - // Convert string limit to integer - limit, err := strconv.Atoi(limitQuery) - if err != nil { - return c.Status(http.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Invalid limit query value: %s", limitQuery))) - } - - // Sort assistants - sort.SliceStable(returnAssistants, func(i, j int) bool { - if orderQuery == "asc" { - return returnAssistants[i].Created < returnAssistants[j].Created - } - return returnAssistants[i].Created > returnAssistants[j].Created - }) - - // After and before cursors - if afterQuery != "" { - returnAssistants = filterAssistantsAfterID(returnAssistants, afterQuery) - } - if beforeQuery != "" { - returnAssistants = filterAssistantsBeforeID(returnAssistants, beforeQuery) - } - - // Apply limit - if limit < len(returnAssistants) { - returnAssistants = returnAssistants[:limit] - } - - return c.JSON(returnAssistants) - } -} - -// FilterAssistantsBeforeID filters out those assistants whose ID comes before the given ID -// We assume that the assistants are already sorted -func filterAssistantsBeforeID(assistants []Assistant, id string) []Assistant { - idInt, err := strconv.Atoi(id) - if err != nil { - return assistants // Return original slice if invalid id format is provided - } - - var filteredAssistants []Assistant - - for _, assistant := range assistants { - aid, err := strconv.Atoi(strings.TrimPrefix(assistant.ID, "asst_")) - if err != nil { - continue // Skip if invalid id in assistant - } - - if aid < idInt { - filteredAssistants = append(filteredAssistants, assistant) - } - } - - return filteredAssistants -} - -// FilterAssistantsAfterID filters out those assistants whose ID comes after the given ID -// We assume that the assistants are already sorted -func filterAssistantsAfterID(assistants []Assistant, id string) []Assistant { - idInt, err := strconv.Atoi(id) - if err != nil { - return assistants // Return original slice if invalid id format is provided - } - - var filteredAssistants []Assistant - - for _, assistant := range assistants { - aid, err := strconv.Atoi(strings.TrimPrefix(assistant.ID, "asst_")) - if err != nil { - continue // Skip if invalid id in assistant - } - - if aid > idInt { - filteredAssistants = append(filteredAssistants, assistant) - } - } - - return filteredAssistants -} - -func modelExists(cl *config.BackendConfigLoader, ml *model.ModelLoader, modelName string) (found bool) { - found = false - models, err := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED) - if err != nil { - return - } - - for _, model := range models { - if model == modelName { - found = true - return - } - } - return -} - -// DeleteAssistantEndpoint is the OpenAI Assistant API endpoint to delete assistents https://platform.openai.com/docs/api-reference/assistants/deleteAssistant -// @Summary Delete assistents -// @Success 200 {object} schema.DeleteAssistantResponse "Response" -// @Router /v1/assistants/{assistant_id} [delete] -func DeleteAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - assistantID := c.Params("assistant_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required") - } - - for i, assistant := range Assistants { - if assistant.ID == assistantID { - Assistants = append(Assistants[:i], Assistants[i+1:]...) - utils.SaveConfig(appConfig.ConfigsDir, AssistantsConfigFile, Assistants) - return c.Status(fiber.StatusOK).JSON(schema.DeleteAssistantResponse{ - ID: assistantID, - Object: "assistant.deleted", - Deleted: true, - }) - } - } - - log.Warn().Msgf("Unable to find assistant %s for deletion", assistantID) - return c.Status(fiber.StatusNotFound).JSON(schema.DeleteAssistantResponse{ - ID: assistantID, - Object: "assistant.deleted", - Deleted: false, - }) - } -} - -// GetAssistantEndpoint is the OpenAI Assistant API endpoint to get assistents https://platform.openai.com/docs/api-reference/assistants/getAssistant -// @Summary Get assistent data -// @Success 200 {object} Assistant "Response" -// @Router /v1/assistants/{assistant_id} [get] -func GetAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - assistantID := c.Params("assistant_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required") - } - - for _, assistant := range Assistants { - if assistant.ID == assistantID { - return c.Status(fiber.StatusOK).JSON(assistant) - } - } - - return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant with id: %s", assistantID))) - } -} - -type AssistantFile struct { - ID string `json:"id"` - Object string `json:"object"` - CreatedAt int64 `json:"created_at"` - AssistantID string `json:"assistant_id"` -} - -var ( - AssistantFiles []AssistantFile - AssistantsFileConfigFile = "assistantsFile.json" -) - -func CreateAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - request := new(schema.AssistantFileRequest) - if err := c.BodyParser(request); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"}) - } - - assistantID := c.Params("assistant_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required") - } - - for _, assistant := range Assistants { - if assistant.ID == assistantID { - if len(assistant.FileIDs) > MaxFileIdSize { - return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("Max files %d for assistant %s reached.", MaxFileIdSize, assistant.Name)) - } - - for _, file := range UploadedFiles { - if file.ID == request.FileID { - assistant.FileIDs = append(assistant.FileIDs, request.FileID) - assistantFile := AssistantFile{ - ID: file.ID, - Object: "assistant.file", - CreatedAt: time.Now().Unix(), - AssistantID: assistant.ID, - } - AssistantFiles = append(AssistantFiles, assistantFile) - utils.SaveConfig(appConfig.ConfigsDir, AssistantsFileConfigFile, AssistantFiles) - return c.Status(fiber.StatusOK).JSON(assistantFile) - } - } - - return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find file_id: %s", request.FileID))) - } - } - - return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find %q", assistantID))) - } -} - -func ListAssistantFilesEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - type ListAssistantFiles struct { - Data []schema.File - Object string - } - - return func(c *fiber.Ctx) error { - assistantID := c.Params("assistant_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required") - } - - limitQuery := c.Query("limit", "20") - order := c.Query("order", "desc") - limit, err := strconv.Atoi(limitQuery) - if err != nil || limit < 1 || limit > 100 { - limit = 20 // Default to 20 if there's an error or the limit is out of bounds - } - - // Sort files by CreatedAt depending on the order query parameter - if order == "asc" { - sort.Slice(AssistantFiles, func(i, j int) bool { - return AssistantFiles[i].CreatedAt < AssistantFiles[j].CreatedAt - }) - } else { // default to "desc" - sort.Slice(AssistantFiles, func(i, j int) bool { - return AssistantFiles[i].CreatedAt > AssistantFiles[j].CreatedAt - }) - } - - // Limit the number of files returned - var limitedFiles []AssistantFile - hasMore := false - if len(AssistantFiles) > limit { - hasMore = true - limitedFiles = AssistantFiles[:limit] - } else { - limitedFiles = AssistantFiles - } - - response := map[string]interface{}{ - "object": "list", - "data": limitedFiles, - "first_id": func() string { - if len(limitedFiles) > 0 { - return limitedFiles[0].ID - } - return "" - }(), - "last_id": func() string { - if len(limitedFiles) > 0 { - return limitedFiles[len(limitedFiles)-1].ID - } - return "" - }(), - "has_more": hasMore, - } - - return c.Status(fiber.StatusOK).JSON(response) - } -} - -func ModifyAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - request := new(AssistantRequest) - if err := c.BodyParser(request); err != nil { - log.Warn().AnErr("Unable to parse AssistantRequest", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"}) - } - - assistantID := c.Params("assistant_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required") - } - - for i, assistant := range Assistants { - if assistant.ID == assistantID { - newAssistant := Assistant{ - ID: assistantID, - Object: assistant.Object, - Created: assistant.Created, - Model: request.Model, - Name: request.Name, - Description: request.Description, - Instructions: request.Instructions, - Tools: request.Tools, - FileIDs: request.FileIDs, // todo: should probably verify fileids exist - Metadata: request.Metadata, - } - - // Remove old one and replace with new one - Assistants = append(Assistants[:i], Assistants[i+1:]...) - Assistants = append(Assistants, newAssistant) - utils.SaveConfig(appConfig.ConfigsDir, AssistantsConfigFile, Assistants) - return c.Status(fiber.StatusOK).JSON(newAssistant) - } - } - return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant with id: %s", assistantID))) - } -} - -func DeleteAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - assistantID := c.Params("assistant_id") - fileId := c.Params("file_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id and file_id are required") - } - // First remove file from assistant - for i, assistant := range Assistants { - if assistant.ID == assistantID { - for j, fileId := range assistant.FileIDs { - Assistants[i].FileIDs = append(Assistants[i].FileIDs[:j], Assistants[i].FileIDs[j+1:]...) - - // Check if the file exists in the assistantFiles slice - for i, assistantFile := range AssistantFiles { - if assistantFile.ID == fileId { - // Remove the file from the assistantFiles slice - AssistantFiles = append(AssistantFiles[:i], AssistantFiles[i+1:]...) - utils.SaveConfig(appConfig.ConfigsDir, AssistantsFileConfigFile, AssistantFiles) - return c.Status(fiber.StatusOK).JSON(schema.DeleteAssistantFileResponse{ - ID: fileId, - Object: "assistant.file.deleted", - Deleted: true, - }) - } - } - } - - log.Warn().Msgf("Unable to locate file_id: %s in assistants: %s. Continuing to delete assistant file.", fileId, assistantID) - for i, assistantFile := range AssistantFiles { - if assistantFile.AssistantID == assistantID { - - AssistantFiles = append(AssistantFiles[:i], AssistantFiles[i+1:]...) - utils.SaveConfig(appConfig.ConfigsDir, AssistantsFileConfigFile, AssistantFiles) - - return c.Status(fiber.StatusNotFound).JSON(schema.DeleteAssistantFileResponse{ - ID: fileId, - Object: "assistant.file.deleted", - Deleted: true, - }) - } - } - } - } - log.Warn().Msgf("Unable to find assistant: %s", assistantID) - - return c.Status(fiber.StatusNotFound).JSON(schema.DeleteAssistantFileResponse{ - ID: fileId, - Object: "assistant.file.deleted", - Deleted: false, - }) - } -} - -func GetAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - assistantID := c.Params("assistant_id") - fileId := c.Params("file_id") - if assistantID == "" { - return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id and file_id are required") - } - - for _, assistantFile := range AssistantFiles { - if assistantFile.AssistantID == assistantID { - if assistantFile.ID == fileId { - return c.Status(fiber.StatusOK).JSON(assistantFile) - } - return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant file with file_id: %s", fileId))) - } - } - return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant file with assistant_id: %s", assistantID))) - } -} diff --git a/core/http/endpoints/openai/assistant_test.go b/core/http/endpoints/openai/assistant_test.go deleted file mode 100644 index 90edb935c..000000000 --- a/core/http/endpoints/openai/assistant_test.go +++ /dev/null @@ -1,460 +0,0 @@ -package openai - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/mudler/LocalAI/core/config" - "github.com/mudler/LocalAI/core/schema" - "github.com/mudler/LocalAI/pkg/model" - "github.com/stretchr/testify/assert" -) - -var configsDir string = "/tmp/localai/configs" - -type MockLoader struct { - models []string -} - -func tearDown() func() { - return func() { - UploadedFiles = []schema.File{} - Assistants = []Assistant{} - AssistantFiles = []AssistantFile{} - _ = os.Remove(filepath.Join(configsDir, AssistantsConfigFile)) - _ = os.Remove(filepath.Join(configsDir, AssistantsFileConfigFile)) - } -} - -func TestAssistantEndpoints(t *testing.T) { - // Preparing the mocked objects - cl := &config.BackendConfigLoader{} - //configsDir := "/tmp/localai/configs" - modelPath := "/tmp/localai/model" - var ml = model.NewModelLoader(modelPath, false) - - appConfig := &config.ApplicationConfig{ - ConfigsDir: configsDir, - UploadLimitMB: 10, - UploadDir: "test_dir", - ModelPath: modelPath, - } - - _ = os.RemoveAll(appConfig.ConfigsDir) - _ = os.MkdirAll(appConfig.ConfigsDir, 0750) - _ = os.MkdirAll(modelPath, 0750) - os.Create(filepath.Join(modelPath, "ggml-gpt4all-j")) - - app := fiber.New(fiber.Config{ - BodyLimit: 20 * 1024 * 1024, // sets the limit to 20MB. - }) - - // Create a Test Server - app.Get("/assistants", ListAssistantsEndpoint(cl, ml, appConfig)) - app.Post("/assistants", CreateAssistantEndpoint(cl, ml, appConfig)) - app.Delete("/assistants/:assistant_id", DeleteAssistantEndpoint(cl, ml, appConfig)) - app.Get("/assistants/:assistant_id", GetAssistantEndpoint(cl, ml, appConfig)) - app.Post("/assistants/:assistant_id", ModifyAssistantEndpoint(cl, ml, appConfig)) - - app.Post("/files", UploadFilesEndpoint(cl, appConfig)) - app.Get("/assistants/:assistant_id/files", ListAssistantFilesEndpoint(cl, ml, appConfig)) - app.Post("/assistants/:assistant_id/files", CreateAssistantFileEndpoint(cl, ml, appConfig)) - app.Delete("/assistants/:assistant_id/files/:file_id", DeleteAssistantFileEndpoint(cl, ml, appConfig)) - app.Get("/assistants/:assistant_id/files/:file_id", GetAssistantFileEndpoint(cl, ml, appConfig)) - - t.Run("CreateAssistantEndpoint", func(t *testing.T) { - t.Cleanup(tearDown()) - ar := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: "3.5-turbo", - Description: "Test Assistant", - Instructions: "You are computer science teacher answering student questions", - Tools: []Tool{{Type: Function}}, - FileIDs: nil, - Metadata: nil, - } - - resultAssistant, resp, err := createAssistant(app, *ar) - assert.NoError(t, err) - assert.Equal(t, fiber.StatusOK, resp.StatusCode) - - assert.Equal(t, 1, len(Assistants)) - //t.Cleanup(cleanupAllAssistants(t, app, []string{resultAssistant.ID})) - - assert.Equal(t, ar.Name, resultAssistant.Name) - assert.Equal(t, ar.Model, resultAssistant.Model) - assert.Equal(t, ar.Tools, resultAssistant.Tools) - assert.Equal(t, ar.Description, resultAssistant.Description) - assert.Equal(t, ar.Instructions, resultAssistant.Instructions) - assert.Equal(t, ar.FileIDs, resultAssistant.FileIDs) - assert.Equal(t, ar.Metadata, resultAssistant.Metadata) - }) - - t.Run("ListAssistantsEndpoint", func(t *testing.T) { - var ids []string - var resultAssistant []Assistant - for i := 0; i < 4; i++ { - ar := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: fmt.Sprintf("3.5-turbo-%d", i), - Description: fmt.Sprintf("Test Assistant - %d", i), - Instructions: fmt.Sprintf("You are computer science teacher answering student questions - %d", i), - Tools: []Tool{{Type: Function}}, - FileIDs: []string{"fid-1234"}, - Metadata: map[string]string{"meta": "data"}, - } - - //var err error - ra, _, err := createAssistant(app, *ar) - // Because we create the assistants so fast all end up with the same created time. - time.Sleep(time.Second) - resultAssistant = append(resultAssistant, ra) - assert.NoError(t, err) - ids = append(ids, resultAssistant[i].ID) - } - - t.Cleanup(cleanupAllAssistants(t, app, ids)) - - tests := []struct { - name string - reqURL string - expectedStatus int - expectedResult []Assistant - expectedStringResult string - }{ - { - name: "Valid Usage - limit only", - reqURL: "/assistants?limit=2", - expectedStatus: http.StatusOK, - expectedResult: Assistants[:2], // Expecting the first two assistants - }, - { - name: "Valid Usage - order asc", - reqURL: "/assistants?order=asc", - expectedStatus: http.StatusOK, - expectedResult: Assistants, // Expecting all assistants in ascending order - }, - { - name: "Valid Usage - order desc", - reqURL: "/assistants?order=desc", - expectedStatus: http.StatusOK, - expectedResult: []Assistant{Assistants[3], Assistants[2], Assistants[1], Assistants[0]}, // Expecting all assistants in descending order - }, - { - name: "Valid Usage - after specific ID", - reqURL: "/assistants?after=2", - expectedStatus: http.StatusOK, - // Note this is correct because it's put in descending order already - expectedResult: Assistants[:3], // Expecting assistants after (excluding) ID 2 - }, - { - name: "Valid Usage - before specific ID", - reqURL: "/assistants?before=4", - expectedStatus: http.StatusOK, - expectedResult: Assistants[2:], // Expecting assistants before (excluding) ID 3. - }, - { - name: "Invalid Usage - non-integer limit", - reqURL: "/assistants?limit=two", - expectedStatus: http.StatusBadRequest, - expectedStringResult: "Invalid limit query value: two", - }, - { - name: "Invalid Usage - non-existing id in after", - reqURL: "/assistants?after=100", - expectedStatus: http.StatusOK, - expectedResult: []Assistant(nil), // Expecting empty list as there are no IDs above 100 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - request := httptest.NewRequest(http.MethodGet, tt.reqURL, nil) - response, err := app.Test(request) - assert.NoError(t, err) - assert.Equal(t, tt.expectedStatus, response.StatusCode) - if tt.expectedStatus != fiber.StatusOK { - all, _ := io.ReadAll(response.Body) - assert.Equal(t, tt.expectedStringResult, string(all)) - } else { - var result []Assistant - err = json.NewDecoder(response.Body).Decode(&result) - assert.NoError(t, err) - - assert.Equal(t, tt.expectedResult, result) - } - }) - } - }) - - t.Run("DeleteAssistantEndpoint", func(t *testing.T) { - ar := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: "3.5-turbo", - Description: "Test Assistant", - Instructions: "You are computer science teacher answering student questions", - Tools: []Tool{{Type: Function}}, - FileIDs: nil, - Metadata: nil, - } - - resultAssistant, _, err := createAssistant(app, *ar) - assert.NoError(t, err) - - target := fmt.Sprintf("/assistants/%s", resultAssistant.ID) - deleteReq := httptest.NewRequest(http.MethodDelete, target, nil) - _, err = app.Test(deleteReq) - assert.NoError(t, err) - assert.Equal(t, 0, len(Assistants)) - }) - - t.Run("GetAssistantEndpoint", func(t *testing.T) { - ar := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: "3.5-turbo", - Description: "Test Assistant", - Instructions: "You are computer science teacher answering student questions", - Tools: []Tool{{Type: Function}}, - FileIDs: nil, - Metadata: nil, - } - - resultAssistant, _, err := createAssistant(app, *ar) - assert.NoError(t, err) - t.Cleanup(cleanupAllAssistants(t, app, []string{resultAssistant.ID})) - - target := fmt.Sprintf("/assistants/%s", resultAssistant.ID) - request := httptest.NewRequest(http.MethodGet, target, nil) - response, err := app.Test(request) - assert.NoError(t, err) - - var getAssistant Assistant - err = json.NewDecoder(response.Body).Decode(&getAssistant) - assert.NoError(t, err) - - assert.Equal(t, resultAssistant.ID, getAssistant.ID) - }) - - t.Run("ModifyAssistantEndpoint", func(t *testing.T) { - ar := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: "3.5-turbo", - Description: "Test Assistant", - Instructions: "You are computer science teacher answering student questions", - Tools: []Tool{{Type: Function}}, - FileIDs: nil, - Metadata: nil, - } - - resultAssistant, _, err := createAssistant(app, *ar) - assert.NoError(t, err) - - modifiedAr := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: "4.0-turbo", - Description: "Modified Test Assistant", - Instructions: "You are math teacher answering student questions", - Tools: []Tool{{Type: CodeInterpreter}}, - FileIDs: nil, - Metadata: nil, - } - - modifiedArJson, err := json.Marshal(modifiedAr) - assert.NoError(t, err) - - target := fmt.Sprintf("/assistants/%s", resultAssistant.ID) - request := httptest.NewRequest(http.MethodPost, target, strings.NewReader(string(modifiedArJson))) - request.Header.Set(fiber.HeaderContentType, "application/json") - - modifyResponse, err := app.Test(request) - assert.NoError(t, err) - var getAssistant Assistant - err = json.NewDecoder(modifyResponse.Body).Decode(&getAssistant) - assert.NoError(t, err) - - t.Cleanup(cleanupAllAssistants(t, app, []string{getAssistant.ID})) - - assert.Equal(t, resultAssistant.ID, getAssistant.ID) // IDs should match even if contents change - assert.Equal(t, modifiedAr.Tools, getAssistant.Tools) - assert.Equal(t, modifiedAr.Name, getAssistant.Name) - assert.Equal(t, modifiedAr.Instructions, getAssistant.Instructions) - assert.Equal(t, modifiedAr.Description, getAssistant.Description) - }) - - t.Run("CreateAssistantFileEndpoint", func(t *testing.T) { - t.Cleanup(tearDown()) - file, assistant, err := createFileAndAssistant(t, app, appConfig) - assert.NoError(t, err) - - afr := schema.AssistantFileRequest{FileID: file.ID} - af, _, err := createAssistantFile(app, afr, assistant.ID) - - assert.NoError(t, err) - assert.Equal(t, assistant.ID, af.AssistantID) - }) - t.Run("ListAssistantFilesEndpoint", func(t *testing.T) { - t.Cleanup(tearDown()) - file, assistant, err := createFileAndAssistant(t, app, appConfig) - assert.NoError(t, err) - - afr := schema.AssistantFileRequest{FileID: file.ID} - af, _, err := createAssistantFile(app, afr, assistant.ID) - assert.NoError(t, err) - - assert.Equal(t, assistant.ID, af.AssistantID) - }) - t.Run("GetAssistantFileEndpoint", func(t *testing.T) { - t.Cleanup(tearDown()) - file, assistant, err := createFileAndAssistant(t, app, appConfig) - assert.NoError(t, err) - - afr := schema.AssistantFileRequest{FileID: file.ID} - af, _, err := createAssistantFile(app, afr, assistant.ID) - assert.NoError(t, err) - t.Cleanup(cleanupAssistantFile(t, app, af.ID, af.AssistantID)) - - target := fmt.Sprintf("/assistants/%s/files/%s", assistant.ID, file.ID) - request := httptest.NewRequest(http.MethodGet, target, nil) - response, err := app.Test(request) - assert.NoError(t, err) - - var assistantFile AssistantFile - err = json.NewDecoder(response.Body).Decode(&assistantFile) - assert.NoError(t, err) - - assert.Equal(t, af.ID, assistantFile.ID) - assert.Equal(t, af.AssistantID, assistantFile.AssistantID) - }) - t.Run("DeleteAssistantFileEndpoint", func(t *testing.T) { - t.Cleanup(tearDown()) - file, assistant, err := createFileAndAssistant(t, app, appConfig) - assert.NoError(t, err) - - afr := schema.AssistantFileRequest{FileID: file.ID} - af, _, err := createAssistantFile(app, afr, assistant.ID) - assert.NoError(t, err) - - cleanupAssistantFile(t, app, af.ID, af.AssistantID)() - - assert.Empty(t, AssistantFiles) - }) - -} - -func createFileAndAssistant(t *testing.T, app *fiber.App, o *config.ApplicationConfig) (schema.File, Assistant, error) { - ar := &AssistantRequest{ - Model: "ggml-gpt4all-j", - Name: "3.5-turbo", - Description: "Test Assistant", - Instructions: "You are computer science teacher answering student questions", - Tools: []Tool{{Type: Function}}, - FileIDs: nil, - Metadata: nil, - } - - assistant, _, err := createAssistant(app, *ar) - if err != nil { - return schema.File{}, Assistant{}, err - } - t.Cleanup(cleanupAllAssistants(t, app, []string{assistant.ID})) - - file := CallFilesUploadEndpointWithCleanup(t, app, "test.txt", "file", "fine-tune", 5, o) - t.Cleanup(func() { - _, err := CallFilesDeleteEndpoint(t, app, file.ID) - assert.NoError(t, err) - }) - return file, assistant, nil -} - -func createAssistantFile(app *fiber.App, afr schema.AssistantFileRequest, assistantId string) (AssistantFile, *http.Response, error) { - afrJson, err := json.Marshal(afr) - if err != nil { - return AssistantFile{}, nil, err - } - - target := fmt.Sprintf("/assistants/%s/files", assistantId) - request := httptest.NewRequest(http.MethodPost, target, strings.NewReader(string(afrJson))) - request.Header.Set(fiber.HeaderContentType, "application/json") - request.Header.Set("OpenAi-Beta", "assistants=v1") - - resp, err := app.Test(request) - if err != nil { - return AssistantFile{}, resp, err - } - - var assistantFile AssistantFile - all, err := io.ReadAll(resp.Body) - if err != nil { - return AssistantFile{}, resp, err - } - err = json.NewDecoder(strings.NewReader(string(all))).Decode(&assistantFile) - if err != nil { - return AssistantFile{}, resp, err - } - - return assistantFile, resp, nil -} - -func createAssistant(app *fiber.App, ar AssistantRequest) (Assistant, *http.Response, error) { - assistant, err := json.Marshal(ar) - if err != nil { - return Assistant{}, nil, err - } - - request := httptest.NewRequest(http.MethodPost, "/assistants", strings.NewReader(string(assistant))) - request.Header.Set(fiber.HeaderContentType, "application/json") - request.Header.Set("OpenAi-Beta", "assistants=v1") - - resp, err := app.Test(request) - if err != nil { - return Assistant{}, resp, err - } - - bodyString, err := io.ReadAll(resp.Body) - if err != nil { - return Assistant{}, resp, err - } - - var resultAssistant Assistant - err = json.NewDecoder(strings.NewReader(string(bodyString))).Decode(&resultAssistant) - return resultAssistant, resp, err -} - -func cleanupAllAssistants(t *testing.T, app *fiber.App, ids []string) func() { - return func() { - for _, assistant := range ids { - target := fmt.Sprintf("/assistants/%s", assistant) - deleteReq := httptest.NewRequest(http.MethodDelete, target, nil) - _, err := app.Test(deleteReq) - if err != nil { - t.Fatalf("Failed to delete assistant %s: %v", assistant, err) - } - } - } -} - -func cleanupAssistantFile(t *testing.T, app *fiber.App, fileId, assistantId string) func() { - return func() { - target := fmt.Sprintf("/assistants/%s/files/%s", assistantId, fileId) - request := httptest.NewRequest(http.MethodDelete, target, nil) - request.Header.Set(fiber.HeaderContentType, "application/json") - request.Header.Set("OpenAi-Beta", "assistants=v1") - - resp, err := app.Test(request) - assert.NoError(t, err) - - var dafr schema.DeleteAssistantFileResponse - err = json.NewDecoder(resp.Body).Decode(&dafr) - assert.NoError(t, err) - assert.True(t, dafr.Deleted) - } -} diff --git a/core/http/endpoints/openai/files.go b/core/http/endpoints/openai/files.go deleted file mode 100644 index bc392e73d..000000000 --- a/core/http/endpoints/openai/files.go +++ /dev/null @@ -1,194 +0,0 @@ -package openai - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "sync/atomic" - "time" - - "github.com/microcosm-cc/bluemonday" - "github.com/mudler/LocalAI/core/config" - "github.com/mudler/LocalAI/core/schema" - - "github.com/gofiber/fiber/v2" - "github.com/mudler/LocalAI/pkg/utils" -) - -var UploadedFiles []schema.File - -const UploadedFilesFile = "uploadedFiles.json" - -// UploadFilesEndpoint https://platform.openai.com/docs/api-reference/files/create -func UploadFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - file, err := c.FormFile("file") - if err != nil { - return err - } - - // Check the file size - if file.Size > int64(appConfig.UploadLimitMB*1024*1024) { - return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("File size %d exceeds upload limit %d", file.Size, appConfig.UploadLimitMB)) - } - - purpose := c.FormValue("purpose", "") //TODO put in purpose dirs - if purpose == "" { - return c.Status(fiber.StatusBadRequest).SendString("Purpose is not defined") - } - - // Sanitize the filename to prevent directory traversal - filename := utils.SanitizeFileName(file.Filename) - - savePath := filepath.Join(appConfig.UploadDir, filename) - - // Check if file already exists - if _, err := os.Stat(savePath); !os.IsNotExist(err) { - return c.Status(fiber.StatusBadRequest).SendString("File already exists") - } - - err = c.SaveFile(file, savePath) - if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString("Failed to save file: " + bluemonday.StrictPolicy().Sanitize(err.Error())) - } - - f := schema.File{ - ID: fmt.Sprintf("file-%d", getNextFileId()), - Object: "file", - Bytes: int(file.Size), - CreatedAt: time.Now(), - Filename: file.Filename, - Purpose: purpose, - } - - UploadedFiles = append(UploadedFiles, f) - utils.SaveConfig(appConfig.UploadDir, UploadedFilesFile, UploadedFiles) - return c.Status(fiber.StatusOK).JSON(f) - } -} - -var currentFileId int64 = 0 - -func getNextFileId() int64 { - atomic.AddInt64(¤tId, 1) - return currentId -} - -// ListFilesEndpoint https://platform.openai.com/docs/api-reference/files/list -// @Summary List files. -// @Success 200 {object} schema.ListFiles "Response" -// @Router /v1/files [get] -func ListFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - - return func(c *fiber.Ctx) error { - var listFiles schema.ListFiles - - purpose := c.Query("purpose") - if purpose == "" { - listFiles.Data = UploadedFiles - } else { - for _, f := range UploadedFiles { - if purpose == f.Purpose { - listFiles.Data = append(listFiles.Data, f) - } - } - } - listFiles.Object = "list" - return c.Status(fiber.StatusOK).JSON(listFiles) - } -} - -func getFileFromRequest(c *fiber.Ctx) (*schema.File, error) { - id := c.Params("file_id") - if id == "" { - return nil, fmt.Errorf("file_id parameter is required") - } - - for _, f := range UploadedFiles { - if id == f.ID { - return &f, nil - } - } - - return nil, fmt.Errorf("unable to find file id %s", id) -} - -// GetFilesEndpoint is the OpenAI API endpoint to get files https://platform.openai.com/docs/api-reference/files/retrieve -// @Summary Returns information about a specific file. -// @Success 200 {object} schema.File "Response" -// @Router /v1/files/{file_id} [get] -func GetFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - file, err := getFileFromRequest(c) - if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) - } - - return c.JSON(file) - } -} - -type DeleteStatus struct { - Id string - Object string - Deleted bool -} - -// DeleteFilesEndpoint is the OpenAI API endpoint to delete files https://platform.openai.com/docs/api-reference/files/delete -// @Summary Delete a file. -// @Success 200 {object} DeleteStatus "Response" -// @Router /v1/files/{file_id} [delete] -func DeleteFilesEndpoint(cm *config.BackendConfigLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - - return func(c *fiber.Ctx) error { - file, err := getFileFromRequest(c) - if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) - } - - err = os.Remove(filepath.Join(appConfig.UploadDir, file.Filename)) - if err != nil { - // If the file doesn't exist then we should just continue to remove it - if !errors.Is(err, os.ErrNotExist) { - return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to delete file: %s, %v", file.Filename, err))) - } - } - - // Remove upload from list - for i, f := range UploadedFiles { - if f.ID == file.ID { - UploadedFiles = append(UploadedFiles[:i], UploadedFiles[i+1:]...) - break - } - } - - utils.SaveConfig(appConfig.UploadDir, UploadedFilesFile, UploadedFiles) - return c.JSON(DeleteStatus{ - Id: file.ID, - Object: "file", - Deleted: true, - }) - } -} - -// GetFilesContentsEndpoint is the OpenAI API endpoint to get files content https://platform.openai.com/docs/api-reference/files/retrieve-contents -// @Summary Returns information about a specific file. -// @Success 200 {string} binary "file" -// @Router /v1/files/{file_id}/content [get] -// GetFilesContentsEndpoint -func GetFilesContentsEndpoint(cm *config.BackendConfigLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - file, err := getFileFromRequest(c) - if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) - } - - fileContents, err := os.ReadFile(filepath.Join(appConfig.UploadDir, file.Filename)) - if err != nil { - return c.Status(fiber.StatusInternalServerError).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) - } - - return c.Send(fileContents) - } -} diff --git a/core/http/endpoints/openai/files_test.go b/core/http/endpoints/openai/files_test.go deleted file mode 100644 index b8cb5da39..000000000 --- a/core/http/endpoints/openai/files_test.go +++ /dev/null @@ -1,301 +0,0 @@ -package openai - -import ( - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - - "github.com/rs/zerolog/log" - - "github.com/mudler/LocalAI/core/config" - "github.com/mudler/LocalAI/core/schema" - - "github.com/gofiber/fiber/v2" - utils2 "github.com/mudler/LocalAI/pkg/utils" - "github.com/stretchr/testify/assert" - - "testing" -) - -func startUpApp() (app *fiber.App, option *config.ApplicationConfig, loader *config.BackendConfigLoader) { - // Preparing the mocked objects - loader = &config.BackendConfigLoader{} - - option = &config.ApplicationConfig{ - UploadLimitMB: 10, - UploadDir: "test_dir", - } - - _ = os.RemoveAll(option.UploadDir) - - app = fiber.New(fiber.Config{ - BodyLimit: 20 * 1024 * 1024, // sets the limit to 20MB. - }) - - // Create a Test Server - app.Post("/files", UploadFilesEndpoint(loader, option)) - app.Get("/files", ListFilesEndpoint(loader, option)) - app.Get("/files/:file_id", GetFilesEndpoint(loader, option)) - app.Delete("/files/:file_id", DeleteFilesEndpoint(loader, option)) - app.Get("/files/:file_id/content", GetFilesContentsEndpoint(loader, option)) - - return -} - -func TestUploadFileExceedSizeLimit(t *testing.T) { - // Preparing the mocked objects - loader := &config.BackendConfigLoader{} - - option := &config.ApplicationConfig{ - UploadLimitMB: 10, - UploadDir: "test_dir", - } - - _ = os.RemoveAll(option.UploadDir) - - app := fiber.New(fiber.Config{ - BodyLimit: 20 * 1024 * 1024, // sets the limit to 20MB. - }) - - // Create a Test Server - app.Post("/files", UploadFilesEndpoint(loader, option)) - app.Get("/files", ListFilesEndpoint(loader, option)) - app.Get("/files/:file_id", GetFilesEndpoint(loader, option)) - app.Delete("/files/:file_id", DeleteFilesEndpoint(loader, option)) - app.Get("/files/:file_id/content", GetFilesContentsEndpoint(loader, option)) - - t.Run("UploadFilesEndpoint file size exceeds limit", func(t *testing.T) { - t.Cleanup(tearDown()) - resp, err := CallFilesUploadEndpoint(t, app, "foo.txt", "file", "fine-tune", 11, option) - assert.NoError(t, err) - - assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) - assert.Contains(t, bodyToString(resp, t), "exceeds upload limit") - }) - t.Run("UploadFilesEndpoint purpose not defined", func(t *testing.T) { - t.Cleanup(tearDown()) - resp, _ := CallFilesUploadEndpoint(t, app, "foo.txt", "file", "", 5, option) - - assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) - assert.Contains(t, bodyToString(resp, t), "Purpose is not defined") - }) - t.Run("UploadFilesEndpoint file already exists", func(t *testing.T) { - t.Cleanup(tearDown()) - f1 := CallFilesUploadEndpointWithCleanup(t, app, "foo.txt", "file", "fine-tune", 5, option) - - resp, err := CallFilesUploadEndpoint(t, app, "foo.txt", "file", "fine-tune", 5, option) - fmt.Println(f1) - fmt.Printf("ERror: %v\n", err) - fmt.Printf("resp: %+v\n", resp) - - assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) - assert.Contains(t, bodyToString(resp, t), "File already exists") - }) - t.Run("UploadFilesEndpoint file uploaded successfully", func(t *testing.T) { - t.Cleanup(tearDown()) - file := CallFilesUploadEndpointWithCleanup(t, app, "test.txt", "file", "fine-tune", 5, option) - - // Check if file exists in the disk - testName := strings.Split(t.Name(), "/")[1] - fileName := testName + "-test.txt" - filePath := filepath.Join(option.UploadDir, utils2.SanitizeFileName(fileName)) - _, err := os.Stat(filePath) - - assert.False(t, os.IsNotExist(err)) - assert.Equal(t, file.Bytes, 5242880) - assert.NotEmpty(t, file.CreatedAt) - assert.Equal(t, file.Filename, fileName) - assert.Equal(t, file.Purpose, "fine-tune") - }) - t.Run("ListFilesEndpoint without purpose parameter", func(t *testing.T) { - t.Cleanup(tearDown()) - resp, err := CallListFilesEndpoint(t, app, "") - assert.NoError(t, err) - - assert.Equal(t, 200, resp.StatusCode) - - listFiles := responseToListFile(t, resp) - if len(listFiles.Data) != len(UploadedFiles) { - t.Errorf("Expected %v files, got %v files", len(UploadedFiles), len(listFiles.Data)) - } - }) - t.Run("ListFilesEndpoint with valid purpose parameter", func(t *testing.T) { - t.Cleanup(tearDown()) - _ = CallFilesUploadEndpointWithCleanup(t, app, "test.txt", "file", "fine-tune", 5, option) - - resp, err := CallListFilesEndpoint(t, app, "fine-tune") - assert.NoError(t, err) - - listFiles := responseToListFile(t, resp) - if len(listFiles.Data) != 1 { - t.Errorf("Expected 1 file, got %v files", len(listFiles.Data)) - } - }) - t.Run("ListFilesEndpoint with invalid query parameter", func(t *testing.T) { - t.Cleanup(tearDown()) - resp, err := CallListFilesEndpoint(t, app, "not-so-fine-tune") - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) - - listFiles := responseToListFile(t, resp) - - if len(listFiles.Data) != 0 { - t.Errorf("Expected 0 file, got %v files", len(listFiles.Data)) - } - }) - t.Run("GetFilesContentsEndpoint get file content", func(t *testing.T) { - t.Cleanup(tearDown()) - req := httptest.NewRequest("GET", "/files", nil) - resp, _ := app.Test(req) - assert.Equal(t, 200, resp.StatusCode) - - var listFiles schema.ListFiles - if err := json.Unmarshal(bodyToByteArray(resp, t), &listFiles); err != nil { - t.Errorf("Failed to decode response: %v", err) - return - } - - if len(listFiles.Data) != 0 { - t.Errorf("Expected 0 file, got %v files", len(listFiles.Data)) - } - }) -} - -func CallListFilesEndpoint(t *testing.T, app *fiber.App, purpose string) (*http.Response, error) { - var target string - if purpose != "" { - target = fmt.Sprintf("/files?purpose=%s", purpose) - } else { - target = "/files" - } - req := httptest.NewRequest("GET", target, nil) - return app.Test(req) -} - -func CallFilesContentEndpoint(t *testing.T, app *fiber.App, fileId string) (*http.Response, error) { - request := httptest.NewRequest("GET", "/files?file_id="+fileId, nil) - return app.Test(request) -} - -func CallFilesUploadEndpoint(t *testing.T, app *fiber.App, fileName, tag, purpose string, fileSize int, appConfig *config.ApplicationConfig) (*http.Response, error) { - testName := strings.Split(t.Name(), "/")[1] - - // Create a file that exceeds the limit - file := createTestFile(t, testName+"-"+fileName, fileSize, appConfig) - - // Creating a new HTTP Request - body, writer := newMultipartFile(file.Name(), tag, purpose) - - req := httptest.NewRequest(http.MethodPost, "/files", body) - req.Header.Set(fiber.HeaderContentType, writer.FormDataContentType()) - return app.Test(req) -} - -func CallFilesUploadEndpointWithCleanup(t *testing.T, app *fiber.App, fileName, tag, purpose string, fileSize int, appConfig *config.ApplicationConfig) schema.File { - // Create a file that exceeds the limit - testName := strings.Split(t.Name(), "/")[1] - file := createTestFile(t, testName+"-"+fileName, fileSize, appConfig) - - // Creating a new HTTP Request - body, writer := newMultipartFile(file.Name(), tag, purpose) - - req := httptest.NewRequest(http.MethodPost, "/files", body) - req.Header.Set(fiber.HeaderContentType, writer.FormDataContentType()) - resp, err := app.Test(req) - assert.NoError(t, err) - f := responseToFile(t, resp) - - //id := f.ID - //t.Cleanup(func() { - // _, err := CallFilesDeleteEndpoint(t, app, id) - // assert.NoError(t, err) - // assert.Empty(t, UploadedFiles) - //}) - - return f - -} - -func CallFilesDeleteEndpoint(t *testing.T, app *fiber.App, fileId string) (*http.Response, error) { - target := fmt.Sprintf("/files/%s", fileId) - req := httptest.NewRequest(http.MethodDelete, target, nil) - return app.Test(req) -} - -// Helper to create multi-part file -func newMultipartFile(filePath, tag, purpose string) (*strings.Reader, *multipart.Writer) { - body := new(strings.Builder) - writer := multipart.NewWriter(body) - file, _ := os.Open(filePath) - defer file.Close() - part, _ := writer.CreateFormFile(tag, filepath.Base(filePath)) - io.Copy(part, file) - - if purpose != "" { - _ = writer.WriteField("purpose", purpose) - } - - writer.Close() - return strings.NewReader(body.String()), writer -} - -// Helper to create test files -func createTestFile(t *testing.T, name string, sizeMB int, option *config.ApplicationConfig) *os.File { - err := os.MkdirAll(option.UploadDir, 0750) - if err != nil { - - t.Fatalf("Error MKDIR: %v", err) - } - - file, err := os.Create(name) - assert.NoError(t, err) - file.WriteString(strings.Repeat("a", sizeMB*1024*1024)) // sizeMB MB File - - t.Cleanup(func() { - os.Remove(name) - os.RemoveAll(option.UploadDir) - }) - return file -} - -func bodyToString(resp *http.Response, t *testing.T) string { - return string(bodyToByteArray(resp, t)) -} - -func bodyToByteArray(resp *http.Response, t *testing.T) []byte { - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - return bodyBytes -} - -func responseToFile(t *testing.T, resp *http.Response) schema.File { - var file schema.File - responseToString := bodyToString(resp, t) - - err := json.NewDecoder(strings.NewReader(responseToString)).Decode(&file) - if err != nil { - t.Errorf("Failed to decode response: %s", err) - } - - return file -} - -func responseToListFile(t *testing.T, resp *http.Response) schema.ListFiles { - var listFiles schema.ListFiles - responseToString := bodyToString(resp, t) - - err := json.NewDecoder(strings.NewReader(responseToString)).Decode(&listFiles) - if err != nil { - log.Error().Err(err).Msg("failed to decode response") - } - - return listFiles -} diff --git a/core/http/routes/openai.go b/core/http/routes/openai.go index b3f1af590..8a2789407 100644 --- a/core/http/routes/openai.go +++ b/core/http/routes/openai.go @@ -54,38 +54,6 @@ func RegisterOpenAIRoutes(app *fiber.App, app.Post("/completions", completionChain...) app.Post("/v1/engines/:model/completions", completionChain...) - // assistant - app.Get("/v1/assistants", openai.ListAssistantsEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/assistants", openai.ListAssistantsEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Post("/v1/assistants", openai.CreateAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Post("/assistants", openai.CreateAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Delete("/v1/assistants/:assistant_id", openai.DeleteAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Delete("/assistants/:assistant_id", openai.DeleteAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/v1/assistants/:assistant_id", openai.GetAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/assistants/:assistant_id", openai.GetAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Post("/v1/assistants/:assistant_id", openai.ModifyAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Post("/assistants/:assistant_id", openai.ModifyAssistantEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/v1/assistants/:assistant_id/files", openai.ListAssistantFilesEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/assistants/:assistant_id/files", openai.ListAssistantFilesEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Post("/v1/assistants/:assistant_id/files", openai.CreateAssistantFileEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Post("/assistants/:assistant_id/files", openai.CreateAssistantFileEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Delete("/v1/assistants/:assistant_id/files/:file_id", openai.DeleteAssistantFileEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Delete("/assistants/:assistant_id/files/:file_id", openai.DeleteAssistantFileEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/v1/assistants/:assistant_id/files/:file_id", openai.GetAssistantFileEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - app.Get("/assistants/:assistant_id/files/:file_id", openai.GetAssistantFileEndpoint(application.BackendLoader(), application.ModelLoader(), application.ApplicationConfig())) - - // files - app.Post("/v1/files", openai.UploadFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Post("/files", openai.UploadFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Get("/v1/files", openai.ListFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Get("/files", openai.ListFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Get("/v1/files/:file_id", openai.GetFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Get("/files/:file_id", openai.GetFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Delete("/v1/files/:file_id", openai.DeleteFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Delete("/files/:file_id", openai.DeleteFilesEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Get("/v1/files/:file_id/content", openai.GetFilesContentsEndpoint(application.BackendLoader(), application.ApplicationConfig())) - app.Get("/files/:file_id/content", openai.GetFilesContentsEndpoint(application.BackendLoader(), application.ApplicationConfig())) - // embeddings embeddingChain := []fiber.Handler{ re.BuildFilteredFirstAvailableDefaultModel(config.BuildUsecaseFilterFn(config.FLAG_EMBEDDINGS)), diff --git a/core/schema/openai.go b/core/schema/openai.go index c8947b99a..c54b52eb8 100644 --- a/core/schema/openai.go +++ b/core/schema/openai.go @@ -2,7 +2,6 @@ package schema import ( "context" - "time" functions "github.com/mudler/LocalAI/pkg/functions" ) @@ -115,37 +114,6 @@ type OpenAIModel struct { Object string `json:"object"` } -type DeleteAssistantResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Deleted bool `json:"deleted"` -} - -// File represents the structure of a file object from the OpenAI API. -type File struct { - ID string `json:"id"` // Unique identifier for the file - Object string `json:"object"` // Type of the object (e.g., "file") - Bytes int `json:"bytes"` // Size of the file in bytes - CreatedAt time.Time `json:"created_at"` // The time at which the file was created - Filename string `json:"filename"` // The name of the file - Purpose string `json:"purpose"` // The purpose of the file (e.g., "fine-tune", "classifications", etc.) -} - -type ListFiles struct { - Data []File - Object string -} - -type AssistantFileRequest struct { - FileID string `json:"file_id"` -} - -type DeleteAssistantFileResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Deleted bool `json:"deleted"` -} - type ImageGenerationResponseFormat string type ChatCompletionResponseFormatType string diff --git a/pkg/utils/config.go b/pkg/utils/config.go deleted file mode 100644 index 8fd0ec0e3..000000000 --- a/pkg/utils/config.go +++ /dev/null @@ -1,42 +0,0 @@ -package utils - -import ( - "encoding/json" - "os" - "path/filepath" - - "github.com/rs/zerolog/log" -) - -func SaveConfig(filePath, fileName string, obj any) { - file, err := json.MarshalIndent(obj, "", " ") - if err != nil { - log.Error().Err(err).Msg("failed to JSON marshal the uploadedFiles") - } - - absolutePath := filepath.Join(filePath, fileName) - err = os.WriteFile(absolutePath, file, 0600) - if err != nil { - log.Error().Err(err).Str("filepath", absolutePath).Msg("failed to save configuration file") - } -} - -func LoadConfig(filePath, fileName string, obj interface{}) { - uploadFilePath := filepath.Join(filePath, fileName) - - _, err := os.Stat(uploadFilePath) - if os.IsNotExist(err) { - log.Debug().Msgf("No configuration file found at %s", uploadFilePath) - return - } - - file, err := os.ReadFile(uploadFilePath) - if err != nil { - log.Error().Err(err).Str("filepath", uploadFilePath).Msg("failed to read file") - } else { - err = json.Unmarshal(file, &obj) - if err != nil { - log.Error().Err(err).Str("filepath", uploadFilePath).Msg("failed to parse file as JSON") - } - } -}