feat(task): resolve channel-mapped aliases and case variants for plugin models

Channel model_mapping keys exposed in a channel's model list now act as
first-class aliases for task-plugin models across the whole line:

- Derived alias view (model/task_model_alias.go): built from enabled
  channels' model_mapping, chain-following with cycle detection, declared
  names always win, cross-plugin conflicts dropped. Rebuilt on channel
  cache refresh, registry generation change, and a 60s TTL.
- Request path: PinTaskPluginEndpoint resolves declared-name case folds
  and mapping aliases before endpoint lookup (never rewriting the body
  until the endpoint is claimed), pins with MappedModel, and the decode
  contract accepts alias echoes without loosening model ownership for
  normal pins. Legacy /v1/tasks submit folds case variants the same way.
  Fixes aliases on POST /v1/responses silently falling through to the
  main relay against task channels.
- Mapping order: ModelMappedHelper now runs before the plugin submit
  hook builds and caches the upstream body, so channel model_mapping
  actually reaches the upstream request. Plugins receive the mapped
  name as ctx.upstreamModel in both decode and submit contexts.
- Billing: identity stays the origin name; when the alias has no tiered
  expression, the selected channel's mapping tail expression applies.
  Pricing page and billing-expr smoke tests resolve aliases to the
  owning plugin's usage schema.
- Case folding: ASCII-only fold with exact-match priority; same-plugin
  and cross-plugin fold collisions rejected at registration.
- Plugins: model-keyed rate tables, req_key derivation, and combo
  validation in doubao/kling/jimeng/hailuo/vidu/sunoapi now key on
  ctx.upstreamModel || ctx.model; render/echo paths keep ctx.model.
This commit is contained in:
CaIon
2026-08-30 19:13:51 +08:00
parent 918427d8ab
commit 6c22550ea3
23 changed files with 968 additions and 59 deletions
+34 -7
View File
@@ -212,6 +212,19 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
info.PublicTaskID = model.GenerateTaskID()
}
adaptor.Init(info)
// Plugin submit hooks run during ValidateRequestAndSetAction and cache the
// upstream body. OriginModelName is already seeded on that line (protocol
// resolved_task_model, legacy submit, or GenRelayInfo original_model), so
// map before validation. The empty-name CoverTaskActionToModelName
// synthesis happens after validate and cannot move; skip the late block
// when early mapping ran so a chain is never applied twice.
mappedBeforeValidate := info.OriginModelName != ""
if mappedBeforeValidate {
info.UpstreamModelName = info.OriginModelName
if err := helper.ModelMappedHelper(c, info, nil); err != nil {
return nil, service.TaskErrorWrapperLocal(err, "model_mapping_failed", http.StatusBadRequest)
}
}
if taskErr := adaptor.ValidateRequestAndSetAction(c, info); taskErr != nil {
return nil, taskErr
}
@@ -222,19 +235,33 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
modelName = service.CoverTaskActionToModelName(platform, info.Action)
}
// 2.5 应用渠道的模型映射(与同步任务对齐)
info.OriginModelName = modelName
info.UpstreamModelName = modelName
if err := helper.ModelMappedHelper(c, info, nil); err != nil {
return nil, service.TaskErrorWrapperLocal(err, "model_mapping_failed", http.StatusBadRequest)
if !mappedBeforeValidate {
info.OriginModelName = modelName
info.UpstreamModelName = modelName
if err := helper.ModelMappedHelper(c, info, nil); err != nil {
return nil, service.TaskErrorWrapperLocal(err, "model_mapping_failed", http.StatusBadRequest)
}
}
// 4. 价格计算:基础模型价格
info.OriginModelName = modelName
var priceData types.PriceData
var err error
if billing_setting.GetBillingMode(modelName) == billing_setting.BillingModeTieredExpr {
exprStr, exists := billing_setting.GetBillingExpr(modelName)
useTiered := billing_setting.GetBillingMode(modelName) == billing_setting.BillingModeTieredExpr
var exprStr string
var exists bool
if useTiered {
exprStr, exists = billing_setting.GetBillingExpr(modelName)
} else if info.IsModelMapped {
if billing_setting.GetBillingMode(info.UpstreamModelName) == billing_setting.BillingModeTieredExpr {
if tailExpr, tailOK := billing_setting.GetBillingExpr(info.UpstreamModelName); tailOK && strings.TrimSpace(tailExpr) != "" {
exprStr = tailExpr
exists = true
useTiered = true
}
}
}
if useTiered {
provider, supported := adaptor.(channel.TaskUsageFactsProvider)
if !exists || !supported {
return nil, service.TaskErrorWrapper(fmt.Errorf("task model %s has no usage expression or meter", modelName), "model_price_error", http.StatusBadRequest)