1447 lines
42 KiB
Go
1447 lines
42 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||
)
|
||
|
||
type ModelCatalogResponse struct {
|
||
Items []ModelCatalogItem `json:"items"`
|
||
Filters ModelCatalogFilters `json:"filters"`
|
||
Summary ModelCatalogSummary `json:"summary"`
|
||
}
|
||
|
||
type ModelCatalogSummary struct {
|
||
ModelCount int `json:"modelCount"`
|
||
SourceCount int `json:"sourceCount"`
|
||
}
|
||
|
||
type ModelCatalogFilters struct {
|
||
Capabilities []ModelCatalogFilterOption `json:"capabilities"`
|
||
Providers []ModelCatalogFilterOption `json:"providers"`
|
||
}
|
||
|
||
type ModelCatalogFilterOption struct {
|
||
Value string `json:"value"`
|
||
Label string `json:"label"`
|
||
Count int `json:"count"`
|
||
IconPath string `json:"iconPath,omitempty"`
|
||
}
|
||
|
||
type ModelCatalogItem struct {
|
||
ID string `json:"id"`
|
||
Alias string `json:"alias"`
|
||
DisplayName string `json:"displayName"`
|
||
ModelName string `json:"modelName"`
|
||
Description string `json:"description,omitempty"`
|
||
IconPath string `json:"iconPath,omitempty"`
|
||
ModelType []string `json:"modelType"`
|
||
CapabilityTags []string `json:"capabilityTags"`
|
||
ProviderKeys []string `json:"providerKeys"`
|
||
Providers []ModelCatalogProviderSummary `json:"providers"`
|
||
SourceCount int `json:"sourceCount"`
|
||
Source ModelCatalogText `json:"source"`
|
||
Discount ModelCatalogText `json:"discount"`
|
||
RateLimits ModelCatalogRateLimits `json:"rateLimits"`
|
||
Permission ModelCatalogPermission `json:"permission"`
|
||
Pricing ModelCatalogPricing `json:"pricing"`
|
||
Enabled bool `json:"enabled"`
|
||
StatusLabel string `json:"statusLabel,omitempty"`
|
||
Sources []ModelCatalogSource `json:"sources,omitempty"`
|
||
}
|
||
|
||
type ModelCatalogProviderSummary struct {
|
||
Key string `json:"key"`
|
||
Name string `json:"name"`
|
||
IconPath string `json:"iconPath,omitempty"`
|
||
SourceCount int `json:"sourceCount"`
|
||
}
|
||
|
||
type ModelCatalogSource struct {
|
||
ID string `json:"id"`
|
||
PlatformID string `json:"platformId,omitempty"`
|
||
PlatformName string `json:"platformName,omitempty"`
|
||
ProviderKey string `json:"providerKey"`
|
||
ProviderName string `json:"providerName"`
|
||
ModelName string `json:"modelName"`
|
||
ModelAlias string `json:"modelAlias,omitempty"`
|
||
DisplayName string `json:"displayName"`
|
||
ModelType []string `json:"modelType"`
|
||
Enabled bool `json:"enabled"`
|
||
RateLimits ModelCatalogRateLimits `json:"rateLimits"`
|
||
}
|
||
|
||
type ModelCatalogText struct {
|
||
Label string `json:"label"`
|
||
Title string `json:"title,omitempty"`
|
||
}
|
||
|
||
type ModelCatalogPermission struct {
|
||
Label string `json:"label"`
|
||
Title string `json:"title,omitempty"`
|
||
AllowGroups []string `json:"allowGroups,omitempty"`
|
||
DenyGroups []string `json:"denyGroups,omitempty"`
|
||
}
|
||
|
||
type ModelCatalogPricing struct {
|
||
Lines []string `json:"lines"`
|
||
Title string `json:"title,omitempty"`
|
||
}
|
||
|
||
type ModelCatalogRateLimits struct {
|
||
RPM *float64 `json:"rpm,omitempty"`
|
||
TPM *float64 `json:"tpm,omitempty"`
|
||
Concurrent *float64 `json:"concurrent,omitempty"`
|
||
Label string `json:"label"`
|
||
Title string `json:"title,omitempty"`
|
||
}
|
||
|
||
type catalogSource struct {
|
||
model store.PlatformModel
|
||
platform store.Platform
|
||
provider store.CatalogProvider
|
||
baseModel store.BaseModel
|
||
providerKey string
|
||
providerName string
|
||
description string
|
||
iconPath string
|
||
rateLimits ModelCatalogRateLimits
|
||
}
|
||
|
||
type catalogGroup struct {
|
||
key string
|
||
alias string
|
||
displayName string
|
||
modelName string
|
||
modelType []string
|
||
capabilities map[string]any
|
||
sources []catalogSource
|
||
enabled bool
|
||
}
|
||
|
||
func (s *Server) listModelCatalog(w http.ResponseWriter, r *http.Request) {
|
||
ctx := r.Context()
|
||
models, err := s.store.ListModels(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog models failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
models = s.platformModelResponses(ctx, models)
|
||
|
||
platforms, err := s.store.ListPlatforms(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog platforms failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
providers, err := s.store.ListCatalogProviders(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog providers failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
runtimePolicies, err := s.store.ListRuntimePolicySets(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog runtime policies failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
accessRules, err := s.store.ListAccessRules(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog access rules failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
userGroups, err := s.store.ListUserGroups(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog user groups failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
baseModels, err := s.store.ListBaseModels(ctx)
|
||
if err != nil {
|
||
s.logger.Error("list model catalog base models failed", "error", err)
|
||
writeError(w, http.StatusInternalServerError, "list model catalog failed")
|
||
return
|
||
}
|
||
|
||
writeJSON(w, http.StatusOK, buildModelCatalog(models, platforms, providers, runtimePolicies, accessRules, userGroups, baseModels))
|
||
}
|
||
|
||
func buildModelCatalog(
|
||
models []store.PlatformModel,
|
||
platforms []store.Platform,
|
||
providers []store.CatalogProvider,
|
||
runtimePolicies []store.RuntimePolicySet,
|
||
accessRules []store.AccessRule,
|
||
userGroups []store.UserGroup,
|
||
baseModels []store.BaseModel,
|
||
) ModelCatalogResponse {
|
||
platformMap := mapPlatforms(platforms)
|
||
providerMap := mapProviders(providers)
|
||
runtimePolicyMap := mapRuntimePolicies(runtimePolicies)
|
||
accessRuleGroups := mapAccessRuleGroups(accessRules, mapUserGroups(userGroups))
|
||
baseModelMap := mapBaseModels(baseModels)
|
||
|
||
groups := map[string]*catalogGroup{}
|
||
sourceCount := 0
|
||
for _, model := range models {
|
||
platform := platformMap[model.PlatformID]
|
||
baseModel := baseModelMap[model.BaseModelID]
|
||
providerKey := modelProviderKey(model, baseModel, platform)
|
||
provider := providerMap[providerKey]
|
||
providerName := firstNonEmpty(
|
||
provider.DisplayName,
|
||
stringValue(baseModel.Metadata["sourceProviderName"]),
|
||
baseModel.ProviderKey,
|
||
model.Provider,
|
||
platform.Provider,
|
||
platform.Name,
|
||
model.PlatformName,
|
||
providerKey,
|
||
)
|
||
description := modelDescription(model, baseModel)
|
||
iconPath := modelIconPath(model, baseModel, provider)
|
||
source := catalogSource{
|
||
model: model,
|
||
platform: platform,
|
||
provider: provider,
|
||
baseModel: baseModel,
|
||
providerKey: providerKey,
|
||
providerName: providerName,
|
||
description: description,
|
||
iconPath: iconPath,
|
||
rateLimits: effectiveModelRateLimits(model, platform, runtimePolicyMap),
|
||
}
|
||
|
||
key := catalogAliasKey(model)
|
||
current := groups[key]
|
||
if current == nil {
|
||
current = &catalogGroup{
|
||
key: key,
|
||
alias: firstNonEmpty(model.ModelAlias, model.DisplayName, model.ModelName),
|
||
displayName: firstNonEmpty(model.ModelAlias, model.DisplayName, model.ModelName),
|
||
modelName: model.ModelName,
|
||
modelType: cloneStringSlice(model.ModelType),
|
||
capabilities: cloneObject(model.Capabilities),
|
||
}
|
||
groups[key] = current
|
||
} else {
|
||
current.modelType = uniqueStrings(append(current.modelType, model.ModelType...))
|
||
current.capabilities = mergeCatalogObjects(current.capabilities, model.Capabilities)
|
||
}
|
||
sourceCount++
|
||
current.sources = append(current.sources, source)
|
||
current.enabled = current.enabled || (model.Enabled && platform.Status == "enabled")
|
||
}
|
||
|
||
items := make([]ModelCatalogItem, 0, len(groups))
|
||
for _, group := range groups {
|
||
items = append(items, modelCatalogItem(group, accessRuleGroups))
|
||
}
|
||
sort.Slice(items, func(i, j int) bool {
|
||
return strings.ToLower(items[i].DisplayName) < strings.ToLower(items[j].DisplayName)
|
||
})
|
||
|
||
return ModelCatalogResponse{
|
||
Items: items,
|
||
Filters: ModelCatalogFilters{
|
||
Capabilities: modelCatalogCapabilityFilters(items),
|
||
Providers: modelCatalogProviderFilters(items),
|
||
},
|
||
Summary: ModelCatalogSummary{
|
||
ModelCount: len(items),
|
||
SourceCount: sourceCount,
|
||
},
|
||
}
|
||
}
|
||
|
||
func modelCatalogItem(group *catalogGroup, accessRuleGroups map[string]accessRuleGroup) ModelCatalogItem {
|
||
sources := sortedCatalogSources(group.sources)
|
||
primary := sources[0]
|
||
providers := modelCatalogProviders(sources)
|
||
description := primary.description
|
||
if description == "" {
|
||
for _, source := range sources {
|
||
if source.description != "" {
|
||
description = source.description
|
||
break
|
||
}
|
||
}
|
||
}
|
||
iconPath := primary.iconPath
|
||
if iconPath == "" {
|
||
for _, source := range sources {
|
||
if source.iconPath != "" {
|
||
iconPath = source.iconPath
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
itemSources := make([]ModelCatalogSource, 0, len(sources))
|
||
for _, source := range sources {
|
||
itemSources = append(itemSources, ModelCatalogSource{
|
||
ID: source.model.ID,
|
||
PlatformID: source.model.PlatformID,
|
||
PlatformName: firstNonEmpty(source.platform.Name, source.model.PlatformName),
|
||
ProviderKey: source.providerKey,
|
||
ProviderName: source.providerName,
|
||
ModelName: source.model.ModelName,
|
||
ModelAlias: source.model.ModelAlias,
|
||
DisplayName: firstNonEmpty(source.model.DisplayName, source.model.ModelAlias, source.model.ModelName),
|
||
ModelType: cloneStringSlice(source.model.ModelType),
|
||
Enabled: source.model.Enabled && source.platform.Status == "enabled",
|
||
RateLimits: source.rateLimits,
|
||
})
|
||
}
|
||
|
||
return ModelCatalogItem{
|
||
ID: group.key,
|
||
Alias: group.alias,
|
||
DisplayName: group.displayName,
|
||
ModelName: group.modelName,
|
||
Description: description,
|
||
IconPath: iconPath,
|
||
ModelType: cloneStringSlice(group.modelType),
|
||
CapabilityTags: capabilityTagsForGroup(group.modelType, group.capabilities),
|
||
ProviderKeys: modelCatalogProviderKeys(providers),
|
||
Providers: providers,
|
||
SourceCount: len(sources),
|
||
Source: sourceText(sources),
|
||
Discount: discountText(sources),
|
||
RateLimits: aggregateRateLimits(sources),
|
||
Permission: permissionText(sources, accessRuleGroups),
|
||
Pricing: pricingText(sources),
|
||
Enabled: group.enabled,
|
||
StatusLabel: statusText(sources),
|
||
Sources: itemSources,
|
||
}
|
||
}
|
||
|
||
func sortedCatalogSources(sources []catalogSource) []catalogSource {
|
||
out := append([]catalogSource(nil), sources...)
|
||
sort.SliceStable(out, func(i, j int) bool {
|
||
leftEnabled := out[i].model.Enabled && out[i].platform.Status == "enabled"
|
||
rightEnabled := out[j].model.Enabled && out[j].platform.Status == "enabled"
|
||
if leftEnabled != rightEnabled {
|
||
return leftEnabled
|
||
}
|
||
leftPriority := out[i].platform.Priority
|
||
rightPriority := out[j].platform.Priority
|
||
if leftPriority == 0 {
|
||
leftPriority = 9999
|
||
}
|
||
if rightPriority == 0 {
|
||
rightPriority = 9999
|
||
}
|
||
if leftPriority != rightPriority {
|
||
return leftPriority < rightPriority
|
||
}
|
||
return out[i].model.ModelName < out[j].model.ModelName
|
||
})
|
||
return out
|
||
}
|
||
|
||
func modelCatalogProviders(sources []catalogSource) []ModelCatalogProviderSummary {
|
||
byKey := map[string]*ModelCatalogProviderSummary{}
|
||
for _, source := range sources {
|
||
key := source.providerKey
|
||
if key == "" {
|
||
key = "unknown"
|
||
}
|
||
current := byKey[key]
|
||
if current == nil {
|
||
current = &ModelCatalogProviderSummary{
|
||
Key: key,
|
||
Name: source.providerName,
|
||
IconPath: source.provider.IconPath,
|
||
}
|
||
byKey[key] = current
|
||
}
|
||
current.SourceCount++
|
||
}
|
||
items := make([]ModelCatalogProviderSummary, 0, len(byKey))
|
||
for _, provider := range byKey {
|
||
items = append(items, *provider)
|
||
}
|
||
sort.Slice(items, func(i, j int) bool {
|
||
return items[i].Name < items[j].Name
|
||
})
|
||
return items
|
||
}
|
||
|
||
func modelCatalogProviderKeys(providers []ModelCatalogProviderSummary) []string {
|
||
keys := make([]string, 0, len(providers))
|
||
for _, provider := range providers {
|
||
keys = append(keys, provider.Key)
|
||
}
|
||
return keys
|
||
}
|
||
|
||
func sourceText(sources []catalogSource) ModelCatalogText {
|
||
label := formatInt(len(sources)) + " 个源"
|
||
return ModelCatalogText{
|
||
Label: label,
|
||
Title: label,
|
||
}
|
||
}
|
||
|
||
func sourceTitles(sources []catalogSource) []string {
|
||
titles := make([]string, 0, len(sources))
|
||
for _, source := range sources {
|
||
titles = append(titles, firstNonEmpty(source.platform.Name, source.providerName, source.providerKey)+": "+source.model.ModelName)
|
||
}
|
||
return titles
|
||
}
|
||
|
||
func discountText(sources []catalogSource) ModelCatalogText {
|
||
values := make([]float64, 0, len(sources))
|
||
titles := make([]string, 0, len(sources))
|
||
for _, source := range sources {
|
||
discount := effectiveDiscount(source.model, source.platform)
|
||
value := discountLabel(discount)
|
||
values = append(values, discount)
|
||
titles = append(titles, firstNonEmpty(source.platform.Name, source.model.ModelName)+": "+discountTitle(value, discount))
|
||
}
|
||
return ModelCatalogText{
|
||
Label: discountRangeLabel(values),
|
||
Title: strings.Join(titles, ";"),
|
||
}
|
||
}
|
||
|
||
func effectiveDiscount(model store.PlatformModel, platform store.Platform) float64 {
|
||
if model.DiscountFactor > 0 {
|
||
return model.DiscountFactor
|
||
}
|
||
if model.PricingMode == "inherit_discount" && platform.DefaultDiscountFactor > 0 {
|
||
return platform.DefaultDiscountFactor
|
||
}
|
||
return 1
|
||
}
|
||
|
||
func discountLabel(discount float64) string {
|
||
if absFloat(discount-1) < 0.000001 {
|
||
return "无折扣"
|
||
}
|
||
return formatPercent(discount)
|
||
}
|
||
|
||
func discountRangeLabel(values []float64) string {
|
||
discounts := uniqueDiscountValues(values)
|
||
if len(discounts) == 0 {
|
||
return "无折扣"
|
||
}
|
||
if len(discounts) == 1 {
|
||
return discountLabel(discounts[0])
|
||
}
|
||
return discountLabel(discounts[0]) + " - " + discountLabel(discounts[len(discounts)-1])
|
||
}
|
||
|
||
func uniqueDiscountValues(values []float64) []float64 {
|
||
if len(values) == 0 {
|
||
return nil
|
||
}
|
||
out := append([]float64(nil), values...)
|
||
sort.Float64s(out)
|
||
unique := out[:0]
|
||
for _, value := range out {
|
||
if len(unique) == 0 || absFloat(value-unique[len(unique)-1]) >= 0.000001 {
|
||
unique = append(unique, value)
|
||
}
|
||
}
|
||
return unique
|
||
}
|
||
|
||
func discountTitle(label string, discount float64) string {
|
||
if label == "无折扣" {
|
||
return "无折扣(100%)"
|
||
}
|
||
return formatPercent(discount)
|
||
}
|
||
|
||
func effectiveModelRateLimits(model store.PlatformModel, platform store.Platform, runtimePolicyMap map[string]store.RuntimePolicySet) ModelCatalogRateLimits {
|
||
overridePolicy := objectValue(model.RuntimePolicyOverride["rateLimitPolicy"])
|
||
runtimePolicy := map[string]any(nil)
|
||
if model.RuntimePolicySetID != "" {
|
||
runtimePolicy = runtimePolicyMap[model.RuntimePolicySetID].RateLimitPolicy
|
||
}
|
||
policies := []map[string]any{
|
||
overridePolicy,
|
||
model.RateLimitPolicy,
|
||
runtimePolicy,
|
||
platform.RateLimitPolicy,
|
||
}
|
||
limits := ModelCatalogRateLimits{
|
||
RPM: firstRateLimit(policies, "rpm"),
|
||
TPM: firstRateLimit(policies, "tpm_total"),
|
||
Concurrent: firstRateLimit(policies, "concurrent"),
|
||
}
|
||
limits.Label = rateLimitLabel(limits)
|
||
return limits
|
||
}
|
||
|
||
func firstRateLimit(policies []map[string]any, metric string) *float64 {
|
||
for _, policy := range policies {
|
||
if value := readRateLimit(policy, metric); value != nil {
|
||
return value
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func readRateLimit(policy map[string]any, metric string) *float64 {
|
||
if len(policy) == 0 {
|
||
return nil
|
||
}
|
||
if rules, ok := policy["rules"].([]any); ok {
|
||
for _, item := range rules {
|
||
rule := objectValue(item)
|
||
if stringValue(rule["metric"]) != metric {
|
||
continue
|
||
}
|
||
if limit, ok := numberValue(rule["limit"]); ok {
|
||
return &limit
|
||
}
|
||
}
|
||
}
|
||
for _, key := range rateLimitKeys(metric) {
|
||
if value, ok := numberValue(policy[key]); ok {
|
||
return &value
|
||
}
|
||
}
|
||
platformLimits := objectValue(policy["platformLimits"])
|
||
for _, key := range rateLimitKeys(metric) {
|
||
if value, ok := numberValue(platformLimits[key]); ok {
|
||
return &value
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func rateLimitKeys(metric string) []string {
|
||
switch metric {
|
||
case "rpm":
|
||
return []string{"rpm", "max_request_per_minute", "maxRequestsPerMinute"}
|
||
case "tpm_total":
|
||
return []string{"tpm_total", "tpm", "max_token_per_minute", "max_tokens_per_minute", "maxTokensPerMinute"}
|
||
case "concurrent":
|
||
return []string{"concurrent", "max_concurrent_requests", "maxConcurrentRequests"}
|
||
default:
|
||
return []string{metric}
|
||
}
|
||
}
|
||
|
||
func aggregateRateLimits(sources []catalogSource) ModelCatalogRateLimits {
|
||
var rpm, tpm, concurrent float64
|
||
var hasRPM, hasTPM, hasConcurrent bool
|
||
titles := make([]string, 0, len(sources))
|
||
for _, source := range sources {
|
||
limits := source.rateLimits
|
||
if limits.RPM != nil {
|
||
rpm += *limits.RPM
|
||
hasRPM = true
|
||
}
|
||
if limits.TPM != nil {
|
||
tpm += *limits.TPM
|
||
hasTPM = true
|
||
}
|
||
if limits.Concurrent != nil {
|
||
concurrent += *limits.Concurrent
|
||
hasConcurrent = true
|
||
}
|
||
titles = append(titles, firstNonEmpty(source.platform.Name, source.model.ModelName)+": "+rateLimitLabel(limits))
|
||
}
|
||
out := ModelCatalogRateLimits{Title: strings.Join(titles, ";")}
|
||
if hasRPM {
|
||
out.RPM = &rpm
|
||
}
|
||
if hasTPM {
|
||
out.TPM = &tpm
|
||
}
|
||
if hasConcurrent {
|
||
out.Concurrent = &concurrent
|
||
}
|
||
out.Label = rateLimitLabel(out)
|
||
return out
|
||
}
|
||
|
||
func rateLimitLabel(limits ModelCatalogRateLimits) string {
|
||
return "RPM " + formatOptionalNumber(limits.RPM) + " / TPM " + formatOptionalNumber(limits.TPM) + " / 并发 " + formatOptionalNumber(limits.Concurrent)
|
||
}
|
||
|
||
func permissionText(sources []catalogSource, rules map[string]accessRuleGroup) ModelCatalogPermission {
|
||
allow := map[string]bool{}
|
||
deny := map[string]bool{}
|
||
for _, source := range sources {
|
||
collectAccessRuleGroups(rules["platform_model:"+source.model.ID], allow, deny)
|
||
if source.model.PlatformID != "" {
|
||
collectAccessRuleGroups(rules["platform:"+source.model.PlatformID], allow, deny)
|
||
}
|
||
collectPermissionConfigGroups(source.model.PermissionConfig, allow)
|
||
}
|
||
allowNames := sortedMapKeys(allow)
|
||
denyNames := sortedMapKeys(deny)
|
||
if len(allowNames) == 0 && len(denyNames) == 0 {
|
||
return ModelCatalogPermission{Label: "不限制", Title: "没有用户组访问限制"}
|
||
}
|
||
labelParts := []string{}
|
||
if len(allowNames) > 0 {
|
||
label := "用户组 " + strings.Join(limitStrings(allowNames, 4), "、")
|
||
if len(allowNames) > 4 {
|
||
label += "等"
|
||
}
|
||
labelParts = append(labelParts, label)
|
||
}
|
||
if len(denyNames) > 0 {
|
||
label := "拒绝 " + strings.Join(limitStrings(denyNames, 3), "、")
|
||
if len(denyNames) > 3 {
|
||
label += "等"
|
||
}
|
||
labelParts = append(labelParts, label)
|
||
}
|
||
titleParts := []string{}
|
||
if len(allowNames) > 0 {
|
||
titleParts = append(titleParts, "允许用户组:"+strings.Join(allowNames, "、"))
|
||
}
|
||
if len(denyNames) > 0 {
|
||
titleParts = append(titleParts, "拒绝用户组:"+strings.Join(denyNames, "、"))
|
||
}
|
||
return ModelCatalogPermission{
|
||
Label: strings.Join(labelParts, ";"),
|
||
Title: strings.Join(titleParts, ";"),
|
||
AllowGroups: allowNames,
|
||
DenyGroups: denyNames,
|
||
}
|
||
}
|
||
|
||
type accessRuleGroup struct {
|
||
allow map[string]bool
|
||
deny map[string]bool
|
||
}
|
||
|
||
func mapAccessRuleGroups(rules []store.AccessRule, userGroupMap map[string]string) map[string]accessRuleGroup {
|
||
groups := map[string]accessRuleGroup{}
|
||
for _, rule := range rules {
|
||
if rule.Status != "active" || rule.SubjectType != "user_group" || (rule.ResourceType != "platform" && rule.ResourceType != "platform_model") {
|
||
continue
|
||
}
|
||
key := rule.ResourceType + ":" + rule.ResourceID
|
||
current := groups[key]
|
||
if current.allow == nil {
|
||
current.allow = map[string]bool{}
|
||
current.deny = map[string]bool{}
|
||
}
|
||
groupName := firstNonEmpty(userGroupMap[rule.SubjectID], rule.SubjectID)
|
||
if rule.Effect == "allow" {
|
||
current.allow[groupName] = true
|
||
}
|
||
if rule.Effect == "deny" {
|
||
current.deny[groupName] = true
|
||
}
|
||
groups[key] = current
|
||
}
|
||
return groups
|
||
}
|
||
|
||
func collectAccessRuleGroups(group accessRuleGroup, allow map[string]bool, deny map[string]bool) {
|
||
for name := range group.allow {
|
||
allow[name] = true
|
||
}
|
||
for name := range group.deny {
|
||
deny[name] = true
|
||
}
|
||
}
|
||
|
||
func collectPermissionConfigGroups(config map[string]any, allow map[string]bool) {
|
||
if len(config) == 0 {
|
||
return
|
||
}
|
||
for _, key := range []string{"userGroupNames", "userGroups", "allowedUserGroups", "allowedUserGroupNames"} {
|
||
for _, value := range stringSliceValue(config[key]) {
|
||
allow[value] = true
|
||
}
|
||
}
|
||
}
|
||
|
||
func pricingText(sources []catalogSource) ModelCatalogPricing {
|
||
lineSets := make([]string, 0, len(sources))
|
||
linesBySource := make([][]string, 0, len(sources))
|
||
for _, source := range sources {
|
||
lines := billingConfigLines(source.model.BillingConfig)
|
||
if len(lines) == 0 {
|
||
lines = []string{pricingModeLabel(source.model.PricingMode)}
|
||
}
|
||
linesBySource = append(linesBySource, lines)
|
||
lineSets = append(lineSets, strings.Join(lines, "\n"))
|
||
}
|
||
uniqueSets := uniqueStrings(lineSets)
|
||
lines := []string{}
|
||
if len(uniqueSets) <= 1 {
|
||
if len(linesBySource) > 0 {
|
||
lines = append(lines, linesBySource[0]...)
|
||
}
|
||
} else {
|
||
for index, source := range sources {
|
||
label := firstNonEmpty(source.platform.Name, source.model.ModelName)
|
||
for _, line := range linesBySource[index] {
|
||
lines = append(lines, label+": "+line)
|
||
}
|
||
}
|
||
}
|
||
if len(lines) == 0 {
|
||
lines = []string{"未配置具体价格"}
|
||
}
|
||
return ModelCatalogPricing{Lines: lines, Title: strings.Join(lines, ";")}
|
||
}
|
||
|
||
func billingConfigLines(config map[string]any) []string {
|
||
if len(config) == 0 {
|
||
return nil
|
||
}
|
||
if textPricingResource(config) {
|
||
if lines := textPricingLines(config); len(lines) > 0 {
|
||
return lines
|
||
}
|
||
}
|
||
if basePrice, ok := numberValue(config["basePrice"]); ok {
|
||
resourceType := firstNonEmpty(stringValue(config["resourceType"]), "model")
|
||
return []string{priceLine(resourceType, map[string]any{"basePrice": basePrice, "dynamicWeight": config["dynamicWeight"], "unit": config["unit"]})}
|
||
}
|
||
lines := textPricingLines(config)
|
||
for _, resource := range []string{"image", "image_edit", "video", "audio", "music", "digital_human", "model"} {
|
||
if nested := objectValue(config[resource]); len(nested) > 0 {
|
||
if _, ok := numberValue(nested["basePrice"]); ok {
|
||
lines = append(lines, priceLine(resource, nested))
|
||
continue
|
||
}
|
||
}
|
||
for _, baseKey := range resourceBaseKeys(resource) {
|
||
if value, ok := numberValue(config[baseKey]); ok {
|
||
lines = append(lines, resourceBaseLine(resource, value))
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return uniqueStrings(lines)
|
||
}
|
||
|
||
func textPricingResource(config map[string]any) bool {
|
||
switch strings.ToLower(stringValue(config["resourceType"])) {
|
||
case "text", "text_total", "text_input", "text_output":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func textPricingLines(config map[string]any) []string {
|
||
if len(config) == 0 {
|
||
return nil
|
||
}
|
||
inputPrice, hasInput := textPriceFromConfig(config, []string{"textInputPer1k", "textInput", "text_input", "inputTokenPrice", "inputPrice"})
|
||
outputPrice, hasOutput := textPriceFromConfig(config, []string{"textOutputPer1k", "textOutput", "text_output", "outputTokenPrice", "outputPrice"})
|
||
|
||
for _, key := range []string{"formulaConfig", "formula_config"} {
|
||
formulaConfig := objectValue(config[key])
|
||
if !hasInput {
|
||
inputPrice, hasInput = textPriceFromConfig(formulaConfig, []string{"inputTokenPrice", "textInputPer1k", "textInput", "text_input", "inputPrice"})
|
||
}
|
||
if !hasOutput {
|
||
outputPrice, hasOutput = textPriceFromConfig(formulaConfig, []string{"outputTokenPrice", "textOutputPer1k", "textOutput", "text_output", "outputPrice"})
|
||
}
|
||
}
|
||
|
||
for _, resource := range []string{"text", "text_total"} {
|
||
nested := objectValue(config[resource])
|
||
if len(nested) == 0 {
|
||
continue
|
||
}
|
||
if !hasInput {
|
||
inputPrice, hasInput = textPriceFromConfig(nested, []string{"inputTokenPrice", "textInputPer1k", "textInput", "text_input", "inputPrice"})
|
||
}
|
||
if !hasOutput {
|
||
outputPrice, hasOutput = textPriceFromConfig(nested, []string{"outputTokenPrice", "textOutputPer1k", "textOutput", "text_output", "outputPrice"})
|
||
}
|
||
for _, key := range []string{"formulaConfig", "formula_config"} {
|
||
formulaConfig := objectValue(nested[key])
|
||
if !hasInput {
|
||
inputPrice, hasInput = textPriceFromConfig(formulaConfig, []string{"inputTokenPrice", "textInputPer1k", "textInput", "text_input", "inputPrice"})
|
||
}
|
||
if !hasOutput {
|
||
outputPrice, hasOutput = textPriceFromConfig(formulaConfig, []string{"outputTokenPrice", "textOutputPer1k", "textOutput", "text_output", "outputPrice"})
|
||
}
|
||
}
|
||
if resource == "text" && !hasOutput {
|
||
outputPrice, hasOutput = textPriceFromConfig(nested, []string{"basePrice"})
|
||
}
|
||
if !hasInput {
|
||
inputPrice, hasInput = textPriceFromConfig(nested, []string{"basePrice"})
|
||
}
|
||
}
|
||
|
||
if basePrice, ok := numberValue(config["basePrice"]); ok {
|
||
switch strings.ToLower(stringValue(config["resourceType"])) {
|
||
case "text":
|
||
if !hasInput {
|
||
inputPrice, hasInput = basePrice, true
|
||
}
|
||
if !hasOutput {
|
||
outputPrice, hasOutput = basePrice, true
|
||
}
|
||
case "text_total", "text_input":
|
||
if !hasInput {
|
||
inputPrice, hasInput = basePrice, true
|
||
}
|
||
case "text_output":
|
||
if !hasOutput {
|
||
outputPrice, hasOutput = basePrice, true
|
||
}
|
||
}
|
||
}
|
||
|
||
lines := []string{}
|
||
if hasInput {
|
||
lines = append(lines, "输入 "+formatNumber(inputPrice)+"/k tokens")
|
||
}
|
||
if hasOutput {
|
||
lines = append(lines, "输出 "+formatNumber(outputPrice)+"/k tokens")
|
||
}
|
||
return uniqueStrings(lines)
|
||
}
|
||
|
||
func textPriceFromConfig(config map[string]any, keys []string) (float64, bool) {
|
||
if len(config) == 0 {
|
||
return 0, false
|
||
}
|
||
for _, key := range keys {
|
||
if value, ok := numberValue(config[key]); ok {
|
||
return value, true
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
func resourceBaseKeys(resource string) []string {
|
||
switch resource {
|
||
case "image":
|
||
return []string{"imageBase"}
|
||
case "image_edit":
|
||
return []string{"editBase", "imageEditBase"}
|
||
case "video":
|
||
return []string{"videoBase"}
|
||
case "audio":
|
||
return []string{"audioBase"}
|
||
case "music":
|
||
return []string{"musicBase"}
|
||
case "digital_human":
|
||
return []string{"digitalHumanBase", "digital_humanBase"}
|
||
case "model":
|
||
return []string{"modelBase"}
|
||
case "text":
|
||
return []string{"textBase"}
|
||
default:
|
||
return []string{resource + "Base"}
|
||
}
|
||
}
|
||
|
||
func priceLine(resourceType string, config map[string]any) string {
|
||
basePrice, _ := numberValue(config["basePrice"])
|
||
weights := resolutionWeightEntries(config["dynamicWeight"])
|
||
if len(weights) > 0 {
|
||
details := make([]string, 0, len(weights))
|
||
for _, item := range weights {
|
||
details = append(details, item.key+" "+formatNumber(basePrice*item.weight))
|
||
}
|
||
line := resourceTypeLabel(resourceType) + ":" + strings.Join(details, " / ")
|
||
if resourceType == "video" {
|
||
line += "(5秒基准)"
|
||
}
|
||
return line
|
||
}
|
||
unit := firstNonEmpty(stringValue(config["unit"]), resourceType)
|
||
if resourceType == "video" && (unit == "video" || unit == "5s") {
|
||
return resourceTypeLabel(resourceType) + ":" + formatNumber(basePrice) + " / 5秒基准"
|
||
}
|
||
return resourceTypeLabel(resourceType) + ":" + formatNumber(basePrice) + " / " + unitLabel(unit)
|
||
}
|
||
|
||
func resourceBaseLine(resourceType string, basePrice float64) string {
|
||
if resourceType == "video" {
|
||
return resourceTypeLabel(resourceType) + ":" + formatNumber(basePrice) + " / 5秒基准"
|
||
}
|
||
return resourceTypeLabel(resourceType) + ":" + formatNumber(basePrice)
|
||
}
|
||
|
||
type resolutionWeight struct {
|
||
key string
|
||
weight float64
|
||
}
|
||
|
||
func resolutionWeightEntries(value any) []resolutionWeight {
|
||
record := objectValue(value)
|
||
entries := make([]resolutionWeight, 0, len(record))
|
||
for key, raw := range record {
|
||
if !isResolutionKey(key) {
|
||
continue
|
||
}
|
||
weight, ok := numberValue(raw)
|
||
if !ok {
|
||
continue
|
||
}
|
||
entries = append(entries, resolutionWeight{key: key, weight: weight})
|
||
}
|
||
sort.Slice(entries, func(i, j int) bool {
|
||
return resolutionSortValue(entries[i].key) < resolutionSortValue(entries[j].key)
|
||
})
|
||
return entries
|
||
}
|
||
|
||
func isResolutionKey(value string) bool {
|
||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||
if strings.HasSuffix(normalized, "k") {
|
||
_, ok := numberValue(strings.TrimSuffix(normalized, "k"))
|
||
return ok
|
||
}
|
||
if strings.HasSuffix(normalized, "p") {
|
||
_, ok := numberValue(strings.TrimSuffix(normalized, "p"))
|
||
return ok
|
||
}
|
||
return false
|
||
}
|
||
|
||
func resolutionSortValue(value string) float64 {
|
||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||
if strings.HasSuffix(normalized, "k") {
|
||
number, _ := numberValue(strings.TrimSuffix(normalized, "k"))
|
||
return number * 1000
|
||
}
|
||
if strings.HasSuffix(normalized, "p") {
|
||
number, _ := numberValue(strings.TrimSuffix(normalized, "p"))
|
||
return number
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func statusText(sources []catalogSource) string {
|
||
statuses := uniqueStringsFromSources(sources, func(source catalogSource) string {
|
||
if source.platform.Status != "" {
|
||
return source.platform.Status
|
||
}
|
||
if source.model.Enabled {
|
||
return "enabled"
|
||
}
|
||
return "disabled"
|
||
})
|
||
priorities := uniqueStringsFromSources(sources, func(source catalogSource) string {
|
||
if source.platform.Priority == 0 {
|
||
return ""
|
||
}
|
||
return "优先级 " + formatInt(source.platform.Priority)
|
||
})
|
||
parts := append(statuses, priorities...)
|
||
if len(parts) == 0 {
|
||
return "平台模型"
|
||
}
|
||
return strings.Join(parts, " · ")
|
||
}
|
||
|
||
func modelCatalogCapabilityFilters(items []ModelCatalogItem) []ModelCatalogFilterOption {
|
||
counts := map[string]int{}
|
||
for _, item := range items {
|
||
for _, value := range capabilityFilterValuesForItem(item) {
|
||
counts[value]++
|
||
}
|
||
}
|
||
options := []ModelCatalogFilterOption{{Value: "all", Label: "全部", Count: len(items)}}
|
||
for _, option := range []ModelCatalogFilterOption{
|
||
{Value: "chat", Label: "对话"},
|
||
{Value: "image", Label: "图像"},
|
||
{Value: "video", Label: "视频"},
|
||
{Value: "audio", Label: "音频"},
|
||
{Value: "embedding", Label: "Embedding"},
|
||
{Value: "tools", Label: "工具调用"},
|
||
{Value: "omni", Label: "全模态"},
|
||
} {
|
||
if count := counts[option.Value]; count > 0 {
|
||
option.Count = count
|
||
options = append(options, option)
|
||
}
|
||
}
|
||
return options
|
||
}
|
||
|
||
func capabilityFilterValuesForItem(item ModelCatalogItem) []string {
|
||
values := capabilityFilterValues(item.ModelType, nil)
|
||
for _, tag := range item.CapabilityTags {
|
||
switch tag {
|
||
case "工具调用":
|
||
values = append(values, "tools")
|
||
case "全模态", "多模态":
|
||
values = append(values, "omni")
|
||
}
|
||
}
|
||
return uniqueStrings(values)
|
||
}
|
||
|
||
func modelCatalogProviderFilters(items []ModelCatalogItem) []ModelCatalogFilterOption {
|
||
counts := map[string]int{}
|
||
labels := map[string]string{}
|
||
icons := map[string]string{}
|
||
for _, item := range items {
|
||
for _, provider := range item.Providers {
|
||
counts[provider.Key]++
|
||
labels[provider.Key] = provider.Name
|
||
if provider.IconPath != "" {
|
||
icons[provider.Key] = provider.IconPath
|
||
}
|
||
}
|
||
}
|
||
options := []ModelCatalogFilterOption{{Value: "all", Label: "全部", Count: len(items)}}
|
||
keys := sortedIntMapKeys(counts)
|
||
sort.Slice(keys, func(i, j int) bool {
|
||
return labels[keys[i]] < labels[keys[j]]
|
||
})
|
||
for _, key := range keys {
|
||
options = append(options, ModelCatalogFilterOption{
|
||
Value: key,
|
||
Label: firstNonEmpty(labels[key], key),
|
||
Count: counts[key],
|
||
IconPath: icons[key],
|
||
})
|
||
}
|
||
return options
|
||
}
|
||
|
||
func capabilityTagsForGroup(modelTypes []string, capabilities map[string]any) []string {
|
||
tags := make([]string, 0, len(modelTypes)+4)
|
||
for _, modelType := range modelTypes {
|
||
tags = append(tags, capabilityLabel(modelType))
|
||
}
|
||
if boolAnyValue(capabilities["multimodal"]) || boolAnyValue(capabilities["vision"]) {
|
||
tags = append(tags, "多模态")
|
||
}
|
||
if boolAnyValue(capabilities["reasoning"]) {
|
||
tags = append(tags, "推理")
|
||
}
|
||
if boolAnyValue(capabilities["structured_output"]) || boolAnyValue(capabilities["structuredOutput"]) {
|
||
tags = append(tags, "结构化输出")
|
||
}
|
||
if boolAnyValue(capabilities["tools_call"]) || boolAnyValue(capabilities["toolCall"]) || boolAnyValue(capabilities["function_call"]) {
|
||
tags = append(tags, "工具调用")
|
||
}
|
||
return uniqueStrings(tags)
|
||
}
|
||
|
||
func capabilityFilterValues(modelTypes []string, capabilities map[string]any) []string {
|
||
values := []string{}
|
||
for _, modelType := range modelTypes {
|
||
normalized := strings.ToLower(strings.TrimSpace(modelType))
|
||
switch {
|
||
case normalized == "text_embedding" || normalized == "embedding":
|
||
values = append(values, "embedding")
|
||
case normalized == "tools_call":
|
||
values = append(values, "tools")
|
||
case normalized == "omni":
|
||
values = append(values, "omni")
|
||
case strings.Contains(normalized, "video"):
|
||
values = append(values, "video")
|
||
case strings.Contains(normalized, "audio") || strings.Contains(normalized, "speech"):
|
||
values = append(values, "audio")
|
||
case strings.Contains(normalized, "image"):
|
||
values = append(values, "image")
|
||
case normalized == "text_generate" || normalized == "chat" || normalized == "responses" || strings.Contains(normalized, "text"):
|
||
values = append(values, "chat")
|
||
}
|
||
}
|
||
if boolAnyValue(capabilities["tools_call"]) || boolAnyValue(capabilities["toolCall"]) || boolAnyValue(capabilities["function_call"]) {
|
||
values = append(values, "tools")
|
||
}
|
||
if boolAnyValue(capabilities["multimodal"]) {
|
||
values = append(values, "omni")
|
||
}
|
||
return uniqueStrings(values)
|
||
}
|
||
|
||
func capabilityLabel(value string) string {
|
||
labels := map[string]string{
|
||
"text_generate": "文本生成",
|
||
"chat": "对话",
|
||
"responses": "Responses",
|
||
"text_embedding": "Embedding",
|
||
"embedding": "Embedding",
|
||
"image_generate": "图像生成",
|
||
"image_edit": "图像编辑",
|
||
"image_analysis": "图像分析",
|
||
"video_generate": "视频生成",
|
||
"image_to_video": "图生视频",
|
||
"text_to_video": "文生视频",
|
||
"video_edit": "视频编辑",
|
||
"video_understanding": "视频理解",
|
||
"audio_generate": "音频生成",
|
||
"text_to_speech": "语音合成",
|
||
"audio_understanding": "音频理解",
|
||
"tools_call": "工具调用",
|
||
"omni": "全模态",
|
||
"omni_video": "全模态视频",
|
||
"structured_output": "结构化输出",
|
||
"digital_human": "数字人",
|
||
"model_3d": "3D 模型",
|
||
}
|
||
if label, ok := labels[value]; ok {
|
||
return label
|
||
}
|
||
return value
|
||
}
|
||
|
||
func modelDescription(model store.PlatformModel, baseModel store.BaseModel) string {
|
||
rawModel := objectValue(baseModel.Metadata["rawModel"])
|
||
capabilityRawModel := objectValue(model.Capabilities["rawModel"])
|
||
return firstNonEmpty(
|
||
stringValue(baseModel.Metadata["description"]),
|
||
stringValue(baseModel.Metadata["modelDescription"]),
|
||
stringValue(rawModel["description"]),
|
||
stringValue(model.Capabilities["description"]),
|
||
stringValue(model.Capabilities["modelDescription"]),
|
||
stringValue(capabilityRawModel["description"]),
|
||
)
|
||
}
|
||
|
||
func modelIconPath(model store.PlatformModel, baseModel store.BaseModel, provider store.CatalogProvider) string {
|
||
rawModel := objectValue(baseModel.Metadata["rawModel"])
|
||
return firstNonEmpty(
|
||
stringValue(baseModel.Metadata["iconPath"]),
|
||
stringValue(rawModel["icon_path"]),
|
||
stringValue(rawModel["iconPath"]),
|
||
stringValue(model.Capabilities["iconPath"]),
|
||
stringValue(model.Capabilities["icon_path"]),
|
||
stringValue(model.BillingConfig["iconPath"]),
|
||
stringValue(model.BillingConfig["icon_path"]),
|
||
provider.IconPath,
|
||
)
|
||
}
|
||
|
||
func catalogAliasKey(model store.PlatformModel) string {
|
||
key := strings.ToLower(strings.TrimSpace(firstNonEmpty(model.ModelAlias, model.DisplayName, model.ModelName)))
|
||
if key == "" {
|
||
return model.ID
|
||
}
|
||
return key
|
||
}
|
||
|
||
func modelProviderKey(model store.PlatformModel, baseModel store.BaseModel, platform store.Platform) string {
|
||
return normalizeCatalogProviderKey(firstNonEmpty(
|
||
baseModel.ProviderKey,
|
||
stringValue(baseModel.Metadata["sourceProviderCode"]),
|
||
stringValue(baseModel.Metadata["providerKey"]),
|
||
model.Provider,
|
||
platform.Provider,
|
||
platform.Name,
|
||
model.PlatformName,
|
||
))
|
||
}
|
||
|
||
func normalizeCatalogProviderKey(value string) string {
|
||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||
if normalized == "" {
|
||
return "unknown"
|
||
}
|
||
normalized = strings.ReplaceAll(normalized, "_", "-")
|
||
normalized = strings.Join(strings.Fields(normalized), "-")
|
||
switch normalized {
|
||
case "google-gemini":
|
||
return "gemini"
|
||
case "open-ai":
|
||
return "openai"
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
func mapPlatforms(platforms []store.Platform) map[string]store.Platform {
|
||
out := map[string]store.Platform{}
|
||
for _, platform := range platforms {
|
||
out[platform.ID] = platform
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mapProviders(providers []store.CatalogProvider) map[string]store.CatalogProvider {
|
||
out := map[string]store.CatalogProvider{}
|
||
for _, provider := range providers {
|
||
keys := []string{
|
||
provider.ProviderKey,
|
||
provider.Code,
|
||
provider.DisplayName,
|
||
stringValue(provider.Metadata["sourceCode"]),
|
||
}
|
||
for _, key := range keys {
|
||
normalized := normalizeCatalogProviderKey(key)
|
||
if normalized != "unknown" {
|
||
out[normalized] = provider
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mapRuntimePolicies(policies []store.RuntimePolicySet) map[string]store.RuntimePolicySet {
|
||
out := map[string]store.RuntimePolicySet{}
|
||
for _, policy := range policies {
|
||
out[policy.ID] = policy
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mapUserGroups(groups []store.UserGroup) map[string]string {
|
||
out := map[string]string{}
|
||
for _, group := range groups {
|
||
out[group.ID] = firstNonEmpty(group.Name, group.GroupKey, group.ID)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mapBaseModels(models []store.BaseModel) map[string]store.BaseModel {
|
||
out := map[string]store.BaseModel{}
|
||
for _, model := range models {
|
||
out[model.ID] = model
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mergeCatalogObjects(left map[string]any, right map[string]any) map[string]any {
|
||
if len(left) == 0 && len(right) == 0 {
|
||
return nil
|
||
}
|
||
out := cloneObject(left)
|
||
for key, value := range right {
|
||
out[key] = value
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cloneObject(value map[string]any) map[string]any {
|
||
if len(value) == 0 {
|
||
return nil
|
||
}
|
||
out := make(map[string]any, len(value))
|
||
for key, item := range value {
|
||
out[key] = item
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cloneStringSlice(values []string) []string {
|
||
if len(values) == 0 {
|
||
return nil
|
||
}
|
||
return append([]string(nil), values...)
|
||
}
|
||
|
||
func uniqueStrings(values []string) []string {
|
||
out := make([]string, 0, len(values))
|
||
seen := map[string]bool{}
|
||
for _, value := range values {
|
||
trimmed := strings.TrimSpace(value)
|
||
if trimmed == "" || seen[trimmed] {
|
||
continue
|
||
}
|
||
seen[trimmed] = true
|
||
out = append(out, trimmed)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func uniqueStringsFromSources(sources []catalogSource, selector func(catalogSource) string) []string {
|
||
values := make([]string, 0, len(sources))
|
||
for _, source := range sources {
|
||
values = append(values, selector(source))
|
||
}
|
||
return uniqueStrings(values)
|
||
}
|
||
|
||
func limitStrings(values []string, limit int) []string {
|
||
if len(values) <= limit {
|
||
return values
|
||
}
|
||
return values[:limit]
|
||
}
|
||
|
||
func sortedMapKeys(values map[string]bool) []string {
|
||
keys := make([]string, 0, len(values))
|
||
for key := range values {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|
||
|
||
func objectValue(value any) map[string]any {
|
||
switch typed := value.(type) {
|
||
case map[string]any:
|
||
return typed
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func stringValue(value any) string {
|
||
if value == nil {
|
||
return ""
|
||
}
|
||
if text, ok := value.(string); ok {
|
||
return strings.TrimSpace(text)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func stringSliceValue(value any) []string {
|
||
switch typed := value.(type) {
|
||
case []string:
|
||
return uniqueStrings(typed)
|
||
case []any:
|
||
out := make([]string, 0, len(typed))
|
||
for _, item := range typed {
|
||
if text := stringValue(item); text != "" {
|
||
out = append(out, text)
|
||
}
|
||
}
|
||
return uniqueStrings(out)
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func sortedIntMapKeys(values map[string]int) []string {
|
||
keys := make([]string, 0, len(values))
|
||
for key := range values {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|
||
|
||
func boolAnyValue(value any) bool {
|
||
typed, ok := value.(bool)
|
||
return ok && typed
|
||
}
|
||
|
||
func numberValue(value any) (float64, bool) {
|
||
switch typed := value.(type) {
|
||
case float64:
|
||
return typed, true
|
||
case float32:
|
||
return float64(typed), true
|
||
case int:
|
||
return float64(typed), true
|
||
case int64:
|
||
return float64(typed), true
|
||
case int32:
|
||
return float64(typed), true
|
||
case string:
|
||
trimmed := strings.TrimSpace(typed)
|
||
if trimmed == "" {
|
||
return 0, false
|
||
}
|
||
if number, err := strconv.ParseFloat(trimmed, 64); err == nil {
|
||
return number, true
|
||
}
|
||
return 0, false
|
||
default:
|
||
return 0, false
|
||
}
|
||
}
|
||
|
||
func formatOptionalNumber(value *float64) string {
|
||
if value == nil {
|
||
return "-"
|
||
}
|
||
return formatLimitNumber(*value)
|
||
}
|
||
|
||
func formatLimitNumber(value float64) string {
|
||
switch {
|
||
case absFloat(value) >= 10000:
|
||
return formatNumber(value/10000) + "万"
|
||
case absFloat(value) >= 1000:
|
||
return formatNumber(value/1000) + "k"
|
||
default:
|
||
return formatNumber(value)
|
||
}
|
||
}
|
||
|
||
func formatPercent(value float64) string {
|
||
return formatNumber(value*100) + "%"
|
||
}
|
||
|
||
func formatNumber(value float64) string {
|
||
return strconv.FormatFloat(value, 'f', -1, 64)
|
||
}
|
||
|
||
func formatInt(value int) string {
|
||
return strconv.Itoa(value)
|
||
}
|
||
|
||
func absFloat(value float64) float64 {
|
||
if value < 0 {
|
||
return -value
|
||
}
|
||
return value
|
||
}
|
||
|
||
func pricingModeLabel(mode string) string {
|
||
switch mode {
|
||
case "inherit":
|
||
return "跟随基准定价"
|
||
case "inherit_discount":
|
||
return "继承并折扣"
|
||
case "custom":
|
||
return "自定义定价"
|
||
default:
|
||
if strings.TrimSpace(mode) != "" {
|
||
return mode
|
||
}
|
||
return "未配置定价"
|
||
}
|
||
}
|
||
|
||
func resourceTypeLabel(value string) string {
|
||
labels := map[string]string{
|
||
"text": "文本",
|
||
"text_total": "文本",
|
||
"text_input": "文本输入",
|
||
"text_output": "文本输出",
|
||
"image": "图像",
|
||
"image_edit": "图像编辑",
|
||
"video": "视频",
|
||
"audio": "音频",
|
||
"music": "音乐",
|
||
"digital_human": "数字人",
|
||
"model": "模型",
|
||
}
|
||
if label, ok := labels[value]; ok {
|
||
return label
|
||
}
|
||
return value
|
||
}
|
||
|
||
func unitLabel(value string) string {
|
||
labels := map[string]string{
|
||
"image": "张",
|
||
"5s": "5秒",
|
||
"second": "秒",
|
||
"item": "次",
|
||
"unit": "单位",
|
||
"1k_tokens": "k tokens",
|
||
"character_1k": "千字符",
|
||
}
|
||
if label, ok := labels[value]; ok {
|
||
return label
|
||
}
|
||
return value
|
||
}
|