chore: commit pending gateway changes
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type AccessRuleInput struct {
|
||||
SubjectType string `json:"subjectType"`
|
||||
SubjectID string `json:"subjectId"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Effect string `json:"effect"`
|
||||
Priority int `json:"priority"`
|
||||
MinPermissionLevel int `json:"minPermissionLevel"`
|
||||
Conditions map[string]any `json:"conditions"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AccessRuleResourceInput struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Priority int `json:"priority"`
|
||||
MinPermissionLevel int `json:"minPermissionLevel"`
|
||||
Conditions map[string]any `json:"conditions"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AccessRuleBatchInput struct {
|
||||
SubjectType string `json:"subjectType"`
|
||||
SubjectID string `json:"subjectId"`
|
||||
Effect string `json:"effect"`
|
||||
UpsertResources []AccessRuleResourceInput `json:"upsertResources"`
|
||||
DeleteResources []AccessRuleResourceInput `json:"deleteResources"`
|
||||
}
|
||||
|
||||
type accessRuleResource struct {
|
||||
Type string
|
||||
ID string
|
||||
}
|
||||
|
||||
func (s *Store) ListAccessRules(ctx context.Context) ([]AccessRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+accessRuleColumns+`
|
||||
FROM gateway_access_rules
|
||||
ORDER BY resource_type ASC, priority ASC, subject_type ASC, created_at DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]AccessRule, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanAccessRule(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) CreateAccessRule(ctx context.Context, input AccessRuleInput) (AccessRule, error) {
|
||||
input = normalizeAccessRuleInput(input)
|
||||
conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanAccessRule(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO gateway_access_rules (
|
||||
subject_type, subject_id, resource_type, resource_id, effect, priority,
|
||||
min_permission_level, conditions, metadata, status
|
||||
)
|
||||
VALUES ($1, $2::uuid, $3, $4::uuid, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
|
||||
RETURNING `+accessRuleColumns,
|
||||
input.SubjectType, input.SubjectID, input.ResourceType, input.ResourceID, input.Effect,
|
||||
input.Priority, input.MinPermissionLevel, string(conditions), string(metadata), input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) UpdateAccessRule(ctx context.Context, id string, input AccessRuleInput) (AccessRule, error) {
|
||||
input = normalizeAccessRuleInput(input)
|
||||
conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanAccessRule(s.pool.QueryRow(ctx, `
|
||||
UPDATE gateway_access_rules
|
||||
SET subject_type = $2,
|
||||
subject_id = $3::uuid,
|
||||
resource_type = $4,
|
||||
resource_id = $5::uuid,
|
||||
effect = $6,
|
||||
priority = $7,
|
||||
min_permission_level = $8,
|
||||
conditions = $9::jsonb,
|
||||
metadata = $10::jsonb,
|
||||
status = $11,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid
|
||||
RETURNING `+accessRuleColumns,
|
||||
id, input.SubjectType, input.SubjectID, input.ResourceType, input.ResourceID, input.Effect,
|
||||
input.Priority, input.MinPermissionLevel, string(conditions), string(metadata), input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAccessRule(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM gateway_access_rules WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) BatchAccessRules(ctx context.Context, input AccessRuleBatchInput) ([]AccessRule, error) {
|
||||
input = normalizeAccessRuleBatchInput(input)
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
for _, resource := range dedupeAccessRuleResources(input.DeleteResources) {
|
||||
resource = normalizeAccessRuleResource(resource, input.Effect)
|
||||
if resource.ResourceType == "" || resource.ResourceID == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM gateway_access_rules
|
||||
WHERE subject_type = $1
|
||||
AND subject_id = $2::uuid
|
||||
AND effect = $3
|
||||
AND resource_type = $4
|
||||
AND resource_id = $5::uuid`,
|
||||
input.SubjectType, input.SubjectID, input.Effect, resource.ResourceType, resource.ResourceID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range dedupeAccessRuleResources(input.UpsertResources) {
|
||||
resource = normalizeAccessRuleResource(resource, input.Effect)
|
||||
if resource.ResourceType == "" || resource.ResourceID == "" {
|
||||
continue
|
||||
}
|
||||
oppositeEffect := "deny"
|
||||
if input.Effect == "deny" {
|
||||
oppositeEffect = "allow"
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM gateway_access_rules
|
||||
WHERE subject_type = $1
|
||||
AND subject_id = $2::uuid
|
||||
AND effect = $3
|
||||
AND resource_type = $4
|
||||
AND resource_id = $5::uuid`,
|
||||
input.SubjectType, input.SubjectID, oppositeEffect, resource.ResourceType, resource.ResourceID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conditions, _ := json.Marshal(emptyObjectIfNil(resource.Conditions))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(resource.Metadata))
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO gateway_access_rules (
|
||||
subject_type, subject_id, resource_type, resource_id, effect, priority,
|
||||
min_permission_level, conditions, metadata, status
|
||||
)
|
||||
VALUES ($1, $2::uuid, $3, $4::uuid, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
|
||||
ON CONFLICT (subject_type, subject_id, resource_type, resource_id, effect)
|
||||
DO UPDATE SET priority = EXCLUDED.priority,
|
||||
min_permission_level = EXCLUDED.min_permission_level,
|
||||
conditions = EXCLUDED.conditions,
|
||||
metadata = EXCLUDED.metadata,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = now()`,
|
||||
input.SubjectType, input.SubjectID, resource.ResourceType, resource.ResourceID, input.Effect,
|
||||
resource.Priority, resource.MinPermissionLevel, string(conditions), string(metadata), resource.Status,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ListAccessRules(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) filterCandidatesByAccessRules(ctx context.Context, user *auth.User, candidates []RuntimeModelCandidate) ([]RuntimeModelCandidate, error) {
|
||||
if len(candidates) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
resources := candidateAccessResources(candidates)
|
||||
if len(resources) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
rules, err := s.listActiveAccessRulesForResources(ctx, resources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
subjects := accessRuleSubjects(user)
|
||||
level := 0
|
||||
if user != nil {
|
||||
level = auth.PermissionLevel(user.Roles)
|
||||
}
|
||||
filtered := candidates[:0]
|
||||
for _, candidate := range candidates {
|
||||
if candidateAllowedByAccessRules(candidate, rules, subjects, level) {
|
||||
filtered = append(filtered, candidate)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListAccessiblePlatformModels(ctx context.Context, user *auth.User) ([]PlatformModel, error) {
|
||||
models, err := s.ListModels(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platforms, err := s.ListPlatforms(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enabledPlatforms := map[string]bool{}
|
||||
for _, platform := range platforms {
|
||||
if platform.Status == "enabled" {
|
||||
enabledPlatforms[platform.ID] = true
|
||||
}
|
||||
}
|
||||
enabled := make([]PlatformModel, 0, len(models))
|
||||
for _, model := range models {
|
||||
if model.Enabled && enabledPlatforms[model.PlatformID] {
|
||||
enabled = append(enabled, model)
|
||||
}
|
||||
}
|
||||
return s.filterPlatformModelsByAccessRules(ctx, user, enabled)
|
||||
}
|
||||
|
||||
func (s *Store) filterPlatformModelsByAccessRules(ctx context.Context, user *auth.User, models []PlatformModel) ([]PlatformModel, error) {
|
||||
if len(models) == 0 {
|
||||
return models, nil
|
||||
}
|
||||
resources := platformModelAccessResources(models)
|
||||
if len(resources) == 0 {
|
||||
return models, nil
|
||||
}
|
||||
rules, err := s.listActiveAccessRulesForResources(ctx, resources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return models, nil
|
||||
}
|
||||
subjects := accessRuleSubjects(user)
|
||||
level := 0
|
||||
if user != nil {
|
||||
level = auth.PermissionLevel(user.Roles)
|
||||
}
|
||||
filtered := models[:0]
|
||||
for _, model := range models {
|
||||
if platformModelAllowedByAccessRules(model, rules, subjects, level) {
|
||||
filtered = append(filtered, model)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (s *Store) listActiveAccessRulesForResources(ctx context.Context, resources []accessRuleResource) ([]AccessRule, error) {
|
||||
values := make([]string, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
if resource.Type == "" || resource.ID == "" {
|
||||
continue
|
||||
}
|
||||
values = append(values, resource.Type+":"+resource.ID)
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+accessRuleColumns+`
|
||||
FROM gateway_access_rules
|
||||
WHERE status = 'active'
|
||||
AND (resource_type || ':' || resource_id::text) = ANY($1)
|
||||
ORDER BY priority ASC, created_at ASC`, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
rules := make([]AccessRule, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanAccessRule(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rules = append(rules, item)
|
||||
}
|
||||
return rules, rows.Err()
|
||||
}
|
||||
|
||||
func candidateAccessResources(candidates []RuntimeModelCandidate) []accessRuleResource {
|
||||
seen := map[string]bool{}
|
||||
out := make([]accessRuleResource, 0, len(candidates)*3)
|
||||
add := func(resourceType string, resourceID string) {
|
||||
key := resourceType + ":" + resourceID
|
||||
if resourceID == "" || seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
out = append(out, accessRuleResource{Type: resourceType, ID: resourceID})
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
add("platform", candidate.PlatformID)
|
||||
add("platform_model", candidate.PlatformModelID)
|
||||
add("base_model", candidate.BaseModelID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func platformModelAccessResources(models []PlatformModel) []accessRuleResource {
|
||||
seen := map[string]bool{}
|
||||
out := make([]accessRuleResource, 0, len(models)*3)
|
||||
add := func(resourceType string, resourceID string) {
|
||||
key := resourceType + ":" + resourceID
|
||||
if resourceID == "" || seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
out = append(out, accessRuleResource{Type: resourceType, ID: resourceID})
|
||||
}
|
||||
for _, model := range models {
|
||||
add("platform", model.PlatformID)
|
||||
add("platform_model", model.ID)
|
||||
add("base_model", model.BaseModelID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func candidateAllowedByAccessRules(candidate RuntimeModelCandidate, rules []AccessRule, subjects map[string]bool, permissionLevel int) bool {
|
||||
resourceKeys := map[string]bool{
|
||||
"platform:" + candidate.PlatformID: true,
|
||||
"platform_model:" + candidate.PlatformModelID: true,
|
||||
"base_model:" + candidate.BaseModelID: candidate.BaseModelID != "",
|
||||
}
|
||||
allowByResource := map[string]bool{}
|
||||
matchedAllowByResource := map[string]bool{}
|
||||
for _, rule := range rules {
|
||||
resourceKey := rule.ResourceType + ":" + rule.ResourceID
|
||||
if !resourceKeys[resourceKey] {
|
||||
continue
|
||||
}
|
||||
subjectKey := rule.SubjectType + ":" + rule.SubjectID
|
||||
if rule.Effect == "deny" && subjects[subjectKey] {
|
||||
return false
|
||||
}
|
||||
if rule.Effect == "allow" {
|
||||
allowByResource[resourceKey] = true
|
||||
if subjects[subjectKey] && permissionLevel >= rule.MinPermissionLevel {
|
||||
matchedAllowByResource[resourceKey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for resourceKey := range allowByResource {
|
||||
if !matchedAllowByResource[resourceKey] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func platformModelAllowedByAccessRules(model PlatformModel, rules []AccessRule, subjects map[string]bool, permissionLevel int) bool {
|
||||
resourceKeys := map[string]bool{
|
||||
"platform:" + model.PlatformID: true,
|
||||
"platform_model:" + model.ID: true,
|
||||
"base_model:" + model.BaseModelID: model.BaseModelID != "",
|
||||
}
|
||||
allowByResource := map[string]bool{}
|
||||
matchedAllowByResource := map[string]bool{}
|
||||
for _, rule := range rules {
|
||||
resourceKey := rule.ResourceType + ":" + rule.ResourceID
|
||||
if !resourceKeys[resourceKey] {
|
||||
continue
|
||||
}
|
||||
subjectKey := rule.SubjectType + ":" + rule.SubjectID
|
||||
if rule.Effect == "deny" && subjects[subjectKey] {
|
||||
return false
|
||||
}
|
||||
if rule.Effect == "allow" {
|
||||
allowByResource[resourceKey] = true
|
||||
if subjects[subjectKey] && permissionLevel >= rule.MinPermissionLevel {
|
||||
matchedAllowByResource[resourceKey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for resourceKey := range allowByResource {
|
||||
if !matchedAllowByResource[resourceKey] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func accessRuleSubjects(user *auth.User) map[string]bool {
|
||||
subjects := map[string]bool{}
|
||||
if user == nil {
|
||||
return subjects
|
||||
}
|
||||
add := func(subjectType string, id string) {
|
||||
id = strings.TrimSpace(id)
|
||||
if id != "" {
|
||||
subjects[subjectType+":"+id] = true
|
||||
}
|
||||
}
|
||||
add("user", firstNonEmpty(user.GatewayUserID, user.ID))
|
||||
add("tenant", firstNonEmpty(user.GatewayTenantID, user.TenantID))
|
||||
add("api_key", user.APIKeyID)
|
||||
add("user_group", user.UserGroupID)
|
||||
for _, groupKey := range user.UserGroupKeys {
|
||||
add("user_group", groupKey)
|
||||
}
|
||||
return subjects
|
||||
}
|
||||
|
||||
const accessRuleColumns = `
|
||||
id::text, subject_type, subject_id::text, resource_type, resource_id::text, effect,
|
||||
priority, min_permission_level, conditions, metadata, status, created_at, updated_at`
|
||||
|
||||
func scanAccessRule(row scanner) (AccessRule, error) {
|
||||
var item AccessRule
|
||||
var conditions []byte
|
||||
var metadata []byte
|
||||
if err := row.Scan(
|
||||
&item.ID,
|
||||
&item.SubjectType,
|
||||
&item.SubjectID,
|
||||
&item.ResourceType,
|
||||
&item.ResourceID,
|
||||
&item.Effect,
|
||||
&item.Priority,
|
||||
&item.MinPermissionLevel,
|
||||
&conditions,
|
||||
&metadata,
|
||||
&item.Status,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return AccessRule{}, err
|
||||
}
|
||||
item.Conditions = decodeObject(conditions)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func normalizeAccessRuleInput(input AccessRuleInput) AccessRuleInput {
|
||||
input.SubjectType = strings.TrimSpace(input.SubjectType)
|
||||
input.SubjectID = strings.TrimSpace(input.SubjectID)
|
||||
input.ResourceType = strings.TrimSpace(input.ResourceType)
|
||||
input.ResourceID = strings.TrimSpace(input.ResourceID)
|
||||
input.Effect = firstNonEmpty(strings.TrimSpace(input.Effect), "allow")
|
||||
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
|
||||
if input.Priority == 0 {
|
||||
input.Priority = 100
|
||||
}
|
||||
if input.MinPermissionLevel < 0 {
|
||||
input.MinPermissionLevel = 0
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func normalizeAccessRuleBatchInput(input AccessRuleBatchInput) AccessRuleBatchInput {
|
||||
input.SubjectType = strings.TrimSpace(input.SubjectType)
|
||||
input.SubjectID = strings.TrimSpace(input.SubjectID)
|
||||
input.Effect = firstNonEmpty(strings.TrimSpace(input.Effect), "allow")
|
||||
return input
|
||||
}
|
||||
|
||||
func normalizeAccessRuleResource(input AccessRuleResourceInput, effect string) AccessRuleResourceInput {
|
||||
input.ResourceType = strings.TrimSpace(input.ResourceType)
|
||||
input.ResourceID = strings.TrimSpace(input.ResourceID)
|
||||
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
|
||||
if input.Priority == 0 {
|
||||
if effect == "deny" {
|
||||
input.Priority = 10
|
||||
} else {
|
||||
input.Priority = 100
|
||||
}
|
||||
}
|
||||
if input.MinPermissionLevel < 0 {
|
||||
input.MinPermissionLevel = 0
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func dedupeAccessRuleResources(resources []AccessRuleResourceInput) []AccessRuleResourceInput {
|
||||
seen := map[string]bool{}
|
||||
out := make([]AccessRuleResourceInput, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
resource.ResourceType = strings.TrimSpace(resource.ResourceType)
|
||||
resource.ResourceID = strings.TrimSpace(resource.ResourceID)
|
||||
key := resource.ResourceType + ":" + resource.ResourceID
|
||||
if resource.ResourceType == "" || resource.ResourceID == "" || seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
out = append(out, resource)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -10,19 +10,27 @@ import (
|
||||
|
||||
const baseModelColumns = `
|
||||
id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
|
||||
capabilities, base_billing_config, default_rate_limit_policy, metadata, pricing_version,
|
||||
status, created_at, updated_at`
|
||||
capabilities, base_billing_config, default_rate_limit_policy, COALESCE(pricing_rule_set_id::text, ''),
|
||||
COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override, metadata,
|
||||
catalog_type, COALESCE(default_snapshot, '{}'::jsonb), COALESCE(customized_at::text, ''),
|
||||
pricing_version, status, created_at, updated_at`
|
||||
|
||||
type BaseModelInput struct {
|
||||
ProviderKey string `json:"providerKey"`
|
||||
CanonicalModelKey string `json:"canonicalModelKey"`
|
||||
ProviderModelName string `json:"providerModelName"`
|
||||
ModelType string `json:"modelType"`
|
||||
ModelType StringList `json:"modelType"`
|
||||
ModelAlias string `json:"modelAlias"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Capabilities map[string]any `json:"capabilities"`
|
||||
BaseBillingConfig map[string]any `json:"baseBillingConfig"`
|
||||
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy"`
|
||||
PricingRuleSetID string `json:"pricingRuleSetId"`
|
||||
RuntimePolicySetID string `json:"runtimePolicySetId"`
|
||||
RuntimePolicyOverride map[string]any `json:"runtimePolicyOverride"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
CatalogType string `json:"catalogType"`
|
||||
DefaultSnapshot map[string]any `json:"defaultSnapshot"`
|
||||
PricingVersion int `json:"pricingVersion"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
@@ -31,6 +39,22 @@ type baseModelScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
type StringList []string
|
||||
|
||||
func (list *StringList) UnmarshalJSON(data []byte) error {
|
||||
var values []string
|
||||
if err := json.Unmarshal(data, &values); err == nil {
|
||||
*list = uniqueStringList(values)
|
||||
return nil
|
||||
}
|
||||
var value string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*list = uniqueStringList([]string{value})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ListBaseModels(ctx context.Context) ([]BaseModel, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+baseModelColumns+`
|
||||
@@ -57,27 +81,39 @@ func (s *Store) CreateBaseModel(ctx context.Context, input BaseModelInput) (Base
|
||||
capabilities, _ := json.Marshal(emptyObjectIfNil(input.Capabilities))
|
||||
billingConfig, _ := json.Marshal(emptyObjectIfNil(input.BaseBillingConfig))
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.DefaultRateLimitPolicy))
|
||||
runtimePolicyOverride, _ := json.Marshal(emptyObjectIfNil(input.RuntimePolicyOverride))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
defaultSnapshot, _ := json.Marshal(emptyObjectIfNil(input.DefaultSnapshot))
|
||||
modelType := primaryString(input.ModelType, "text_generate")
|
||||
|
||||
return scanBaseModel(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO base_model_catalog (
|
||||
provider_id, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
|
||||
capabilities, base_billing_config, default_rate_limit_policy, metadata, pricing_version, status
|
||||
capabilities, base_billing_config, default_rate_limit_policy, pricing_rule_set_id, runtime_policy_set_id, runtime_policy_override,
|
||||
metadata, catalog_type, default_snapshot, pricing_version, status
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM model_catalog_providers WHERE provider_key = $1 OR provider_code = $1 LIMIT 1),
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
COALESCE(NULLIF($9, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
|
||||
COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
|
||||
$11, $12, NULLIF($13, ''), NULLIF($14::jsonb, '{}'::jsonb), $15, $16
|
||||
)
|
||||
RETURNING `+baseModelColumns,
|
||||
input.ProviderKey,
|
||||
input.CanonicalModelKey,
|
||||
input.ProviderModelName,
|
||||
input.ModelType,
|
||||
input.DisplayName,
|
||||
modelType,
|
||||
input.ModelAlias,
|
||||
capabilities,
|
||||
billingConfig,
|
||||
rateLimitPolicy,
|
||||
input.PricingRuleSetID,
|
||||
input.RuntimePolicySetID,
|
||||
runtimePolicyOverride,
|
||||
metadata,
|
||||
input.CatalogType,
|
||||
string(defaultSnapshot),
|
||||
input.PricingVersion,
|
||||
input.Status,
|
||||
))
|
||||
@@ -88,7 +124,10 @@ func (s *Store) UpdateBaseModel(ctx context.Context, id string, input BaseModelI
|
||||
capabilities, _ := json.Marshal(emptyObjectIfNil(input.Capabilities))
|
||||
billingConfig, _ := json.Marshal(emptyObjectIfNil(input.BaseBillingConfig))
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.DefaultRateLimitPolicy))
|
||||
runtimePolicyOverride, _ := json.Marshal(emptyObjectIfNil(input.RuntimePolicyOverride))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
defaultSnapshot, _ := json.Marshal(emptyObjectIfNil(input.DefaultSnapshot))
|
||||
modelType := primaryString(input.ModelType, "text_generate")
|
||||
|
||||
return scanBaseModel(s.pool.QueryRow(ctx, `
|
||||
UPDATE base_model_catalog
|
||||
@@ -101,9 +140,15 @@ SET provider_id = (SELECT id FROM model_catalog_providers WHERE provider_key = $
|
||||
capabilities = $7,
|
||||
base_billing_config = $8,
|
||||
default_rate_limit_policy = $9,
|
||||
metadata = $10,
|
||||
pricing_version = $11,
|
||||
status = $12,
|
||||
pricing_rule_set_id = COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
|
||||
runtime_policy_set_id = COALESCE(NULLIF($11, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
|
||||
runtime_policy_override = $12,
|
||||
metadata = $13,
|
||||
catalog_type = NULLIF($14, ''),
|
||||
default_snapshot = COALESCE(NULLIF($15::jsonb, '{}'::jsonb), default_snapshot),
|
||||
customized_at = CASE WHEN NULLIF($14, '') = 'system' THEN now() ELSE NULL END,
|
||||
pricing_version = $16,
|
||||
status = $17,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid
|
||||
RETURNING `+baseModelColumns,
|
||||
@@ -111,17 +156,117 @@ RETURNING `+baseModelColumns,
|
||||
input.ProviderKey,
|
||||
input.CanonicalModelKey,
|
||||
input.ProviderModelName,
|
||||
input.ModelType,
|
||||
input.DisplayName,
|
||||
modelType,
|
||||
input.ModelAlias,
|
||||
capabilities,
|
||||
billingConfig,
|
||||
rateLimitPolicy,
|
||||
input.PricingRuleSetID,
|
||||
input.RuntimePolicySetID,
|
||||
runtimePolicyOverride,
|
||||
metadata,
|
||||
input.CatalogType,
|
||||
string(defaultSnapshot),
|
||||
input.PricingVersion,
|
||||
input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) ResetBaseModelToDefault(ctx context.Context, id string) (BaseModel, error) {
|
||||
var catalogType string
|
||||
var snapshotBytes []byte
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT catalog_type, COALESCE(default_snapshot, '{}'::jsonb)
|
||||
FROM base_model_catalog
|
||||
WHERE id = $1::uuid`, id).Scan(&catalogType, &snapshotBytes); err != nil {
|
||||
return BaseModel{}, err
|
||||
}
|
||||
if catalogType != "system" {
|
||||
return BaseModel{}, ErrProtectedDefault
|
||||
}
|
||||
snapshot := decodeObject(snapshotBytes)
|
||||
if len(snapshot) == 0 {
|
||||
return BaseModel{}, ErrProtectedDefault
|
||||
}
|
||||
|
||||
return scanBaseModel(s.pool.QueryRow(ctx, `
|
||||
UPDATE base_model_catalog
|
||||
SET provider_id = (SELECT id FROM model_catalog_providers WHERE provider_key = COALESCE($2::text, provider_key) OR provider_code = COALESCE($2::text, provider_key) LIMIT 1),
|
||||
provider_key = COALESCE($2::text, provider_key),
|
||||
canonical_model_key = COALESCE($3::text, canonical_model_key),
|
||||
provider_model_name = COALESCE($4::text, provider_model_name),
|
||||
model_type = COALESCE($5::text, model_type),
|
||||
display_name = COALESCE($6::text, display_name),
|
||||
capabilities = COALESCE($7::jsonb, capabilities),
|
||||
base_billing_config = COALESCE($8::jsonb, base_billing_config),
|
||||
default_rate_limit_policy = COALESCE($9::jsonb, default_rate_limit_policy),
|
||||
pricing_rule_set_id = COALESCE(NULLIF($10::text, '')::uuid, pricing_rule_set_id),
|
||||
runtime_policy_set_id = COALESCE(NULLIF($11::text, '')::uuid, runtime_policy_set_id),
|
||||
runtime_policy_override = COALESCE($12::jsonb, runtime_policy_override),
|
||||
metadata = COALESCE($13::jsonb, metadata),
|
||||
pricing_version = COALESCE($14::integer, pricing_version),
|
||||
status = COALESCE($15::text, status),
|
||||
customized_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid
|
||||
RETURNING `+baseModelColumns,
|
||||
id,
|
||||
stringFromSnapshot(snapshot, "providerKey"),
|
||||
stringFromSnapshot(snapshot, "canonicalModelKey"),
|
||||
stringFromSnapshot(snapshot, "providerModelName"),
|
||||
stringFromSnapshot(snapshot, "modelType"),
|
||||
stringFromSnapshot(snapshot, "modelAlias", "displayName"),
|
||||
jsonFromSnapshot(snapshot, "capabilities"),
|
||||
jsonFromSnapshot(snapshot, "baseBillingConfig"),
|
||||
jsonFromSnapshot(snapshot, "defaultRateLimitPolicy"),
|
||||
stringFromSnapshot(snapshot, "pricingRuleSetId"),
|
||||
stringFromSnapshot(snapshot, "runtimePolicySetId"),
|
||||
jsonFromSnapshot(snapshot, "runtimePolicyOverride"),
|
||||
jsonFromSnapshot(snapshot, "metadata"),
|
||||
intFromSnapshot(snapshot, "pricingVersion"),
|
||||
stringFromSnapshot(snapshot, "status"),
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) ResetAllBaseModelsToDefault(ctx context.Context) ([]BaseModel, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
UPDATE base_model_catalog
|
||||
SET provider_id = (
|
||||
SELECT id
|
||||
FROM model_catalog_providers
|
||||
WHERE provider_key = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key)
|
||||
OR provider_code = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key)
|
||||
LIMIT 1
|
||||
),
|
||||
provider_key = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key),
|
||||
canonical_model_key = COALESCE(NULLIF(default_snapshot->>'canonicalModelKey', ''), canonical_model_key),
|
||||
provider_model_name = COALESCE(NULLIF(default_snapshot->>'providerModelName', ''), provider_model_name),
|
||||
model_type = COALESCE(NULLIF(CASE
|
||||
WHEN jsonb_typeof(default_snapshot->'modelType') = 'array' THEN default_snapshot->'modelType'->>0
|
||||
ELSE default_snapshot->>'modelType'
|
||||
END, ''), model_type),
|
||||
display_name = COALESCE(NULLIF(COALESCE(default_snapshot->>'modelAlias', default_snapshot->>'displayName'), ''), display_name),
|
||||
capabilities = COALESCE(default_snapshot->'capabilities', capabilities),
|
||||
base_billing_config = COALESCE(default_snapshot->'baseBillingConfig', base_billing_config),
|
||||
default_rate_limit_policy = COALESCE(default_snapshot->'defaultRateLimitPolicy', default_rate_limit_policy),
|
||||
pricing_rule_set_id = COALESCE(NULLIF(default_snapshot->>'pricingRuleSetId', '')::uuid, pricing_rule_set_id),
|
||||
runtime_policy_set_id = COALESCE(NULLIF(default_snapshot->>'runtimePolicySetId', '')::uuid, runtime_policy_set_id),
|
||||
runtime_policy_override = COALESCE(default_snapshot->'runtimePolicyOverride', runtime_policy_override),
|
||||
metadata = COALESCE(default_snapshot->'metadata', metadata),
|
||||
pricing_version = COALESCE(NULLIF(default_snapshot->>'pricingVersion', '')::integer, pricing_version),
|
||||
status = COALESCE(NULLIF(default_snapshot->>'status', ''), status),
|
||||
customized_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE catalog_type = 'system'
|
||||
AND COALESCE(default_snapshot, '{}'::jsonb) <> '{}'::jsonb
|
||||
RETURNING `+baseModelColumns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanBaseModelRows(rows)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteBaseModel(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM base_model_catalog WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
@@ -133,23 +278,45 @@ func (s *Store) DeleteBaseModel(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanBaseModelRows(rows pgx.Rows) ([]BaseModel, error) {
|
||||
items := make([]BaseModel, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanBaseModel(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
|
||||
var item BaseModel
|
||||
var modelType string
|
||||
var modelAlias string
|
||||
var capabilities []byte
|
||||
var billingConfig []byte
|
||||
var rateLimitPolicy []byte
|
||||
var runtimePolicyOverride []byte
|
||||
var metadata []byte
|
||||
var defaultSnapshot []byte
|
||||
if err := scanner.Scan(
|
||||
&item.ID,
|
||||
&item.ProviderKey,
|
||||
&item.CanonicalModelKey,
|
||||
&item.ProviderModelName,
|
||||
&item.ModelType,
|
||||
&item.DisplayName,
|
||||
&modelType,
|
||||
&modelAlias,
|
||||
&capabilities,
|
||||
&billingConfig,
|
||||
&rateLimitPolicy,
|
||||
&item.PricingRuleSetID,
|
||||
&item.RuntimePolicySetID,
|
||||
&runtimePolicyOverride,
|
||||
&metadata,
|
||||
&item.CatalogType,
|
||||
&defaultSnapshot,
|
||||
&item.CustomizedAt,
|
||||
&item.PricingVersion,
|
||||
&item.Status,
|
||||
&item.CreatedAt,
|
||||
@@ -160,7 +327,12 @@ func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
|
||||
item.Capabilities = decodeObject(capabilities)
|
||||
item.BaseBillingConfig = decodeObject(billingConfig)
|
||||
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
|
||||
item.RuntimePolicyOverride = decodeObject(runtimePolicyOverride)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
item.DefaultSnapshot = decodeObject(defaultSnapshot)
|
||||
item.ModelType = baseModelTypes(item.Capabilities, item.Metadata, modelType)
|
||||
item.ModelAlias = modelAlias
|
||||
item.DisplayName = modelAlias
|
||||
return item, nil
|
||||
}
|
||||
|
||||
@@ -168,17 +340,27 @@ func normalizeBaseModelInput(input BaseModelInput) BaseModelInput {
|
||||
input.ProviderKey = strings.TrimSpace(input.ProviderKey)
|
||||
input.CanonicalModelKey = strings.TrimSpace(input.CanonicalModelKey)
|
||||
input.ProviderModelName = strings.TrimSpace(input.ProviderModelName)
|
||||
input.ModelType = strings.TrimSpace(input.ModelType)
|
||||
input.ModelType = uniqueStringList(input.ModelType)
|
||||
input.ModelAlias = strings.TrimSpace(input.ModelAlias)
|
||||
input.DisplayName = strings.TrimSpace(input.DisplayName)
|
||||
input.PricingRuleSetID = strings.TrimSpace(input.PricingRuleSetID)
|
||||
input.RuntimePolicySetID = strings.TrimSpace(input.RuntimePolicySetID)
|
||||
input.CatalogType = strings.TrimSpace(input.CatalogType)
|
||||
input.Status = strings.TrimSpace(input.Status)
|
||||
if input.CanonicalModelKey == "" && input.ProviderKey != "" && input.ProviderModelName != "" {
|
||||
input.CanonicalModelKey = input.ProviderKey + ":" + input.ProviderModelName
|
||||
}
|
||||
if input.DisplayName == "" {
|
||||
input.DisplayName = input.ProviderModelName
|
||||
if input.ModelAlias == "" {
|
||||
input.ModelAlias = input.DisplayName
|
||||
}
|
||||
if input.ModelType == "" {
|
||||
input.ModelType = "text_generate"
|
||||
if input.ModelAlias == "" {
|
||||
input.ModelAlias = input.ProviderModelName
|
||||
}
|
||||
if len(input.ModelType) == 0 {
|
||||
input.ModelType = StringList{"text_generate"}
|
||||
}
|
||||
if input.CatalogType == "" {
|
||||
input.CatalogType = "custom"
|
||||
}
|
||||
if input.PricingVersion <= 0 {
|
||||
input.PricingVersion = 1
|
||||
@@ -188,3 +370,102 @@ func normalizeBaseModelInput(input BaseModelInput) BaseModelInput {
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func stringFromSnapshot(snapshot map[string]any, keys ...string) any {
|
||||
for _, key := range keys {
|
||||
value, ok := snapshot[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if strings.TrimSpace(typed) != "" {
|
||||
return typed
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
if text, ok := item.(string); ok && strings.TrimSpace(text) != "" {
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
if primary := primaryString(typed, ""); primary != "" {
|
||||
return primary
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func intFromSnapshot(snapshot map[string]any, key string) any {
|
||||
switch value := snapshot[key].(type) {
|
||||
case float64:
|
||||
return int(value)
|
||||
case int:
|
||||
return value
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func jsonFromSnapshot(snapshot map[string]any, key string) any {
|
||||
value, ok := snapshot[key]
|
||||
if !ok || value == nil {
|
||||
return nil
|
||||
}
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func baseModelTypes(capabilities map[string]any, metadata map[string]any, fallback string) StringList {
|
||||
values := make([]string, 0)
|
||||
values = append(values, stringListFromAny(capabilities["originalTypes"])...)
|
||||
values = append(values, stringListFromAny(metadata["originalTypes"])...)
|
||||
if fallback != "" {
|
||||
values = append(values, fallback)
|
||||
}
|
||||
return uniqueStringList(values)
|
||||
}
|
||||
|
||||
func stringListFromAny(value any) []string {
|
||||
switch typed := value.(type) {
|
||||
case []string:
|
||||
return typed
|
||||
case []any:
|
||||
values := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
if text, ok := item.(string); ok {
|
||||
values = append(values, text)
|
||||
}
|
||||
}
|
||||
return values
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueStringList(values []string) StringList {
|
||||
out := make([]string, 0, len(values))
|
||||
seen := map[string]bool{}
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func primaryString(values []string, fallback string) string {
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
)
|
||||
|
||||
func (s *Store) ListModelCandidates(ctx context.Context, model string, modelType string) ([]RuntimeModelCandidate, error) {
|
||||
func (s *Store) ListModelCandidates(ctx context.Context, model string, modelType string, user *auth.User) ([]RuntimeModelCandidate, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT p.id::text, p.platform_key, p.name, p.provider, COALESCE(p.base_url, ''),
|
||||
SELECT p.id::text, p.platform_key, p.name, p.provider,
|
||||
COALESCE(NULLIF(p.config->>'specType', ''), NULLIF(cp.provider_type, ''), NULLIF(p.config->>'sourceSpecType', ''), p.provider) AS spec_type,
|
||||
COALESCE(p.base_url, ''),
|
||||
p.auth_type, p.credentials, p.config, p.default_pricing_mode,
|
||||
p.default_discount_factor::float8, COALESCE(p.pricing_rule_set_id::text, ''),
|
||||
p.retry_policy, p.rate_limit_policy,
|
||||
@@ -17,16 +21,24 @@ SELECT p.id::text, p.platform_key, p.name, p.provider, COALESCE(p.base_url, ''),
|
||||
m.model_type, m.display_name, m.capabilities, m.capability_override,
|
||||
COALESCE(b.base_billing_config, '{}'::jsonb), m.billing_config, m.billing_config_override,
|
||||
m.pricing_mode, COALESCE(m.discount_factor, 0)::float8, COALESCE(m.pricing_rule_set_id::text, ''),
|
||||
m.retry_policy, m.rate_limit_policy
|
||||
m.permission_config, m.retry_policy, m.rate_limit_policy, COALESCE(m.runtime_policy_set_id::text, COALESCE(b.runtime_policy_set_id::text, '')),
|
||||
COALESCE(NULLIF(m.runtime_policy_override, '{}'::jsonb), b.runtime_policy_override, '{}'::jsonb)
|
||||
FROM platform_models m
|
||||
JOIN integration_platforms p ON p.id = m.platform_id
|
||||
LEFT JOIN model_catalog_providers cp ON cp.provider_key = p.provider OR cp.provider_code = p.provider
|
||||
LEFT JOIN base_model_catalog b ON b.id = m.base_model_id
|
||||
LEFT JOIN runtime_client_states s
|
||||
ON s.client_id = p.platform_key || ':' || m.model_type || ':' || m.model_name
|
||||
WHERE p.status = 'enabled'
|
||||
AND p.deleted_at IS NULL
|
||||
AND m.enabled = true
|
||||
AND m.model_type = $2
|
||||
AND (
|
||||
m.model_type = $2
|
||||
OR ($2 = 'text_generate' AND m.model_type IN ('chat', 'responses', 'text'))
|
||||
OR ($2 = 'image_generate' AND m.model_type IN ('image', 'images.generations'))
|
||||
OR ($2 = 'image_edit' AND m.model_type IN ('images.edits'))
|
||||
OR ($2 = 'video_generate' AND m.model_type IN ('video', 'videos.generations'))
|
||||
)
|
||||
AND (p.cooldown_until IS NULL OR p.cooldown_until <= now())
|
||||
AND (
|
||||
m.model_name = $1
|
||||
@@ -57,13 +69,16 @@ ORDER BY effective_priority ASC,
|
||||
var baseBilling []byte
|
||||
var billing []byte
|
||||
var billingOverride []byte
|
||||
var permissionConfig []byte
|
||||
var modelRetryPolicy []byte
|
||||
var modelRateLimitPolicy []byte
|
||||
var runtimePolicyOverride []byte
|
||||
if err := rows.Scan(
|
||||
&item.PlatformID,
|
||||
&item.PlatformKey,
|
||||
&item.PlatformName,
|
||||
&item.Provider,
|
||||
&item.SpecType,
|
||||
&item.BaseURL,
|
||||
&item.AuthType,
|
||||
&credentials,
|
||||
@@ -90,8 +105,11 @@ ORDER BY effective_priority ASC,
|
||||
&item.PricingMode,
|
||||
&item.DiscountFactor,
|
||||
&item.ModelPricingRuleSetID,
|
||||
&permissionConfig,
|
||||
&modelRetryPolicy,
|
||||
&modelRateLimitPolicy,
|
||||
&item.RuntimePolicySetID,
|
||||
&runtimePolicyOverride,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,8 +122,10 @@ ORDER BY effective_priority ASC,
|
||||
item.BaseBillingConfig = decodeObject(baseBilling)
|
||||
item.BillingConfig = decodeObject(billing)
|
||||
item.BillingConfigOverride = decodeObject(billingOverride)
|
||||
item.PermissionConfig = decodeObject(permissionConfig)
|
||||
item.ModelRetryPolicy = decodeObject(modelRetryPolicy)
|
||||
item.ModelRateLimitPolicy = decodeObject(modelRateLimitPolicy)
|
||||
item.RuntimePolicyOverride = decodeObject(runtimePolicyOverride)
|
||||
item.ClientID = fmt.Sprintf("%s:%s:%s", item.PlatformKey, item.ModelType, item.ModelName)
|
||||
item.QueueKey = item.ClientID
|
||||
items = append(items, item)
|
||||
@@ -116,5 +136,12 @@ ORDER BY effective_priority ASC,
|
||||
if len(items) == 0 {
|
||||
return nil, ErrNoModelCandidate
|
||||
}
|
||||
items, err = s.filterCandidatesByAccessRules(ctx, user, items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, ErrNoModelCandidate
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
const catalogProviderColumns = `
|
||||
id::text, provider_key, COALESCE(NULLIF(provider_code, ''), provider_key) AS provider_code,
|
||||
display_name, provider_type, COALESCE(icon_path, '') AS icon_path,
|
||||
COALESCE(default_base_url, '') AS default_base_url, COALESCE(default_auth_type, '') AS default_auth_type,
|
||||
COALESCE(source, '') AS source, capability_schema, default_rate_limit_policy,
|
||||
metadata, status, created_at, updated_at`
|
||||
|
||||
@@ -20,6 +21,8 @@ type CatalogProviderInput struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
ProviderType string `json:"providerType"`
|
||||
IconPath string `json:"iconPath"`
|
||||
DefaultBaseURL string `json:"defaultBaseUrl"`
|
||||
DefaultAuthType string `json:"defaultAuthType"`
|
||||
Source string `json:"source"`
|
||||
CapabilitySchema map[string]any `json:"capabilitySchema"`
|
||||
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy"`
|
||||
@@ -39,16 +42,18 @@ func (s *Store) CreateCatalogProvider(ctx context.Context, input CatalogProvider
|
||||
|
||||
return scanCatalogProvider(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO model_catalog_providers (
|
||||
provider_key, provider_code, display_name, provider_type, icon_path, source,
|
||||
provider_key, provider_code, display_name, provider_type, icon_path, default_base_url, default_auth_type, source,
|
||||
capability_schema, default_rate_limit_policy, metadata, status
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, NULLIF($5, ''), $6, $7, $8, $9, $10)
|
||||
VALUES ($1, $2, $3, $4, NULLIF($5, ''), NULLIF($6, ''), $7, $8, $9, $10, $11, $12)
|
||||
RETURNING `+catalogProviderColumns,
|
||||
input.ProviderKey,
|
||||
input.Code,
|
||||
input.DisplayName,
|
||||
input.ProviderType,
|
||||
input.IconPath,
|
||||
input.DefaultBaseURL,
|
||||
input.DefaultAuthType,
|
||||
input.Source,
|
||||
capabilitySchema,
|
||||
rateLimitPolicy,
|
||||
@@ -70,11 +75,13 @@ SET provider_key = $2,
|
||||
display_name = $4,
|
||||
provider_type = $5,
|
||||
icon_path = NULLIF($6, ''),
|
||||
source = $7,
|
||||
capability_schema = $8,
|
||||
default_rate_limit_policy = $9,
|
||||
metadata = $10,
|
||||
status = $11,
|
||||
default_base_url = NULLIF($7, ''),
|
||||
default_auth_type = $8,
|
||||
source = $9,
|
||||
capability_schema = $10,
|
||||
default_rate_limit_policy = $11,
|
||||
metadata = $12,
|
||||
status = $13,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid
|
||||
RETURNING `+catalogProviderColumns,
|
||||
@@ -84,6 +91,8 @@ RETURNING `+catalogProviderColumns,
|
||||
input.DisplayName,
|
||||
input.ProviderType,
|
||||
input.IconPath,
|
||||
input.DefaultBaseURL,
|
||||
input.DefaultAuthType,
|
||||
input.Source,
|
||||
capabilitySchema,
|
||||
rateLimitPolicy,
|
||||
@@ -115,6 +124,8 @@ func scanCatalogProvider(scanner catalogProviderScanner) (CatalogProvider, error
|
||||
&item.DisplayName,
|
||||
&item.ProviderType,
|
||||
&item.IconPath,
|
||||
&item.DefaultBaseURL,
|
||||
&item.DefaultAuthType,
|
||||
&item.Source,
|
||||
&capabilitySchema,
|
||||
&rateLimitPolicy,
|
||||
@@ -137,6 +148,8 @@ func normalizeCatalogProviderInput(input CatalogProviderInput) CatalogProviderIn
|
||||
input.DisplayName = strings.TrimSpace(input.DisplayName)
|
||||
input.ProviderType = strings.TrimSpace(input.ProviderType)
|
||||
input.IconPath = strings.TrimSpace(input.IconPath)
|
||||
input.DefaultBaseURL = strings.TrimSpace(input.DefaultBaseURL)
|
||||
input.DefaultAuthType = strings.TrimSpace(input.DefaultAuthType)
|
||||
input.Source = strings.TrimSpace(input.Source)
|
||||
input.Status = strings.TrimSpace(input.Status)
|
||||
if input.Code == "" {
|
||||
@@ -145,6 +158,9 @@ func normalizeCatalogProviderInput(input CatalogProviderInput) CatalogProviderIn
|
||||
if input.ProviderType == "" {
|
||||
input.ProviderType = "openai"
|
||||
}
|
||||
if input.DefaultAuthType == "" {
|
||||
input.DefaultAuthType = "APIKey"
|
||||
}
|
||||
if input.Source == "" {
|
||||
input.Source = "gateway"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type GatewayTenantInput struct {
|
||||
TenantKey string `json:"tenantKey"`
|
||||
Source string `json:"source"`
|
||||
ExternalTenantID string `json:"externalTenantId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DefaultUserGroupID string `json:"defaultUserGroupId"`
|
||||
PlanKey string `json:"planKey"`
|
||||
BillingProfile map[string]any `json:"billingProfile"`
|
||||
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
|
||||
AuthPolicy map[string]any `json:"authPolicy"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type GatewayUserInput struct {
|
||||
UserKey string `json:"userKey"`
|
||||
Source string `json:"source"`
|
||||
ExternalUserID string `json:"externalUserId"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
Password string `json:"password"`
|
||||
GatewayTenantID string `json:"gatewayTenantId"`
|
||||
TenantID string `json:"tenantId"`
|
||||
TenantKey string `json:"tenantKey"`
|
||||
DefaultUserGroupID string `json:"defaultUserGroupId"`
|
||||
Roles []string `json:"roles"`
|
||||
AuthProfile map[string]any `json:"authProfile"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type UserGroupInput struct {
|
||||
GroupKey string `json:"groupKey"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Priority int `json:"priority"`
|
||||
RechargeDiscountPolicy map[string]any `json:"rechargeDiscountPolicy"`
|
||||
BillingDiscountPolicy map[string]any `json:"billingDiscountPolicy"`
|
||||
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
|
||||
QuotaPolicy map[string]any `json:"quotaPolicy"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (s *Store) CreateTenant(ctx context.Context, input GatewayTenantInput) (GatewayTenant, error) {
|
||||
input = normalizeTenantInput(input)
|
||||
billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
|
||||
authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanTenant(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO gateway_tenants (
|
||||
tenant_key, source, external_tenant_id, name, description, default_user_group_id, plan_key,
|
||||
billing_profile, rate_limit_policy, auth_policy, metadata, status
|
||||
)
|
||||
VALUES ($1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, '')::uuid, NULLIF($7, ''), $8, $9, $10, $11, $12)
|
||||
RETURNING `+tenantColumns,
|
||||
input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID, input.PlanKey,
|
||||
billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) UpdateTenant(ctx context.Context, id string, input GatewayTenantInput) (GatewayTenant, error) {
|
||||
input = normalizeTenantInput(input)
|
||||
billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
|
||||
authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanTenant(s.pool.QueryRow(ctx, `
|
||||
UPDATE gateway_tenants
|
||||
SET tenant_key = $2,
|
||||
source = $3,
|
||||
external_tenant_id = NULLIF($4, ''),
|
||||
name = $5,
|
||||
description = NULLIF($6, ''),
|
||||
default_user_group_id = NULLIF($7, '')::uuid,
|
||||
plan_key = NULLIF($8, ''),
|
||||
billing_profile = $9,
|
||||
rate_limit_policy = $10,
|
||||
auth_policy = $11,
|
||||
metadata = $12,
|
||||
status = $13,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid AND deleted_at IS NULL
|
||||
RETURNING `+tenantColumns,
|
||||
id, input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID,
|
||||
input.PlanKey, billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) DeleteTenant(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `
|
||||
UPDATE gateway_tenants
|
||||
SET deleted_at = now(),
|
||||
status = 'deleted',
|
||||
tenant_key = tenant_key || ':deleted:' || left(id::text, 8),
|
||||
external_tenant_id = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateGatewayUser(ctx context.Context, input GatewayUserInput) (GatewayUser, error) {
|
||||
input = normalizeUserInput(input)
|
||||
passwordHash := ""
|
||||
if strings.TrimSpace(input.Password) != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return GatewayUser{}, err
|
||||
}
|
||||
passwordHash = string(hash)
|
||||
}
|
||||
roles, _ := json.Marshal(input.Roles)
|
||||
authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanUser(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO gateway_users (
|
||||
user_key, source, external_user_id, username, display_name, email, phone, avatar_url, password_hash,
|
||||
gateway_tenant_id, tenant_id, tenant_key, default_user_group_id, roles, auth_profile, metadata, status
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''), NULLIF($8, ''), NULLIF($9, ''),
|
||||
NULLIF($10, '')::uuid, NULLIF($11, ''), NULLIF($12, ''), NULLIF($13, '')::uuid, $14, $15, $16, $17
|
||||
)
|
||||
RETURNING `+userColumns,
|
||||
input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone, input.AvatarURL, passwordHash,
|
||||
input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID, roles, authProfile, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) UpdateGatewayUser(ctx context.Context, id string, input GatewayUserInput) (GatewayUser, error) {
|
||||
input = normalizeUserInput(input)
|
||||
passwordHash := ""
|
||||
if strings.TrimSpace(input.Password) != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return GatewayUser{}, err
|
||||
}
|
||||
passwordHash = string(hash)
|
||||
}
|
||||
roles, _ := json.Marshal(input.Roles)
|
||||
authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanUser(s.pool.QueryRow(ctx, `
|
||||
UPDATE gateway_users
|
||||
SET user_key = $2,
|
||||
source = $3,
|
||||
external_user_id = NULLIF($4, ''),
|
||||
username = $5,
|
||||
display_name = NULLIF($6, ''),
|
||||
email = NULLIF($7, ''),
|
||||
phone = NULLIF($8, ''),
|
||||
avatar_url = NULLIF($9, ''),
|
||||
password_hash = COALESCE(NULLIF($10, ''), password_hash),
|
||||
gateway_tenant_id = NULLIF($11, '')::uuid,
|
||||
tenant_id = NULLIF($12, ''),
|
||||
tenant_key = NULLIF($13, ''),
|
||||
default_user_group_id = NULLIF($14, '')::uuid,
|
||||
roles = $15,
|
||||
auth_profile = $16,
|
||||
metadata = $17,
|
||||
status = $18,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid AND deleted_at IS NULL
|
||||
RETURNING `+userColumns,
|
||||
id, input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone,
|
||||
input.AvatarURL, passwordHash, input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID,
|
||||
roles, authProfile, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) DeleteGatewayUser(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `
|
||||
UPDATE gateway_users
|
||||
SET deleted_at = now(),
|
||||
status = 'deleted',
|
||||
user_key = user_key || ':deleted:' || left(id::text, 8),
|
||||
external_user_id = NULL,
|
||||
email = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateUserGroup(ctx context.Context, input UserGroupInput) (UserGroup, error) {
|
||||
input = normalizeUserGroupInput(input)
|
||||
rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
|
||||
billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
|
||||
quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanUserGroup(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO gateway_user_groups (
|
||||
group_key, name, description, source, priority, recharge_discount_policy, billing_discount_policy,
|
||||
rate_limit_policy, quota_policy, metadata, status
|
||||
)
|
||||
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING `+userGroupColumns,
|
||||
input.GroupKey, input.Name, input.Description, input.Source, input.Priority, rechargeDiscountPolicy, billingDiscountPolicy,
|
||||
rateLimitPolicy, quotaPolicy, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) UpdateUserGroup(ctx context.Context, id string, input UserGroupInput) (UserGroup, error) {
|
||||
input = normalizeUserGroupInput(input)
|
||||
rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
|
||||
billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
|
||||
quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanUserGroup(s.pool.QueryRow(ctx, `
|
||||
UPDATE gateway_user_groups
|
||||
SET group_key = $2,
|
||||
name = $3,
|
||||
description = NULLIF($4, ''),
|
||||
source = $5,
|
||||
priority = $6,
|
||||
recharge_discount_policy = $7,
|
||||
billing_discount_policy = $8,
|
||||
rate_limit_policy = $9,
|
||||
quota_policy = $10,
|
||||
metadata = $11,
|
||||
status = $12,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid
|
||||
RETURNING `+userGroupColumns,
|
||||
id, input.GroupKey, input.Name, input.Description, input.Source, input.Priority,
|
||||
rechargeDiscountPolicy, billingDiscountPolicy, rateLimitPolicy, quotaPolicy, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) DeleteUserGroup(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM gateway_user_groups WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const tenantColumns = `
|
||||
id::text, tenant_key, source, COALESCE(external_tenant_id, ''), name, COALESCE(description, ''),
|
||||
COALESCE(default_user_group_id::text, ''), COALESCE(plan_key, ''), billing_profile, rate_limit_policy,
|
||||
auth_policy, metadata, status, COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
|
||||
created_at, updated_at`
|
||||
|
||||
const userColumns = `
|
||||
id::text, user_key, source, COALESCE(external_user_id, ''), username,
|
||||
COALESCE(display_name, ''), COALESCE(email, ''), COALESCE(phone, ''), COALESCE(avatar_url, ''),
|
||||
COALESCE(gateway_tenant_id::text, ''), COALESCE(tenant_id, ''), COALESCE(tenant_key, ''),
|
||||
COALESCE(default_user_group_id::text, ''), roles, auth_profile, metadata,
|
||||
status, COALESCE(last_login_at::text, ''), COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
|
||||
created_at, updated_at`
|
||||
|
||||
const userGroupColumns = `
|
||||
id::text, group_key, name, COALESCE(description, ''), source, priority,
|
||||
recharge_discount_policy, billing_discount_policy, rate_limit_policy, quota_policy, metadata,
|
||||
status, created_at, updated_at`
|
||||
|
||||
type scanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanTenant(row scanner) (GatewayTenant, error) {
|
||||
var item GatewayTenant
|
||||
var billingProfile []byte
|
||||
var rateLimitPolicy []byte
|
||||
var authPolicy []byte
|
||||
var metadata []byte
|
||||
if err := row.Scan(
|
||||
&item.ID, &item.TenantKey, &item.Source, &item.ExternalTenantID, &item.Name, &item.Description,
|
||||
&item.DefaultUserGroupID, &item.PlanKey, &billingProfile, &rateLimitPolicy, &authPolicy, &metadata,
|
||||
&item.Status, &item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
|
||||
); err != nil {
|
||||
return GatewayTenant{}, err
|
||||
}
|
||||
item.BillingProfile = decodeObject(billingProfile)
|
||||
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
|
||||
item.AuthPolicy = decodeObject(authPolicy)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func scanUser(row scanner) (GatewayUser, error) {
|
||||
var item GatewayUser
|
||||
var roles []byte
|
||||
var authProfile []byte
|
||||
var metadata []byte
|
||||
if err := row.Scan(
|
||||
&item.ID, &item.UserKey, &item.Source, &item.ExternalUserID, &item.Username, &item.DisplayName,
|
||||
&item.Email, &item.Phone, &item.AvatarURL, &item.GatewayTenantID, &item.TenantID, &item.TenantKey,
|
||||
&item.DefaultUserGroupID, &roles, &authProfile, &metadata, &item.Status, &item.LastLoginAt,
|
||||
&item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
|
||||
); err != nil {
|
||||
return GatewayUser{}, err
|
||||
}
|
||||
item.Roles = decodeStringArray(roles)
|
||||
item.AuthProfile = decodeObject(authProfile)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func scanUserGroup(row scanner) (UserGroup, error) {
|
||||
var item UserGroup
|
||||
var rechargeDiscountPolicy []byte
|
||||
var billingDiscountPolicy []byte
|
||||
var rateLimitPolicy []byte
|
||||
var quotaPolicy []byte
|
||||
var metadata []byte
|
||||
if err := row.Scan(
|
||||
&item.ID, &item.GroupKey, &item.Name, &item.Description, &item.Source, &item.Priority,
|
||||
&rechargeDiscountPolicy, &billingDiscountPolicy, &rateLimitPolicy, "aPolicy, &metadata,
|
||||
&item.Status, &item.CreatedAt, &item.UpdatedAt,
|
||||
); err != nil {
|
||||
return UserGroup{}, err
|
||||
}
|
||||
item.RechargeDiscountPolicy = decodeObject(rechargeDiscountPolicy)
|
||||
item.BillingDiscountPolicy = decodeObject(billingDiscountPolicy)
|
||||
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
|
||||
item.QuotaPolicy = decodeObject(quotaPolicy)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func normalizeTenantInput(input GatewayTenantInput) GatewayTenantInput {
|
||||
input.TenantKey = strings.TrimSpace(input.TenantKey)
|
||||
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
|
||||
return input
|
||||
}
|
||||
|
||||
func normalizeUserInput(input GatewayUserInput) GatewayUserInput {
|
||||
input.Username = strings.TrimSpace(input.Username)
|
||||
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
|
||||
input.UserKey = firstNonEmpty(strings.TrimSpace(input.UserKey), input.Source+":"+input.Username)
|
||||
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
|
||||
input.Roles = normalizeGatewayRoles(input.Roles)
|
||||
return input
|
||||
}
|
||||
|
||||
func normalizeGatewayRoles(roles []string) []string {
|
||||
for _, role := range roles {
|
||||
switch strings.TrimSpace(role) {
|
||||
case "manager":
|
||||
return []string{"manager"}
|
||||
case "admin":
|
||||
return []string{"admin"}
|
||||
case "operator":
|
||||
return []string{"operator"}
|
||||
case "creator":
|
||||
return []string{"creator"}
|
||||
case "user":
|
||||
return []string{"user"}
|
||||
}
|
||||
}
|
||||
return []string{"user"}
|
||||
}
|
||||
|
||||
func normalizeUserGroupInput(input UserGroupInput) UserGroupInput {
|
||||
input.GroupKey = strings.TrimSpace(input.GroupKey)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
|
||||
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
|
||||
if input.Priority == 0 {
|
||||
input.Priority = 100
|
||||
}
|
||||
return input
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type platformModelQuerier interface {
|
||||
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
||||
}
|
||||
|
||||
type modelCatalogSnapshot struct {
|
||||
ID string
|
||||
ProviderKey string
|
||||
@@ -18,10 +22,50 @@ type modelCatalogSnapshot struct {
|
||||
Capabilities map[string]any
|
||||
BaseBillingConfig map[string]any
|
||||
DefaultRateLimitPolicy map[string]any
|
||||
RuntimePolicySetID string
|
||||
RuntimePolicyOverride map[string]any
|
||||
}
|
||||
|
||||
func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformModelInput) (PlatformModel, error) {
|
||||
base, err := s.lookupBaseModel(ctx, input.BaseModelID, input.CanonicalModelKey, input.ModelName)
|
||||
return s.createPlatformModel(ctx, s.pool, input)
|
||||
}
|
||||
|
||||
func (s *Store) ReplacePlatformModels(ctx context.Context, platformID string, inputs []CreatePlatformModelInput) ([]PlatformModel, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
keptIDs := make([]string, 0, len(inputs))
|
||||
for _, input := range inputs {
|
||||
input.PlatformID = platformID
|
||||
model, err := s.createPlatformModel(ctx, tx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keptIDs = append(keptIDs, model.ID)
|
||||
}
|
||||
|
||||
if len(keptIDs) == 0 {
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM platform_models WHERE platform_id = $1::uuid`, platformID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM platform_models
|
||||
WHERE platform_id = $1::uuid
|
||||
AND NOT (id::text = ANY($2::text[]))`, platformID, keptIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ListPlatformModels(ctx, platformID)
|
||||
}
|
||||
|
||||
func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier, input CreatePlatformModelInput) (PlatformModel, error) {
|
||||
base, err := s.lookupBaseModel(ctx, q, input.BaseModelID, input.CanonicalModelKey, input.ModelName)
|
||||
if err != nil && !IsNotFound(err) {
|
||||
return PlatformModel{}, err
|
||||
}
|
||||
@@ -34,6 +78,7 @@ func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformMod
|
||||
if input.DisplayName == "" {
|
||||
input.DisplayName = firstNonEmpty(base.DisplayName, input.ModelName)
|
||||
}
|
||||
input.ModelAlias = normalizePlatformModelAlias(input.ModelAlias, base)
|
||||
if input.PricingMode == "" {
|
||||
input.PricingMode = "inherit_discount"
|
||||
}
|
||||
@@ -49,6 +94,14 @@ func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformMod
|
||||
if len(rateLimitPolicy) == 0 {
|
||||
rateLimitPolicy = base.DefaultRateLimitPolicy
|
||||
}
|
||||
runtimePolicySetID := strings.TrimSpace(input.RuntimePolicySetID)
|
||||
if runtimePolicySetID == "" {
|
||||
runtimePolicySetID = base.RuntimePolicySetID
|
||||
}
|
||||
runtimePolicyOverride := input.RuntimePolicyOverride
|
||||
if len(runtimePolicyOverride) == 0 {
|
||||
runtimePolicyOverride = base.RuntimePolicyOverride
|
||||
}
|
||||
|
||||
capabilityOverrideJSON, _ := json.Marshal(emptyObjectIfNil(input.CapabilityOverride))
|
||||
capabilitiesJSON, _ := json.Marshal(emptyObjectIfNil(capabilities))
|
||||
@@ -57,6 +110,7 @@ func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformMod
|
||||
permissionJSON, _ := json.Marshal(emptyObjectIfNil(input.PermissionConfig))
|
||||
retryJSON, _ := json.Marshal(emptyObjectIfNil(input.RetryPolicy))
|
||||
rateLimitJSON, _ := json.Marshal(emptyObjectIfNil(rateLimitPolicy))
|
||||
runtimePolicyOverrideJSON, _ := json.Marshal(emptyObjectIfNil(runtimePolicyOverride))
|
||||
|
||||
discount := any(nil)
|
||||
if input.DiscountFactor > 0 {
|
||||
@@ -72,16 +126,22 @@ func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformMod
|
||||
var capabilitiesBytes []byte
|
||||
var billingOverrideBytes []byte
|
||||
var billingBytes []byte
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
var permissionBytes []byte
|
||||
var retryPolicyBytes []byte
|
||||
var rateLimitPolicyBytes []byte
|
||||
var runtimePolicyOverrideBytes []byte
|
||||
err = q.QueryRow(ctx, `
|
||||
INSERT INTO platform_models (
|
||||
platform_id, base_model_id, model_name, model_alias, model_type, display_name,
|
||||
capability_override, capabilities, pricing_mode, discount_factor,
|
||||
pricing_rule_set_id, billing_config_override, billing_config, permission_config, retry_policy, rate_limit_policy, enabled
|
||||
pricing_rule_set_id, billing_config_override, billing_config, permission_config, retry_policy, rate_limit_policy,
|
||||
runtime_policy_set_id, runtime_policy_override, enabled
|
||||
)
|
||||
VALUES (
|
||||
$1::uuid, $2::uuid, $3, NULLIF($4, ''), $5, $6,
|
||||
$7::jsonb, $8::jsonb, $9, $10::numeric,
|
||||
NULLIF($11, '')::uuid, $12::jsonb, $13::jsonb, $14::jsonb, $15::jsonb, $16::jsonb, true
|
||||
NULLIF($11, '')::uuid, $12::jsonb, $13::jsonb, $14::jsonb, $15::jsonb, $16::jsonb,
|
||||
NULLIF($17, '')::uuid, $18::jsonb, true
|
||||
)
|
||||
ON CONFLICT (platform_id, model_name, model_type) DO UPDATE
|
||||
SET base_model_id = EXCLUDED.base_model_id,
|
||||
@@ -97,12 +157,16 @@ SET base_model_id = EXCLUDED.base_model_id,
|
||||
permission_config = EXCLUDED.permission_config,
|
||||
retry_policy = EXCLUDED.retry_policy,
|
||||
rate_limit_policy = EXCLUDED.rate_limit_policy,
|
||||
runtime_policy_set_id = EXCLUDED.runtime_policy_set_id,
|
||||
runtime_policy_override = EXCLUDED.runtime_policy_override,
|
||||
enabled = true,
|
||||
updated_at = now()
|
||||
RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_name,
|
||||
COALESCE(model_alias, ''), model_type, display_name, capability_override,
|
||||
capabilities, pricing_mode, COALESCE(discount_factor, 0)::float8,
|
||||
COALESCE(pricing_rule_set_id::text, ''), billing_config_override, billing_config, enabled, created_at, updated_at`,
|
||||
COALESCE(pricing_rule_set_id::text, ''), billing_config_override, billing_config,
|
||||
permission_config, retry_policy, rate_limit_policy, COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override,
|
||||
enabled, created_at, updated_at`,
|
||||
input.PlatformID,
|
||||
baseID,
|
||||
input.ModelName,
|
||||
@@ -119,6 +183,8 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
string(permissionJSON),
|
||||
string(retryJSON),
|
||||
string(rateLimitJSON),
|
||||
runtimePolicySetID,
|
||||
string(runtimePolicyOverrideJSON),
|
||||
).Scan(
|
||||
&model.ID,
|
||||
&model.PlatformID,
|
||||
@@ -134,6 +200,11 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
&model.PricingRuleSetID,
|
||||
&billingOverrideBytes,
|
||||
&billingBytes,
|
||||
&permissionBytes,
|
||||
&retryPolicyBytes,
|
||||
&rateLimitPolicyBytes,
|
||||
&model.RuntimePolicySetID,
|
||||
&runtimePolicyOverrideBytes,
|
||||
&model.Enabled,
|
||||
&model.CreatedAt,
|
||||
&model.UpdatedAt,
|
||||
@@ -145,17 +216,34 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
model.Capabilities = decodeObject(capabilitiesBytes)
|
||||
model.BillingConfigOverride = decodeObject(billingOverrideBytes)
|
||||
model.BillingConfig = decodeObject(billingBytes)
|
||||
model.PermissionConfig = decodeObject(permissionBytes)
|
||||
model.RetryPolicy = decodeObject(retryPolicyBytes)
|
||||
model.RateLimitPolicy = decodeObject(rateLimitPolicyBytes)
|
||||
model.RuntimePolicyOverride = decodeObject(runtimePolicyOverrideBytes)
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func (s *Store) lookupBaseModel(ctx context.Context, id string, canonicalKey string, modelName string) (modelCatalogSnapshot, error) {
|
||||
func (s *Store) DeletePlatformModel(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM platform_models WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) lookupBaseModel(ctx context.Context, q platformModelQuerier, id string, canonicalKey string, modelName string) (modelCatalogSnapshot, error) {
|
||||
var item modelCatalogSnapshot
|
||||
var capabilities []byte
|
||||
var billingConfig []byte
|
||||
var rateLimitPolicy []byte
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
var runtimePolicyOverride []byte
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
|
||||
capabilities, base_billing_config, default_rate_limit_policy
|
||||
capabilities, base_billing_config, default_rate_limit_policy,
|
||||
COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override
|
||||
FROM base_model_catalog
|
||||
WHERE ($1 <> '' AND id = NULLIF($1, '')::uuid)
|
||||
OR ($2 <> '' AND canonical_model_key = $2)
|
||||
@@ -171,6 +259,8 @@ LIMIT 1`, strings.TrimSpace(id), strings.TrimSpace(canonicalKey), strings.TrimSp
|
||||
&capabilities,
|
||||
&billingConfig,
|
||||
&rateLimitPolicy,
|
||||
&item.RuntimePolicySetID,
|
||||
&runtimePolicyOverride,
|
||||
)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
@@ -181,9 +271,31 @@ LIMIT 1`, strings.TrimSpace(id), strings.TrimSpace(canonicalKey), strings.TrimSp
|
||||
item.Capabilities = decodeObject(capabilities)
|
||||
item.BaseBillingConfig = decodeObject(billingConfig)
|
||||
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
|
||||
item.RuntimePolicyOverride = decodeObject(runtimePolicyOverride)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func normalizePlatformModelAlias(alias string, base modelCatalogSnapshot) string {
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" {
|
||||
alias = firstNonEmpty(base.ProviderModelName, base.DisplayName, base.CanonicalModelKey)
|
||||
}
|
||||
if base.ProviderKey != "" {
|
||||
alias = strings.TrimPrefix(alias, base.ProviderKey+":")
|
||||
}
|
||||
if alias == base.CanonicalModelKey {
|
||||
alias = stripAliasPrefix(alias)
|
||||
}
|
||||
return strings.TrimSpace(alias)
|
||||
}
|
||||
|
||||
func stripAliasPrefix(alias string) string {
|
||||
if before, after, ok := strings.Cut(alias, ":"); ok && before != "" && after != "" {
|
||||
return after
|
||||
}
|
||||
return alias
|
||||
}
|
||||
|
||||
func mergeObjects(base map[string]any, override map[string]any) map[string]any {
|
||||
out := map[string]any{}
|
||||
for key, value := range base {
|
||||
|
||||
@@ -159,6 +159,13 @@ RETURNING `+pricingRuleSetColumns,
|
||||
}
|
||||
|
||||
func (s *Store) DeletePricingRuleSet(ctx context.Context, id string) error {
|
||||
var ruleSetKey string
|
||||
if err := s.pool.QueryRow(ctx, `SELECT rule_set_key FROM model_pricing_rule_sets WHERE id = $1::uuid`, id).Scan(&ruleSetKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if ruleSetKey == "default-multimodal-v1" {
|
||||
return ErrProtectedDefault
|
||||
}
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM model_pricing_rule_sets WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const runtimePolicyColumns = `
|
||||
id::text, policy_key, name, COALESCE(description, ''), rate_limit_policy, retry_policy,
|
||||
auto_disable_policy, degrade_policy, metadata, status, created_at, updated_at`
|
||||
|
||||
type RuntimePolicySetInput struct {
|
||||
PolicyKey string `json:"policyKey"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
|
||||
RetryPolicy map[string]any `json:"retryPolicy"`
|
||||
AutoDisablePolicy map[string]any `json:"autoDisablePolicy"`
|
||||
DegradePolicy map[string]any `json:"degradePolicy"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type runtimePolicyScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func (s *Store) ListRuntimePolicySets(ctx context.Context) ([]RuntimePolicySet, error) {
|
||||
rows, err := s.pool.Query(ctx, `SELECT `+runtimePolicyColumns+` FROM model_runtime_policy_sets ORDER BY policy_key ASC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]RuntimePolicySet, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanRuntimePolicySet(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) CreateRuntimePolicySet(ctx context.Context, input RuntimePolicySetInput) (RuntimePolicySet, error) {
|
||||
input = normalizeRuntimePolicyInput(input)
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
|
||||
retryPolicy, _ := json.Marshal(emptyObjectIfNil(input.RetryPolicy))
|
||||
autoDisablePolicy, _ := json.Marshal(emptyObjectIfNil(input.AutoDisablePolicy))
|
||||
degradePolicy, _ := json.Marshal(emptyObjectIfNil(input.DegradePolicy))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanRuntimePolicySet(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO model_runtime_policy_sets (
|
||||
policy_key, name, description, rate_limit_policy, retry_policy, auto_disable_policy, degrade_policy, metadata, status
|
||||
)
|
||||
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7, $8, $9)
|
||||
RETURNING `+runtimePolicyColumns,
|
||||
input.PolicyKey, input.Name, input.Description, rateLimitPolicy, retryPolicy,
|
||||
autoDisablePolicy, degradePolicy, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) UpdateRuntimePolicySet(ctx context.Context, id string, input RuntimePolicySetInput) (RuntimePolicySet, error) {
|
||||
input = normalizeRuntimePolicyInput(input)
|
||||
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
|
||||
retryPolicy, _ := json.Marshal(emptyObjectIfNil(input.RetryPolicy))
|
||||
autoDisablePolicy, _ := json.Marshal(emptyObjectIfNil(input.AutoDisablePolicy))
|
||||
degradePolicy, _ := json.Marshal(emptyObjectIfNil(input.DegradePolicy))
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanRuntimePolicySet(s.pool.QueryRow(ctx, `
|
||||
UPDATE model_runtime_policy_sets
|
||||
SET policy_key = $2,
|
||||
name = $3,
|
||||
description = NULLIF($4, ''),
|
||||
rate_limit_policy = $5,
|
||||
retry_policy = $6,
|
||||
auto_disable_policy = $7,
|
||||
degrade_policy = $8,
|
||||
metadata = $9,
|
||||
status = $10,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid
|
||||
RETURNING `+runtimePolicyColumns,
|
||||
id, input.PolicyKey, input.Name, input.Description, rateLimitPolicy, retryPolicy,
|
||||
autoDisablePolicy, degradePolicy, metadata, input.Status,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) DeleteRuntimePolicySet(ctx context.Context, id string) error {
|
||||
var policyKey string
|
||||
if err := s.pool.QueryRow(ctx, `SELECT policy_key FROM model_runtime_policy_sets WHERE id = $1::uuid`, id).Scan(&policyKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if policyKey == "default-runtime-v1" {
|
||||
return ErrProtectedDefault
|
||||
}
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM model_runtime_policy_sets WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanRuntimePolicySet(scanner runtimePolicyScanner) (RuntimePolicySet, error) {
|
||||
var item RuntimePolicySet
|
||||
var rateLimitPolicy []byte
|
||||
var retryPolicy []byte
|
||||
var autoDisablePolicy []byte
|
||||
var degradePolicy []byte
|
||||
var metadata []byte
|
||||
if err := scanner.Scan(
|
||||
&item.ID,
|
||||
&item.PolicyKey,
|
||||
&item.Name,
|
||||
&item.Description,
|
||||
&rateLimitPolicy,
|
||||
&retryPolicy,
|
||||
&autoDisablePolicy,
|
||||
°radePolicy,
|
||||
&metadata,
|
||||
&item.Status,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return RuntimePolicySet{}, err
|
||||
}
|
||||
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
|
||||
item.RetryPolicy = decodeObject(retryPolicy)
|
||||
item.AutoDisablePolicy = decodeObject(autoDisablePolicy)
|
||||
item.DegradePolicy = decodeObject(degradePolicy)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func normalizeRuntimePolicyInput(input RuntimePolicySetInput) RuntimePolicySetInput {
|
||||
input.PolicyKey = strings.TrimSpace(input.PolicyKey)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Description = strings.TrimSpace(input.Description)
|
||||
input.Status = strings.TrimSpace(input.Status)
|
||||
if input.Status == "" {
|
||||
input.Status = "active"
|
||||
}
|
||||
return input
|
||||
}
|
||||
Reference in New Issue
Block a user