feat(openai): 完善 Chat 与 Responses 参数转发

原生 Chat/Responses 改为透明转发,保留标准工具结构并保护调用方显式参数。补齐 Responses 到 Chat 的兼容转换、协议路由边界、完整响应和流式事件,并同步更新 Swagger、回归测试与真实验收脚本。

验证:
- cd apps/api && env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1
- pnpm openapi
- pnpm lint
- pnpm test
- pnpm build
- gofmt -l 无输出
- git diff --check 通过

风险:
- Chat 回退无法等价表达的 Responses 原生能力现在会返回 unsupported_response_parameter
- 真实供应商 E2E 因本地没有已启用的平台模型候选而未完成
This commit is contained in:
2026-08-04 19:26:48 +08:00
parent b2c9b4f6d9
commit fe8dcb40ca
22 changed files with 2098 additions and 309 deletions
+32 -7
View File
@@ -54,10 +54,14 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
return Response{}, normalizeErr
}
body = normalizedBody
applyOpenAIChatReasoningParams(body, request.Candidate)
if err := applyOpenAIChatReasoningParamsWithSource(body, request.Candidate, openAIAdaptationSource(request, endpointKind)); err != nil {
return Response{}, err
}
body = FilterOpenAIChatRequestBody(body)
} else if request.Kind == "responses" {
applyOpenAIResponsesReasoningParams(body, request.Candidate)
if err := applyOpenAIResponsesReasoningParamsWithSource(body, request.Candidate, request.OriginalBody); err != nil {
return Response{}, err
}
body = FilterOpenAIResponsesRequestBody(body)
if _, hasInput := body["input"]; !hasInput {
if messages, hasMessages := request.Body["messages"]; hasMessages {
@@ -79,8 +83,9 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
responseStartedAt := time.Now()
correctionScope := newParameterCorrectionScope(request, endpointKind)
correctionEnabled := endpointKind == "chat.completions" || endpointKind == "responses"
protectedCorrections := callerProtectedCorrectionParameters(request, endpointKind)
if correctionEnabled {
c.Corrections.apply(correctionScope, body)
c.Corrections.apply(correctionScope, body, protectedCorrections)
}
provisionalRules := make([]parameterCorrectionRule, 0, 2)
seenCorrectionErrors := make(map[string]struct{})
@@ -96,6 +101,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Authorization", "Bearer "+apiKey)
applyUpstreamIdempotency(req, request)
if err := notifySubmissionStarted(request); err != nil {
return Response{}, err
}
@@ -119,7 +125,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
return Response{}, annotateResponseError(upstreamErr, requestIDFromParameterError(upstreamErr), responseStartedAt, time.Now())
}
seenCorrectionErrors[fingerprint] = struct{}{}
rule, safe := deriveParameterCorrection(upstreamErr, body, request.Candidate)
rule, safe := deriveParameterCorrection(upstreamErr, body, request.Candidate, protectedCorrections)
if !safe || !applyParameterCorrectionRule(body, rule) {
return Response{}, annotateResponseError(upstreamErr, requestIDFromParameterError(upstreamErr), responseStartedAt, time.Now())
}
@@ -142,7 +148,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
var streamDelta StreamDelta = nativeStreamDelta
var adapter *chatResponsesStreamAdapter
if request.Kind == "responses" && protocol == ProtocolOpenAIChatCompletions && stream {
adapter = newChatResponsesStreamAdapter(request.PublicResponseID, request.Model)
adapter = newChatResponsesStreamAdapter(request.PublicResponseID, request.Model, request.Body)
streamDelta = func(event StreamDeltaEvent) error { return adapter.delta(event, request.StreamDelta) }
}
if stream {
@@ -212,6 +218,19 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
}, nil
}
func openAIAdaptationSource(request Request, endpointKind string) map[string]any {
if request.OriginalBody == nil || request.Kind != "responses" || endpointKind != "chat.completions" {
return request.OriginalBody
}
out := cloneBody(request.OriginalBody)
if reasoning, ok := request.OriginalBody["reasoning"].(map[string]any); ok {
if effort, explicit := reasoning["effort"]; explicit {
out["reasoning_effort"] = effort
}
}
return out
}
func normalizeOpenAIImageRequestBody(endpointKind string, body map[string]any, originalBody map[string]any) {
if endpointKind != "images.generations" && endpointKind != "images.edits" {
return
@@ -541,12 +560,18 @@ func ensureOpenAIStreamUsage(body map[string]any, kind string, stream bool) {
return
}
streamOptions := map[string]any{}
if existing, ok := body["stream_options"].(map[string]any); ok {
if raw, explicit := body["stream_options"]; explicit {
existing, ok := raw.(map[string]any)
if !ok {
return
}
for key, value := range existing {
streamOptions[key] = value
}
}
streamOptions["include_usage"] = true
if _, explicit := streamOptions["include_usage"]; !explicit {
streamOptions["include_usage"] = true
}
body["stream_options"] = streamOptions
}