a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
7.72 kB
// hint.go 网关错误附加说明字段(error.gateway_hint)的单一事实来源。
//
// 纪律(任务书 gateway-hint):
// - error.message 永远是上游 body 原文透传(5755fe3 透传原则不动);
// gateway_hint 只做与 message **并列**的网关视角补充说明,绝不替换/包装 message。
// - 文案集中在本文件(一张 Kind 表 + 11133/11135 形态判定),按 ErrKind + 上下文
// (请求带图/模型目录能力)映射,不散落 handler 的 if-else。
// - 未覆盖形态返回空串 → 响应不带该字段(不编造)。
// - hint 措辞是英文(错误响应面向客户端工具链,英文是通用口径)。
package upstream
import (
"encoding/json"
"net/http"
"strings"
)
// GatewayHint 按错误形态返回网关视角的补充说明(error.gateway_hint 字段值)。
// msg 是上游错误 body 原文(或 SSE error 帧 payload);kind 是权威分类
// (Classify / *Error 信封)。返回空串 = 未覆盖形态,调用方不带字段。
//
// ctx 携带判定 hint 所需的请求侧上下文(零值合法,信息缺失时相关形态退为中性
// hint 或无 hint):
// - HasImage:请求体是否携带 image_url part(11133 的「模型不支持图片」指向前提);
// - Model / ModelInCatalog / ModelSupportsImages:模型目录对该模型的
// supports_images 声明(目录未收录 → 不做「不支持」判定,防查不到误判成不支持)。
//
// 判定次序:11133/11135 上游业务码**先于** Kind 表——实测这两族归 ErrClient/
// ErrBadParams 皆有可能(Classify 词表不含 11133),hint 层自带判定(hint 是补充
// 说明非权威分类,误判代价只是多一条中性补充说明);其余走 Kind 一对一映射。
func GatewayHint(kind ErrKind, msg string, ctx HintContext) string {
// 11133 model_param_invalid 家族(图片回归实测:不支持图片的模型传图,或任意
// 参数被模型供应商拒绝)。只有请求确实带图、且目录能对该模型做出「不支持图片」
// 的判定时才给「换模型」指向,否则退中性参数形态(可能是任意参数问题,不点名图片)。
if isModelParamInvalid(msg) {
if ctx.HasImage && ctx.ModelInCatalog && !ctx.ModelSupportsImages {
return "model " + ctx.Model + " does not support images; pick one with supports_images=true from /v1/models"
}
return "request parameters were rejected by the model provider; check message format and model capabilities"
}
// 11135 invalid_image_data 家族(图片数据无效,Discussion #77 实测形态)。
if isInvalidImageData(msg) {
return "image data rejected by upstream; use a real/valid image, may need a new conversation"
}
switch kind {
case ErrPromptTooLong:
return "request context exceeds the model's limit; reduce history/message size"
case ErrImageInvalid:
return "image request was rejected by upstream; check image_url format and image data"
case ErrWafBlock:
// 账号级 WAF 403 与 IP 级 fail-fast 同 hint:两者对客户端的动作一致
// (等待窗口过去再试,换号/立刻重试无意义)。
return "upstream WAF blocked the gateway; retry after the block window"
case ErrSoftRate:
return "rate limited by upstream; retry after reset"
case ErrAccountFault:
return "account-level fault at upstream (auth/quota state); the gateway will rotate or disable this account"
case ErrSessionDead:
return "account session expired at upstream; the account is disabled until re-login"
case ErrHardCredit:
return "account credits exhausted at upstream; waiting for daily check-in to restore"
case ErrModelBlocked:
return "upstream has no such model on this backend; switch model or retry on another account"
case ErrContentBlocked:
// 措辞不含 "upstream":content_blocked 响应有不含上游字样的既有口径
// (handler_test 的泄漏守卫),hint 遵守同一口径。
return "request content was rejected by content policy; adjust the prompt and retry"
default:
// ErrNone/ErrNotFound/ErrServer/ErrBadParams/ErrClient 等未覆盖形态:无 hint。
return ""
}
}
// HintContext gateway_hint 判定所需的请求侧上下文(handler 侧组装,见
// Handler.hintContext)。零值合法。
type HintContext struct {
Model string // 请求裸模型名(可空)
HasImage bool // 请求体是否携带 image_url part
ModelSupportsImages bool // 模型目录 supports_images 声明(仅 ModelInCatalog 时有意义)
ModelInCatalog bool // 模型目录是否收录该模型(「不支持」判定的前提)
}
// noHealthyHint 本地调度类错误(池中无健康号可用/传输层抖动,无上游原文可透传)
// 的固定 hint。不进 GatewayHint:它没有 ErrKind,是网关自己的调度事实。
const noHealthyHint = "no healthy account available in pool; check /status or retry later"
// NoHealthyAccountHint 本地调度错误的 gateway_hint(与 no_healthy_account code 配套)。
func NoHealthyAccountHint() string { return noHealthyHint }
// FrameHintFunc 返回 SSE error 帧的 gateway_hint 判定函数(Stream 的可选参数)。
// ctxFn 惰性求值:仅在实际撞到 error 帧才调用(正常流零开销,模型目录查询
// 不会为每个成功请求触发)。
func FrameHintFunc(ctxFn func() HintContext) func(string) string {
return func(payload string) string {
if payload == "" || payload == "[DONE]" {
return ""
}
return GatewayHint(FrameKind(payload), payload, ctxFn())
}
}
// FrameKind 从 SSE error 帧 payload 判定 ErrKind:6004 模型级限流的流式形态
// (IsModelRateLimit 对帧 JSON 直接命中)优先;其余取帧内 error.message 走
// Classify(请求级 400 口径)。判不出 → ErrNone(无 hint)。
func FrameKind(payload string) ErrKind {
if IsModelRateLimit(payload) {
return ErrSoftRate
}
var f struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
if json.Unmarshal([]byte(payload), &f) != nil || f.Error.Message == "" {
return ErrNone
}
return Classify(http.StatusBadRequest, f.Error.Message)
}
// isModelParamInvalid 上游 11133 body 判定(code 11133 / extError.code=
// model_param_invalid / msg 文案家族)。子串口径:hint 是补充说明非权威分类,
// 宁宽勿漏。
func isModelParamInvalid(body string) bool {
lower := strings.ToLower(body)
return codeMarker(lower, "11133") ||
strings.Contains(lower, "model_param_invalid") ||
strings.Contains(lower, "invalid request parameters") ||
strings.Contains(lower, "request parameters do not meet the current model requirements")
}
// isInvalidImageData 上游 11135 body 判定(code 11135 / invalid_image_data /
// "replace the image" msg 家族)。
func isInvalidImageData(body string) bool {
lower := strings.ToLower(body)
return codeMarker(lower, "11135") ||
strings.Contains(lower, "invalid_image_data") ||
strings.Contains(lower, "replace the image")
}
// codeMarker JSON code 字段命中(`"code":N` / `"code": N` / `"code":"N"` 形态,
// 与 IsModelBlocked 的 code 判定同容差口径)。lower 须为小写 body。
func codeMarker(lower, code string) bool {
for _, v := range []string{`"code":` + code, `"code": ` + code, `"code":"` + code + `"`, `"code": "` + code + `"`, `"code":" ` + code + `"`, `"code": '` + code + `'`} {
if strings.Contains(lower, v) {
return true
}
}
return false
}