Commit e9047e7

HPCesia <me@hpcesia.com>
2026-10-17 07:09:46
crush: reasoning_levels support in crushrc patch
Assisted-by: crush:deepseek-v4.1-flash
1 parent bb879d8
Changed files (2)
modules
modules/dev/agents/crush/default.nix
@@ -30,9 +30,13 @@
           jail = inputs.jail-nix.lib.init pkgs;
         }).mkJailedAgent;
 
-      crush = pkgs.nur.repos.charmbracelet.crush.overrideAttrs (old: {
-        meta = (old.meta or {}) // {mainProgram = "crush";};
-      });
+      crush = pkgs.crush.overrideAttrs {
+        patches = [
+          # `--reasoning-levels` for crushrc `model add`, from https://github.com/charmbracelet/crush/pull/3857
+          # TODO: drop once upstreamed.
+          ./reasoning-level.patch
+        ];
+      };
 
       checkPath = pkgs.callPackage ./hooks/check_path/_package.nix {};
 
modules/dev/agents/crush/reasoning-level.patch
@@ -0,0 +1,521 @@
+diff --git a/internal/config/load.go b/internal/config/load.go
+index 3b4b59a770..0c25daecf2 100644
+--- a/internal/config/load.go
++++ b/internal/config/load.go
+@@ -235,18 +235,26 @@ func (c *Config) configureProviders(ctx context.Context, store *ConfigStore, env
+ 				p.APIKey = config.APIKey
+ 			}
+ 			if len(config.Models) > 0 {
++				catalogMap := make(map[string]catwalk.Model, len(p.Models))
++				for _, m := range p.Models {
++					catalogMap[m.ID] = m
++				}
++
+ 				models := []catwalk.Model{}
+ 				seen := make(map[string]bool)
+ 
+-				for _, model := range config.Models {
+-					if seen[model.ID] {
++				for _, userModel := range config.Models {
++					if seen[userModel.ID] {
+ 						continue
+ 					}
+-					seen[model.ID] = true
+-					if model.Name == "" {
+-						model.Name = model.ID
++					seen[userModel.ID] = true
++					if catalogModel, exists := catalogMap[userModel.ID]; exists {
++						userModel = mergeCatalogModel(catalogModel, userModel)
+ 					}
+-					models = append(models, model)
++					if userModel.Name == "" {
++						userModel.Name = userModel.ID
++					}
++					models = append(models, userModel)
+ 				}
+ 				for _, model := range p.Models {
+ 					if seen[model.ID] {
+@@ -514,6 +522,102 @@ func (c *Config) configureProviders(ctx context.Context, store *ConfigStore, env
+ 	return nil
+ }
+ 
++// mergeCatalogModel merges a user model override on top of a base catalog model.
++// Fields explicitly set on the override replace the base values; unset fields
++// inherit from the catalog entry.
++func mergeCatalogModel(base, override catwalk.Model) catwalk.Model {
++	merged := base
++
++	if override.Name != "" {
++		merged.Name = override.Name
++	}
++	if override.CostPer1MIn > 0 {
++		merged.CostPer1MIn = override.CostPer1MIn
++	}
++	if override.CostPer1MOut > 0 {
++		merged.CostPer1MOut = override.CostPer1MOut
++	}
++	if override.CostPer1MInCached > 0 {
++		merged.CostPer1MInCached = override.CostPer1MInCached
++	}
++	if override.CostPer1MOutCached > 0 {
++		merged.CostPer1MOutCached = override.CostPer1MOutCached
++	}
++	if override.ContextWindow > 0 {
++		merged.ContextWindow = override.ContextWindow
++	}
++	if override.DefaultMaxTokens > 0 {
++		merged.DefaultMaxTokens = override.DefaultMaxTokens
++	}
++	if override.CanReason {
++		merged.CanReason = true
++	}
++	if override.SupportsImages {
++		merged.SupportsImages = true
++	}
++
++	// Merge reasoning levels: keep base catalog levels, union with override levels.
++	if len(override.ReasoningLevels) > 0 {
++		seen := make(map[string]bool, len(base.ReasoningLevels)+len(override.ReasoningLevels))
++		var mergedLevels []string
++		for _, l := range base.ReasoningLevels {
++			if !seen[l] {
++				seen[l] = true
++				mergedLevels = append(mergedLevels, l)
++			}
++		}
++		for _, l := range override.ReasoningLevels {
++			if !seen[l] {
++				seen[l] = true
++				mergedLevels = append(mergedLevels, l)
++			}
++		}
++		merged.ReasoningLevels = mergedLevels
++	} else if len(base.ReasoningLevels) > 0 {
++		merged.ReasoningLevels = slices.Clone(base.ReasoningLevels)
++	}
++
++	if override.DefaultReasoningEffort != "" {
++		merged.DefaultReasoningEffort = override.DefaultReasoningEffort
++		// Ensure configured default effort is present in ReasoningLevels if levels are used
++		if len(merged.ReasoningLevels) > 0 && !slices.Contains(merged.ReasoningLevels, override.DefaultReasoningEffort) {
++			merged.ReasoningLevels = append(merged.ReasoningLevels, override.DefaultReasoningEffort)
++		}
++		merged.CanReason = true
++	}
++
++	// Merge ModelOptions (temperature, top_p, provider_options, etc.)
++	merged.Options = mergeModelOptions(base.Options, override.Options)
++
++	return merged
++}
++
++func mergeModelOptions(base, override catwalk.ModelOptions) catwalk.ModelOptions {
++	merged := base
++	if override.Temperature != nil {
++		merged.Temperature = override.Temperature
++	}
++	if override.TopP != nil {
++		merged.TopP = override.TopP
++	}
++	if override.TopK != nil {
++		merged.TopK = override.TopK
++	}
++	if override.FrequencyPenalty != nil {
++		merged.FrequencyPenalty = override.FrequencyPenalty
++	}
++	if override.PresencePenalty != nil {
++		merged.PresencePenalty = override.PresencePenalty
++	}
++	if len(override.ProviderOptions) > 0 {
++		opts := make(map[string]any, len(base.ProviderOptions)+len(override.ProviderOptions))
++		maps.Copy(opts, base.ProviderOptions)
++		maps.Copy(opts, override.ProviderOptions)
++		merged.ProviderOptions = opts
++	}
++	return merged
++}
++
+ // applyEnv sets top-level env vars from the config. Keys are sorted for
+ // deterministic ordering so that vars referencing other vars via the
+ // value resolver produce consistent results.
+diff --git a/internal/config/load_test.go b/internal/config/load_test.go
+index 123d2d5ccc..d5e9333351 100644
+--- a/internal/config/load_test.go
++++ b/internal/config/load_test.go
+@@ -498,6 +498,181 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
+ 	require.Equal(t, "Updated", pc.Models[0].Name)
+ }
+ 
++func TestConfig_configureProviders_MergesCatalogModelMetadata(t *testing.T) {
++	temp := 0.5
++	knownProviders := []catwalk.Provider{
++		{
++			ID:          "zai",
++			APIKey:      "$ZAI_API_KEY",
++			APIEndpoint: "https://api.zai.com/v1",
++			Models: []catwalk.Model{
++				{
++					ID:                     "glm-5.3-flash",
++					Name:                   "GLM-5.3 Flash",
++					CostPer1MIn:            0.1,
++					CostPer1MOut:           0.2,
++					CostPer1MInCached:      0.05,
++					CostPer1MOutCached:     0.1,
++					ContextWindow:          128000,
++					DefaultMaxTokens:       4096,
++					CanReason:              true,
++					ReasoningLevels:        []string{"low", "high", "max"},
++					DefaultReasoningEffort: "high",
++					SupportsImages:         true,
++					Options: catwalk.ModelOptions{
++						Temperature: &temp,
++					},
++				},
++			},
++		},
++	}
++
++	cfg := &Config{
++		Providers: csync.NewMap[string, ProviderConfig](),
++	}
++	cfg.Providers.Set("zai", ProviderConfig{
++		Models: []catwalk.Model{
++			{
++				ID:                     "glm-5.3-flash",
++				DefaultReasoningEffort: "xhigh",
++			},
++		},
++	})
++	cfg.setDefaults(t.TempDir(), "")
++
++	env := env.NewFromMap(map[string]string{
++		"ZAI_API_KEY": "test-key",
++	})
++	resolver := NewShellVariableResolver(env)
++	err := cfg.configureProviders(context.Background(), testStore(cfg), env, resolver, knownProviders)
++	require.NoError(t, err)
++
++	pc, ok := cfg.Providers.Get("zai")
++	require.True(t, ok)
++	require.Len(t, pc.Models, 1)
++
++	m := pc.Models[0]
++	require.Equal(t, "glm-5.3-flash", m.ID)
++	require.Equal(t, "GLM-5.3 Flash", m.Name)
++	require.Equal(t, 0.1, m.CostPer1MIn)
++	require.Equal(t, 0.2, m.CostPer1MOut)
++	require.Equal(t, 0.05, m.CostPer1MInCached)
++	require.Equal(t, 0.1, m.CostPer1MOutCached)
++	require.Equal(t, int64(128000), m.ContextWindow)
++	require.Equal(t, int64(4096), m.DefaultMaxTokens)
++	require.True(t, m.CanReason)
++	require.True(t, m.SupportsImages)
++	require.Equal(t, "xhigh", m.DefaultReasoningEffort)
++	require.Equal(t, []string{"low", "high", "max", "xhigh"}, m.ReasoningLevels)
++	require.NotNil(t, m.Options.Temperature)
++	require.Equal(t, 0.5, *m.Options.Temperature)
++}
++
++func TestConfig_configureProviders_OverridesCatalogModelExplicitFields(t *testing.T) {
++	knownProviders := []catwalk.Provider{
++		{
++			ID:          "zai",
++			APIKey:      "$ZAI_API_KEY",
++			APIEndpoint: "https://api.zai.com/v1",
++			Models: []catwalk.Model{
++				{
++					ID:               "glm-5.3-flash",
++					Name:             "GLM-5.3 Flash",
++					ContextWindow:    128000,
++					DefaultMaxTokens: 4096,
++					CanReason:        true,
++					ReasoningLevels:  []string{"low", "high", "max"},
++					SupportsImages:   true,
++				},
++			},
++		},
++	}
++
++	cfg := &Config{
++		Providers: csync.NewMap[string, ProviderConfig](),
++	}
++	cfg.Providers.Set("zai", ProviderConfig{
++		Models: []catwalk.Model{
++			{
++				ID:              "glm-5.3-flash",
++				Name:            "My Custom GLM",
++				ContextWindow:   65536,
++				ReasoningLevels: []string{"minimal", "extreme"},
++			},
++		},
++	})
++	cfg.setDefaults(t.TempDir(), "")
++
++	env := env.NewFromMap(map[string]string{
++		"ZAI_API_KEY": "test-key",
++	})
++	resolver := NewShellVariableResolver(env)
++	err := cfg.configureProviders(context.Background(), testStore(cfg), env, resolver, knownProviders)
++	require.NoError(t, err)
++
++	pc, ok := cfg.Providers.Get("zai")
++	require.True(t, ok)
++	require.Len(t, pc.Models, 1)
++
++	m := pc.Models[0]
++	require.Equal(t, "My Custom GLM", m.Name)
++	require.Equal(t, int64(65536), m.ContextWindow)
++	require.Equal(t, int64(4096), m.DefaultMaxTokens)
++	require.True(t, m.CanReason)
++	require.True(t, m.SupportsImages)
++	require.Equal(t, []string{"low", "high", "max", "minimal", "extreme"}, m.ReasoningLevels)
++}
++
++func TestConfig_configureProviders_ReasoningEffortValidationWithShadowedModel(t *testing.T) {
++	knownProviders := []catwalk.Provider{
++		{
++			ID:          "zai",
++			APIKey:      "$ZAI_API_KEY",
++			APIEndpoint: "https://api.zai.com/v1",
++			Models: []catwalk.Model{
++				{
++					ID:              "glm-5.3-flash",
++					CanReason:       true,
++					ReasoningLevels: []string{"low", "high", "max"},
++				},
++			},
++		},
++	}
++
++	cfg := &Config{
++		Providers: csync.NewMap[string, ProviderConfig](),
++	}
++	cfg.Providers.Set("zai", ProviderConfig{
++		Models: []catwalk.Model{
++			{
++				ID:                     "glm-5.3-flash",
++				DefaultReasoningEffort: "xhigh",
++			},
++		},
++	})
++	cfg.setDefaults(t.TempDir(), "")
++
++	env := env.NewFromMap(map[string]string{
++		"ZAI_API_KEY": "test-key",
++	})
++	resolver := NewShellVariableResolver(env)
++	err := cfg.configureProviders(context.Background(), testStore(cfg), env, resolver, knownProviders)
++	require.NoError(t, err)
++
++	// Catalog level should pass
++	require.NoError(t, cfg.ValidateReasoningEffort("zai", "glm-5.3-flash", "low"))
++	require.NoError(t, cfg.ValidateReasoningEffort("zai", "glm-5.3-flash", "high"))
++	require.NoError(t, cfg.ValidateReasoningEffort("zai", "glm-5.3-flash", "max"))
++
++	// User-specified effort level should pass
++	require.NoError(t, cfg.ValidateReasoningEffort("zai", "glm-5.3-flash", "xhigh"))
++
++	// Non-existent level should fail with list of all accepted values
++	err = cfg.ValidateReasoningEffort("zai", "glm-5.3-flash", "invalid")
++	require.Error(t, err)
++	require.Contains(t, err.Error(), "accepted values: low, high, max, xhigh")
++}
++
+ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
+ 	knownProviders := []catwalk.Provider{
+ 		{
+diff --git a/internal/shellconfig/flags.go b/internal/shellconfig/flags.go
+index eeb510b380..10a35ccc24 100644
+--- a/internal/shellconfig/flags.go
++++ b/internal/shellconfig/flags.go
+@@ -28,6 +28,8 @@ const (
+ 	flagJSONObject
+ 	// flagJSONAny parses the value as arbitrary JSON.
+ 	flagJSONAny
++	// flagStringSlice parses comma-separated strings or JSON arrays of strings.
++	flagStringSlice
+ )
+ 
+ // flagOp is how a parsed flag value is written into the target map.
+@@ -174,6 +176,26 @@ func parseFlagValue(spec flagSpec, args []string, i int) (any, int, error) {
+ 		}
+ 		return parsed, i + 2, nil
+ 
++	case flagStringSlice:
++		v, err := nextArg(args, i, name)
++		if err != nil {
++			return nil, 0, err
++		}
++		if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
++			var raw []any
++			if err := json.Unmarshal([]byte(v), &raw); err == nil {
++				return raw, i + 2, nil
++			}
++		}
++		var list []any
++		for _, part := range strings.Split(v, ",") {
++			part = strings.TrimSpace(part)
++			if part != "" {
++				list = append(list, part)
++			}
++		}
++		return list, i + 2, nil
++
+ 	default:
+ 		return nil, 0, fmt.Errorf("%s: --%s has unknown flag kind", args[0], name)
+ 	}
+@@ -194,7 +216,11 @@ func storeFlag(target map[string]any, spec flagSpec, val any) {
+ 		target[spec.jsonKey] = val
+ 	case opAppend:
+ 		arr, _ := target[spec.jsonKey].([]any)
+-		target[spec.jsonKey] = append(arr, val)
++		if slice, ok := val.([]any); ok {
++			target[spec.jsonKey] = append(arr, slice...)
++		} else {
++			target[spec.jsonKey] = append(arr, val)
++		}
+ 	case opSetChild:
+ 		if kv, ok := val.([2]string); ok {
+ 			childMap(target, spec.child)[kv[0]] = kv[1]
+diff --git a/internal/shellconfig/model.go b/internal/shellconfig/model.go
+index eb8236dfe5..8399a69cdc 100644
+--- a/internal/shellconfig/model.go
++++ b/internal/shellconfig/model.go
+@@ -71,11 +71,13 @@ var modelAddFlags = []flagSpec{
+ 	{name: "--price-cache-create", jsonKey: "cost_per_1m_in_cached", kind: flagFloat, op: opSet},
+ 	{name: "--price-cache-hit", jsonKey: "cost_per_1m_out_cached", kind: flagFloat, op: opSet},
+ 	{name: "--reasoning-effort", jsonKey: "default_reasoning_effort", kind: flagString, op: opSet},
++	{name: "--reasoning-levels", jsonKey: "reasoning_levels", kind: flagStringSlice, op: opAppend},
++	{name: "--reasoning-level", jsonKey: "reasoning_levels", kind: flagStringSlice, op: opAppend},
+ }
+ 
+ func modelAdd(b *ConfigBuilder, args []string, stderr io.Writer) error {
+ 	if len(args) < 3 {
+-		return usage(stderr, "usage: model add <provider>/<id> [--name NAME] [--context-window N] [--default-max-tokens N] [--can-reason true|false] [--supports-images true|false] [--price-input F] [--price-output F] [--price-cache-create F] [--price-cache-hit F] [--reasoning-effort low|medium|high]")
++		return usage(stderr, "usage: model add <provider>/<id> [--name NAME] [--context-window N] [--default-max-tokens N] [--can-reason true|false] [--supports-images true|false] [--price-input F] [--price-output F] [--price-cache-create F] [--price-cache-hit F] [--reasoning-effort EFFORT] [--reasoning-levels LEVEL,...]")
+ 	}
+ 	provider, id, ok := splitProviderModel(args[2])
+ 	if !ok {
+@@ -92,6 +94,41 @@ func modelAdd(b *ConfigBuilder, args []string, stderr io.Writer) error {
+ 		return err
+ 	}
+ 
++	if levels, ok := model["reasoning_levels"].([]any); ok {
++		seen := make(map[any]bool, len(levels))
++		dedup := make([]any, 0, len(levels))
++		for _, l := range levels {
++			if !seen[l] {
++				seen[l] = true
++				dedup = append(dedup, l)
++			}
++		}
++		model["reasoning_levels"] = dedup
++	}
++
++	if levels, ok := model["reasoning_levels"].([]any); ok && len(levels) > 0 {
++		if _, hasCanReason := model["can_reason"]; !hasCanReason {
++			model["can_reason"] = true
++		}
++		if effort, ok := model["default_reasoning_effort"].(string); ok && effort != "" {
++			found := false
++			for _, l := range levels {
++				if s, ok := l.(string); ok && s == effort {
++					found = true
++					break
++				}
++			}
++			if !found {
++				model["reasoning_levels"] = append(levels, effort)
++			}
++		}
++	} else if effort, ok := model["default_reasoning_effort"].(string); ok && effort != "" {
++		model["reasoning_levels"] = []any{effort}
++		if _, hasCanReason := model["can_reason"]; !hasCanReason {
++			model["can_reason"] = true
++		}
++	}
++
+ 	p := childMap(providers, provider)
+ 	// Re-adding a model id replaces the existing entry, matching the
+ 	// update-in-place behavior of `provider add` and `lsp add`.
+diff --git a/internal/shellconfig/model_test.go b/internal/shellconfig/model_test.go
+index add30e9474..4317bfe502 100644
+--- a/internal/shellconfig/model_test.go
++++ b/internal/shellconfig/model_test.go
+@@ -192,3 +192,76 @@ provider rm anthropic`)
+ 	require.Len(t, models, 1)
+ 	require.Equal(t, "b", models[0].(map[string]any)["id"])
+ }
++
++func TestModelAdd_ReasoningLevelsCommaSeparated(t *testing.T) {
++	t.Parallel()
++
++	result := loadScript(t, `provider add openai --api-key k
++model add openai/custom-reasoning --reasoning-levels low,high,max --reasoning-effort max`)
++
++	providers := result["providers"].(map[string]any)
++	models := providers["openai"].(map[string]any)["models"].([]any)
++	require.Len(t, models, 1)
++	m := models[0].(map[string]any)
++	require.Equal(t, "custom-reasoning", m["id"])
++	require.Equal(t, true, m["can_reason"])
++	require.Equal(t, "max", m["default_reasoning_effort"])
++	require.Equal(t, []any{"low", "high", "max"}, m["reasoning_levels"])
++}
++
++func TestModelAdd_ReasoningLevelsRepeated(t *testing.T) {
++	t.Parallel()
++
++	result := loadScript(t, `provider add openai --api-key k
++model add openai/custom-reasoning --reasoning-level low --reasoning-levels high,max`)
++
++	providers := result["providers"].(map[string]any)
++	models := providers["openai"].(map[string]any)["models"].([]any)
++	require.Len(t, models, 1)
++	m := models[0].(map[string]any)
++	require.Equal(t, true, m["can_reason"])
++	require.Equal(t, []any{"low", "high", "max"}, m["reasoning_levels"])
++}
++
++func TestModelAdd_ReasoningLevelsJSON(t *testing.T) {
++	t.Parallel()
++
++	result := loadScript(t, `provider add anthropic --api-key k
++model add anthropic/custom-reasoning --reasoning-levels '["low", "high", "xhigh"]'`)
++
++	providers := result["providers"].(map[string]any)
++	models := providers["anthropic"].(map[string]any)["models"].([]any)
++	require.Len(t, models, 1)
++	m := models[0].(map[string]any)
++	require.Equal(t, true, m["can_reason"])
++	require.Equal(t, []any{"low", "high", "xhigh"}, m["reasoning_levels"])
++}
++
++func TestModelAdd_ReasoningEffortAutoPopulatesLevel(t *testing.T) {
++	t.Parallel()
++
++	result := loadScript(t, `provider add openai --api-key k
++model add openai/custom-reasoning --reasoning-effort max`)
++
++	providers := result["providers"].(map[string]any)
++	models := providers["openai"].(map[string]any)["models"].([]any)
++	require.Len(t, models, 1)
++	m := models[0].(map[string]any)
++	require.Equal(t, true, m["can_reason"])
++	require.Equal(t, "max", m["default_reasoning_effort"])
++	require.Equal(t, []any{"max"}, m["reasoning_levels"])
++}
++
++func TestModelAdd_ExplicitCanReasonFalsePreserved(t *testing.T) {
++	t.Parallel()
++
++	result := loadScript(t, `provider add anthropic --api-key k
++model add anthropic/custom-reasoning --can-reason false --reasoning-levels low,high`)
++
++	providers := result["providers"].(map[string]any)
++	models := providers["anthropic"].(map[string]any)["models"].([]any)
++	require.Len(t, models, 1)
++	m := models[0].(map[string]any)
++	require.Equal(t, false, m["can_reason"])
++	require.Equal(t, []any{"low", "high"}, m["reasoning_levels"])
++}