From e314e09fdbc1c2422c7a642bfb53f3a296149180 Mon Sep 17 00:00:00 2001 From: Liujian <824010343@qq.com> Date: Mon, 23 Dec 2024 15:48:19 +0800 Subject: [PATCH] finish ai key api --- controller/ai-key/controller.go | 26 +++ controller/ai-key/iml.go | 61 +++++++ controller/ai/controller.go | 8 +- controller/ai/iml.go | 13 +- go.mod | 2 +- go.sum | 2 - module/ai-api/iml.go | 30 ++++ module/ai-key/dto/enum.go | 49 +++++ module/ai-key/dto/input.go | 20 +++ module/ai-key/dto/output.go | 21 +++ module/ai-key/iml.go | 304 ++++++++++++++++++++++++++++++++ module/ai-key/module.go | 25 +++ module/ai/dto/enum.go | 39 ++++ module/ai/dto/input.go | 2 + module/ai/dto/output.go | 42 +++-- module/ai/iml.go | 190 ++++++++++++++++---- module/ai/module.go | 8 +- plugins/core/ai.go | 18 +- plugins/core/core.go | 4 + service/ai-api/iml.go | 12 +- service/ai-api/model.go | 3 + service/ai-api/service.go | 5 +- service/ai-key/iml.go | 210 ++++++++++++++++++++++ service/ai-key/model.go | 61 +++++++ service/ai-key/service.go | 29 +++ service/ai/iml.go | 27 ++- service/ai/model.go | 10 +- service/ai/service.go | 4 +- stores/access_log/model.go | 30 ++++ stores/access_log/store.go | 22 +++ stores/ai/model.go | 48 ++++- stores/ai/store.go | 27 ++- stores/api/model.go | 1 + 33 files changed, 1283 insertions(+), 70 deletions(-) create mode 100644 controller/ai-key/controller.go create mode 100644 controller/ai-key/iml.go create mode 100644 module/ai-key/dto/enum.go create mode 100644 module/ai-key/dto/input.go create mode 100644 module/ai-key/dto/output.go create mode 100644 module/ai-key/iml.go create mode 100644 module/ai-key/module.go create mode 100644 module/ai/dto/enum.go create mode 100644 service/ai-key/iml.go create mode 100644 service/ai-key/model.go create mode 100644 service/ai-key/service.go create mode 100644 stores/access_log/model.go create mode 100644 stores/access_log/store.go diff --git a/controller/ai-key/controller.go b/controller/ai-key/controller.go new file mode 100644 index 00000000..1a8229d1 --- /dev/null +++ b/controller/ai-key/controller.go @@ -0,0 +1,26 @@ +package ai_key + +import ( + "reflect" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type IKeyController interface { + Create(ctx *gin.Context, providerId string, input *ai_key_dto.Create) error + Edit(ctx *gin.Context, providerId string, id string, input *ai_key_dto.Edit) error + Delete(ctx *gin.Context, providerId string, id string) error + Get(ctx *gin.Context, providerId string, id string) (*ai_key_dto.Key, error) + List(ctx *gin.Context, providerId string, keyword string, page string, pageSize string) ([]*ai_key_dto.Item, int64, error) + Enable(ctx *gin.Context, providerId string, id string) error + Disable(ctx *gin.Context, providerId string, id string) error + Sort(ctx *gin.Context, providerId string, input *ai_key_dto.Sort) error +} + +func init() { + autowire.Auto[IKeyController](func() reflect.Value { + return reflect.ValueOf(new(imlAIKeyController)) + }) +} diff --git a/controller/ai-key/iml.go b/controller/ai-key/iml.go new file mode 100644 index 00000000..73ae6399 --- /dev/null +++ b/controller/ai-key/iml.go @@ -0,0 +1,61 @@ +package ai_key + +import ( + "strconv" + + ai_key "github.com/APIParkLab/APIPark/module/ai-key" + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + "github.com/gin-gonic/gin" +) + +var _ IKeyController = &imlAIKeyController{} + +type imlAIKeyController struct { + module ai_key.IKeyModule `autowired:""` +} + +func (i *imlAIKeyController) Enable(ctx *gin.Context, providerId string, id string) error { + return i.module.UpdateKeyStatus(ctx, providerId, id, true) +} + +func (i *imlAIKeyController) Disable(ctx *gin.Context, providerId string, id string) error { + return i.module.UpdateKeyStatus(ctx, providerId, id, false) +} + +func (i *imlAIKeyController) Create(ctx *gin.Context, providerId string, input *ai_key_dto.Create) error { + return i.module.Create(ctx, providerId, input) +} + +func (i *imlAIKeyController) Edit(ctx *gin.Context, providerId string, id string, input *ai_key_dto.Edit) error { + return i.module.Edit(ctx, providerId, id, input) +} + +func (i *imlAIKeyController) Delete(ctx *gin.Context, providerId string, id string) error { + return i.module.Delete(ctx, providerId, id) +} + +func (i *imlAIKeyController) Get(ctx *gin.Context, providerId string, id string) (*ai_key_dto.Key, error) { + return i.module.Get(ctx, providerId, id) +} + +func (i *imlAIKeyController) List(ctx *gin.Context, providerId string, keyword string, page string, pageSize string) ([]*ai_key_dto.Item, int64, error) { + p, err := strconv.Atoi(page) + if err != nil { + if page != "" { + return nil, 0, err + } + p = 1 + } + ps, err := strconv.Atoi(pageSize) + if err != nil { + if pageSize != "" { + return nil, 0, err + } + ps = 15 + } + return i.module.List(ctx, providerId, keyword, p, ps) +} + +func (i *imlAIKeyController) Sort(ctx *gin.Context, providerId string, input *ai_key_dto.Sort) error { + return i.module.Sort(ctx, providerId, input) +} diff --git a/controller/ai/controller.go b/controller/ai/controller.go index 9a35bb77..23b8b345 100644 --- a/controller/ai/controller.go +++ b/controller/ai/controller.go @@ -1,14 +1,18 @@ package ai import ( + "reflect" + + "github.com/eolinker/go-common/auto" + ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" "github.com/eolinker/go-common/autowire" "github.com/gin-gonic/gin" - "reflect" ) type IProviderController interface { - Providers(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) + ConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ConfiguredProviderItem, *auto.Label, error) + UnConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) diff --git a/controller/ai/iml.go b/controller/ai/iml.go index e16e70bd..a2f122bc 100644 --- a/controller/ai/iml.go +++ b/controller/ai/iml.go @@ -3,6 +3,7 @@ package ai import ( "github.com/APIParkLab/APIPark/module/ai" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" + "github.com/eolinker/go-common/auto" "github.com/gin-gonic/gin" ) @@ -14,12 +15,16 @@ type imlProviderController struct { module ai.IProviderModule `autowired:""` } -func (i *imlProviderController) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) { - return i.module.SimpleProviders(ctx) +func (i *imlProviderController) ConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ConfiguredProviderItem, *auto.Label, error) { + return i.module.ConfiguredProviders(ctx) } -func (i *imlProviderController) Providers(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) { - return i.module.Providers(ctx) +func (i *imlProviderController) UnConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) { + return i.module.UnConfiguredProviders(ctx) +} + +func (i *imlProviderController) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) { + return i.module.SimpleProviders(ctx) } func (i *imlProviderController) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) { diff --git a/go.mod b/go.mod index 1d48fab3..a46deef4 100644 --- a/go.mod +++ b/go.mod @@ -81,4 +81,4 @@ require ( //replace github.com/eolinker/ap-account => ../aoaccount // -//replace github.com/eolinker/go-common => ../go-common +replace github.com/eolinker/go-common => ../../eolinker/go-common diff --git a/go.sum b/go.sum index 9bc1954c..e5fd313e 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,6 @@ github.com/eolinker/ap-account v1.0.15 h1:n6DJeL6RHZ8eLlZUcY2U3H4d/GPaA5oelAx3R0 github.com/eolinker/ap-account v1.0.15/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g= github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A= github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk= -github.com/eolinker/go-common v1.1.1 h1:3WqqecGqcHDgpa8Ljp156c1uWeZKP1CKScdU+6sOfcc= -github.com/eolinker/go-common v1.1.1/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= diff --git a/module/ai-api/iml.go b/module/ai-api/iml.go index 55a60fc2..fac22270 100644 --- a/module/ai-api/iml.go +++ b/module/ai-api/iml.go @@ -8,6 +8,8 @@ import ( "net/http" "strings" + "github.com/eolinker/eosc/log" + model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime" ai_api_dto "github.com/APIParkLab/APIPark/module/ai-api/dto" @@ -114,6 +116,7 @@ func (i *imlAPIModule) Create(ctx context.Context, serviceId string, input *ai_a Timeout: input.Timeout, Retry: input.Retry, Model: input.AiModel.Id, + Provider: input.AiModel.Provider, AdditionalConfig: map[string]interface{}{ "ai_prompt": input.AiPrompt, "ai_model": input.AiModel, @@ -148,8 +151,10 @@ func (i *imlAPIModule) Edit(ctx context.Context, serviceId string, apiId string, return err } var modelId *string + var providerId *string if input.AiModel != nil { modelId = &input.AiModel.Id + providerId = &input.AiModel.Provider } if input.AiPrompt != nil { apiInfo.AdditionalConfig["ai_prompt"] = input.AiPrompt @@ -164,6 +169,7 @@ func (i *imlAPIModule) Edit(ctx context.Context, serviceId string, apiId string, Timeout: input.Timeout, Retry: input.Retry, Model: modelId, + Provider: providerId, AdditionalConfig: &apiInfo.AdditionalConfig, }) }) @@ -304,3 +310,27 @@ func (i *imlAPIModule) Prefix(ctx context.Context, serviceId string) (string, er } return strings.TrimSuffix(pInfo.Prefix, "/"), nil } + +func (i *imlAPIModule) OnInit() { + ctx := context.Background() + list, err := i.aiAPIService.List(ctx) + if err != nil { + return + } + for _, item := range list { + if item.Provider == "" { + aiModel, err := ConvertStruct[ai_api_dto.AiModel](item.AdditionalConfig["ai_model"]) + if err != nil { + log.Errorf("convert ai model error:%v", err) + continue + } + err = i.aiAPIService.Save(ctx, item.ID, &ai_api.Edit{ + Provider: &aiModel.Provider, + }) + if err != nil { + log.Errorf("update ai api provider error:%v", err) + continue + } + } + } +} diff --git a/module/ai-key/dto/enum.go b/module/ai-key/dto/enum.go new file mode 100644 index 00000000..f5a224f0 --- /dev/null +++ b/module/ai-key/dto/enum.go @@ -0,0 +1,49 @@ +package ai_key_dto + +var ( + KeyNormal KeyStatus = "normal" + KeyExceed KeyStatus = "exceed" + KeyExpired KeyStatus = "expired" + KeyDisable KeyStatus = "disable" + KeyError KeyStatus = "error" +) + +type KeyStatus string + +func (s KeyStatus) String() string { + return string(s) +} + +func (s KeyStatus) Int() int { + switch s { + case KeyDisable: + return 0 + case KeyNormal: + return 1 + case KeyError: + return 2 + case KeyExceed: + return 3 + case KeyExpired: + return 4 + default: + return 0 + } +} + +func ToKeyStatus(status int) KeyStatus { + switch status { + case 0: + return KeyDisable + case 1: + return KeyNormal + case 2: + return KeyError + case 3: + return KeyExceed + case 4: + return KeyExpired + default: + return KeyDisable + } +} diff --git a/module/ai-key/dto/input.go b/module/ai-key/dto/input.go new file mode 100644 index 00000000..2559a4ee --- /dev/null +++ b/module/ai-key/dto/input.go @@ -0,0 +1,20 @@ +package ai_key_dto + +type Create struct { + Id string `json:"id"` + Name string `json:"name"` + Config string `json:"config"` + ExpireTime int `json:"expire_time"` +} + +type Edit struct { + Name *string `json:"name"` + Config *string `json:"config"` + ExpireTime *int `json:"expire_time"` +} + +type Sort struct { + Origin string `json:"origin"` + Target string `json:"target"` + Sort string `json:"sort"` +} diff --git a/module/ai-key/dto/output.go b/module/ai-key/dto/output.go new file mode 100644 index 00000000..4e1da2c9 --- /dev/null +++ b/module/ai-key/dto/output.go @@ -0,0 +1,21 @@ +package ai_key_dto + +import "github.com/eolinker/go-common/auto" + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` + Status KeyStatus `json:"status"` + UseToken int `json:"use_token"` + UpdateTime auto.TimeLabel `json:"update_time"` + ExpireTime int `json:"expire_time"` + Priority int `json:"priority"` + CanDelete bool `json:"can_delete"` +} + +type Key struct { + Id string `json:"id"` + Name string `json:"name"` + Config string `json:"config"` + ExpireTime int `json:"expire_time"` +} diff --git a/module/ai-key/iml.go b/module/ai-key/iml.go new file mode 100644 index 00000000..7a8b3973 --- /dev/null +++ b/module/ai-key/iml.go @@ -0,0 +1,304 @@ +package ai_key + +import ( + "context" + "errors" + "fmt" + "time" + + "gorm.io/gorm" + + "github.com/eolinker/go-common/auto" + + model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime" + + "github.com/google/uuid" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/ai" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + ai_key "github.com/APIParkLab/APIPark/service/ai-key" +) + +var _ IKeyModule = &imlKeyModule{} + +type imlKeyModule struct { + providerService ai.IProviderService `autowired:""` + aiKeyService ai_key.IKeyService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlKeyModule) Create(ctx context.Context, providerId string, input *ai_key_dto.Create) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + p, has := model_runtime.GetProvider(providerId) + if !has { + return fmt.Errorf("provider not found: %w", err) + } + err = p.Check(input.Config) + if err != nil { + return fmt.Errorf("config check failed: %w", err) + } + priority, err := i.aiKeyService.MaxPriority(ctx, providerId) + if err != nil { + return fmt.Errorf("get key error: %v", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + if input.Id == "" { + input.Id = uuid.NewString() + } + status := ai_key_dto.KeyNormal.Int() + if input.ExpireTime > 0 && time.Unix(int64(input.ExpireTime), 0).Before(time.Now()) { + status = ai_key_dto.KeyExpired.Int() + } else { + // TODO: 发布Key到网关 + } + + return i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: input.Id, + Name: input.Name, + Config: input.Config, + Provider: providerId, + Status: status, + ExpireTime: input.ExpireTime, + Priority: priority + 1, + }) + }) +} + +func (i *imlKeyModule) Edit(ctx context.Context, providerId string, id string, input *ai_key_dto.Edit) error { + p, has := model_runtime.GetProvider(providerId) + if !has { + return fmt.Errorf("provider not found: %s", providerId) + } + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + return fmt.Errorf("key not found: %w", err) + } + if input.Config != nil { + err = p.Check(*input.Config) + if err != nil { + return fmt.Errorf("config check failed: %w", err) + } + cfg, err := p.GenConfig(info.Config, *input.Config) + if err != nil { + return fmt.Errorf("config gen failed: %w", err) + } + input.Config = &cfg + if info.Default { + err = i.providerService.Save(ctx, info.Provider, &ai.SetProvider{ + Config: input.Config, + }) + if err != nil { + return err + } + } + } + + status := ai_key_dto.KeyNormal.Int() + orgStatus := ai_key_dto.ToKeyStatus(info.Status) + switch orgStatus { + case ai_key_dto.KeyNormal, ai_key_dto.KeyError, ai_key_dto.KeyExpired: + if input.ExpireTime != nil { + expireTime := *input.ExpireTime + if expireTime > 0 && time.Unix(int64(expireTime), 0).Before(time.Now()) { + status = ai_key_dto.KeyExpired.Int() + } + } else if info.ExpireTime > 0 && time.Unix(int64(info.ExpireTime), 0).Before(time.Now()) { + // 如果过期时间未更改,且已过期,则设置为过期状态 + status = ai_key_dto.KeyExpired.Int() + } + default: + // 停用、超额需要启用,所以维持原状态 + status = orgStatus.Int() + } + if status == ai_key_dto.KeyNormal.Int() { + // TODO: 发布Key到网关 + } + + return i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Name: input.Name, + Config: input.Config, + ExpireTime: input.ExpireTime, + Status: &status, + }) + }) + +} + +func (i *imlKeyModule) Delete(ctx context.Context, providerId string, id string) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + if info.Default { + return fmt.Errorf("default key can't be deleted: %s", id) + } + keys, err := i.aiKeyService.KeysAfterPriority(ctx, providerId, info.Priority) + if err != nil { + return err + } + for _, key := range keys { + key.Priority-- + err = i.aiKeyService.Save(ctx, key.ID, &ai_key.Edit{ + Priority: &key.Priority, + }) + if err != nil { + return err + } + } + + // TODO: 操作网关下线Key + return i.aiKeyService.Delete(ctx, id) + }) +} + +func (i *imlKeyModule) Get(ctx context.Context, providerId string, id string) (*ai_key_dto.Key, error) { + p, has := model_runtime.GetProvider(providerId) + if !has { + return nil, fmt.Errorf("provider not found: %s", providerId) + } + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return nil, fmt.Errorf("provider not found: %w", err) + } + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + return nil, fmt.Errorf("key not found: %w", err) + } + + return &ai_key_dto.Key{ + Id: info.ID, + Name: info.Name, + Config: p.MaskConfig(info.Config), + ExpireTime: info.ExpireTime, + }, nil +} + +func (i *imlKeyModule) List(ctx context.Context, providerId string, keyword string, page, pageSize int) ([]*ai_key_dto.Item, int64, error) { + _, err := i.aiKeyService.DefaultKey(ctx, providerId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("get default key failed: %w", err) + } + info, err := i.providerService.Get(ctx, providerId) + if err != nil { + return nil, 0, fmt.Errorf("provider is unconfigued,id is %s", providerId) + } + err = i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: info.Id, + Name: info.Name, + Config: info.Config, + Provider: info.Id, + Status: ai_key_dto.KeyNormal.Int(), + Priority: 1, + ExpireTime: 0, + UseToken: 0, + Default: true, + }) + if err != nil { + return nil, 0, fmt.Errorf("create default key failed: %w", err) + } + } + list, total, err := i.aiKeyService.SearchByPage(ctx, keyword, map[string]interface{}{ + "provider": providerId, + }, page, pageSize, "sort asc") + if err != nil { + return nil, 0, err + } + var result []*ai_key_dto.Item + for _, item := range list { + status := ai_key_dto.ToKeyStatus(item.Status) + if item.ExpireTime > 0 && time.Unix(int64(item.ExpireTime), 0).Before(time.Now()) { + status = ai_key_dto.KeyExpired + } + result = append(result, &ai_key_dto.Item{ + Id: item.ID, + Name: item.Name, + Status: status, + UseToken: item.UseToken, + UpdateTime: auto.TimeLabel(item.UpdateAt), + ExpireTime: item.ExpireTime, + CanDelete: !item.Default, + Priority: item.Priority, + }) + } + + return result, total, nil +} + +func (i *imlKeyModule) UpdateKeyStatus(ctx context.Context, providerId string, id string, enable bool) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + info, err := i.aiKeyService.Get(ctx, id) + if err != nil { + return fmt.Errorf("key not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + if !enable { + // TODO:下线Key + status := ai_key_dto.KeyDisable.Int() + return i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Status: &status, + }) + } + if info.Status == ai_key_dto.KeyDisable.Int() || info.Status == ai_key_dto.KeyExceed.Int() { + // 超额 或 停用状态,可启用 + if info.ExpireTime > 0 && time.Unix(int64(info.ExpireTime), 0).Before(time.Now()) { + // 如果过期时间未更改,且已过期,则设置为过期状态 + status := ai_key_dto.KeyExpired.Int() + return i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Status: &status, + }) + } + // TODO:发布Key到网关 + status := ai_key_dto.KeyNormal.Int() + return i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Status: &status, + }) + } + return nil + }) +} + +func (i *imlKeyModule) Sort(ctx context.Context, providerId string, input *ai_key_dto.Sort) error { + _, err := i.providerService.Get(ctx, providerId) + if err != nil { + return fmt.Errorf("provider not found: %w", err) + } + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + switch input.Sort { + case "before": + _, err = i.aiKeyService.SortBefore(ctx, providerId, input.Origin, input.Target) + case "after": + _, err = i.aiKeyService.SortAfter(ctx, providerId, input.Origin, input.Target) + default: + return fmt.Errorf("invalid sort type: %s", input.Sort) + } + if err != nil { + return err + } + // TODO: 全量更新key配置到网关 + + return nil + }) +} diff --git a/module/ai-key/module.go b/module/ai-key/module.go new file mode 100644 index 00000000..8a97bf51 --- /dev/null +++ b/module/ai-key/module.go @@ -0,0 +1,25 @@ +package ai_key + +import ( + "context" + "reflect" + + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + "github.com/eolinker/go-common/autowire" +) + +type IKeyModule interface { + Create(ctx context.Context, providerId string, input *ai_key_dto.Create) error + Edit(ctx context.Context, providerId string, id string, input *ai_key_dto.Edit) error + Delete(ctx context.Context, providerId string, id string) error + Get(ctx context.Context, providerId string, id string) (*ai_key_dto.Key, error) + List(ctx context.Context, providerId string, keyword string, page, pageSize int) ([]*ai_key_dto.Item, int64, error) + UpdateKeyStatus(ctx context.Context, providerId string, id string, enable bool) error + Sort(ctx context.Context, providerId string, input *ai_key_dto.Sort) error +} + +func init() { + autowire.Auto[IKeyModule](func() reflect.Value { + return reflect.ValueOf(new(imlKeyModule)) + }) +} diff --git a/module/ai/dto/enum.go b/module/ai/dto/enum.go new file mode 100644 index 00000000..3c1b96a4 --- /dev/null +++ b/module/ai/dto/enum.go @@ -0,0 +1,39 @@ +package ai_dto + +var ( + ProviderEnabled ProviderStatus = "enabled" + ProviderDisabled ProviderStatus = "disabled" + ProviderAbnormal ProviderStatus = "abnormal" +) + +type ProviderStatus string + +func (p ProviderStatus) Int() int { + switch p { + case ProviderAbnormal: + return 2 + case ProviderEnabled: + return 0 + case ProviderDisabled: + return 1 + default: + return 1 + } +} + +func (p ProviderStatus) String() string { + return string(p) +} + +func ToProviderStatus(status int) ProviderStatus { + switch status { + case 2: + return ProviderAbnormal + case 0: + return ProviderEnabled + case 1: + return ProviderDisabled + default: + return ProviderDisabled + } +} diff --git a/module/ai/dto/input.go b/module/ai/dto/input.go index 8f1b4c48..924e8830 100644 --- a/module/ai/dto/input.go +++ b/module/ai/dto/input.go @@ -7,4 +7,6 @@ type UpdateLLM struct { type UpdateConfig struct { DefaultLLM string `json:"default_llm"` Config string `json:"config"` + Priority *int `json:"priority"` + Enable *bool `json:"enable"` } diff --git a/module/ai/dto/output.go b/module/ai/dto/output.go index c21f7ceb..8318e21a 100644 --- a/module/ai/dto/output.go +++ b/module/ai/dto/output.go @@ -1,26 +1,34 @@ package ai_dto -import "time" - type Provider struct { - Id string `json:"id"` - Name string `json:"name"` - Config string `json:"config"` - GetAPIKeyUrl string `json:"get_apikey_url"` - DefaultLLM string `json:"defaultLLM"` - DefaultLLMConfig string `json:"-"` + Id string `json:"id"` + Name string `json:"name"` + Config string `json:"config"` + GetAPIKeyUrl string `json:"get_apikey_url"` + DefaultLLM string `json:"defaultLLM"` + DefaultLLMConfig string `json:"-"` + Priority int `json:"priority"` + Status ProviderStatus `json:"status"` +} + +type ConfiguredProviderItem struct { + Id string `json:"id"` + Name string `json:"name"` + Logo string `json:"logo"` + DefaultLLM string `json:"default_llm"` + Status ProviderStatus `json:"status"` + APICount int64 `json:"api_count"` + KeyCount int `json:"key_count"` + KeyStatus []string `json:"key_status"` + Priority int `json:"priority"` } type ProviderItem struct { - Id string `json:"id"` - Name string `json:"name"` - DefaultLLM string `json:"default_llm"` - DefaultLLMLogo string `json:"default_llm_logo"` - Logo string `json:"logo"` - Configured bool `json:"configured"` - Recommend bool `json:"recommend"` - Sort int `json:"sort"` - UpdateTime time.Time `json:"-"` + Id string `json:"id"` + Name string `json:"name"` + Logo string `json:"logo"` + DefaultLLM string `json:"default_llm"` + Sort int `json:"-"` } type SimpleProviderItem struct { diff --git a/module/ai/iml.go b/module/ai/iml.go index f830a8ac..1ce6fa9e 100644 --- a/module/ai/iml.go +++ b/module/ai/iml.go @@ -8,6 +8,14 @@ import ( "sort" "time" + ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto" + + ai_key "github.com/APIParkLab/APIPark/service/ai-key" + + "github.com/eolinker/go-common/auto" + + ai_api "github.com/APIParkLab/APIPark/service/ai-api" + model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime" "github.com/APIParkLab/APIPark/gateway" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" @@ -46,9 +54,82 @@ var _ IProviderModule = (*imlProviderModule)(nil) type imlProviderModule struct { providerService ai.IProviderService `autowired:""` clusterService cluster.IClusterService `autowired:""` + aiAPIService ai_api.IAPIService `autowired:""` + aiKeyService ai_key.IKeyService `autowired:""` transaction store.ITransaction `autowired:""` } +func (i *imlProviderModule) ConfiguredProviders(ctx context.Context) ([]*ai_dto.ConfiguredProviderItem, *auto.Label, error) { + // 获取已配置的AI服务商 + list, err := i.providerService.List(ctx) + if err != nil { + return nil, nil, fmt.Errorf("get provider list error:%v", err) + } + aiAPIMap, err := i.aiAPIService.CountMapByProvider(ctx, "", nil) + if err != nil { + return nil, nil, fmt.Errorf("get ai api count error:%v", err) + } + providers := make([]*ai_dto.ConfiguredProviderItem, 0, len(list)) + for _, l := range list { + p, has := model_runtime.GetProvider(l.Id) + if !has { + continue + } + keys, err := i.aiKeyService.KeysByProvider(ctx, l.Id) + if err != nil { + return nil, nil, fmt.Errorf("get provider keys error:%v", err) + } + + keysStatus := make([]string, 0, len(keys)) + for _, k := range keys { + status := ai_key_dto.ToKeyStatus(k.Status) + switch status { + case ai_key_dto.KeyNormal, ai_key_dto.KeyDisable, ai_key_dto.KeyError: + default: + status = ai_key_dto.KeyError + } + keysStatus = append(keysStatus, status.String()) + } + if len(keysStatus) == 0 { + keysStatus = []string{ai_key_dto.KeyNormal.String()} + } + providers = append(providers, &ai_dto.ConfiguredProviderItem{ + Id: l.Id, + Name: l.Name, + Logo: p.Logo(), + DefaultLLM: l.DefaultLLM, + Status: ai_dto.ToProviderStatus(l.Status), + APICount: aiAPIMap[l.Id], + KeyCount: len(keysStatus), + KeyStatus: keysStatus, + Priority: l.Priority, + }) + } + sort.Slice(providers, func(i, j int) bool { + if providers[i].Priority != providers[j].Priority { + if providers[i].Priority == 0 { + return false + } + if providers[j].Priority == 0 { + return true + } + return providers[i].Priority < providers[j].Priority + } + return providers[i].Name < providers[j].Name + }) + var backup *auto.Label + for _, p := range providers { + if p.Status == ai_dto.ProviderEnabled { + backup = &auto.Label{ + Id: p.Id, + Name: p.Name, + } + break + } + } + return providers, backup, nil +} + func (i *imlProviderModule) SimpleProviders(ctx context.Context) ([]*ai_dto.SimpleProviderItem, error) { list, err := i.providerService.List(ctx) if err != nil { @@ -74,7 +155,7 @@ func (i *imlProviderModule) SimpleProviders(ctx context.Context) ([]*ai_dto.Simp return items, nil } -func (i *imlProviderModule) Providers(ctx context.Context) ([]*ai_dto.ProviderItem, error) { +func (i *imlProviderModule) UnConfiguredProviders(ctx context.Context) ([]*ai_dto.ProviderItem, error) { list, err := i.providerService.List(ctx) if err != nil { return nil, err @@ -85,30 +166,21 @@ func (i *imlProviderModule) Providers(ctx context.Context) ([]*ai_dto.ProviderIt }) items := make([]*ai_dto.ProviderItem, 0, len(providers)) for _, v := range providers { - - item := &ai_dto.ProviderItem{ - Id: v.ID(), - Name: v.Name(), - Logo: v.Logo(), - Recommend: v.Recommend(), - Sort: v.Sort(), + if _, has := providerMap[v.ID()]; has { + // 已配置,跳过 + continue } - if info, has := providerMap[v.ID()]; has { - defaultLLM, has := v.GetModel(info.DefaultLLM) - if !has { - continue - } - item.Configured = true - item.DefaultLLM = defaultLLM.ID() - item.DefaultLLMLogo = defaultLLM.Logo() - item.UpdateTime = info.UpdateAt + defaultLLM, _ := v.DefaultModel(model_runtime.ModelTypeLLM) + item := &ai_dto.ProviderItem{ + Id: v.ID(), + Name: v.Name(), + Logo: v.Logo(), + DefaultLLM: defaultLLM.ID(), + Sort: v.Sort(), } items = append(items, item) } sort.Slice(items, func(i, j int) bool { - if items[i].Configured == items[j].Configured && items[i].Configured { - return items[i].Name < items[j].Name - } if items[i].Sort != items[j].Sort { if items[i].Sort == 0 { return false @@ -128,6 +200,13 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr if !has { return nil, fmt.Errorf("ai provider not found") } + maxPriority, err := i.providerService.MaxPriority(ctx) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + maxPriority = maxPriority + 1 info, err := i.providerService.Get(ctx, id) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { @@ -144,6 +223,8 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr GetAPIKeyUrl: p.HelpUrl(), DefaultLLM: defaultLLM.ID(), DefaultLLMConfig: defaultLLM.Logo(), + Status: ai_dto.ProviderDisabled, + Priority: maxPriority, }, nil } defaultLLM, has := p.GetModel(info.DefaultLLM) @@ -154,6 +235,10 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr } defaultLLM = model } + if info.Priority == 0 { + info.Priority = maxPriority + } + return &ai_dto.Provider{ Id: info.Id, Name: info.Name, @@ -161,6 +246,8 @@ func (i *imlProviderModule) Provider(ctx context.Context, id string) (*ai_dto.Pr GetAPIKeyUrl: p.HelpUrl(), DefaultLLM: defaultLLM.ID(), DefaultLLMConfig: defaultLLM.DefaultConfig(), + Priority: info.Priority, + Status: ai_dto.ProviderEnabled, }, nil } @@ -196,16 +283,19 @@ func (i *imlProviderModule) LLMs(ctx context.Context, driver string) ([]*ai_dto. return nil, nil, fmt.Errorf("ai provider default llm not found") } return items, &ai_dto.ProviderItem{ - Id: p.ID(), - Name: p.Name(), - DefaultLLM: defaultLLM.ID(), - DefaultLLMLogo: defaultLLM.Logo(), - Logo: p.Logo(), - Configured: false, + Id: p.ID(), + Name: p.Name(), + DefaultLLM: defaultLLM.ID(), + Logo: p.Logo(), }, nil } - return items, &ai_dto.ProviderItem{Id: info.Id, Name: info.Name, DefaultLLM: info.DefaultLLM, Logo: p.Logo(), Configured: true}, nil + return items, &ai_dto.ProviderItem{ + Id: info.Id, + Name: info.Name, + DefaultLLM: info.DefaultLLM, + Logo: p.Logo(), + }, nil } func (i *imlProviderModule) UpdateProviderStatus(ctx context.Context, id string, enable bool) error { @@ -222,8 +312,12 @@ func (i *imlProviderModule) UpdateProviderStatus(ctx context.Context, id string, } return i.transaction.Transaction(ctx, func(txCtx context.Context) error { + status := 0 + if enable { + status = 1 + } err = i.providerService.Save(txCtx, id, &ai.SetProvider{ - Status: &enable, + Status: &status, }) if err != nil { return err @@ -303,11 +397,46 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string, return err } return i.transaction.Transaction(ctx, func(txCtx context.Context) error { - err = i.providerService.Save(ctx, id, &ai.SetProvider{ + status := 0 + if input.Enable != nil && *input.Enable { + status = 1 + } + pInfo := &ai.SetProvider{ Name: &info.Name, DefaultLLM: &info.DefaultLLM, Config: &input.Config, - }) + Priority: input.Priority, + } + _, err = i.aiKeyService.DefaultKey(ctx, id) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = i.aiKeyService.Create(ctx, &ai_key.Create{ + ID: id, + Name: info.Name, + Config: info.Config, + Provider: id, + Status: 1, + ExpireTime: 0, + Default: true, + }) + } else { + err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{ + Config: &info.Config, + }) + } + if err != nil { + return err + } + if input.Enable != nil { + status = 0 + if *input.Enable { + status = 1 + } + pInfo.Status = &status + } + err = i.providerService.Save(ctx, id, pInfo) if err != nil { return err } @@ -376,6 +505,7 @@ func (i *imlProviderModule) getAiProviders(ctx context.Context) ([]*gateway.Dyna } return providers, nil } + func (i *imlProviderModule) initGateway(ctx context.Context, clusterId string, clientDriver gateway.IClientDriver) error { providers, err := i.getAiProviders(ctx) if err != nil { diff --git a/module/ai/module.go b/module/ai/module.go index 4bc67358..eb8a3513 100644 --- a/module/ai/module.go +++ b/module/ai/module.go @@ -2,14 +2,18 @@ package ai import ( "context" + "reflect" + + "github.com/eolinker/go-common/auto" + "github.com/APIParkLab/APIPark/gateway" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" "github.com/eolinker/go-common/autowire" - "reflect" ) type IProviderModule interface { - Providers(ctx context.Context) ([]*ai_dto.ProviderItem, error) + ConfiguredProviders(ctx context.Context) ([]*ai_dto.ConfiguredProviderItem, *auto.Label, error) + UnConfiguredProviders(ctx context.Context) ([]*ai_dto.ProviderItem, error) SimpleProviders(ctx context.Context) ([]*ai_dto.SimpleProviderItem, error) Provider(ctx context.Context, id string) (*ai_dto.Provider, error) LLMs(ctx context.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) diff --git a/plugins/core/ai.go b/plugins/core/ai.go index c73e8173..b2ad201d 100644 --- a/plugins/core/ai.go +++ b/plugins/core/ai.go @@ -10,8 +10,9 @@ import ( func (p *plugin) aiAPIs() []pm3.Api { return []pm3.Api{ - pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/providers", []string{"context"}, []string{"providers"}, p.aiProviderController.Providers, access.SystemSettingsAiProviderView), - pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/ai/providers", []string{"context"}, []string{"providers"}, p.aiProviderController.SimpleProviders), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/providers/unconfigured", []string{"context"}, []string{"providers"}, p.aiProviderController.UnConfiguredProviders, access.SystemSettingsAiProviderView), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/providers/configured", []string{"context"}, []string{"providers", "backup"}, p.aiProviderController.ConfiguredProviders, access.SystemSettingsAiProviderView), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/ai/providers/configured", []string{"context"}, []string{"providers"}, p.aiProviderController.SimpleProviders), 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/ai/provider/llms", []string{"context", "query:provider"}, []string{"llms", "provider"}, p.aiProviderController.LLMs), //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/enable", []string{"context", "query:provider"}, nil, p.aiProviderController.isStop), @@ -20,3 +21,16 @@ func (p *plugin) aiAPIs() []pm3.Api { //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/provider/default-llm", []string{"context", "query:provider", "body"}, nil, p.aiProviderController.UpdateProviderDefaultLLM), } } + +func (p *plugin) aiKeyApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/resource/key", []string{"context", "query:provider", "query:id"}, []string{"info"}, p.aiKeyController.Get), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/ai/resource/keys", []string{"context", "query:provider", "query:keyword", "query:page", "query:page_size"}, []string{"keys", "total"}, p.aiKeyController.List), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/ai/resource/key", []string{"context", "query:provider", "body"}, nil, p.aiKeyController.Create), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key", []string{"context", "query:provider", "query:id", "body"}, nil, p.aiKeyController.Edit), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/ai/resource/key", []string{"context", "query:provider", "query:id"}, nil, p.aiKeyController.Delete), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key/enable", []string{"context", "query:provider", "query:id"}, nil, p.aiKeyController.Enable), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key/disable", []string{"context", "query:provider", "query:id"}, nil, p.aiKeyController.Disable), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/ai/resource/key/sort", []string{"context", "query:provider", "body"}, nil, p.aiKeyController.Sort), + } +} diff --git a/plugins/core/core.go b/plugins/core/core.go index 7a3e57d6..c8244129 100644 --- a/plugins/core/core.go +++ b/plugins/core/core.go @@ -3,6 +3,8 @@ package core import ( "net/http" + ai_key "github.com/APIParkLab/APIPark/controller/ai-key" + "github.com/APIParkLab/APIPark/controller/log" "github.com/APIParkLab/APIPark/controller/strategy" @@ -74,6 +76,7 @@ type plugin struct { upstreamController upstream.IUpstreamController `autowired:""` routerController router.IRouterController `autowired:""` aiAPIController ai_api.IAPIController `autowired:""` + aiKeyController ai_key.IKeyController `autowired:""` apiDocController router.IAPIDocController `autowired:""` subscribeController subscribe.ISubscribeController `autowired:""` strategyController strategy.IStrategyController `autowired:""` @@ -111,6 +114,7 @@ func (p *plugin) OnComplete() { p.apis = append(p.apis, p.commonApis()...) p.apis = append(p.apis, p.systemApis()...) p.apis = append(p.apis, p.aiAPIs()...) + p.apis = append(p.apis, p.aiKeyApis()...) p.apis = append(p.apis, p.strategyApis()...) p.apis = append(p.apis, p.logApis()...) } diff --git a/service/ai-api/iml.go b/service/ai-api/iml.go index 4971dcbf..24a2eedb 100644 --- a/service/ai-api/iml.go +++ b/service/ai-api/iml.go @@ -1,10 +1,12 @@ package ai_api import ( + "context" "encoding/json" + "time" + "github.com/APIParkLab/APIPark/service/universally" "github.com/APIParkLab/APIPark/stores/api" - "time" ) var _ IAPIService = (*imlAPIService)(nil) @@ -18,6 +20,10 @@ type imlAPIService struct { universally.IServiceDelete } +func (i *imlAPIService) CountMapByProvider(ctx context.Context, keyword string, conditions map[string]interface{}) (map[string]int64, error) { + return i.store.CountByGroup(ctx, keyword, conditions, "provider") +} + func (i *imlAPIService) OnComplete() { i.IServiceGet = universally.NewGetSoftDelete[API, api.AiAPIInfo](i.store, FromEntity) i.IServiceCreate = universally.NewCreatorSoftDelete[Create, api.AiAPIInfo](i.store, "ai_api_info", createEntityHandler, uniquestHandler, labelHandler) @@ -43,6 +49,7 @@ func createEntityHandler(i *Create) *api.AiAPIInfo { Timeout: i.Timeout, Retry: i.Retry, Model: i.Model, + Provider: i.Provider, CreateAt: now, UpdateAt: now, AdditionalConfig: string(cfg), @@ -67,6 +74,9 @@ func updateHandler(e *api.AiAPIInfo, i *Edit) { if i.Model != nil { e.Model = *i.Model } + if i.Provider != nil { + e.Provider = *i.Provider + } if i.AdditionalConfig != nil { cfg, _ := json.Marshal(i.AdditionalConfig) e.AdditionalConfig = string(cfg) diff --git a/service/ai-api/model.go b/service/ai-api/model.go index bf9e7d41..7ce20a72 100644 --- a/service/ai-api/model.go +++ b/service/ai-api/model.go @@ -16,6 +16,7 @@ type API struct { Timeout int Retry int Model string + Provider string CreateAt time.Time UpdateAt time.Time Creator string @@ -33,6 +34,7 @@ type Create struct { Timeout int Retry int Model string + Provider string AdditionalConfig map[string]interface{} } @@ -42,6 +44,7 @@ type Edit struct { Description *string Timeout *int Retry *int + Provider *string Model *string AdditionalConfig *map[string]interface{} } diff --git a/service/ai-api/service.go b/service/ai-api/service.go index 6cf068ee..7128aaae 100644 --- a/service/ai-api/service.go +++ b/service/ai-api/service.go @@ -1,9 +1,11 @@ package ai_api import ( + "context" + "reflect" + "github.com/APIParkLab/APIPark/service/universally" "github.com/eolinker/go-common/autowire" - "reflect" ) type IAPIService interface { @@ -11,6 +13,7 @@ type IAPIService interface { universally.IServiceCreate[Create] universally.IServiceEdit[Edit] universally.IServiceDelete + CountMapByProvider(ctx context.Context, keyword string, conditions map[string]interface{}) (map[string]int64, error) //ListByServices(ctx context.Context, serviceIds ...string) ([]*API, error) } diff --git a/service/ai-key/iml.go b/service/ai-key/iml.go new file mode 100644 index 00000000..414b5028 --- /dev/null +++ b/service/ai-key/iml.go @@ -0,0 +1,210 @@ +package ai_key + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/ai" +) + +var _ IKeyService = &imlAIKeyService{} + +type imlAIKeyService struct { + store ai.IKeyStore `autowired:""` + transaction store.ITransaction `autowired:""` + universally.IServiceGet[Key] + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] + universally.IServiceDelete +} + +func (i *imlAIKeyService) KeysAfterPriority(ctx context.Context, providerId string, priority int) ([]*Key, error) { + list, err := i.store.ListQuery(ctx, "sort > ? and provider = ?", []interface{}{priority, providerId}, "sort asc") + if err != nil { + return nil, err + } + var result []*Key + for _, item := range list { + result = append(result, FromEntity(item)) + } + return result, nil +} + +func (i *imlAIKeyService) MaxPriority(ctx context.Context, providerId string) (int, error) { + info, err := i.store.First(ctx, map[string]interface{}{"provider": providerId}, "sort desc") + if err != nil { + return 0, err + } + return info.Sort, nil +} + +func (i *imlAIKeyService) DefaultKey(ctx context.Context, providerId string) (*Key, error) { + info, err := i.store.First(ctx, map[string]interface{}{"provider": providerId, "default": true}) + if err != nil { + return nil, err + } + return FromEntity(info), nil +} + +func (i *imlAIKeyService) KeysByProvider(ctx context.Context, providerId string) ([]*Key, error) { + list, err := i.store.List(ctx, map[string]interface{}{"provider": providerId}) + if err != nil { + return nil, err + } + var result []*Key + for _, item := range list { + result = append(result, FromEntity(item)) + } + return result, nil +} + +func (i *imlAIKeyService) SortBefore(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) { + originKey, err := i.store.GetByUUID(ctx, originID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, originID) + } + targetKey, err := i.store.GetByUUID(ctx, targetID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, targetID) + } + originKeySort, targetKeySort := originKey.Sort, targetKey.Sort + // 初始化顺序,假设原始Key在目标Key之后,中间的key往后移动,原始Key移动到`targetKeySort`位置 + originKey.Sort = targetKeySort + fn := func(priority int) int { + return priority + 1 + } + sql := "sort < ? and sort >= ?" + if originKeySort < targetKeySort { + // 如果原始Key在目标Key之前,中间的key往前移动,原始Key移动到`targetKeySort - 1`位置 + sql = "sort > ? and sort < ?" + originKey.Sort = targetKeySort - 1 + fn = func(priority int) int { + return priority - 1 + } + } + list, err := i.store.ListQuery(ctx, sql, []interface{}{originKeySort, targetKeySort}, "sort asc") + if err != nil { + return nil, err + } + result := make([]*Key, 0, len(list)+1) + err = i.transaction.Transaction(ctx, func(txCtx context.Context) error { + for _, l := range list { + l.Sort = fn(l.Sort) + _, err := i.store.Update(ctx, l) + if err != nil { + return err + } + result = append(result, FromEntity(l)) + } + _, err = i.store.Update(ctx, originKey) + return err + }) + if err != nil { + return nil, err + } + result = append(result, FromEntity(originKey)) + sort.Slice(list, func(i, j int) bool { return list[i].Sort < list[j].Sort }) + return result, nil +} + +func (i *imlAIKeyService) SortAfter(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) { + originKey, err := i.store.GetByUUID(ctx, originID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, originID) + } + targetKey, err := i.store.GetByUUID(ctx, targetID) + if err != nil { + return nil, fmt.Errorf("get key error: %v,id is %s", err, targetID) + } + originKeySort, targetKeySort := originKey.Sort, targetKey.Sort + // 初始化顺序,假设原始Key在目标Key之后,中间的Key往后移动,原始Key移动到`targetKeySort + 1`位置 + originKey.Sort = targetKeySort + 1 + fn := func(priority int) int { + return priority + 1 + } + sql := "sort < ? and sort > ?" + if originKeySort < targetKeySort { + // 如果原始Key在目标Key之前,中间的Key往前移动,原始Key移动到`targetKeySort`位置 + sql = "sort > ? and sort <= ?" + originKey.Sort = targetKeySort + fn = func(priority int) int { + return priority - 1 + } + } + list, err := i.store.ListQuery(ctx, sql, []interface{}{originKeySort, targetKeySort}, "sort asc") + if err != nil { + return nil, err + } + result := make([]*Key, 0, len(list)+1) + err = i.transaction.Transaction(ctx, func(txCtx context.Context) error { + for _, l := range list { + l.Sort = fn(l.Sort) + _, err := i.store.Update(ctx, l) + if err != nil { + return err + } + result = append(result, FromEntity(l)) + } + _, err = i.store.Update(ctx, originKey) + return err + }) + if err != nil { + return nil, err + } + result = append(result, FromEntity(originKey)) + sort.Slice(list, func(i, j int) bool { return list[i].Sort < list[j].Sort }) + return result, nil +} + +func (i *imlAIKeyService) OnComplete() { + i.IServiceGet = universally.NewGet[Key, ai.Key](i.store, FromEntity) + i.IServiceCreate = universally.NewCreator[Create, ai.Key](i.store, "ai_api_info", createEntityHandler, uniquestHandler, labelHandler) + i.IServiceEdit = universally.NewEdit[Edit, ai.Key](i.store, updateHandler) + i.IServiceDelete = universally.NewDelete[ai.Key](i.store) +} + +func labelHandler(e *ai.Key) []string { + return []string{e.Name, e.Uuid} +} +func uniquestHandler(i *Create) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.ID}} +} +func createEntityHandler(i *Create) *ai.Key { + now := time.Now() + return &ai.Key{ + Uuid: i.ID, + Name: i.Name, + Config: i.Config, + Provider: i.Provider, + Status: i.Status, + ExpireTime: i.ExpireTime, + Sort: i.Priority, + UseToken: 0, + CreateAt: now, + UpdateAt: now, + Default: i.Default, + } +} +func updateHandler(e *ai.Key, i *Edit) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Config != nil { + e.Config = *i.Config + } + if i.Status != nil { + e.Status = *i.Status + } + if i.ExpireTime != nil { + e.ExpireTime = *i.ExpireTime + } + if i.Priority != nil { + e.Sort = *i.Priority + } + e.UpdateAt = time.Now() +} diff --git a/service/ai-key/model.go b/service/ai-key/model.go new file mode 100644 index 00000000..61148922 --- /dev/null +++ b/service/ai-key/model.go @@ -0,0 +1,61 @@ +package ai_key + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/ai" +) + +type Key struct { + ID string + Name string + Config string + Provider string + Status int + ExpireTime int + UseToken int + Creator string + Updater string + Priority int + CreateAt time.Time + UpdateAt time.Time + Default bool +} + +func FromEntity(e *ai.Key) *Key { + return &Key{ + ID: e.Uuid, + Name: e.Name, + Config: e.Config, + Provider: e.Provider, + Status: e.Status, + ExpireTime: e.ExpireTime, + UseToken: e.UseToken, + Creator: e.Creator, + Updater: e.Updater, + CreateAt: e.CreateAt, + UpdateAt: e.UpdateAt, + Priority: e.Sort, + Default: e.Default, + } +} + +type Create struct { + ID string + Name string + Config string + Provider string + Priority int + Status int + ExpireTime int + UseToken int + Default bool +} + +type Edit struct { + Name *string + Config *string + Status *int + ExpireTime *int + Priority *int +} diff --git a/service/ai-key/service.go b/service/ai-key/service.go new file mode 100644 index 00000000..6cbd8739 --- /dev/null +++ b/service/ai-key/service.go @@ -0,0 +1,29 @@ +package ai_key + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + "github.com/APIParkLab/APIPark/service/universally" +) + +type IKeyService interface { + universally.IServiceGet[Key] + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] + universally.IServiceDelete + DefaultKey(ctx context.Context, providerId string) (*Key, error) + KeysByProvider(ctx context.Context, providerId string) ([]*Key, error) + MaxPriority(ctx context.Context, providerId string) (int, error) + SortBefore(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) + SortAfter(ctx context.Context, provider string, originID string, targetID string) ([]*Key, error) + KeysAfterPriority(ctx context.Context, providerId string, priority int) ([]*Key, error) +} + +func init() { + autowire.Auto[IKeyService](func() reflect.Value { + return reflect.ValueOf(new(imlAIKeyService)) + }) +} diff --git a/service/ai/iml.go b/service/ai/iml.go index 2eea283f..453d63a7 100644 --- a/service/ai/iml.go +++ b/service/ai/iml.go @@ -4,12 +4,13 @@ import ( "context" "encoding/base64" "errors" + "time" + "github.com/APIParkLab/APIPark/service/universally" "github.com/APIParkLab/APIPark/stores/ai" "github.com/eolinker/go-common/auto" "github.com/eolinker/go-common/utils" "gorm.io/gorm" - "time" ) var _ IProviderService = (*imlProviderService)(nil) @@ -19,6 +20,14 @@ type imlProviderService struct { store ai.IProviderStore `autowired:""` } +func (i *imlProviderService) MaxPriority(ctx context.Context) (int, error) { + t, err := i.store.First(ctx, nil, "priority desc") + if err != nil { + return 0, err + } + return t.Priority, nil +} + func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvider) error { userId := utils.UserId(ctx) now := time.Now() @@ -30,10 +39,20 @@ func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvid if cfg.Name == nil || cfg.Config == nil || cfg.DefaultLLM == nil { return errors.New("invalid params") } - status := false + status := 1 if cfg.Status != nil { status = *cfg.Status } + priority := 1 + if cfg.Priority == nil { + count, err := i.store.Count(ctx, "", nil) + if err != nil { + return err + } + priority = int(count) + 1 + } else { + priority = *cfg.Priority + } info = &ai.Provider{ UUID: id, Name: *cfg.Name, @@ -42,6 +61,7 @@ func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvid Status: status, Creator: userId, Updater: userId, + Priority: priority, CreateAt: now, UpdateAt: now, } @@ -58,6 +78,9 @@ func (i *imlProviderService) Save(ctx context.Context, id string, cfg *SetProvid if cfg.Status != nil { info.Status = *cfg.Status } + if cfg.Priority != nil { + info.Priority = *cfg.Priority + } info.Updater = userId info.UpdateAt = now } diff --git a/service/ai/model.go b/service/ai/model.go index 6865f5bf..80c6b43f 100644 --- a/service/ai/model.go +++ b/service/ai/model.go @@ -2,8 +2,9 @@ package ai import ( "encoding/base64" - "github.com/APIParkLab/APIPark/stores/ai" "time" + + "github.com/APIParkLab/APIPark/stores/ai" ) type Provider struct { @@ -13,7 +14,8 @@ type Provider struct { Config string Creator string Updater string - Status bool + Status int + Priority int CreateAt time.Time UpdateAt time.Time } @@ -22,7 +24,8 @@ type SetProvider struct { Name *string DefaultLLM *string Config *string - Status *bool + Status *int + Priority *int } func FromEntity(e *ai.Provider) *Provider { @@ -40,5 +43,6 @@ func FromEntity(e *ai.Provider) *Provider { CreateAt: e.CreateAt, UpdateAt: e.UpdateAt, Status: e.Status, + Priority: e.Priority, } } diff --git a/service/ai/service.go b/service/ai/service.go index 39dd9c7b..83760314 100644 --- a/service/ai/service.go +++ b/service/ai/service.go @@ -2,14 +2,16 @@ package ai import ( "context" + "reflect" + "github.com/APIParkLab/APIPark/service/universally" "github.com/eolinker/go-common/autowire" - "reflect" ) type IProviderService interface { universally.IServiceGet[Provider] Save(ctx context.Context, id string, cfg *SetProvider) error + MaxPriority(ctx context.Context) (int, error) } func init() { diff --git a/stores/access_log/model.go b/stores/access_log/model.go new file mode 100644 index 00000000..369ecef5 --- /dev/null +++ b/stores/access_log/model.go @@ -0,0 +1,30 @@ +package access_log + +import "time" + +type Log struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Cluster string `gorm:"column:cluster;type:varchar(36);NOT NULL;comment:集群ID"` + Node string `gorm:"column:node;type:varchar(36);NOT NULL;comment:节点ID"` + Service string `gorm:"column:service;type:varchar(36);NOT NULL;comment:服务ID"` + API string `gorm:"column:api;type:varchar(36);NOT NULL;comment:API ID"` + Application string `gorm:"column:application;type:varchar(36);NOT NULL;comment:应用ID"` + Auth string `gorm:"column:auth;type:varchar(36);NOT NULL;comment:认证ID"` + Type string `gorm:"column:type;type:varchar(36);NOT NULL;comment:日志类型;index:idx_type"` + Target string `gorm:"column:target;type:varchar(36);NOT NULL;comment:目标ID"` + IP string `gorm:"column:ip;type:varchar(36);NOT NULL;comment:IP"` + RequestPath string `gorm:"column:request_path;type:varchar(255);NOT NULL;comment:请求路径"` + Method string `gorm:"column:method;type:varchar(36);NOT NULL;comment:请求方法"` + ResponseTime float64 `gorm:"column:response_time;type:float;NOT NULL;comment:响应时间"` + StatusCode int64 `gorm:"column:status_code;type:BIGINT(20);NOT NULL;comment:响应状态码"` + ReportTime time.Time `gorm:"column:report_time;type:timestamp;NOT NULL;comment:上报时间;index:idx_report_time"` +} + +func (c *Log) IdValue() int64 { + return c.Id +} + +func (c *Log) TableName() string { + return "access_log" +} diff --git a/stores/access_log/store.go b/stores/access_log/store.go new file mode 100644 index 00000000..a702d7db --- /dev/null +++ b/stores/access_log/store.go @@ -0,0 +1,22 @@ +package access_log + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type ILogStore interface { + store.ISearchStore[Log] +} + +type imlLogStore struct { + store.SearchStore[Log] +} + +func init() { + autowire.Auto[ILogStore](func() reflect.Value { + return reflect.ValueOf(new(imlLogStore)) + }) +} diff --git a/stores/ai/model.go b/stores/ai/model.go index 7b82e702..1f1ee178 100644 --- a/stores/ai/model.go +++ b/stores/ai/model.go @@ -8,7 +8,8 @@ type Provider struct { Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` DefaultLLM string `gorm:"type:varchar(255);not null;column:default_llm;comment:默认模型ID"` Config string `gorm:"type:text;not null;column:config;comment:配置信息"` - Status bool `gorm:"type:tinyint(1);not null;column:status;comment:状态"` + Status int `gorm:"type:tinyint(1);not null;column:status;comment:状态,0:停用;1:启用,2:异常"` + Priority int `gorm:"type:int;not null;column:priority;comment:优先级,值越小优先级越高"` Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人 Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人 CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` @@ -22,3 +23,48 @@ func (i *Provider) TableName() string { func (i *Provider) IdValue() int64 { return i.Id } + +type LogMetrics struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Provider string `gorm:"type:varchar(36);not null;column:provider;comment:供应商ID"` + Model string `gorm:"type:varchar(36);not null;column:model;comment:模型ID"` + InputToken int `gorm:"type:int;not null;column:input_token;comment:输入token"` + OutputToken int `gorm:"type:int;not null;column:output_token;comment:输出token"` + TotalToken int `gorm:"type:int;not null;column:total_token;comment:总token"` + Cost float64 `gorm:"type:int;not null;column:cost;comment:费用"` + Per float64 `gorm:"type:int;not null;column:per;comment:每个token的价格"` +} + +func (i *LogMetrics) TableName() string { + return "ai_log_metrics" +} + +func (i *LogMetrics) IdValue() int64 { + return i.Id +} + +type Key struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Uuid string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:名称"` + Config string `gorm:"type:text;not null;column:config;comment:配置"` + Provider string `gorm:"type:varchar(36);not null;column:provider;comment:供应商ID"` + Status int `gorm:"type:tinyint(1);not null;column:status;comment:状态,0:停用;1:启用,2:错误;3:超额;4:过期"` + ExpireTime int `gorm:"type:int;not null;column:expire_time;comment:过期时间"` + UseToken int `gorm:"type:int;not null;column:use_token;comment:使用token数"` + Sort int `gorm:"type:int;not null;column:sort;comment:排序"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人 + Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` + Default bool `gorm:"type:tinyint(1);not null;column:default;comment:是否默认"` +} + +func (i *Key) TableName() string { + return "ai_key" +} + +func (i *Key) IdValue() int64 { + return i.Id +} diff --git a/stores/ai/store.go b/stores/ai/store.go index 107f625c..4afa193f 100644 --- a/stores/ai/store.go +++ b/stores/ai/store.go @@ -1,9 +1,10 @@ package ai import ( + "reflect" + "github.com/eolinker/go-common/autowire" "github.com/eolinker/go-common/store" - "reflect" ) type IProviderStore interface { @@ -14,8 +15,32 @@ type imlProviderStore struct { store.SearchStore[Provider] } +type ILogMetricsStore interface { + store.ISearchStore[LogMetrics] +} + +type imlLogMetricsStore struct { + store.SearchStore[LogMetrics] +} + +type IKeyStore interface { + store.ISearchStore[Key] +} + +type imlKeyStore struct { + store.SearchStore[Key] +} + func init() { autowire.Auto[IProviderStore](func() reflect.Value { return reflect.ValueOf(new(imlProviderStore)) }) + + autowire.Auto[ILogMetricsStore](func() reflect.Value { + return reflect.ValueOf(new(imlLogMetricsStore)) + }) + + autowire.Auto[IKeyStore](func() reflect.Value { + return reflect.ValueOf(new(imlKeyStore)) + }) } diff --git a/stores/api/model.go b/stores/api/model.go index 0c81e9db..db6b55ef 100644 --- a/stores/api/model.go +++ b/stores/api/model.go @@ -79,6 +79,7 @@ type AiAPIInfo struct { Timeout int `gorm:"type:int(11);not null;column:timeout;comment:超时时间"` Retry int `gorm:"type:int(11);not null;column:retry;comment:重试次数"` Model string `gorm:"size:255;not null;column:model;comment:模型"` + Provider string `gorm:"size:36;not null;column:provider;comment:提供者;index:provider"` Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"`