chore: commit pending gateway changes
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func (s *Server) listAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := s.store.ListAccessRules(r.Context())
|
||||
if err != nil {
|
||||
s.logger.Error("list access rules failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list access rules failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) createAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.AccessRuleInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validAccessRuleInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "subject, resource and effect are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.CreateAccessRule(r.Context(), input)
|
||||
if err != nil {
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "access rule already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("create access rule failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "create access rule failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) batchAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.AccessRuleBatchInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validAccessRuleBatchInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "subject, effect and resources are required")
|
||||
return
|
||||
}
|
||||
items, err := s.store.BatchAccessRules(r.Context(), input)
|
||||
if err != nil {
|
||||
s.logger.Error("batch access rules failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "batch access rules failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) updateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.AccessRuleInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validAccessRuleInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "subject, resource and effect are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.UpdateAccessRule(r.Context(), r.PathValue("ruleID"), input)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "access rule not found")
|
||||
return
|
||||
}
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "access rule already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("update access rule failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "update access rule failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) deleteAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeleteAccessRule(r.Context(), r.PathValue("ruleID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "access rule not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete access rule failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete access rule failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func validAccessRuleInput(input store.AccessRuleInput) bool {
|
||||
return validOneOf(input.SubjectType, "user_group", "tenant", "user", "api_key") &&
|
||||
strings.TrimSpace(input.SubjectID) != "" &&
|
||||
validOneOf(input.ResourceType, "platform", "platform_model", "base_model") &&
|
||||
strings.TrimSpace(input.ResourceID) != "" &&
|
||||
validOneOf(input.Effect, "allow", "deny") &&
|
||||
(input.Status == "" || validOneOf(input.Status, "active", "disabled"))
|
||||
}
|
||||
|
||||
func validAccessRuleBatchInput(input store.AccessRuleBatchInput) bool {
|
||||
if !validOneOf(input.SubjectType, "user_group", "tenant", "user", "api_key") ||
|
||||
strings.TrimSpace(input.SubjectID) == "" ||
|
||||
!validOneOf(input.Effect, "allow", "deny") {
|
||||
return false
|
||||
}
|
||||
if len(input.UpsertResources) == 0 && len(input.DeleteResources) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, resource := range append(input.UpsertResources, input.DeleteResources...) {
|
||||
if !validOneOf(resource.ResourceType, "platform", "platform_model", "base_model") ||
|
||||
strings.TrimSpace(resource.ResourceID) == "" ||
|
||||
(resource.Status != "" && !validOneOf(resource.Status, "active", "disabled")) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validOneOf(value string, allowed ...string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
for _, item := range allowed {
|
||||
if value == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -141,6 +142,34 @@ func (s *Server) updateBaseModel(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) resetBaseModel(w http.ResponseWriter, r *http.Request) {
|
||||
item, err := s.store.ResetBaseModelToDefault(r.Context(), r.PathValue("baseModelID"))
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "base model not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, store.ErrProtectedDefault) {
|
||||
writeError(w, http.StatusConflict, "base model has no system default snapshot")
|
||||
return
|
||||
}
|
||||
s.logger.Error("reset base model failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "reset base model failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) resetAllBaseModels(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := s.store.ResetAllBaseModelsToDefault(r.Context())
|
||||
if err != nil {
|
||||
s.logger.Error("reset all base models failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "reset all base models failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) deleteBaseModel(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeleteBaseModel(r.Context(), r.PathValue("baseModelID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
@@ -157,5 +186,5 @@ func (s *Server) deleteBaseModel(w http.ResponseWriter, r *http.Request) {
|
||||
func validBaseModelInput(input store.BaseModelInput) bool {
|
||||
return strings.TrimSpace(input.ProviderKey) != "" &&
|
||||
strings.TrimSpace(input.ProviderModelName) != "" &&
|
||||
strings.TrimSpace(input.ModelType) != ""
|
||||
len(input.ModelType) > 0
|
||||
}
|
||||
|
||||
@@ -148,6 +148,9 @@ func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
input.Provider = strings.TrimSpace(input.Provider)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.InternalName = strings.TrimSpace(input.InternalName)
|
||||
if input.Provider == "" || input.Name == "" {
|
||||
writeError(w, http.StatusBadRequest, "provider and name are required")
|
||||
return
|
||||
@@ -164,6 +167,52 @@ func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, platform)
|
||||
}
|
||||
|
||||
func (s *Server) updatePlatform(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.CreatePlatformInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
input.Provider = strings.TrimSpace(input.Provider)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.InternalName = strings.TrimSpace(input.InternalName)
|
||||
if input.Provider == "" || input.Name == "" {
|
||||
writeError(w, http.StatusBadRequest, "provider and name are required")
|
||||
return
|
||||
}
|
||||
if input.AuthType == "" {
|
||||
input.AuthType = "bearer"
|
||||
}
|
||||
platform, err := s.store.UpdatePlatform(r.Context(), r.PathValue("platformID"), input)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "platform not found")
|
||||
return
|
||||
}
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "platform key already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("update platform failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "update platform failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, platform)
|
||||
}
|
||||
|
||||
func (s *Server) deletePlatform(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeletePlatform(r.Context(), r.PathValue("platformID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "platform not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete platform failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete platform failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) createPlatformModel(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.CreatePlatformModelInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
@@ -190,6 +239,47 @@ func (s *Server) createPlatformModel(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, model)
|
||||
}
|
||||
|
||||
func (s *Server) replacePlatformModels(w http.ResponseWriter, r *http.Request) {
|
||||
platformID := r.PathValue("platformID")
|
||||
if platformID == "" {
|
||||
writeError(w, http.StatusBadRequest, "platformId is required")
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
Models []store.CreatePlatformModelInput `json:"models"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
|
||||
models, err := s.store.ReplacePlatformModels(r.Context(), platformID, input.Models)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "base model not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("replace platform models failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "replace platform models failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": models})
|
||||
}
|
||||
|
||||
func (s *Server) deletePlatformModel(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeletePlatformModel(r.Context(), r.PathValue("modelID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "platform model not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete platform model failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete platform model failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
|
||||
models, err := s.store.ListModels(r.Context())
|
||||
if err != nil {
|
||||
@@ -200,6 +290,17 @@ func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": models})
|
||||
}
|
||||
|
||||
func (s *Server) listPlayableModels(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
models, err := s.store.ListAccessiblePlatformModels(r.Context(), user)
|
||||
if err != nil {
|
||||
s.logger.Error("list playable models failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list playable models failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": models})
|
||||
}
|
||||
|
||||
func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := s.store.ListPricingRules(r.Context())
|
||||
if err != nil {
|
||||
@@ -251,6 +352,21 @@ func (s *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) listPlayableAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
items, err := s.store.ListPlayableAPIKeys(r.Context(), user)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrLocalUserRequired) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.logger.Error("list playable api keys failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list playable api keys failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) createAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
var input store.CreateAPIKeyInput
|
||||
@@ -359,19 +475,38 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler {
|
||||
writeError(w, http.StatusInternalServerError, "create task failed")
|
||||
return
|
||||
}
|
||||
result, runErr := s.runner.Execute(r.Context(), task, user)
|
||||
if compatible {
|
||||
if runErr != nil {
|
||||
writeError(w, statusFromRunError(runErr), runErr.Error())
|
||||
if boolValue(body, "stream") {
|
||||
flusher := prepareCompatibleStream(w)
|
||||
result, runErr := s.runner.ExecuteStream(r.Context(), task, user, func(delta string) error {
|
||||
writeCompatibleDelta(w, kind, model, delta)
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if runErr != nil {
|
||||
sendSSE(w, "error", map[string]any{"error": map[string]any{"message": runErr.Error(), "status": statusFromRunError(runErr)}})
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
writeCompatibleDone(w, kind, model, result.Output)
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
if boolValue(body, "stream") {
|
||||
writeCompatibleStream(w, kind, model, result.Output)
|
||||
result, runErr := s.runner.Execute(r.Context(), task, user)
|
||||
if runErr != nil {
|
||||
writeError(w, statusFromRunError(runErr), runErr.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result.Output)
|
||||
return
|
||||
}
|
||||
result, runErr := s.runner.Execute(r.Context(), task, user)
|
||||
if runErr != nil {
|
||||
s.logger.Warn("task completed with failure", "kind", kind, "taskId", task.ID, "error", runErr)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func (s *Server) createTenant(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.GatewayTenantInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validTenantInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "tenantKey and name are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.CreateTenant(r.Context(), input)
|
||||
if err != nil {
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "tenant key or external tenant id already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("create tenant failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "create tenant failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) updateTenant(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.GatewayTenantInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validTenantInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "tenantKey and name are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.UpdateTenant(r.Context(), r.PathValue("tenantID"), input)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "tenant not found")
|
||||
return
|
||||
}
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "tenant key or external tenant id already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("update tenant failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "update tenant failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) deleteTenant(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeleteTenant(r.Context(), r.PathValue("tenantID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "tenant not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete tenant failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete tenant failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) createGatewayUser(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.GatewayUserInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validGatewayUserInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "username is required")
|
||||
return
|
||||
}
|
||||
if !validOptionalPassword(input.Password) {
|
||||
writeError(w, http.StatusBadRequest, store.ErrWeakPassword.Error())
|
||||
return
|
||||
}
|
||||
item, err := s.store.CreateGatewayUser(r.Context(), input)
|
||||
if err != nil {
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "user key, email or external user id already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("create gateway user failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "create gateway user failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) updateGatewayUser(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.GatewayUserInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validGatewayUserInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "username is required")
|
||||
return
|
||||
}
|
||||
if !validOptionalPassword(input.Password) {
|
||||
writeError(w, http.StatusBadRequest, store.ErrWeakPassword.Error())
|
||||
return
|
||||
}
|
||||
item, err := s.store.UpdateGatewayUser(r.Context(), r.PathValue("userID"), input)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "user key, email or external user id already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("update gateway user failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "update gateway user failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) deleteGatewayUser(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeleteGatewayUser(r.Context(), r.PathValue("userID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete gateway user failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete gateway user failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) createUserGroup(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.UserGroupInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validUserGroupInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "groupKey and name are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.CreateUserGroup(r.Context(), input)
|
||||
if err != nil {
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "user group key already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("create user group failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "create user group failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) updateUserGroup(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.UserGroupInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validUserGroupInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "groupKey and name are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.UpdateUserGroup(r.Context(), r.PathValue("groupID"), input)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "user group not found")
|
||||
return
|
||||
}
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "user group key already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("update user group failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "update user group failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) deleteUserGroup(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeleteUserGroup(r.Context(), r.PathValue("groupID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "user group not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete user group failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete user group failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func validTenantInput(input store.GatewayTenantInput) bool {
|
||||
return strings.TrimSpace(input.TenantKey) != "" && strings.TrimSpace(input.Name) != ""
|
||||
}
|
||||
|
||||
func validGatewayUserInput(input store.GatewayUserInput) bool {
|
||||
return strings.TrimSpace(input.Username) != ""
|
||||
}
|
||||
|
||||
func validOptionalPassword(password string) bool {
|
||||
password = strings.TrimSpace(password)
|
||||
return password == "" || len(password) >= 8
|
||||
}
|
||||
|
||||
func validUserGroupInput(input store.UserGroupInput) bool {
|
||||
return strings.TrimSpace(input.GroupKey) != "" && strings.TrimSpace(input.Name) != ""
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -74,6 +75,10 @@ func (s *Server) deletePricingRuleSet(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "pricing rule set not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, store.ErrProtectedDefault) {
|
||||
writeError(w, http.StatusForbidden, "default pricing rule set cannot be deleted")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete pricing rule set failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete pricing rule set failed")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func (s *Server) listRuntimePolicySets(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := s.store.ListRuntimePolicySets(r.Context())
|
||||
if err != nil {
|
||||
s.logger.Error("list runtime policy sets failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list runtime policy sets failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) createRuntimePolicySet(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.RuntimePolicySetInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validRuntimePolicyInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "policyKey and name are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.CreateRuntimePolicySet(r.Context(), input)
|
||||
if err != nil {
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "runtime policy key already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("create runtime policy set failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "create runtime policy set failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) updateRuntimePolicySet(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.RuntimePolicySetInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validRuntimePolicyInput(input) {
|
||||
writeError(w, http.StatusBadRequest, "policyKey and name are required")
|
||||
return
|
||||
}
|
||||
item, err := s.store.UpdateRuntimePolicySet(r.Context(), r.PathValue("policySetID"), input)
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "runtime policy set not found")
|
||||
return
|
||||
}
|
||||
if store.IsUniqueViolation(err) {
|
||||
writeError(w, http.StatusConflict, "runtime policy key already exists")
|
||||
return
|
||||
}
|
||||
s.logger.Error("update runtime policy set failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "update runtime policy set failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) deleteRuntimePolicySet(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.store.DeleteRuntimePolicySet(r.Context(), r.PathValue("policySetID")); err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "runtime policy set not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, store.ErrProtectedDefault) {
|
||||
writeError(w, http.StatusForbidden, "default runtime policy set cannot be deleted")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete runtime policy set failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete runtime policy set failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func validRuntimePolicyInput(input store.RuntimePolicySetInput) bool {
|
||||
return strings.TrimSpace(input.PolicyKey) != "" && strings.TrimSpace(input.Name) != ""
|
||||
}
|
||||
@@ -44,25 +44,51 @@ func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Han
|
||||
mux.Handle("DELETE /api/v1/catalog/providers/{providerID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteCatalogProvider)))
|
||||
mux.Handle("GET /api/v1/catalog/base-models", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
|
||||
mux.Handle("POST /api/v1/catalog/base-models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createBaseModel)))
|
||||
mux.Handle("POST /api/v1/catalog/base-models/reset-all", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.resetAllBaseModels)))
|
||||
mux.Handle("PATCH /api/v1/catalog/base-models/{baseModelID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateBaseModel)))
|
||||
mux.Handle("POST /api/v1/catalog/base-models/{baseModelID}/reset", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.resetBaseModel)))
|
||||
mux.Handle("DELETE /api/v1/catalog/base-models/{baseModelID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteBaseModel)))
|
||||
mux.Handle("GET /api/v1/tenants", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listTenants)))
|
||||
mux.Handle("POST /api/v1/tenants", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createTenant)))
|
||||
mux.Handle("PATCH /api/v1/tenants/{tenantID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateTenant)))
|
||||
mux.Handle("DELETE /api/v1/tenants/{tenantID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteTenant)))
|
||||
mux.Handle("GET /api/v1/users", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listUsers)))
|
||||
mux.Handle("POST /api/v1/users", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createGatewayUser)))
|
||||
mux.Handle("PATCH /api/v1/users/{userID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateGatewayUser)))
|
||||
mux.Handle("DELETE /api/v1/users/{userID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteGatewayUser)))
|
||||
mux.Handle("GET /api/v1/user-groups", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listUserGroups)))
|
||||
mux.Handle("POST /api/v1/user-groups", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createUserGroup)))
|
||||
mux.Handle("PATCH /api/v1/user-groups/{groupID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateUserGroup)))
|
||||
mux.Handle("DELETE /api/v1/user-groups/{groupID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteUserGroup)))
|
||||
mux.Handle("GET /api/v1/access-rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listAccessRules)))
|
||||
mux.Handle("POST /api/v1/access-rules", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createAccessRule)))
|
||||
mux.Handle("POST /api/v1/access-rules/batch", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.batchAccessRules)))
|
||||
mux.Handle("PATCH /api/v1/access-rules/{ruleID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateAccessRule)))
|
||||
mux.Handle("DELETE /api/v1/access-rules/{ruleID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteAccessRule)))
|
||||
mux.Handle("GET /api/v1/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeys)))
|
||||
mux.Handle("POST /api/v1/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.createAPIKey)))
|
||||
mux.Handle("PATCH /api/v1/api-keys/{apiKeyID}/disable", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.disableAPIKey)))
|
||||
mux.Handle("GET /api/playground/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableAPIKeys)))
|
||||
mux.Handle("GET /api/v1/pricing/rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
|
||||
mux.Handle("GET /api/v1/pricing/rule-sets", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRuleSets)))
|
||||
mux.Handle("POST /api/v1/pricing/rule-sets", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPricingRuleSet)))
|
||||
mux.Handle("PATCH /api/v1/pricing/rule-sets/{ruleSetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updatePricingRuleSet)))
|
||||
mux.Handle("DELETE /api/v1/pricing/rule-sets/{ruleSetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deletePricingRuleSet)))
|
||||
mux.Handle("POST /api/v1/pricing/estimate", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.estimatePricing)))
|
||||
mux.Handle("GET /api/v1/runtime/policy-sets", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRuntimePolicySets)))
|
||||
mux.Handle("POST /api/v1/runtime/policy-sets", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createRuntimePolicySet)))
|
||||
mux.Handle("PATCH /api/v1/runtime/policy-sets/{policySetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateRuntimePolicySet)))
|
||||
mux.Handle("DELETE /api/v1/runtime/policy-sets/{policySetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteRuntimePolicySet)))
|
||||
mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
|
||||
mux.Handle("POST /api/v1/platforms", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPlatform)))
|
||||
mux.Handle("PATCH /api/v1/platforms/{platformID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updatePlatform)))
|
||||
mux.Handle("DELETE /api/v1/platforms/{platformID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deletePlatform)))
|
||||
mux.Handle("PUT /api/v1/platforms/{platformID}/models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.replacePlatformModels)))
|
||||
mux.Handle("POST /api/v1/platforms/{platformID}/models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
|
||||
mux.Handle("POST /api/v1/platform-models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
|
||||
mux.Handle("DELETE /api/v1/platform-models/{modelID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deletePlatformModel)))
|
||||
mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listModels)))
|
||||
mux.Handle("GET /api/v1/playground/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
|
||||
mux.Handle("GET /api/v1/runtime/rate-limit-windows", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
|
||||
mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", false)))
|
||||
mux.Handle("POST /api/v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", false)))
|
||||
@@ -91,7 +117,7 @@ func (s *Server) cors(next http.Handler) http.Handler {
|
||||
w.Header().Set("Vary", "Origin")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Comfy-Api-Key")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
@@ -2,10 +2,42 @@ package httpapi
|
||||
|
||||
import "net/http"
|
||||
|
||||
func writeCompatibleStream(w http.ResponseWriter, kind string, model string, output map[string]any) {
|
||||
func prepareCompatibleStream(w http.ResponseWriter) http.Flusher {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
flusher, _ := w.(http.Flusher)
|
||||
return flusher
|
||||
}
|
||||
|
||||
func writeCompatibleDelta(w http.ResponseWriter, kind string, model string, content string) {
|
||||
if kind == "responses" {
|
||||
sendSSE(w, "response.output_text.delta", map[string]any{"type": "response.output_text.delta", "delta": content})
|
||||
return
|
||||
}
|
||||
sendSSE(w, "message", map[string]any{
|
||||
"id": "chatcmpl-stream",
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": content}, "finish_reason": nil}},
|
||||
})
|
||||
}
|
||||
|
||||
func writeCompatibleDone(w http.ResponseWriter, kind string, model string, output map[string]any) {
|
||||
if kind == "responses" {
|
||||
sendSSE(w, "response.completed", map[string]any{"type": "response.completed", "response": output})
|
||||
return
|
||||
}
|
||||
sendSSE(w, "message", map[string]any{
|
||||
"id": firstString(output["id"], "chatcmpl-stream"),
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{}, "finish_reason": "stop"}},
|
||||
})
|
||||
}
|
||||
|
||||
func writeCompatibleStream(w http.ResponseWriter, kind string, model string, output map[string]any) {
|
||||
prepareCompatibleStream(w)
|
||||
content := extractOutputText(output)
|
||||
if content == "" {
|
||||
content = "done"
|
||||
@@ -29,6 +61,13 @@ func writeCompatibleStream(w http.ResponseWriter, kind string, model string, out
|
||||
})
|
||||
}
|
||||
|
||||
func firstString(value any, fallback string) string {
|
||||
if text, ok := value.(string); ok && text != "" {
|
||||
return text
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func extractOutputText(output map[string]any) string {
|
||||
if text, ok := output["output_text"].(string); ok {
|
||||
return text
|
||||
|
||||
Reference in New Issue
Block a user