on main help sign in
fix(agent): fall back to first reasoning level when default is unset (#3218)
f75435a石岳峰
internal/agent/coordinator.go+33−11
@@ -313,6 +313,26 @@ func (c *coordinator) run(ctx context.Context, accept *AcceptedRun, sessionID st
313 return result, originalErr
313 return result, originalErr
314}
314}
315
315
316+// effectiveReasoningEffort returns the reasoning effort to apply for provider calls.
317+// It prefers the user-selected effort when valid, otherwise the model default when
318+// valid, and finally falls back to the first configured reasoning level.
319+func effectiveReasoningEffort(model Model) string {
320+ if !model.CatwalkCfg.CanReason {
321+ return ""
322+ }
323+
324+ if effort := model.ModelCfg.ReasoningEffort; effort != "" && slices.Contains(model.CatwalkCfg.ReasoningLevels, effort) {
325+ return effort
326+ }
327+ if effort := model.CatwalkCfg.DefaultReasoningEffort; effort != "" && slices.Contains(model.CatwalkCfg.ReasoningLevels, effort) {
328+ return effort
329+ }
330+ if len(model.CatwalkCfg.ReasoningLevels) > 0 {
331+ return model.CatwalkCfg.ReasoningLevels[0]
332+ }
333+ return ""
334+}
335+
316func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
336func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
317 options := fantasy.ProviderOptions{}
337 options := fantasy.ProviderOptions{}
318
338
@@ -361,14 +381,16 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
361 return options
381 return options
362 }
382 }
363
383
384+ reasoningEffort := effectiveReasoningEffort(model)
364 shouldSetEffort := model.CatwalkCfg.CanReason &&
385 shouldSetEffort := model.CatwalkCfg.CanReason &&
386+ reasoningEffort != "" &&
365− slices.Contains(model.CatwalkCfg.ReasoningLevels, model.ModelCfg.ReasoningEffort)
387+ slices.Contains(model.CatwalkCfg.ReasoningLevels, reasoningEffort)
366
388
367 switch providerCfg.Type {
389 switch providerCfg.Type {
368 case openai.Name, azure.Name:
390 case openai.Name, azure.Name:
369 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
391 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
370 if !hasReasoningEffort && shouldSetEffort {
392 if !hasReasoningEffort && shouldSetEffort {
371− mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
393+ mergedOptions["reasoning_effort"] = reasoningEffort
372 }
394 }
373 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
395 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
374 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
396 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
@@ -396,7 +418,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
396 case string(catwalk.InferenceProviderAlibabaSingapore):
418 case string(catwalk.InferenceProviderAlibabaSingapore):
397 switch {
419 switch {
398 case !hasEffort && shouldSetEffort:
420 case !hasEffort && shouldSetEffort:
399− extraBody["reasoning_effort"] = model.ModelCfg.ReasoningEffort
421+ extraBody["reasoning_effort"] = reasoningEffort
400 case !hasThink && model.CatwalkCfg.CanReason:
422 case !hasThink && model.CatwalkCfg.CanReason:
401 if model.ModelCfg.Think {
423 if model.ModelCfg.Think {
402 extraBody["thinking"] = map[string]any{"type": "enabled"}
424 extraBody["thinking"] = map[string]any{"type": "enabled"}
@@ -409,7 +431,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
409 default:
431 default:
410 switch {
432 switch {
411 case !hasEffort && shouldSetEffort:
433 case !hasEffort && shouldSetEffort:
412− mergedOptions["effort"] = model.ModelCfg.ReasoningEffort
434+ mergedOptions["effort"] = reasoningEffort
413 case !hasThink && model.ModelCfg.Think:
435 case !hasThink && model.ModelCfg.Think:
414 mergedOptions["thinking"] = map[string]any{"budget_tokens": 2000}
436 mergedOptions["thinking"] = map[string]any{"budget_tokens": 2000}
415 }
437 }
@@ -425,7 +447,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
425 if !hasReasoning && shouldSetEffort {
447 if !hasReasoning && shouldSetEffort {
426 mergedOptions["reasoning"] = map[string]any{
448 mergedOptions["reasoning"] = map[string]any{
427 "enabled": true,
449 "enabled": true,
428− "effort": model.ModelCfg.ReasoningEffort,
450+ "effort": reasoningEffort,
429 }
451 }
430 }
452 }
431 parsed, err := openrouter.ParseOptions(mergedOptions)
453 parsed, err := openrouter.ParseOptions(mergedOptions)
@@ -437,7 +459,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
437 if !hasReasoning && shouldSetEffort {
459 if !hasReasoning && shouldSetEffort {
438 mergedOptions["reasoning"] = map[string]any{
460 mergedOptions["reasoning"] = map[string]any{
439 "enabled": true,
461 "enabled": true,
440− "effort": model.ModelCfg.ReasoningEffort,
462+ "effort": reasoningEffort,
441 }
463 }
442 }
464 }
443 parsed, err := vercel.ParseOptions(mergedOptions)
465 parsed, err := vercel.ParseOptions(mergedOptions)
@@ -454,7 +476,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
454 }
476 }
455 } else {
477 } else {
456 mergedOptions["thinking_config"] = map[string]any{
478 mergedOptions["thinking_config"] = map[string]any{
457− "thinking_level": model.ModelCfg.ReasoningEffort,
479+ "thinking_level": reasoningEffort,
458 "include_thoughts": true,
480 "include_thoughts": true,
459 }
481 }
460 }
482 }
@@ -470,9 +492,9 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
470 if !hasReasoningEffort && shouldSetEffort {
492 if !hasReasoningEffort && shouldSetEffort {
471 switch providerCfg.ID {
493 switch providerCfg.ID {
472 case string(catwalk.InferenceProviderIoNet):
494 case string(catwalk.InferenceProviderIoNet):
473− extraBody["reasoning"] = map[string]string{"effort": model.ModelCfg.ReasoningEffort}
495+ extraBody["reasoning"] = map[string]string{"effort": reasoningEffort}
474 default:
496 default:
475− mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
497+ mergedOptions["reasoning_effort"] = reasoningEffort
476 }
498 }
477 }
499 }
478
500
@@ -492,7 +514,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
492 }
514 }
493 }
515 }
494 case string(catwalk.InferenceProviderZAI), string(catwalk.InferenceProviderDeepSeek):
516 case string(catwalk.InferenceProviderZAI), string(catwalk.InferenceProviderDeepSeek):
495− if model.ModelCfg.Think || model.ModelCfg.ReasoningEffort != "" {
517+ if model.ModelCfg.Think || reasoningEffort != "" {
496 extraBody["thinking"] = map[string]any{
518 extraBody["thinking"] = map[string]any{
497 "type": "enabled",
519 "type": "enabled",
498 }
520 }
@@ -503,7 +525,7 @@ func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.
503 }
525 }
504 case string(catwalk.InferenceProviderFireworks):
526 case string(catwalk.InferenceProviderFireworks):
505 // NOTE: Fireworks break if we set both `reasoning_effort` and `thinking`.
527 // NOTE: Fireworks break if we set both `reasoning_effort` and `thinking`.
506− if model.ModelCfg.ReasoningEffort == "" {
528+ if reasoningEffort == "" {
507 if model.ModelCfg.Think {
529 if model.ModelCfg.Think {
508 extraBody["thinking"] = map[string]any{"type": "enabled"}
530 extraBody["thinking"] = map[string]any{"type": "enabled"}
509 } else {
531 } else {
internal/agent/coordinator_test.go+31
@@ -9,6 +9,7 @@ import (
9 "charm.land/fantasy"
9 "charm.land/fantasy"
10 "charm.land/fantasy/providers/anthropic"
10 "charm.land/fantasy/providers/anthropic"
11 "charm.land/fantasy/providers/bedrock"
11 "charm.land/fantasy/providers/bedrock"
12+ "charm.land/fantasy/providers/openaicompat"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15 "github.com/stretchr/testify/require"
@@ -521,3 +522,33 @@ func TestGetProviderOptionsReasoningEffort(t *testing.T) {
521 })
522 })
522 }
523 }
523}
524}
525+
526+func TestGetProviderOptionsReasoningEffortFallback(t *testing.T) {
527+ model := Model{
528+ CatwalkCfg: catwalk.Model{
529+ ID: "glm-5.2",
530+ CanReason: true,
531+ ReasoningLevels: []string{"high", "max"},
532+ },
533+ ModelCfg: config.SelectedModel{
534+ Provider: "zai",
535+ },
536+ }
537+ providerCfg := config.ProviderConfig{
538+ ID: string(catwalk.InferenceProviderZAI),
539+ Type: openaicompat.Name,
540+ }
541+
542+ opts := getProviderOptions(model, providerCfg)
543+
544+ raw, ok := opts[openaicompat.Name]
545+ require.True(t, ok)
546+ parsed, ok := raw.(*openaicompat.ProviderOptions)
547+ require.True(t, ok)
548+ require.NotNil(t, parsed.ReasoningEffort)
549+ assert.Equal(t, "high", string(*parsed.ReasoningEffort))
550+
551+ thinking, ok := parsed.ExtraBody["thinking"].(map[string]any)
552+ require.True(t, ok)
553+ assert.Equal(t, "enabled", thinking["type"])
554+}