package helper import ( "encoding/json" "errors" "fmt" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) func ModelMappedHelper(c *gin.Context, info *relaycommon.RelayInfo, request dto.Request) error { if info != nil { info.TFOpenUpstreamRouteApplied = false } if info.ChannelMeta == nil { info.ChannelMeta = &relaycommon.ChannelMeta{} } isResponsesCompact := info.RelayMode == relayconstant.RelayModeResponsesCompact originModelName := info.OriginModelName mappingModelName := originModelName if isResponsesCompact && strings.HasSuffix(originModelName, ratio_setting.CompactModelSuffix) { mappingModelName = strings.TrimSuffix(originModelName, ratio_setting.CompactModelSuffix) } // TokenFactoryOpen 渠道指向上游 TokenFactory 平台,上游 distributor 会将含 "/" 的模型名 // 误解析为路由格式({model}/{route_slug} 或 {alias}/{model}/{channel_no})。 // 因此当上游是 TF 平台时,跳过 model_mapping,保留本地原始模型名。 // TFOpen 同步渠道(source=tokenfactory_open)会在下方 tfRoute 逻辑中拼接三段式路由, // 同样使用原始模型名。 channelType := common.GetContextKeyInt(c, constant.ContextKeyChannelType) isTFOpenUpstream := channelType == constant.ChannelTypeTokenFactoryOpen // map model name modelMapping := c.GetString("model_mapping") if modelMapping != "" && modelMapping != "{}" && !isTFOpenUpstream { modelMap := make(map[string]string) err := json.Unmarshal([]byte(modelMapping), &modelMap) if err != nil { return fmt.Errorf("unmarshal_model_mapping_failed") } // 若模型名形如「Seedance2.0/route_slug」:优先用已解析的路由得到基础名; // 若路由未命中(子站与上游库不一致、slug 在上游不存在等),仍用「最后一段为合法 route_slug」时的基础名走 model_mapping, // 避免把整串当作上游真实 model_id 送给外部网关(会导致 Invalid input params)。 currentModel := mappingModelName if idx, matched, _ := service.ParseModelRouteIndex(mappingModelName); matched && idx != nil { currentModel = idx.ModelName } else if strings.Contains(mappingModelName, "/") { lastSlash := strings.LastIndex(mappingModelName, "/") if lastSlash > 0 && lastSlash < len(mappingModelName)-1 { potentialSlug := strings.TrimSpace(mappingModelName[lastSlash+1:]) potentialBase := strings.TrimSpace(mappingModelName[:lastSlash]) if potentialBase != "" && model.IsValidRouteSlug(potentialSlug) { currentModel = potentialBase } } } // 支持链式模型重定向,最终使用链尾的模型 visitedModels := map[string]bool{ currentModel: true, } for { if mappedModel, exists := modelMap[currentModel]; exists && mappedModel != "" { // 模型重定向循环检测,避免无限循环 if visitedModels[mappedModel] { if mappedModel == currentModel { if currentModel == info.OriginModelName { info.IsModelMapped = false return nil } else { info.IsModelMapped = true break } } return errors.New("model_mapping_contains_cycle") } visitedModels[mappedModel] = true currentModel = mappedModel info.IsModelMapped = true } else { break } } if info.IsModelMapped { info.UpstreamModelName = currentModel } } if isResponsesCompact { finalUpstreamModelName := mappingModelName if info.IsModelMapped && info.UpstreamModelName != "" { finalUpstreamModelName = info.UpstreamModelName } info.UpstreamModelName = finalUpstreamModelName info.OriginModelName = ratio_setting.WithCompactModelSuffix(finalUpstreamModelName) } // TFOpen 上游渠道精准路由: // 新版:route_slug 格式(优先),将 UpstreamModelName 改写为 "{model}/{route_slug}", // 上游的 ParseModelRouteIndex 解析此格式精准路由到对应渠道。 // 旧版(兼容):alias|channelNo 三段式路由,格式为 "legacy|{alias}|{channelNo}", // 将 UpstreamModelName 改写为 "{alias}/{model}/{channelNo}"。 // 当上游也是 TokenFactory 平台时,使用原始模型名(上游可识别的本地模型名)而非 // model_mapping 映射后的名称(如 HuggingFace 格式),避免上游 distributor 误解析。 if tfRoute := c.GetString(string(constant.ContextKeyTFOpenUpstreamChannelRoute)); tfRoute != "" { // 使用原始模型名(而非映射后的名称),因为上游 TF 平台理解本地原始模型名 modelForUpstream := info.OriginModelName if isResponsesCompact && strings.HasSuffix(modelForUpstream, ratio_setting.CompactModelSuffix) { modelForUpstream = strings.TrimSuffix(modelForUpstream, ratio_setting.CompactModelSuffix) } if strings.HasPrefix(tfRoute, "legacy|") { // 旧版三段式路由兼容:legacy|alias|channelNo → alias/model/channelNo parts := strings.SplitN(tfRoute, "|", 3) if len(parts) == 3 { alias := parts[1] channelNo := parts[2] if alias != "" && channelNo != "" { info.UpstreamModelName = alias + "/" + modelForUpstream + "/" + channelNo info.IsModelMapped = false info.TFOpenUpstreamRouteApplied = true } } } else { // 新版二段式路由:route_slug → model/route_slug routeSlug := strings.TrimSpace(tfRoute) if routeSlug != "" { info.UpstreamModelName = modelForUpstream + "/" + routeSlug info.IsModelMapped = false info.TFOpenUpstreamRouteApplied = true } } } // 未命中 model_mapping、且未走 TFOpen 精准路由时:请求里仍可能是「Seedance2.0/route_slug」 //(例如子站 other_info 里的 slug 在上游库不存在,Distribute 未能改写 body)。 // 此时至少剥掉「最后一段为合法 route_slug」的后缀,避免把整串当作外部视频网关的 model_id //(Hidream/MaaS 会返回 Invalid input params)。 if info != nil && !isTFOpenUpstream && !info.TFOpenUpstreamRouteApplied && !info.IsModelMapped { um := strings.TrimSpace(info.UpstreamModelName) if um == "" { um = strings.TrimSpace(mappingModelName) } if um != "" && strings.Contains(um, "/") { if idx, matched, _ := service.ParseModelRouteIndex(um); matched && idx != nil { info.UpstreamModelName = idx.ModelName } else { lastSlash := strings.LastIndex(um, "/") if lastSlash > 0 && lastSlash < len(um)-1 { potentialSlug := strings.TrimSpace(um[lastSlash+1:]) potentialBase := strings.TrimSpace(um[:lastSlash]) if potentialBase != "" && model.IsValidRouteSlug(potentialSlug) { info.UpstreamModelName = potentialBase } } } } } if request != nil { request.SetModelName(info.UpstreamModelName) } return nil }