From ee0f45a6e1b765ec0eca68b2eee2535a2a5bd737 Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Fri, 14 Feb 2025 23:27:24 +0800 Subject: [PATCH] finish:quick create service --- controller/ai/controller.go | 1 + controller/ai/iml.go | 4 + controller/service/iml.go | 292 +++++++++++++++++++++++++++++++--- controller/service/service.go | 3 + module/ai-local/iml.go | 10 +- module/ai/iml.go | 75 +++++++-- module/service/dto/input.go | 9 +- plugins/core/ai.go | 2 +- plugins/core/service.go | 3 + service/ai-key/iml.go | 8 + service/ai-key/service.go | 1 + service/ai/iml.go | 2 + service/ai/service.go | 1 + 13 files changed, 370 insertions(+), 41 deletions(-) diff --git a/controller/ai/controller.go b/controller/ai/controller.go index bccbc4c0..e4b2a5a5 100644 --- a/controller/ai/controller.go +++ b/controller/ai/controller.go @@ -20,6 +20,7 @@ type IProviderController interface { Disable(ctx *gin.Context, id string) error UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error + Delete(ctx *gin.Context, id string) error //Sort(ctx *gin.Context, input *ai_dto.Sort) error } diff --git a/controller/ai/iml.go b/controller/ai/iml.go index 0febda5c..c7eee3b9 100644 --- a/controller/ai/iml.go +++ b/controller/ai/iml.go @@ -17,6 +17,10 @@ type imlProviderController struct { module ai.IProviderModule `autowired:""` } +func (i *imlProviderController) Delete(ctx *gin.Context, id string) error { + return i.module.Delete(ctx, id) +} + //func (i *imlProviderController) Sort(ctx *gin.Context, input *ai_dto.Sort) error { // return i.module.Sort(ctx, input) //} diff --git a/controller/service/iml.go b/controller/service/iml.go index f75d66bf..d52616af 100644 --- a/controller/service/iml.go +++ b/controller/service/iml.go @@ -3,10 +3,21 @@ package service import ( "context" "fmt" + "io" "net/http" "strings" "time" + ai_local "github.com/APIParkLab/APIPark/module/ai-local" + + ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" + + api_doc_dto "github.com/APIParkLab/APIPark/module/api-doc/dto" + + "github.com/APIParkLab/APIPark/module/catalogue" + + "github.com/APIParkLab/APIPark/module/team" + "github.com/eolinker/go-common/pm3" "github.com/APIParkLab/APIPark/module/system" @@ -40,6 +51,10 @@ import ( "github.com/google/uuid" ) +var ( + ollamaConfig = "{\n \"mirostat\": 0,\n \"mirostat_eta\": 0.1,\n \"mirostat_tau\": 5.0,\n \"num_ctx\": 4096,\n \"repeat_last_n\":64,\n \"repeat_penalty\": 1.1,\n \"temperature\": 0.7,\n \"seed\": 42,\n \"num_predict\": 42,\n \"top_k\": 40,\n \"top_p\": 0.9,\n \"min_p\": 0.5\n}\n" +) + var ( _ IServiceController = (*imlServiceController)(nil) @@ -47,15 +62,226 @@ var ( ) type imlServiceController struct { - module service.IServiceModule `autowired:""` - docModule service.IServiceDocModule `autowired:""` - aiAPIModule ai_api.IAPIModule `autowired:""` - routerModule router.IRouterModule `autowired:""` - apiDocModule api_doc.IAPIDocModule `autowired:""` - providerModule ai.IProviderModule `autowired:""` - upstreamModule upstream.IUpstreamModule `autowired:""` - settingModule system.ISettingModule `autowired:""` - transaction store.ITransaction `autowired:""` + module service.IServiceModule `autowired:""` + docModule service.IServiceDocModule `autowired:""` + aiAPIModule ai_api.IAPIModule `autowired:""` + routerModule router.IRouterModule `autowired:""` + apiDocModule api_doc.IAPIDocModule `autowired:""` + providerModule ai.IProviderModule `autowired:""` + aiLocalModel ai_local.ILocalModelModule `autowired:""` + upstreamModule upstream.IUpstreamModule `autowired:""` + settingModule system.ISettingModule `autowired:""` + teamModule team.ITeamModule `autowired:""` + catalogueModule catalogue.ICatalogueModule `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlServiceController) QuickCreateAIService(ctx *gin.Context, input *service_dto.QuickCreateAIService) error { + return i.transaction.Transaction(ctx, func(txCtx context.Context) error { + enable := true + err := i.providerModule.UpdateProviderConfig(ctx, input.Provider, &ai_dto.UpdateConfig{ + Config: input.Config, + Enable: &enable, + }) + if err != nil { + return err + } + pv, err := i.providerModule.Provider(ctx, input.Provider) + if err != nil { + return err + } + p, has := model_runtime.GetProvider(input.Provider) + if !has { + return fmt.Errorf("provider not found") + } + m, has := p.GetModel(pv.DefaultLLM) + if !has { + return fmt.Errorf("model %s not found", pv.DefaultLLM) + } + + var info *service_dto.Service + id := uuid.NewString() + prefix := fmt.Sprintf("/%s", id[:8]) + catalogueInfo, err := i.catalogueModule.DefaultCatalogue(ctx) + if err != nil { + return err + } + info, err = i.module.Create(ctx, input.Team, &service_dto.CreateService{ + Id: uuid.NewString(), + Name: input.Provider + " AI Service", + Prefix: prefix, + Description: "Quick create by AI provider", + ServiceType: "public", + State: "normal", + Catalogue: catalogueInfo.Id, + ApprovalType: "auto", + Provider: &input.Provider, + Kind: "ai", + }) + if err != nil { + return err + } + + path := fmt.Sprintf("%s/demo_translation_api", prefix) + timeout := 300000 + retry := 0 + aiPrompt := &ai_api_dto.AiPrompt{ + Variables: []*ai_api_dto.AiPromptVariable{ + { + Key: "source_lang", + Description: "", + Require: true, + }, + { + Key: "target_lang", + Description: "", + Require: true, + }, + { + Key: "text", + Description: "", + Require: true, + }, + }, + Prompt: "You need to translate {{source_lang}} into {{target_lang}}, and the following is the content that needs to be translated.\n---\n{{text}}", + } + aiModel := &ai_api_dto.AiModel{ + Id: m.ID(), + Config: m.DefaultConfig(), + Provider: input.Provider, + } + name := "Demo Translation API" + description := "A demo that shows you how to use a prompt to create a Translation API." + apiId := uuid.New().String() + err = i.aiAPIModule.Create( + ctx, + info.Id, + &ai_api_dto.CreateAPI{ + Id: apiId, + Name: name, + Path: path, + Description: description, + Disable: false, + AiPrompt: aiPrompt, + AiModel: aiModel, + Timeout: timeout, + Retry: retry, + }, + ) + if err != nil { + return err + } + plugins := make(map[string]api.PluginSetting) + plugins["ai_prompt"] = api.PluginSetting{ + Config: plugin_model.ConfigType{ + "prompt": aiPrompt.Prompt, + "variables": aiPrompt.Variables, + }, + } + plugins["ai_formatter"] = api.PluginSetting{ + Config: plugin_model.ConfigType{ + "model": aiModel.Id, + "provider": info.Provider.Id, + "config": aiModel.Config, + }, + } + _, err = i.routerModule.Create(ctx, info.Id, &router_dto.Create{ + Id: apiId, + Name: name, + Path: path, + Methods: []string{ + http.MethodPost, + }, + Description: description, + Protocols: []string{"http", "https"}, + MatchRules: nil, + Proxy: &router_dto.InputProxy{ + Path: path, + Timeout: timeout, + Retry: retry, + Plugins: plugins, + }, + Disable: false, + }) + if err != nil { + return err + } + + return i.docModule.SaveServiceDoc(ctx, info.Id, &service_dto.SaveServiceDoc{ + Doc: "The Translation API allows developers to translate text from one language to another. It supports multiple languages and enables easy integration of high-quality translation features into applications. With simple API requests, you can quickly translate content into different target languages.", + }) + }) +} + +func (i *imlServiceController) QuickCreateRestfulService(ctx *gin.Context) error { + fileHeader, err := ctx.FormFile("file") + if err != nil { + return err + } + file, err := fileHeader.Open() + if err != nil { + return err + } + content, err := io.ReadAll(file) + if err != nil { + return err + } + typ := ctx.PostForm("type") + switch typ { + case "swagger", "": + default: + return fmt.Errorf("type %s not support", typ) + } + + return i.transaction.Transaction(ctx, func(txCtx context.Context) error { + teamId := ctx.PostForm("team") + id := uuid.NewString() + prefix := fmt.Sprintf("/%s", id[:8]) + catalogueInfo, err := i.catalogueModule.DefaultCatalogue(ctx) + if err != nil { + return err + } + s, err := i.module.Create(ctx, teamId, &service_dto.CreateService{ + Id: uuid.NewString(), + Name: "Restful Service By Swagger", + Prefix: prefix, + Description: "Auto create by upload swagger", + ServiceType: "public", + State: "normal", + Catalogue: catalogueInfo.Id, + ApprovalType: "auto", + Kind: "rest", + }) + if err != nil { + return err + } + _, err = i.apiDocModule.UpdateDoc(ctx, s.Id, &api_doc_dto.UpdateDoc{ + Id: s.Id, + Content: string(content), + }) + if err != nil { + return err + } + path := prefix + "/" + _, err = i.routerModule.Create(ctx, s.Id, &router_dto.Create{ + Id: uuid.NewString(), + Name: "", + Path: path + "*", + Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions}, + Description: "auto create by create service", + Protocols: []string{"http", "https"}, + Proxy: &router_dto.InputProxy{ + Path: path, + Timeout: 30000, + Retry: 0, + }, + Disable: false, + }) + if err != nil { + return err + } + return nil + }) } var ( @@ -192,21 +418,40 @@ func (i *imlServiceController) createAIService(ctx *gin.Context, teamID string, input.Prefix = input.Id[:8] } } - pv, err := i.providerModule.Provider(ctx, *input.Provider) - if err != nil { - return nil, err - } - p, has := model_runtime.GetProvider(*input.Provider) - if !has { - return nil, fmt.Errorf("provider not found") - } - m, has := p.GetModel(pv.DefaultLLM) - if !has { - return nil, fmt.Errorf("model %s not found", pv.DefaultLLM) + modelId := "" + modelCfg := "" + modelType := "online" + if *input.Provider == "ollama" { + modelType = "local" + list, err := i.aiLocalModel.SimpleList(ctx) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, fmt.Errorf("no local model") + } + modelId = list[0].Id + modelCfg = ollamaConfig + } else { + pv, err := i.providerModule.Provider(ctx, *input.Provider) + if err != nil { + return nil, err + } + p, has := model_runtime.GetProvider(*input.Provider) + if !has { + return nil, fmt.Errorf("provider not found") + } + m, has := p.GetModel(pv.DefaultLLM) + if !has { + return nil, fmt.Errorf("model %s not found", pv.DefaultLLM) + } + modelId = m.ID() + modelCfg = m.DefaultConfig() + } var info *service_dto.Service - err = i.transaction.Transaction(ctx, func(txCtx context.Context) error { + err := i.transaction.Transaction(ctx, func(txCtx context.Context) error { var err error info, err = i.module.Create(ctx, teamID, input) if err != nil { @@ -236,9 +481,10 @@ func (i *imlServiceController) createAIService(ctx *gin.Context, teamID string, Prompt: "You need to translate {{source_lang}} into {{target_lang}}, and the following is the content that needs to be translated.\n---\n{{text}}", } aiModel := &ai_api_dto.AiModel{ - Id: m.ID(), - Config: m.DefaultConfig(), + Id: modelId, + Config: modelCfg, Provider: *input.Provider, + Type: modelType, } name := "Demo Translation API" description := "A demo that shows you how to use a prompt to create a Translation API." diff --git a/controller/service/service.go b/controller/service/service.go index c46ad21f..e3be79f8 100644 --- a/controller/service/service.go +++ b/controller/service/service.go @@ -16,6 +16,9 @@ type IServiceController interface { // SearchMyServices 搜索服务 SearchMyServices(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) Search(ctx *gin.Context, teamIDs string, keyword string) ([]*service_dto.ServiceItem, error) + QuickCreateRestfulService(ctx *gin.Context) error + + QuickCreateAIService(ctx *gin.Context, input *service_dto.QuickCreateAIService) error // Create 创建 Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) // Edit 编辑 diff --git a/module/ai-local/iml.go b/module/ai-local/iml.go index ea423666..f50483b7 100644 --- a/module/ai-local/iml.go +++ b/module/ai-local/iml.go @@ -169,7 +169,15 @@ func (i *imlLocalModel) pullHook() func(msg ai_provider_local.PullMessage) error if msg.Msg != "" { info.Msg = msg.Msg } - return i.localModelStateService.Save(ctx, msg.Model, &ai_local.EditLocalModelInstallState{State: &state, Complete: &info.Complete, Total: &info.Total, Msg: &info.Msg}) + err = i.localModelStateService.Save(ctx, msg.Model, &ai_local.EditLocalModelInstallState{State: &state, Complete: &info.Complete, Total: &info.Total, Msg: &info.Msg}) + if err != nil { + return err + } + serviceState := 0 + if msg.Status == "error" { + state = 2 + } + return i.serviceService.Save(ctx, msg.Model, &service.Edit{State: &serviceState}) }) } } diff --git a/module/ai/iml.go b/module/ai/iml.go index f68539f8..dc69b65e 100644 --- a/module/ai/iml.go +++ b/module/ai/iml.go @@ -8,6 +8,8 @@ import ( "sort" "time" + ai_local "github.com/APIParkLab/APIPark/service/ai-local" + ai_balance "github.com/APIParkLab/APIPark/service/ai-balance" "github.com/APIParkLab/APIPark/service/service" @@ -63,9 +65,36 @@ type imlProviderModule struct { } func (i *imlProviderModule) Delete(ctx context.Context, id string) error { - return i.transaction.Transaction(ctx, func(txCtx context.Context) error { - // TODO: implement Delete - return nil + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + keys, err := i.aiKeyService.KeysByProvider(ctx, id) + if err != nil { + return err + } + err = i.aiKeyService.DeleteByProvider(ctx, id) + if err != nil { + return err + } + + err = i.providerService.Delete(ctx, id) + if err != nil { + return err + } + releases := make([]*gateway.DynamicRelease, 0, len(keys)) + for _, key := range keys { + releases = append(releases, newKey(key)) + } + err = i.syncGateway(ctx, cluster.DefaultClusterID, releases, false) + if err != nil { + return err + } + return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{ + { + BasicItem: &gateway.BasicItem{ + ID: id, + Resource: "ai-provider", + }, + }, + }, false) }) } @@ -670,16 +699,38 @@ func (i *imlProviderModule) syncGateway(ctx context.Context, clusterId string, r var _ IAIAPIModule = (*imlAIApiModule)(nil) type imlAIApiModule struct { - aiAPIService ai_api.IAPIService `autowired:""` - aiAPIUseService ai_api.IAPIUseService `autowired:""` - serviceService service.IServiceService `autowired:""` + aiAPIService ai_api.IAPIService `autowired:""` + aiAPIUseService ai_api.IAPIUseService `autowired:""` + serviceService service.IServiceService `autowired:""` + aiLocalModelService ai_local.ILocalModelService `autowired:""` } func (i *imlAIApiModule) APIs(ctx context.Context, keyword string, providerId string, start int64, end int64, page int, pageSize int, sortCondition string, asc bool, models []string, serviceIds []string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error) { - p, has := model_runtime.GetProvider(providerId) - if !has { - return nil, nil, 0, fmt.Errorf("ai provider not found") + modelItems := make([]*ai_dto.BasicInfo, 0) + if providerId == "ollama" { + items, err := i.aiLocalModelService.Search(ctx, "", nil, "update_at desc") + if err != nil { + return nil, nil, 0, err + } + modelItems = utils.SliceToSlice(items, func(e *ai_local.LocalModel) *ai_dto.BasicInfo { + return &ai_dto.BasicInfo{ + Id: e.Id, + Name: e.Name, + } + }) + } else { + p, has := model_runtime.GetProvider(providerId) + if !has { + return nil, nil, 0, fmt.Errorf("ai provider not found") + } + modelItems = utils.SliceToSlice(p.Models(), func(e model_runtime.IModel) *ai_dto.BasicInfo { + return &ai_dto.BasicInfo{ + Id: e.ID(), + Name: e.ID(), + } + }) } + sortRule := "desc" if asc { sortRule = "asc" @@ -699,12 +750,6 @@ func (i *imlAIApiModule) APIs(ctx context.Context, keyword string, providerId st } - modelItems := utils.SliceToSlice(p.Models(), func(e model_runtime.IModel) *ai_dto.BasicInfo { - return &ai_dto.BasicInfo{ - Id: e.ID(), - Name: e.ID(), - } - }) condition := &ai_dto.Condition{Services: serviceItems, Models: modelItems} switch sortCondition { default: diff --git a/module/service/dto/input.go b/module/service/dto/input.go index 07569deb..4a4229ed 100644 --- a/module/service/dto/input.go +++ b/module/service/dto/input.go @@ -1,5 +1,12 @@ package service_dto +type QuickCreateAIService struct { + Provider string `json:"provider"` + Model string `json:"model"` + Config string `json:"config"` + Team string `json:"team"` +} + type CreateService struct { Id string `json:"id"` Name string `json:"name"` @@ -12,7 +19,7 @@ type CreateService struct { ApprovalType string `json:"approval_type"` Kind string `json:"service_kind"` State string `json:"state"` - Provider *string `json:"provider" aocheck:"ai_provider"` + Provider *string `json:"provider"` AsApp *bool `json:"as_app"` AsServer *bool `json:"as_server"` } diff --git a/plugins/core/ai.go b/plugins/core/ai.go index a1ef62f7..6bf7af69 100644 --- a/plugins/core/ai.go +++ b/plugins/core/ai.go @@ -17,7 +17,7 @@ func (p *plugin) aiAPIs() []pm3.Api { pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/provider/config", []string{"context", "query:provider"}, []string{"provider"}, p.aiProviderController.Provider, access.SystemSettingsAiProviderView), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/ai/provider", []string{"context", "query:provider"}, []string{"provider"}, p.aiProviderController.SimpleProvider), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/provider/llms", []string{"context", "query:provider"}, []string{"llms", "provider"}, p.aiProviderController.LLMs), - //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/sort", []string{"context", "body"}, nil, p.aiProviderController.Sort), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/ai/provider", []string{"context", "query:provider"}, nil, p.aiProviderController.Delete), pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/config", []string{"context", "query:provider", "body"}, nil, p.aiProviderController.UpdateProviderConfig, access.SystemSettingsAiProviderManager), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/apis", []string{"context", "query:keyword", "query:provider", "query:start", "query:end", "query:page", "query:page_size", "query:sort", "query:asc", "query:models", "query:services"}, []string{"apis", "condition", "total"}, p.aiStatisticController.APIs), diff --git a/plugins/core/service.go b/plugins/core/service.go index e929e6ef..82ac2107 100644 --- a/plugins/core/service.go +++ b/plugins/core/service.go @@ -20,6 +20,9 @@ func (p *plugin) ServiceApis() []pm3.Api { pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/services", []string{"context", "query:team", "query:keyword"}, []string{"services"}, p.serviceController.Search, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/services", []string{"context"}, []string{"services"}, p.serviceController.Simple), pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/services/mine", []string{"context"}, []string{"services"}, p.serviceController.MySimple), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/quick/service/rest", []string{"context"}, []string{}, p.serviceController.QuickCreateRestfulService, access.SystemWorkspaceServiceManagerAll, access.TeamTeamServiceManager), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/quick/service/ai", []string{"context", "body"}, []string{}, p.serviceController.QuickCreateAIService, access.SystemWorkspaceServiceManagerAll, access.TeamTeamServiceManager), + // 应用相关 pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/info", []string{"context", "query:app"}, []string{"app"}, p.appController.GetApp, access.SystemWorkspaceApplicationViewAll, access.TeamTeamConsumerView), pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/app", []string{"context", "query:app"}, nil, p.appController.DeleteApp, access.SystemWorkspaceApplicationManagerAll, access.TeamTeamConsumerManager), diff --git a/service/ai-key/iml.go b/service/ai-key/iml.go index c0ae15a3..29c27214 100644 --- a/service/ai-key/iml.go +++ b/service/ai-key/iml.go @@ -23,6 +23,14 @@ type imlAIKeyService struct { universally.IServiceDelete } +func (i *imlAIKeyService) DeleteByProvider(ctx context.Context, providerId string) error { + _, err := i.store.DeleteWhere(ctx, map[string]interface{}{"provider": providerId}) + if err != nil { + return err + } + return nil +} + func (i *imlAIKeyService) CountMapByProvider(ctx context.Context, keyword string, conditions map[string]interface{}) (map[string]int64, error) { return i.store.CountByGroup(ctx, keyword, conditions, "provider") } diff --git a/service/ai-key/service.go b/service/ai-key/service.go index eee91a0c..aac33499 100644 --- a/service/ai-key/service.go +++ b/service/ai-key/service.go @@ -14,6 +14,7 @@ type IKeyService interface { universally.IServiceCreate[Create] universally.IServiceEdit[Edit] universally.IServiceDelete + DeleteByProvider(ctx context.Context, providerId string) error DefaultKey(ctx context.Context, providerId string) (*Key, error) KeysByProvider(ctx context.Context, providerId string) ([]*Key, error) CountMapByProvider(ctx context.Context, keyword string, conditions map[string]interface{}) (map[string]int64, error) diff --git a/service/ai/iml.go b/service/ai/iml.go index 453d63a7..a2217919 100644 --- a/service/ai/iml.go +++ b/service/ai/iml.go @@ -17,6 +17,7 @@ var _ IProviderService = (*imlProviderService)(nil) type imlProviderService struct { universally.IServiceGet[Provider] + universally.IServiceDelete store ai.IProviderStore `autowired:""` } @@ -102,5 +103,6 @@ func (i *imlProviderService) GetLabels(ctx context.Context, ids ...string) map[s func (i *imlProviderService) OnComplete() { i.IServiceGet = universally.NewGet[Provider, ai.Provider](i.store, FromEntity) + i.IServiceDelete = universally.NewDelete[ai.Provider](i.store) auto.RegisterService("ai_provider", i) } diff --git a/service/ai/service.go b/service/ai/service.go index 83760314..7c1f7e27 100644 --- a/service/ai/service.go +++ b/service/ai/service.go @@ -10,6 +10,7 @@ import ( type IProviderService interface { universally.IServiceGet[Provider] + universally.IServiceDelete Save(ctx context.Context, id string, cfg *SetProvider) error MaxPriority(ctx context.Context) (int, error) }