From 8dde9db229c367db61a4b6b66ee46af8209080df Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Mon, 13 Jul 2026 10:58:14 +0700 Subject: [PATCH] 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 --- llms/anthropic/adaptive_thinking_test.go | 8 +++++--- llms/anthropic/anthropicllm.go | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/llms/anthropic/adaptive_thinking_test.go b/llms/anthropic/adaptive_thinking_test.go index 3103f1be..f808847f 100644 --- a/llms/anthropic/adaptive_thinking_test.go +++ b/llms/anthropic/adaptive_thinking_test.go @@ -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) } diff --git a/llms/anthropic/anthropicllm.go b/llms/anthropic/anthropicllm.go index bcb0e896..72da22b6 100644 --- a/llms/anthropic/anthropicllm.go +++ b/llms/anthropic/anthropicllm.go @@ -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 }