fix(apicompat): strip temperature/top_p for reasoning models in Responses conversion
gpt-5.x models served via the OpenAI Responses API reject requests that
include temperature or top_p with:
{"detail":"Unsupported parameter: temperature"}
This caused ClaudeCode agent/subagent tool requests to fail with a 400
error when an OpenAI group had the Messages-format support enabled.
Root cause: AnthropicToResponses and ChatCompletionsToResponses were
unconditionally forwarding temperature and top_p from the incoming
request to the ResponsesRequest, even though all gpt-5.x reasoning
models reject these sampling parameters.
Fix:
- Add isReasoningModel(model string) bool helper that returns true for
any model whose name starts with "gpt-5".
- Skip temperature and top_p when converting to ResponsesRequest for
reasoning models. Non-reasoning models (e.g. gpt-4o) are unaffected.
- ResponsesRequest.Temperature and TopP are already *float64 with
omitempty, so nil values are safely omitted from the JSON body.
Tests:
- TestAnthropicToResponses_TemperatureStrippedForReasoningModel
- TestAnthropicToResponses_TemperatureStrippedForAllGpt5Variants
- TestChatCompletionsToResponses_TemperatureStrippedForReasoningModel
- TestChatCompletionsToResponses_TemperaturePreservedForNonReasoningModel
Fixes #2487
This commit is contained in:
parent
8927ab091e
commit
276b5c7755
@ -1524,3 +1524,49 @@ func TestAnthropicToResponses_ToolWithNilSchema(t *testing.T) {
|
|||||||
assert.JSONEq(t, `"object"`, string(params["type"]))
|
assert.JSONEq(t, `"object"`, string(params["type"]))
|
||||||
assert.JSONEq(t, `{}`, string(params["properties"]))
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -22,12 +22,19 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := &ResponsesRequest{
|
out := &ResponsesRequest{
|
||||||
Model: req.Model,
|
Model: req.Model,
|
||||||
Input: inputJSON,
|
Input: inputJSON,
|
||||||
Temperature: req.Temperature,
|
Stream: req.Stream,
|
||||||
TopP: req.TopP,
|
Include: []string{"reasoning.encrypted_content"},
|
||||||
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
|
storeFalse := false
|
||||||
@ -437,6 +444,14 @@ func boolPtr(v bool) *bool {
|
|||||||
return &v
|
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
|
// normalizeToolParameters ensures the tool parameter schema is valid for
|
||||||
// OpenAI's Responses API, which requires "properties" on object schemas.
|
// OpenAI's Responses API, which requires "properties" on object schemas.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -331,6 +331,48 @@ func TestChatCompletionsToResponses_ServiceTier(t *testing.T) {
|
|||||||
assert.Equal(t, "flex", resp.ServiceTier)
|
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) {
|
func TestChatCompletionsToResponses_AssistantWithTextAndToolCalls(t *testing.T) {
|
||||||
req := &ChatCompletionsRequest{
|
req := &ChatCompletionsRequest{
|
||||||
Model: "gpt-4o",
|
Model: "gpt-4o",
|
||||||
|
|||||||
@ -30,13 +30,18 @@ func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest,
|
|||||||
Model: req.Model,
|
Model: req.Model,
|
||||||
Instructions: req.Instructions,
|
Instructions: req.Instructions,
|
||||||
Input: inputJSON,
|
Input: inputJSON,
|
||||||
Temperature: req.Temperature,
|
|
||||||
TopP: req.TopP,
|
|
||||||
Stream: true, // upstream always streams
|
Stream: true, // upstream always streams
|
||||||
Include: []string{"reasoning.encrypted_content"},
|
Include: []string{"reasoning.encrypted_content"},
|
||||||
ServiceTier: req.ServiceTier,
|
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
|
storeFalse := false
|
||||||
out.Store = &storeFalse
|
out.Store = &storeFalse
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user