fix(openai): 和解 #4304 与 #4306 的透传错误语义冲突

两个 PR 分别合并后在 main 上语义相撞(文本无冲突,CI 才暴露):
- #4306 把所有非 failover 透传错误重建为通用净化信封;
- #4304 要求确定性 context-window 错误不切号且文案对客户端可见
  (如触发客户端自动压缩),其测试断言原文可达。

和解方案(保留双方意图):
- 净化器拆出 writeOpenAIPassthroughErrorEnvelope;context-window 错误
  仍走本地 JSON 信封 + 净化头策略,但 message 使用脱敏后的上游消息,
  不再抹成通用文案;其余错误净化行为不变。
- RebuildsUpstreamErrors 的两个 5xx 用例改用非瞬时状态码(530/501):
  瞬时 5xx 对 API-key 账号已按 #4304 契约走 failover(由
  APIKeyPassthrough_Transient5xxTriggersFailover 覆盖),净化重建
  路径的 5xx 覆盖由非瞬时状态码继续承担。
This commit is contained in:
shaw
2026-07-15 10:40:14 +08:00
parent 40a59c9e86
commit cf6aa1a5c3
2 changed files with 25 additions and 9 deletions
@@ -517,9 +517,6 @@ func validOpenAIPassthroughRetryAfter(raw string, now time.Time) bool {
}
func writeSanitizedOpenAIPassthroughError(c *gin.Context, upstreamStatus int, upstreamHeaders http.Header) {
if c == nil {
return
}
downstreamStatus := upstreamStatus
message := "Upstream request failed"
switch upstreamStatus {
@@ -534,6 +531,15 @@ func writeSanitizedOpenAIPassthroughError(c *gin.Context, upstreamStatus int, up
message = "Upstream service temporarily unavailable"
}
}
writeOpenAIPassthroughErrorEnvelope(c, downstreamStatus, upstreamHeaders, message)
}
// writeOpenAIPassthroughErrorEnvelope 以本地 JSON 信封 + 净化后的头策略写出
// 错误响应;message 由调用方决定(净化通用文案或脱敏后的上游消息)。
func writeOpenAIPassthroughErrorEnvelope(c *gin.Context, downstreamStatus int, upstreamHeaders http.Header, message string) {
if c == nil {
return
}
body, _ := json.Marshal(gin.H{
"error": gin.H{
"type": "upstream_error",
@@ -645,7 +651,14 @@ func (s *OpenAIGatewayService) handleErrorResponsePassthrough(
Detail: upstreamDetail,
UpstreamResponseBody: upstreamDetail,
})
writeSanitizedOpenAIPassthroughError(c, resp.StatusCode, resp.Header)
// context-window 超限是确定性请求失败(shouldFailoverOpenAIPassthroughResponse
// 已保证不切号),其文案对客户端可操作(如触发自动压缩);在净化信封内保留
// 脱敏后的上游消息,而不是抹成通用文案。
if isOpenAIContextWindowError(upstreamMsg, body) && upstreamMsg != "" {
writeOpenAIPassthroughErrorEnvelope(c, resp.StatusCode, resp.Header, upstreamMsg)
} else {
writeSanitizedOpenAIPassthroughError(c, resp.StatusCode, resp.Header)
}
return fmt.Errorf("upstream error: %d (client response sanitized)", resp.StatusCode)
}
@@ -1049,20 +1049,23 @@ func TestOpenAIGatewayService_APIKeyPassthrough_RebuildsUpstreamErrors(t *testin
wantStatus: http.StatusBadGateway,
wantMessage: "Upstream authentication failed",
},
// 瞬时 5xx(500/502/503/504/520-524)对 API-key 账号已改走多账号
// failover(见 APIKeyPassthrough_Transient5xxTriggersFailover),此处
// 改用非瞬时 5xx 状态码,继续覆盖净化重建路径。
{
name: "html 5xx",
statusCode: http.StatusBadGateway,
statusCode: 530,
contentType: "text/html; charset=UTF-8",
responseBody: `<!DOCTYPE html><title>secret-upstream.example | 502: Bad gateway</title>`,
wantStatus: http.StatusBadGateway,
responseBody: `<!DOCTYPE html><title>secret-upstream.example | 530: Origin DNS error</title>`,
wantStatus: 530,
wantMessage: "Upstream service temporarily unavailable",
},
{
name: "structured 5xx",
statusCode: http.StatusInternalServerError,
statusCode: http.StatusNotImplemented,
contentType: "application/json",
responseBody: `{"error":{"message":"secret-upstream.example internal failure"}}`,
wantStatus: http.StatusInternalServerError,
wantStatus: http.StatusNotImplemented,
wantMessage: "Upstream service temporarily unavailable",
},
{