feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
+67
-113
@@ -34,6 +34,18 @@ type ModelRequest struct {
|
||||
func Distribute() func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var channel *model.Channel
|
||||
defer func() {
|
||||
if c.Writer.Status() >= 400 {
|
||||
service.RecordRequestPolicyTermination(c, types.NewErrorWithStatusCode(errors.New("request rejected"), types.ErrorCodeInvalidRequest, c.Writer.Status(), types.ErrOptionWithSkipRetry()))
|
||||
// Rejections before the relay starts have no per-channel error log.
|
||||
if constant.ErrorLogEnabled && service.RequestPolicy(c).Attempts == 0 && len(service.RequestPolicy(c).Events()) > 1 {
|
||||
other := model.NewLogOther()
|
||||
other.SetPublic("status_code", c.Writer.Status())
|
||||
service.AppendRelayLogAdminInfo(c, nil, other)
|
||||
model.RecordErrorLog(c, c.GetInt("id"), 0, c.GetString("original_model"), c.GetString("token_name"), "No available channel for this request.", c.GetInt("token_id"), 0, false, "", other)
|
||||
}
|
||||
}
|
||||
}()
|
||||
constraints := service.GetChannelConstraints(c)
|
||||
constraints.AddFilter(taskdto.ChannelFilter{
|
||||
Kind: taskdto.FilterRequestPath,
|
||||
@@ -45,38 +57,8 @@ func Distribute() func(c *gin.Context) {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()}))
|
||||
return
|
||||
}
|
||||
if pin, found, overridden := constraints.ResolvedPin(); found {
|
||||
for _, lost := range overridden {
|
||||
logger.LogWarn(c, fmt.Sprintf(
|
||||
"channel pin overridden: winning_source=%s winning_channel_id=%d overridden_source=%s overridden_channel_id=%d",
|
||||
pin.Source, pin.ChannelId, lost.Source, lost.ChannelId,
|
||||
))
|
||||
}
|
||||
channel, err = model.CacheGetChannel(pin.ChannelId)
|
||||
if err != nil {
|
||||
if pin.Source == taskdto.PinSourceOriginTask {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "origin_task_channel_disabled", types.ErrorCode("origin_task_channel_disabled"))
|
||||
} else {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId))
|
||||
}
|
||||
return
|
||||
}
|
||||
if channel.Status != common.ChannelStatusEnabled {
|
||||
if pin.Source == taskdto.PinSourceOriginTask {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "origin_task_channel_disabled", types.ErrorCode("origin_task_channel_disabled"))
|
||||
} else {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled))
|
||||
}
|
||||
return
|
||||
}
|
||||
if ok, kind := model.ChannelSatisfiesFilters(channel, modelRequest.Model, constraints.Filters); !ok {
|
||||
if kind == taskdto.FilterTaskPluginIdentity {
|
||||
logTaskPluginChannelDecision(c, channel, modelRequest.Model, "channel_rejected", "identity_mismatch")
|
||||
}
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorNoAvailableChannel, map[string]any{"Group": common.GetContextKeyString(c, constant.ContextKeyUsingGroup), "Model": modelRequest.Model}), types.ErrorCode(kind))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, pinned, _ := constraints.ResolvedPin()
|
||||
if !pinned {
|
||||
// Select a channel for the user
|
||||
// check token model mapping
|
||||
modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
|
||||
@@ -92,7 +74,7 @@ func Distribute() func(c *gin.Context) {
|
||||
if !ok {
|
||||
tokenModelLimit = map[string]bool{}
|
||||
}
|
||||
if !tokenModelLimitAllows(tokenModelLimit, modelRequest.Model) {
|
||||
if !TokenModelLimitAllows(tokenModelLimit, modelRequest.Model) {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorTokenModelForbidden, map[string]any{"Model": modelRequest.Model}))
|
||||
return
|
||||
}
|
||||
@@ -103,10 +85,9 @@ func Distribute() func(c *gin.Context) {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorModelNameRequired))
|
||||
return
|
||||
}
|
||||
var selectGroup string
|
||||
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
// check path is /pg/chat/completions
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/pg/chat/completions") {
|
||||
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
playgroundRequest := &dto.PlayGroundRequest{}
|
||||
err = common.UnmarshalBodyReusable(c, playgroundRequest)
|
||||
if err != nil {
|
||||
@@ -118,79 +99,32 @@ func Distribute() func(c *gin.Context) {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorGroupAccessDenied))
|
||||
return
|
||||
}
|
||||
usingGroup = playgroundRequest.Group
|
||||
common.SetContextKey(c, constant.ContextKeyUsingGroup, usingGroup)
|
||||
}
|
||||
}
|
||||
|
||||
if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found {
|
||||
affinityUsable := false
|
||||
preferred, err := model.CacheGetChannel(preferredChannelID)
|
||||
affinitySatisfied := false
|
||||
if err == nil && preferred != nil && preferred.Status == common.ChannelStatusEnabled {
|
||||
affinitySatisfied, _ = model.ChannelSatisfiesFilters(preferred, modelRequest.Model, constraints.Filters)
|
||||
}
|
||||
if affinitySatisfied {
|
||||
if usingGroup == "auto" {
|
||||
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
|
||||
autoGroups := service.GetRequestAutoGroups(c, userGroup)
|
||||
for _, g := range autoGroups {
|
||||
if model.IsChannelEnabledForGroupModel(g, modelRequest.Model, preferred.Id) {
|
||||
selectGroup = g
|
||||
common.SetContextKey(c, constant.ContextKeyAutoGroup, g)
|
||||
channel = preferred
|
||||
affinityUsable = true
|
||||
service.MarkChannelAffinityUsed(c, g, preferred.Id)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, preferred.Id) {
|
||||
channel = preferred
|
||||
selectGroup = usingGroup
|
||||
affinityUsable = true
|
||||
service.MarkChannelAffinityUsed(c, usingGroup, preferred.Id)
|
||||
}
|
||||
}
|
||||
if !affinityUsable && !service.ShouldKeepChannelAffinityOnChannelDisabled() {
|
||||
service.ClearCurrentChannelAffinityCache(c)
|
||||
}
|
||||
}
|
||||
|
||||
if channel == nil {
|
||||
channel, selectGroup, err = service.CacheGetRandomSatisfiedChannel(&service.RetryParam{
|
||||
Ctx: c,
|
||||
ModelName: modelRequest.Model,
|
||||
TokenGroup: usingGroup,
|
||||
RequestPath: c.Request.URL.Path,
|
||||
Retry: common.GetPointer(0),
|
||||
})
|
||||
if err != nil {
|
||||
showGroup := usingGroup
|
||||
if usingGroup == "auto" {
|
||||
showGroup = fmt.Sprintf("auto(%s)", selectGroup)
|
||||
}
|
||||
message := i18n.T(c, i18n.MsgDistributorGetChannelFailed, map[string]any{"Group": showGroup, "Model": modelRequest.Model, "Error": err.Error()})
|
||||
// 如果错误,但是渠道不为空,说明是数据库一致性问题
|
||||
//if channel != nil {
|
||||
// common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
|
||||
// message = "数据库一致性已被破坏,请联系管理员"
|
||||
//}
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, message, types.ErrorCodeModelNotFound)
|
||||
return
|
||||
}
|
||||
if channel == nil {
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, noAvailableChannelMessage(c, usingGroup, modelRequest.Model), types.ErrorCodeModelNotFound)
|
||||
return
|
||||
common.SetContextKey(c, constant.ContextKeyUsingGroup, playgroundRequest.Group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if channel != nil {
|
||||
if ok, kind := model.ChannelSatisfiesFilters(channel, modelRequest.Model, constraints.Filters); !ok {
|
||||
if kind == taskdto.FilterTaskPluginIdentity {
|
||||
logTaskPluginChannelDecision(c, channel, modelRequest.Model, "channel_rejected", "identity_mismatch")
|
||||
if pinned || shouldSelectChannel {
|
||||
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
var selectErr *service.ChannelSelectError
|
||||
channel, _, selectErr = service.SelectChannelForRequest(c, modelRequest.Model, &service.RetryParam{
|
||||
Ctx: c,
|
||||
ModelName: modelRequest.Model,
|
||||
TokenGroup: usingGroup,
|
||||
RequestPath: c.Request.URL.Path,
|
||||
Retry: common.GetPointer(0),
|
||||
})
|
||||
if selectErr != nil {
|
||||
if selectErr.FilterKind == taskdto.FilterTaskPluginIdentity {
|
||||
logTaskPluginChannelDecision(c, selectErr.Channel, modelRequest.Model, "channel_rejected", "identity_mismatch")
|
||||
}
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, noAvailableChannelMessage(c, common.GetContextKeyString(c, constant.ContextKeyUsingGroup), modelRequest.Model), types.ErrorCodeModelNotFound)
|
||||
message := selectErr.Message
|
||||
if selectErr.NoAvailableChannel {
|
||||
message = noAvailableChannelMessage(c, usingGroup, modelRequest.Model)
|
||||
} else if selectErr.MessageID != "" {
|
||||
message = i18n.T(c, selectErr.MessageID, selectErr.Params)
|
||||
}
|
||||
abortWithOpenAiMessage(c, selectErr.StatusCode, message, selectErr.Code)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -198,22 +132,35 @@ func Distribute() func(c *gin.Context) {
|
||||
SetupContextForSelectedChannel(c, channel, modelRequest.Model)
|
||||
c.Next()
|
||||
if channel != nil && c.Writer != nil && c.Writer.Status() < http.StatusBadRequest {
|
||||
service.RecordChannelAffinity(c, channel.Id)
|
||||
if service.RequestRoutingPolicy(c) != nil {
|
||||
service.RecordPoolAffinity(c)
|
||||
} else {
|
||||
service.RecordChannelAffinity(c, channel.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// noAvailableChannelMessage explains a 503 for a task-plugin-claimed model.
|
||||
// A model claimed by a plugin is served only by that plugin's channels, so the
|
||||
// generic "no channel" text hides the real cause: the claiming plugin has no
|
||||
// enabled channel, and the operator must disable or override that plugin for
|
||||
// any other plugin or channel to take the model. Non-plugin requests keep the
|
||||
// generic message.
|
||||
// The response tells the caller the model is plugin-claimed without naming the
|
||||
// plugin; the candidate plugin keys go to the server log under the request id.
|
||||
func noAvailableChannelMessage(c *gin.Context, group, modelName string) string {
|
||||
value, exists := c.Get(jsplugin.ContextKeyPinnedPlugin)
|
||||
pinned, ok := value.(jsplugin.PinnedPlugin)
|
||||
if exists && ok && pinned.Plugin != nil {
|
||||
return i18n.T(c, i18n.MsgDistributorNoAvailableChannelTaskPlugin, map[string]any{"Group": group, "Model": modelName, "Plugin": pinned.Plugin.Meta.Key})
|
||||
keys := []string{pinned.Plugin.Meta.Key}
|
||||
if value, exists := c.Get(jsplugin.ContextKeyPinnedEndpoint); exists {
|
||||
if endpoint, ok := value.(jsplugin.PinnedEndpoint); ok && len(endpoint.Candidates) > 0 {
|
||||
keys = nil
|
||||
for _, candidate := range endpoint.Candidates {
|
||||
if candidate.Plugin != nil {
|
||||
keys = append(keys, candidate.Plugin.Meta.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.LogWarn(c, "task_plugin subsystem=distribution event=no_available_channel group=%q model=%q plugins=%q reason=no_eligible_channel", group, modelName, strings.Join(keys, ","))
|
||||
return i18n.T(c, i18n.MsgDistributorNoAvailableChannelTaskPlugin, map[string]any{"Group": group, "Model": modelName})
|
||||
}
|
||||
return i18n.T(c, i18n.MsgDistributorNoAvailableChannel, map[string]any{"Group": group, "Model": modelName})
|
||||
}
|
||||
@@ -327,6 +274,12 @@ func getModelFromJSONBody(c *gin.Context) (*ModelRequest, error) {
|
||||
}
|
||||
|
||||
values := gjson.GetManyBytes(requestBody, "model", "group")
|
||||
for _, key := range []string{"session_id", "thread_id"} {
|
||||
value := gjson.GetBytes(requestBody, key)
|
||||
if value.Type == gjson.String {
|
||||
c.Set("request_"+key, value.String())
|
||||
}
|
||||
}
|
||||
model, err := getJSONStringValue(values[0], "model")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -569,10 +522,11 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
return &modelRequest, shouldSelectChannel, nil
|
||||
}
|
||||
|
||||
// tokenModelLimitAllows reports whether a token model-limit map authorizes
|
||||
// TokenModelLimitAllows reports whether a token model-limit map authorizes
|
||||
// model. Exact name, wildcard-normalized name, and routing-normalized name
|
||||
// (modifiers and legacy aliases stripped) are all accepted.
|
||||
func tokenModelLimitAllows(limit map[string]bool, model string) bool {
|
||||
// (modifiers and legacy aliases stripped) are all accepted. The Responses
|
||||
// WebSocket relay shares this rule so both transports admit the same names.
|
||||
func TokenModelLimitAllows(limit map[string]bool, model string) bool {
|
||||
if limit[model] {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user