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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user