mirror of
https://github.com/vxcontrol/langchaingo.git
synced 2026-07-19 13:45:47 -04:00
fix(llms): default adaptive reasoning with no effort to high in one place
WithAdaptiveReasoning with an empty effort was handled inconsistently: the Anthropic and Bedrock paths coerced it to high inline, while OpenAI and googleai read GetEffort/GetTokens, which returned none/zero and silently disabled reasoning. The same option produced opposite behavior per provider. Move the default into GetEffort and GetTokens so every provider resolves an empty adaptive effort to high, and drop the now-redundant inline coercions in the three Anthropic paths. Covered by GetEffort/GetTokens table cases and an OpenAI wire test asserting reasoning_effort=high is sent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -177,15 +177,13 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
|
||||
// flag: adaptive-only generations reject budget thinking and vice versa,
|
||||
// so honor the caller's preference only where the model accepts it.
|
||||
if reasoning.ResolveClaudeAdaptive(model, opts.Reasoning.Adaptive) {
|
||||
effort := opts.Reasoning.Effort
|
||||
if effort == llms.ReasoningNone {
|
||||
effort = llms.ReasoningHigh // match the server default instead of sending an empty output_config
|
||||
}
|
||||
thinking = &anthropicclient.ThinkingPayload{
|
||||
Type: "adaptive",
|
||||
Display: "summarized",
|
||||
}
|
||||
outputConfig = &anthropicclient.OutputConfig{Effort: string(effort)}
|
||||
outputConfig = &anthropicclient.OutputConfig{
|
||||
Effort: string(opts.Reasoning.GetEffort(opts.GetMaxTokens())),
|
||||
}
|
||||
} else {
|
||||
thinking = &anthropicclient.ThinkingPayload{
|
||||
Type: "enabled",
|
||||
|
||||
@@ -143,10 +143,7 @@ func (c *ConverseClient) buildConverseInput(input *ConverseInput) (*bedrockrunti
|
||||
if input.ReasoningConfig != nil {
|
||||
additionalModelFields := converseAdditionalModelRequestFields{}
|
||||
setAdaptive := func() {
|
||||
effort := input.ReasoningConfig.Effort
|
||||
if effort == llms.ReasoningNone {
|
||||
effort = llms.ReasoningHigh // match the server default instead of sending an empty output_config
|
||||
}
|
||||
effort := input.ReasoningConfig.GetEffort(0)
|
||||
additionalModelFields.Thinking = &converseThinkingPayload{Type: "adaptive", Display: "summarized"}
|
||||
additionalModelFields.OutputConfig = &converseOutputConfig{Effort: string(effort)}
|
||||
// Adaptive models reject sampling params.
|
||||
|
||||
@@ -670,12 +670,8 @@ func applyAnthropicReasoning(
|
||||
}
|
||||
|
||||
setAdaptive := func() {
|
||||
effort := cfg.Effort
|
||||
if effort == llms.ReasoningNone {
|
||||
effort = llms.ReasoningHigh // match the server default instead of sending an empty output_config
|
||||
}
|
||||
input.Thinking = &anthropicThinkingPayload{Type: "adaptive", Display: "summarized"}
|
||||
input.OutputConfig = &anthropicOutputConfig{Effort: string(effort)}
|
||||
input.OutputConfig = &anthropicOutputConfig{Effort: string(cfg.GetEffort(maxTokens))}
|
||||
input.Temperature = 0
|
||||
input.TopP = 0
|
||||
input.TopK = 0
|
||||
|
||||
@@ -66,3 +66,37 @@ func TestReasoningEffortPassthroughToWire(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdaptiveReasoning with no explicit effort must not silently disable
|
||||
// reasoning here: it defaults to high, matching the Anthropic/Bedrock paths.
|
||||
func TestAdaptiveReasoningNoEffortDefaultsToHighOnWire(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const completion = `{"id":"x","object":"chat.completion","created":1,"model":"m",` +
|
||||
`"choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],` +
|
||||
`"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`
|
||||
|
||||
var body string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
body = string(b)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, completion)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
llm, err := New(WithBaseURL(srv.URL), WithToken("test"), WithModel("reasoning-model"))
|
||||
if err != nil {
|
||||
t.Fatalf("New() error: %v", err)
|
||||
}
|
||||
if _, err := llm.GenerateContent(
|
||||
context.Background(),
|
||||
[]llms.MessageContent{llms.TextParts(llms.ChatMessageTypeHuman, "hi")},
|
||||
llms.WithAdaptiveReasoning(llms.ReasoningNone),
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateContent() error: %v", err)
|
||||
}
|
||||
if !strings.Contains(body, `"reasoning_effort":"high"`) {
|
||||
t.Fatalf("adaptive-no-effort must send reasoning_effort=high, got body: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -91,6 +91,10 @@ func (r *ReasoningConfig) GetEffort(maxTokens int) ReasoningEffort {
|
||||
return r.Effort
|
||||
}
|
||||
|
||||
if r.Adaptive {
|
||||
return ReasoningHigh // adaptive with no explicit effort defaults to high
|
||||
}
|
||||
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = 8192
|
||||
}
|
||||
@@ -141,7 +145,13 @@ func (r *ReasoningConfig) GetTokens(maxTokens int) int {
|
||||
// no distinct token budget for xhigh/max; map them to the high budget.
|
||||
tokens = max(maxTokens/2, 4096)
|
||||
case ReasoningNone:
|
||||
return 0 // disabled
|
||||
if r.Adaptive {
|
||||
// adaptive with no explicit effort defaults to the high budget,
|
||||
// so providers that map reasoning to a token budget don't disable it.
|
||||
tokens = max(maxTokens/2, 4096)
|
||||
} else {
|
||||
return 0 // disabled
|
||||
}
|
||||
default:
|
||||
return -1 // error value to be handled on the server side
|
||||
}
|
||||
|
||||
@@ -596,6 +596,18 @@ func TestReasoningConfig_GetEffort(t *testing.T) {
|
||||
maxTokens: maxTokens,
|
||||
expected: llms.ReasoningHigh,
|
||||
},
|
||||
{
|
||||
name: "adaptive with no effort defaults to high",
|
||||
config: &llms.ReasoningConfig{Adaptive: true},
|
||||
maxTokens: maxTokens,
|
||||
expected: llms.ReasoningHigh,
|
||||
},
|
||||
{
|
||||
name: "adaptive with explicit effort keeps it",
|
||||
config: &llms.ReasoningConfig{Adaptive: true, Effort: llms.ReasoningLow},
|
||||
maxTokens: maxTokens,
|
||||
expected: llms.ReasoningLow,
|
||||
},
|
||||
{
|
||||
name: "xhigh effort passes through on the effort path",
|
||||
config: &llms.ReasoningConfig{Effort: llms.ReasoningXHigh},
|
||||
@@ -680,6 +692,12 @@ func TestReasoningConfig_GetTokens(t *testing.T) {
|
||||
maxTokens: maxTokens,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "adaptive with no effort defaults to high budget",
|
||||
config: &llms.ReasoningConfig{Adaptive: true},
|
||||
maxTokens: maxTokens,
|
||||
expected: max(maxTokens/2, 4096),
|
||||
},
|
||||
{
|
||||
name: "config with explicit tokens",
|
||||
config: &llms.ReasoningConfig{Tokens: 3000},
|
||||
|
||||
Reference in New Issue
Block a user