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 }