fixup! fix(openai-gateway): route APIKey accounts to /v1/chat/completions when upstream lacks Responses API

Address self-review findings:

R7: Use a narrow per-trust-domain header allowlist for CC raw forwarding.
The previously reused openaiAllowedHeaders contains Codex client-only headers
(originator/session_id/x-codex-turn-state/x-codex-turn-metadata/conversation_id)
that must not leak to third-party OpenAI-compatible upstreams (DeepSeek/Kimi/
GLM/Qwen). Strict upstreams may 400 with 'unknown parameter'; lenient ones
silently pollute their request statistics. New openaiCCRawAllowedHeaders only
allows generic HTTP headers (accept-language, user-agent); content-type/
authorization/accept are set explicitly by callers.

R4: Drop the dead includeUsage parameter from streamRawChatCompletions.
The CC pass-through path doesn't need to inspect the client's stream_options
flag — the upstream handles it and we only extract usage when it appears in
chunks. Killing the unused parameter removes a misleading 'parameter read
but discarded' code smell.

Sediment refs:
- pensieve/short-term/maxims/dont-reuse-shared-headers-whitelist-across-different-upstream-trust-domains
- pensieve/short-term/knowledge/openai-gateway-shared-state-quirks
- pensieve/short-term/pipelines/run-when-self-reviewing-forwarder-implementation
This commit is contained in:
alfadb-bot
2026-04-30 20:16:44 +08:00
parent 4e4cc80971
commit 4d145300c3

View File

@@ -20,6 +20,25 @@ import (
"go.uber.org/zap"
)
// openaiCCRawAllowedHeaders 是 CC 直转路径专用的客户端 header 透传白名单。
//
// **关键**:不能复用 openaiAllowedHeaders——后者含 Codex 客户端专属 header
// originator / session_id / x-codex-turn-state / x-codex-turn-metadata / conversation_id
// 这些在 ChatGPT OAuth 上游是必需的,但透传给 DeepSeek/Kimi/GLM 等第三方
// OpenAI 兼容上游会造成:
// - 完全忽略(多数友好厂商)——隐性污染上游统计
// - 400 "unknown parameter"(严格上游)——可见错误
//
// 这里仅放行通用 HTTP headercontent-type / authorization / accept 由上下文
// 显式设置,不依赖透传。
//
// 参见决策记录:
// pensieve/short-term/maxims/dont-reuse-shared-headers-whitelist-across-different-upstream-trust-domains
var openaiCCRawAllowedHeaders = map[string]bool{
"accept-language": true,
"user-agent": true,
}
// forwardAsRawChatCompletions 直转客户端的 Chat Completions 请求到上游
// `{base_url}/v1/chat/completions`**不**做 CC↔Responses 协议转换。
//
@@ -53,7 +72,6 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
return nil, fmt.Errorf("missing model in request")
}
clientStream := gjson.GetBytes(body, "stream").Bool()
includeUsage := gjson.GetBytes(body, "stream_options.include_usage").Bool()
// 2. Resolve model mapping (same as ForwardAsChatCompletions)
billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
@@ -111,10 +129,10 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
upstreamReq.Header.Set("Accept", "application/json")
}
// Whitelist passthrough headers (subset of openaiAllowedHeaders relevant to CC).
// 透传白名单中的客户端 header。详见 openaiCCRawAllowedHeaders 的设计说明。
for key, values := range c.Request.Header {
lowerKey := strings.ToLower(key)
if openaiAllowedHeaders[lowerKey] {
if openaiCCRawAllowedHeaders[lowerKey] {
for _, v := range values {
upstreamReq.Header.Add(key, v)
}
@@ -188,20 +206,23 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
// 8. Forward response
if clientStream {
return s.streamRawChatCompletions(c, resp, originalModel, billingModel, upstreamModel, includeUsage, startTime)
return s.streamRawChatCompletions(c, resp, originalModel, billingModel, upstreamModel, startTime)
}
return s.bufferRawChatCompletions(c, resp, originalModel, billingModel, upstreamModel, startTime)
}
// streamRawChatCompletions 透传上游 CC SSE 流到客户端,并提取 usage包括
// 末尾 [DONE] 之前的 chunk 中的 usage 字段,按 OpenAI CC 协议)。
//
// usage 字段仅在客户端请求 stream_options.include_usage=true 时出现于上游响应中。
// 本函数不检查客户端的请求 flag——上游会自行处理我们仅在上游响应
// chunk 中出现 usage 时提取。
func (s *OpenAIGatewayService) streamRawChatCompletions(
c *gin.Context,
resp *http.Response,
originalModel string,
billingModel string,
upstreamModel string,
includeUsage bool,
startTime time.Time,
) (*OpenAIForwardResult, error) {
requestID := resp.Header.Get("x-request-id")
@@ -266,8 +287,6 @@ func (s *OpenAIGatewayService) streamRawChatCompletions(
}
}
_ = includeUsage // CC 协议下 usage 是否包含由客户端的 stream_options 决定,上游会自行处理;我们仅做提取。
return &OpenAIForwardResult{
RequestID: requestID,
Usage: usage,