Merge pull request #2580 from wucm667/fix/openai-responses-strip-temperature

fix(apicompat): Responses 转换为推理模型时剥离不支持的 temperature 参数
This commit is contained in:
Wesley Liddick 2026-05-20 08:42:26 +08:00 committed by GitHub
commit ec283cb072
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 116 additions and 8 deletions

View File

@ -1524,3 +1524,49 @@ func TestAnthropicToResponses_ToolWithNilSchema(t *testing.T) {
assert.JSONEq(t, `"object"`, string(params["type"]))
assert.JSONEq(t, `{}`, string(params["properties"]))
}
// ---------------------------------------------------------------------------
// isReasoningModel / temperature-stripping tests
// ---------------------------------------------------------------------------
func TestAnthropicToResponses_TemperatureStrippedForReasoningModel(t *testing.T) {
temp := 0.7
req := &AnthropicRequest{
Model: "gpt-5.2",
MaxTokens: 1024,
Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
Temperature: &temp,
TopP: &temp,
}
resp, err := AnthropicToResponses(req)
require.NoError(t, err)
assert.Nil(t, resp.Temperature, "reasoning model: temperature must be stripped")
assert.Nil(t, resp.TopP, "reasoning model: top_p must be stripped")
// Verify the fields are absent from the serialised JSON.
b, err := json.Marshal(resp)
require.NoError(t, err)
assert.NotContains(t, string(b), `"temperature"`)
assert.NotContains(t, string(b), `"top_p"`)
}
func TestAnthropicToResponses_TemperatureStrippedForAllGpt5Variants(t *testing.T) {
temp := 1.0
models := []string{"gpt-5.2", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.5"}
for _, model := range models {
t.Run(model, func(t *testing.T) {
req := &AnthropicRequest{
Model: model,
MaxTokens: 1024,
Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
Temperature: &temp,
TopP: &temp,
}
resp, err := AnthropicToResponses(req)
require.NoError(t, err)
assert.Nil(t, resp.Temperature, "model %s: temperature must be stripped", model)
assert.Nil(t, resp.TopP, "model %s: top_p must be stripped", model)
})
}
}

View File

@ -22,12 +22,19 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
}
out := &ResponsesRequest{
Model: req.Model,
Input: inputJSON,
Temperature: req.Temperature,
TopP: req.TopP,
Stream: req.Stream,
Include: []string{"reasoning.encrypted_content"},
Model: req.Model,
Input: inputJSON,
Stream: req.Stream,
Include: []string{"reasoning.encrypted_content"},
}
// Reasoning models (gpt-5.x) served via the Responses API do not accept
// sampling parameters. Sending temperature or top_p causes a 400
// "Unsupported parameter" error, so we only forward them for non-reasoning
// models.
if !isReasoningModel(req.Model) {
out.Temperature = req.Temperature
out.TopP = req.TopP
}
storeFalse := false
@ -437,6 +444,14 @@ func boolPtr(v bool) *bool {
return &v
}
// isReasoningModel reports whether model is a reasoning model that does not
// support sampling parameters (temperature, top_p) via the Responses API.
// All gpt-5.x models are reasoning-only; the Responses API returns
// "Unsupported parameter: temperature" if these fields are present.
func isReasoningModel(model string) bool {
return strings.HasPrefix(model, "gpt-5")
}
// normalizeToolParameters ensures the tool parameter schema is valid for
// OpenAI's Responses API, which requires "properties" on object schemas.
//

View File

@ -331,6 +331,48 @@ func TestChatCompletionsToResponses_ServiceTier(t *testing.T) {
assert.Equal(t, "flex", resp.ServiceTier)
}
// ---------------------------------------------------------------------------
// temperature / top_p stripping for reasoning models
// ---------------------------------------------------------------------------
func TestChatCompletionsToResponses_TemperatureStrippedForReasoningModel(t *testing.T) {
temp := 0.7
req := &ChatCompletionsRequest{
Model: "gpt-5.2",
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
Temperature: &temp,
TopP: &temp,
}
resp, err := ChatCompletionsToResponses(req)
require.NoError(t, err)
assert.Nil(t, resp.Temperature, "reasoning model: temperature must be stripped")
assert.Nil(t, resp.TopP, "reasoning model: top_p must be stripped")
// Must not appear in the serialised request body sent to the upstream.
b, err := json.Marshal(resp)
require.NoError(t, err)
assert.NotContains(t, string(b), `"temperature"`)
assert.NotContains(t, string(b), `"top_p"`)
}
func TestChatCompletionsToResponses_TemperaturePreservedForNonReasoningModel(t *testing.T) {
temp := 0.7
req := &ChatCompletionsRequest{
Model: "gpt-4o",
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
Temperature: &temp,
TopP: &temp,
}
resp, err := ChatCompletionsToResponses(req)
require.NoError(t, err)
require.NotNil(t, resp.Temperature, "non-reasoning model: temperature must be preserved")
assert.InDelta(t, 0.7, *resp.Temperature, 1e-9)
require.NotNil(t, resp.TopP, "non-reasoning model: top_p must be preserved")
assert.InDelta(t, 0.7, *resp.TopP, 1e-9)
}
func TestChatCompletionsToResponses_AssistantWithTextAndToolCalls(t *testing.T) {
req := &ChatCompletionsRequest{
Model: "gpt-4o",

View File

@ -30,13 +30,18 @@ func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest,
Model: req.Model,
Instructions: req.Instructions,
Input: inputJSON,
Temperature: req.Temperature,
TopP: req.TopP,
Stream: true, // upstream always streams
Include: []string{"reasoning.encrypted_content"},
ServiceTier: req.ServiceTier,
}
// Reasoning models (gpt-5.x) do not accept sampling parameters.
// See isReasoningModel in anthropic_to_responses.go.
if !isReasoningModel(req.Model) {
out.Temperature = req.Temperature
out.TopP = req.TopP
}
storeFalse := false
out.Store = &storeFalse