fix(anthropic): drop top_p with budget thinking to satisfy the API

Budget thinking pins temperature to 1.0, but the Messages API rejects
temperature and top_p specified together (and rejects top_p below 0.95
with thinking enabled). The code forwarded the caller's top_p unchanged,
so WithReasoning(...) + WithTopP(anything) returned 400. Drop top_p on the
budget path, matching the adaptive path which already omits both.

Live-verified against the real API: WithReasoning(Medium,0)+WithTopP(0.9)
and +WithTopP(0.97) both returned 400 before, 200 after. The budget wire
test now asserts top_p is absent rather than passed through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-13 10:58:14 +07:00
parent 49510f5d85
commit 8dde9db229
2 changed files with 10 additions and 5 deletions
+5 -3
View File
@@ -16,7 +16,7 @@ import (
// captureMessagesRequest drives a real GenerateContent call against a recording
// server and returns the decoded outbound /v1/messages body and headers, so
// assertions cover generateMessagesContent (thinking construction, sampling-param
// handling, beta headers), not a hand-built payload.
// handling, beta headers).
func captureMessagesRequest(t *testing.T, callOpts ...llms.CallOption) (map[string]any, http.Header) {
t.Helper()
@@ -124,9 +124,11 @@ func TestAnthropic_BudgetThinkingRequest(t *testing.T) {
_, hasOutputConfig := payload["output_config"]
assert.False(t, hasOutputConfig, "budget thinking has no output_config")
// Budget thinking pins temperature to 1.0 and keeps top_p as provided.
// Budget thinking pins temperature to 1.0 and drops top_p — the API rejects
// temperature and top_p together.
assert.EqualValues(t, 1.0, payload["temperature"])
assert.EqualValues(t, 0.9, payload["top_p"])
_, hasTopP := payload["top_p"]
assert.False(t, hasTopP, "budget thinking must drop top_p")
assert.EqualValues(t, 4096, payload["max_tokens"]) // max(budget*2, maxTokens)
}
+5 -2
View File
@@ -210,8 +210,10 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
}
}
// Budget thinking pins temperature to 1.0; adaptive models reject sampling
// params, so omit temperature/top_p entirely for them.
// Thinking constrains sampling params: the API rejects temperature and top_p
// together, and requires temperature=1.0 with budget thinking. So pin
// temperature and drop top_p for budget; drop both for adaptive (which
// rejects sampling params outright).
temperature, topP, maxTokens := opts.Temperature, opts.TopP, opts.GetMaxTokens()
switch {
case thinking != nil && thinking.Type == "adaptive":
@@ -219,6 +221,7 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
topP = nil
case thinking != nil && thinking.Type == "enabled" && thinking.Budget > 0:
temperature = getFloatPointer(1.0)
topP = nil
maxTokens = max(thinking.Budget*2, maxTokens) // 2x the budget for thinking
}