File size: 7,719 Bytes
6d60378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// 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
}