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
@@ -108,7 +108,7 @@ func NewParameterCorrectionCache() *ParameterCorrectionCache {
}
}
func (cache *ParameterCorrectionCache) apply(scope parameterCorrectionScope, body map[string]any) []string {
func (cache *ParameterCorrectionCache) apply(scope parameterCorrectionScope, body map[string]any, protected ...map[string]struct{}) []string {
if cache == nil {
return nil
}
@@ -127,6 +127,9 @@ func (cache *ParameterCorrectionCache) apply(scope parameterCorrectionScope, bod
applied := make([]string, 0, len(elements))
for _, element := range elements {
rule := element.Value.(parameterCorrectionCacheEntry).rule
if correctionParamProtected(rule.Param, protected...) {
continue
}
if applyParameterCorrectionRule(body, rule) {
applied = append(applied, rule.Param)
cache.lru.MoveToFront(element)
@@ -183,7 +186,7 @@ func (cache *ParameterCorrectionCache) size() int {
return cache.lru.Len()
}
func deriveParameterCorrection(err error, body map[string]any, candidate store.RuntimeModelCandidate) (parameterCorrectionRule, bool) {
func deriveParameterCorrection(err error, body map[string]any, candidate store.RuntimeModelCandidate, protected map[string]struct{}) (parameterCorrectionRule, bool) {
var clientErr *ClientError
if !errors.As(err, &clientErr) || (clientErr.StatusCode != 400 && clientErr.StatusCode != 422) {
return parameterCorrectionRule{}, false
@@ -196,9 +199,15 @@ func deriveParameterCorrection(err error, body map[string]any, candidate store.R
if !isSafeCorrectionParam(param) {
return parameterCorrectionRule{}, false
}
if correctionParamProtected(param, protected) {
return parameterCorrectionRule{}, false
}
lowerMessage := strings.ToLower(message)
lowerCode := strings.ToLower(code)
if conflictRule, ok := deriveConflictCorrection(param, lowerMessage); ok {
if correctionParamProtected(conflictRule.Param, protected) {
return parameterCorrectionRule{}, false
}
return conflictRule, true
}
if strings.Contains(lowerMessage, "unknown parameter") ||
@@ -228,6 +237,76 @@ func deriveParameterCorrection(err error, body map[string]any, candidate store.R
return parameterCorrectionRule{}, false
}
func correctionParamProtected(param string, protected ...map[string]struct{}) bool {
for _, values := range protected {
if _, ok := values[normalizeCorrectionParam(param)]; ok {
return true
}
}
return false
}
func callerProtectedCorrectionParameters(request Request, endpointKind string) map[string]struct{} {
if request.OriginalBody == nil {
return nil
}
protected := make(map[string]struct{})
protect := func(param string) {
param = normalizeCorrectionParam(param)
if isSafeCorrectionParam(param) {
protected[param] = struct{}{}
}
}
for key := range request.OriginalBody {
protect(key)
}
if reasoning, ok := request.OriginalBody["reasoning"].(map[string]any); ok {
if _, explicit := reasoning["effort"]; explicit {
if endpointKind == "chat.completions" {
protect("reasoning_effort")
} else {
protect("reasoning.effort")
}
}
}
if request.Kind == "responses" && endpointKind == "chat.completions" {
if _, explicit := request.OriginalBody["max_output_tokens"]; explicit {
protect("max_completion_tokens")
}
if _, explicit := request.OriginalBody["top_logprobs"]; explicit {
protect("top_logprobs")
protect("logprobs")
}
if responseIncludeContains(request.OriginalBody["include"], "message.output_text.logprobs") {
protect("logprobs")
}
if text, ok := request.OriginalBody["text"].(map[string]any); ok {
if _, explicit := text["verbosity"]; explicit {
protect("verbosity")
}
}
}
return protected
}
func responseIncludeContains(value any, target string) bool {
switch values := value.(type) {
case []any:
for _, value := range values {
if stringFromAny(value) == target {
return true
}
}
case []string:
for _, value := range values {
if value == target {
return true
}
}
}
return false
}
func requestIDFromParameterError(err error) string {
var clientErr *ClientError
if errors.As(err, &clientErr) {