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
@@ -1,15 +1,9 @@
package clients
import (
"fmt"
"net/http"
"sort"
)
// Keep these lists aligned with openai-node 6.47.0 and the public OpenAI API
// reference. The Gateway accepts a small, explicit set of routing extensions at
// ingress, but only protocol fields (plus controlled provider adaptations) are
// allowed across the upstream boundary.
// reference. They document and test the currently known surface, but are not an
// upstream allowlist: unknown fields may be introduced by OpenAI after a
// Gateway release and must remain transparent.
var openAIChatRequestParameters = stringSet(
"messages", "model", "audio", "frequency_penalty", "function_call", "functions",
"logit_bias", "logprobs", "max_completion_tokens", "max_tokens", "metadata",
@@ -50,56 +44,36 @@ var controlledOpenAIChatProviderParameters = stringSet(
var controlledOpenAIResponsesProviderParameters = stringSet("presence_penalty", "frequency_penalty")
func ValidateOpenAIRequestParameters(kind string, body map[string]any) error {
allowed := openAIChatRequestParameters
if kind == "responses" {
allowed = openAIResponsesRequestParameters
}
unknown := make([]string, 0)
for key := range body {
if _, ok := allowed[key]; ok {
continue
}
if _, ok := gatewayOpenAIRequestExtensions[key]; ok {
continue
}
if kind == "responses" {
if _, ok := gatewayResponsesRequestExtensions[key]; ok {
continue
}
}
unknown = append(unknown, key)
}
if len(unknown) == 0 {
return nil
}
sort.Strings(unknown)
return &ClientError{
Code: "invalid_parameter",
Message: fmt.Sprintf("Unknown parameter: %s", unknown[0]),
Param: unknown[0],
StatusCode: http.StatusBadRequest,
Retryable: false,
}
// OpenAI-compatible public endpoints intentionally accept future official
// fields. Provider validation remains authoritative for fields the selected
// upstream does not support.
_ = kind
_ = body
return nil
}
func FilterOpenAIChatRequestBody(body map[string]any) map[string]any {
return filterOpenAIRequestBody(body, openAIChatRequestParameters, controlledOpenAIChatProviderParameters)
return filterOpenAIRequestBody(body, controlledOpenAIChatProviderParameters, nil)
}
func FilterOpenAIResponsesRequestBody(body map[string]any) map[string]any {
return filterOpenAIRequestBody(body, openAIResponsesRequestParameters, controlledOpenAIResponsesProviderParameters)
return filterOpenAIRequestBody(body, controlledOpenAIResponsesProviderParameters, gatewayResponsesRequestExtensions)
}
func filterOpenAIRequestBody(body map[string]any, allowed map[string]struct{}, extensions map[string]struct{}) map[string]any {
func filterOpenAIRequestBody(body map[string]any, controlled map[string]struct{}, protocolInternal map[string]struct{}) map[string]any {
out := make(map[string]any, len(body))
for key, value := range body {
if _, ok := allowed[key]; ok {
if _, ok := controlled[key]; ok {
out[key] = value
continue
}
if _, ok := extensions[key]; ok {
out[key] = value
if _, internal := gatewayOpenAIRequestExtensions[key]; internal {
continue
}
if _, internal := protocolInternal[key]; internal {
continue
}
out[key] = value
}
return out
}