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
+81 -4
View File
@@ -50,26 +50,49 @@ func ValidateOpenAIReasoningEffort(value any) error {
}
func applyOpenAIChatReasoningParams(body map[string]any, candidate store.RuntimeModelCandidate) {
_ = applyOpenAIChatReasoningParamsWithSource(body, candidate, nil)
}
func applyOpenAIChatReasoningParamsWithSource(body map[string]any, candidate store.RuntimeModelCandidate, original map[string]any) error {
effort := normalizedReasoningString(body["reasoning_effort"])
_, explicitEffort := original["reasoning_effort"]
_, explicitTemperature := original["temperature"]
model := chatReasoningModelName(body, candidate)
if isAliyunBailianOpenAI(candidate) && isAliyunQwen38MaxPreview(model) {
if explicitEffort && qwen38MaxPreviewReasoningEffort(effort) != effort {
return explicitParameterAdaptationError("reasoning_effort", "the selected upstream only supports low, medium, or xhigh without changing the requested reasoning semantics")
}
if temperature, ok := finiteFloatFromAny(body["temperature"]); explicitTemperature && ok && temperature < 0.6 {
return explicitParameterAdaptationError("temperature", "the selected upstream requires temperature >= 0.6")
}
applyAliyunQwen38Reasoning(body, effort)
if temperature, ok := finiteFloatFromAny(body["temperature"]); ok && temperature < 0.6 {
body["temperature"] = 0.6
}
return
return nil
}
if effort == "" || !isOpenAIReasoningEffort(effort) {
return
return nil
}
resolved, state := resolveCandidateReasoningEffort(effort, candidate)
if state == reasoningCapabilityUnsupported {
if explicitEffort {
return explicitParameterAdaptationError("reasoning_effort", "the selected upstream does not support reasoning_effort")
}
delete(body, "reasoning_effort")
return
return nil
}
if resolved != "" {
if explicitEffort && resolved != effort {
return explicitParameterAdaptationError("reasoning_effort", "the selected upstream does not support the requested reasoning effort exactly")
}
effort = resolved
}
if explicitEffort {
if err := validateExplicitProviderReasoningEffort(effort, candidate, model); err != nil {
return err
}
}
body["reasoning_effort"] = effort
switch {
@@ -82,9 +105,42 @@ func applyOpenAIChatReasoningParams(body map[string]any, candidate store.Runtime
case isVolcesOpenAI(candidate):
applyVolcesReasoning(body, candidate, effort)
}
return nil
}
func validateExplicitProviderReasoningEffort(effort string, candidate store.RuntimeModelCandidate, model string) error {
if effort == "none" {
return nil
}
unsupported := func() error {
return explicitParameterAdaptationError("reasoning_effort", "the selected upstream cannot preserve the requested reasoning effort exactly")
}
switch {
case isAliyunBailianOpenAI(candidate):
if !isAliyunHighMaxReasoningModel(model) || highMaxReasoningEffort(effort) != effort {
return unsupported()
}
case isDeepSeekOpenAI(candidate):
if highMaxReasoningEffort(effort) != effort {
return unsupported()
}
case isZhipuOpenAI(candidate):
if !isZhipuReasoningEffortModel(model) || zhipuReasoningEffort(effort) != effort {
return unsupported()
}
case isVolcesOpenAI(candidate):
if !isVolcesReasoningEffortModel(model) || volcesChatReasoningEffort(effort) != effort {
return unsupported()
}
}
return nil
}
func applyOpenAIResponsesReasoningParams(body map[string]any, candidate store.RuntimeModelCandidate) {
_ = applyOpenAIResponsesReasoningParamsWithSource(body, candidate, nil)
}
func applyOpenAIResponsesReasoningParamsWithSource(body map[string]any, candidate store.RuntimeModelCandidate, original map[string]any) error {
reasoning, _ := body["reasoning"].(map[string]any)
if reasoning != nil {
reasoning = cloneBody(reasoning)
@@ -93,6 +149,9 @@ func applyOpenAIResponsesReasoningParams(body map[string]any, candidate store.Ru
if reasoning != nil {
effort = normalizedReasoningString(reasoning["effort"])
}
originalReasoning, _ := original["reasoning"].(map[string]any)
_, explicitEffort := originalReasoning["effort"]
_, explicitTemperature := original["temperature"]
model := chatReasoningModelName(body, candidate)
qwen38 := isAliyunBailianOpenAI(candidate) && isAliyunQwen38MaxPreview(model)
if qwen38 {
@@ -101,13 +160,23 @@ func applyOpenAIResponsesReasoningParams(body map[string]any, candidate store.Ru
effort = "low"
}
} else {
effort = qwen38MaxPreviewReasoningEffort(effort)
mapped := qwen38MaxPreviewReasoningEffort(effort)
if explicitEffort && mapped != effort {
return explicitParameterAdaptationError("reasoning.effort", "the selected upstream only supports low, medium, or xhigh without changing the requested reasoning semantics")
}
effort = mapped
}
} else if effort != "" && isOpenAIReasoningEffort(effort) {
resolved, state := resolveCandidateReasoningEffort(effort, candidate)
if state == reasoningCapabilityUnsupported {
if explicitEffort {
return explicitParameterAdaptationError("reasoning.effort", "the selected upstream does not support reasoning.effort")
}
effort = ""
} else if resolved != "" {
if explicitEffort && resolved != effort {
return explicitParameterAdaptationError("reasoning.effort", "the selected upstream does not support the requested reasoning effort exactly")
}
effort = resolved
}
}
@@ -129,9 +198,17 @@ func applyOpenAIResponsesReasoningParams(body map[string]any, candidate store.Ru
delete(body, "enable_thinking")
if qwen38 {
if temperature, ok := finiteFloatFromAny(body["temperature"]); ok && temperature < 0.6 {
if explicitTemperature {
return explicitParameterAdaptationError("temperature", "the selected upstream requires temperature >= 0.6")
}
body["temperature"] = 0.6
}
}
return nil
}
func explicitParameterAdaptationError(param string, message string) error {
return &ClientError{Code: "invalid_parameter", Message: message, Param: param, StatusCode: 400, Retryable: false}
}
func applyAliyunQwen38Reasoning(body map[string]any, effort string) {