on main help sign in
chore: generate schema, fix tests
commit28c85ea0cacafca9e2408366f710fb6c610c802c authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 30 Jun 2025, 12:11 (1 year ago) parent19f1a2d
9 files changed+2572−1417
Taskfile.yaml+6 cmd/schema/README.md+145−27 cmd/schema/main.go+501−265 crush-schema.json+1425−303 internal/config/config.go+56 internal/config/config_test.go+257−476 internal/config/provider_mock.go+171−135 internal/config/provider_test.go+11−210 internal/config/shell.go−1
Taskfile.yaml+6
@@ -44,3 +44,9 @@ tasks:
4444 desc: Allocations profile
4545 cmds:
4646 - go tool pprof -http :6061 'http://localhost:6060/debug/pprof/allocs'
47+
48+ schema:
49+ desc: Generate JSON schema for configuration
50+ cmds:
51+ - go run cmd/schema/main.go > crush-schema.json
52+ - echo "Generated crush-schema.json"
cmd/schema/README.md+145−27
@@ -1,64 +1,182 @@
11# Crush Configuration Schema Generator
22
3−This tool generates a JSON Schema for the Crush configuration file. The schema can be used to validate configuration files and provide autocompletion in editors that support JSON Schema.
3+This tool automatically generates a JSON Schema for the Crush configuration file by using Go reflection to analyze the configuration structs. The schema provides validation, autocompletion, and documentation for configuration files.
4+
5+## Features
6+
7+- **Automated Generation**: Uses reflection to automatically generate schemas from Go structs
8+- **Always Up-to-Date**: Schema stays in sync with code changes automatically
9+- **Comprehensive**: Includes all configuration options, types, and validation rules
10+- **Enhanced**: Adds provider enums, model lists, and custom descriptions
11+- **Extensible**: Easy to add new fields and modify existing ones
412
513## Usage
614
715```bash
16+# Generate the schema
817go run cmd/schema/main.go > crush-schema.json
18+
19+# Or use the task runner
20+task schema
921```
1022
11−This will generate a JSON Schema file that can be used to validate configuration files.
23+## How It Works
24+
25+The generator:
26+
27+1. **Reflects on Config Structs**: Analyzes the `config.Config` struct and all related types
28+2. **Generates Base Schema**: Creates JSON Schema definitions for all struct fields
29+3. **Enhances with Runtime Data**: Adds provider lists, model enums, and tool lists from the actual codebase
30+4. **Adds Custom Descriptions**: Provides meaningful descriptions for configuration options
31+5. **Sets Default Values**: Includes appropriate defaults for optional fields
1232
1333## Schema Features
1434
1535The generated schema includes:
1636
17−- All configuration options with descriptions
18−- Default values where applicable
19−- Validation for enum values (e.g., model IDs, provider types)
20−- Required fields
21−- Type checking
37+- **Type Safety**: Proper type definitions for all configuration fields
38+- **Validation**: Required fields, enum constraints, and format validation
39+- **Documentation**: Descriptions for all configuration options
40+- **Defaults**: Default values for optional settings
41+- **Provider Enums**: Current list of supported providers
42+- **Model Enums**: Available models from all configured providers
43+- **Tool Lists**: Valid tool names for agent configurations
44+- **Cross-References**: Proper relationships between different config sections
45+
46+## Adding New Configuration Fields
47+
48+To add new configuration options:
49+
50+1. **Add to Config Structs**: Add the field to the appropriate struct in `internal/config/`
51+2. **Add JSON Tags**: Include proper JSON tags with field names
52+3. **Regenerate Schema**: Run the schema generator to update the JSON schema
53+4. **Update Validation**: Add any custom validation logic if needed
54+
55+Example:
56+```go
57+type Options struct {
58+ // ... existing fields ...
59+
60+ // New field with JSON tag and description
61+ NewFeature bool `json:"new_feature,omitempty"`
62+}
63+```
64+
65+The schema generator will automatically:
66+- Detect the new field
67+- Generate appropriate JSON schema
68+- Add type information
69+- Include in validation
2270
2371## Using the Schema
2472
25−You can use the generated schema in several ways:
73+### Editor Integration
74+
75+Most modern editors support JSON Schema:
76+
77+**VS Code**: Add to your workspace settings:
78+```json
79+{
80+ "json.schemas": [
81+ {
82+ "fileMatch": ["crush.json", ".crush.json"],
83+ "url": "./crush-schema.json"
84+ }
85+ ]
86+}
87+```
2688
27−1. **Editor Integration**: Many editors (VS Code, JetBrains IDEs, etc.) support JSON Schema for validation and autocompletion. You can configure your editor to use the generated schema for `.crush.json` files.
89+**JetBrains IDEs**: Configure in Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings
2890
29−2. **Validation Tools**: You can use tools like [jsonschema](https://github.com/Julian/jsonschema) to validate your configuration files against the schema.
91+### Validation Tools
3092
31−3. **Documentation**: The schema serves as documentation for the configuration options.
93+```bash
94+# Using jsonschema (Python)
95+pip install jsonschema
96+jsonschema -i crush.json crush-schema.json
3297
33−## Example Configuration
98+# Using ajv-cli (Node.js)
99+npm install -g ajv-cli
100+ajv validate -s crush-schema.json -d crush.json
101+```
34102
35−Here's an example configuration that conforms to the schema:
103+### Configuration Example
36104
37105```json
38106{
39− "data": {
40− "directory": ".crush"
107+ "models": {
108+ "large": {
109+ "model_id": "claude-3-5-sonnet-20241022",
110+ "provider": "anthropic",
111+ "reasoning_effort": "medium",
112+ "max_tokens": 8192
113+ },
114+ "small": {
115+ "model_id": "claude-3-5-haiku-20241022",
116+ "provider": "anthropic"
117+ }
41118 },
42− "debug": false,
43119 "providers": {
44120 "anthropic": {
45− "apiKey": "your-api-key"
121+ "id": "anthropic",
122+ "provider_type": "anthropic",
123+ "api_key": "your-api-key",
124+ "disabled": false
46125 }
47126 },
48127 "agents": {
49128 "coder": {
50− "model": "claude-3.7-sonnet",
51− "maxTokens": 5000,
52− "reasoningEffort": "medium"
53− },
54− "task": {
55− "model": "claude-3.7-sonnet",
56− "maxTokens": 5000
129+ "id": "coder",
130+ "name": "Coder",
131+ "model": "large",
132+ "disabled": false
57133 },
58− "title": {
59− "model": "claude-3.7-sonnet",
60− "maxTokens": 80
134+ "custom-agent": {
135+ "id": "custom-agent",
136+ "name": "Custom Agent",
137+ "description": "A custom agent for specific tasks",
138+ "model": "small",
139+ "allowed_tools": ["glob", "grep", "view"],
140+ "allowed_mcp": {
141+ "filesystem": ["read", "write"]
142+ }
143+ }
144+ },
145+ "mcp": {
146+ "filesystem": {
147+ "command": "mcp-filesystem",
148+ "args": ["--root", "/workspace"],
149+ "type": "stdio"
150+ }
151+ },
152+ "lsp": {
153+ "typescript": {
154+ "command": "typescript-language-server",
155+ "args": ["--stdio"],
156+ "enabled": true
157+ }
158+ },
159+ "options": {
160+ "context_paths": [
161+ "README.md",
162+ "docs/",
163+ ".cursorrules"
164+ ],
165+ "data_directory": ".crush",
166+ "debug": false,
167+ "tui": {
168+ "compact_mode": false
61169 }
62170 }
63171}
64172```
173+
174+## Maintenance
175+
176+The schema generator is designed to be maintenance-free. As long as:
177+
178+- Configuration structs have proper JSON tags
179+- New enums are added to the enhancement functions
180+- The generator is run after significant config changes
181+
182+The schema will stay current with the codebase automatically.
cmd/schema/main.go+501−265
@@ -1,30 +1,70 @@
1−// TODO: FIX THIS
21package main
32
43import (
54 "encoding/json"
65 "fmt"
76 "os"
7+ "reflect"
8+ "slices"
9+ "strings"
810
911 "github.com/charmbracelet/crush/internal/config"
1012)
1113
12−// JSONSchemaType represents a JSON Schema type
13−type JSONSchemaType struct {
14− Type string `json:"type,omitempty"`
15− Description string `json:"description,omitempty"`
16− Properties map[string]any `json:"properties,omitempty"`
17− Required []string `json:"required,omitempty"`
18− AdditionalProperties any `json:"additionalProperties,omitempty"`
19− Enum []any `json:"enum,omitempty"`
20− Items map[string]any `json:"items,omitempty"`
21− OneOf []map[string]any `json:"oneOf,omitempty"`
22− AnyOf []map[string]any `json:"anyOf,omitempty"`
23− Default any `json:"default,omitempty"`
14+// JSONSchema represents a JSON Schema
15+type JSONSchema struct {
16+ Schema string `json:"$schema,omitempty"`
17+ Title string `json:"title,omitempty"`
18+ Description string `json:"description,omitempty"`
19+ Type string `json:"type,omitempty"`
20+ Properties map[string]*JSONSchema `json:"properties,omitempty"`
21+ Items *JSONSchema `json:"items,omitempty"`
22+ Required []string `json:"required,omitempty"`
23+ AdditionalProperties any `json:"additionalProperties,omitempty"`
24+ Enum []any `json:"enum,omitempty"`
25+ Default any `json:"default,omitempty"`
26+ Definitions map[string]*JSONSchema `json:"definitions,omitempty"`
27+ Ref string `json:"$ref,omitempty"`
28+ OneOf []*JSONSchema `json:"oneOf,omitempty"`
29+ AnyOf []*JSONSchema `json:"anyOf,omitempty"`
30+ AllOf []*JSONSchema `json:"allOf,omitempty"`
31+ Not *JSONSchema `json:"not,omitempty"`
32+ Format string `json:"format,omitempty"`
33+ Pattern string `json:"pattern,omitempty"`
34+ MinLength *int `json:"minLength,omitempty"`
35+ MaxLength *int `json:"maxLength,omitempty"`
36+ Minimum *float64 `json:"minimum,omitempty"`
37+ Maximum *float64 `json:"maximum,omitempty"`
38+ ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
39+ ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
40+ MultipleOf *float64 `json:"multipleOf,omitempty"`
41+ MinItems *int `json:"minItems,omitempty"`
42+ MaxItems *int `json:"maxItems,omitempty"`
43+ UniqueItems *bool `json:"uniqueItems,omitempty"`
44+ MinProperties *int `json:"minProperties,omitempty"`
45+ MaxProperties *int `json:"maxProperties,omitempty"`
46+}
47+
48+// SchemaGenerator generates JSON schemas from Go types
49+type SchemaGenerator struct {
50+ definitions map[string]*JSONSchema
51+ visited map[reflect.Type]bool
52+}
53+
54+// NewSchemaGenerator creates a new schema generator
55+func NewSchemaGenerator() *SchemaGenerator {
56+ return &SchemaGenerator{
57+ definitions: make(map[string]*JSONSchema),
58+ visited: make(map[reflect.Type]bool),
59+ }
2460}
2561
2662func main() {
27− schema := generateSchema()
63+ // Enable mock providers to avoid API calls during schema generation
64+ config.UseMockProviders = true
65+
66+ generator := NewSchemaGenerator()
67+ schema := generator.GenerateSchema()
2868
2969 // Pretty print the schema
3070 encoder := json.NewEncoder(os.Stdout)
@@ -35,261 +75,457 @@ func main() {
3575 }
3676}
3777
38−func generateSchema() map[string]any {
39− schema := map[string]any{
40− "$schema": "http://json-schema.org/draft-07/schema#",
41− "title": "Crush Configuration",
42− "description": "Configuration schema for the Crush application",
43− "type": "object",
44− "properties": map[string]any{},
45− }
46−
47− // Add Data configuration
48− schema["properties"].(map[string]any)["data"] = map[string]any{
49− "type": "object",
50− "description": "Storage configuration",
51− "properties": map[string]any{
52− "directory": map[string]any{
53− "type": "string",
54− "description": "Directory where application data is stored",
55− "default": ".crush",
56− },
57− },
58− "required": []string{"directory"},
59− }
60−
61− // Add working directory
62− schema["properties"].(map[string]any)["wd"] = map[string]any{
63− "type": "string",
64− "description": "Working directory for the application",
65− }
66−
67− // Add debug flags
68− schema["properties"].(map[string]any)["debug"] = map[string]any{
69− "type": "boolean",
70− "description": "Enable debug mode",
71− "default": false,
72− }
73−
74− schema["properties"].(map[string]any)["debugLSP"] = map[string]any{
75− "type": "boolean",
76− "description": "Enable LSP debug mode",
77− "default": false,
78− }
79−
80− schema["properties"].(map[string]any)["contextPaths"] = map[string]any{
81− "type": "array",
82− "description": "Context paths for the application",
83− "items": map[string]any{
84− "type": "string",
85− },
86− "default": []string{
87− ".github/copilot-instructions.md",
88− ".cursorrules",
89− ".cursor/rules/",
90− "CLAUDE.md",
91− "CLAUDE.local.md",
92− "GEMINI.md",
93− "gemini.md",
94− "crush.md",
95− "crush.local.md",
96− "Crush.md",
97− "Crush.local.md",
98− "CRUSH.md",
99− "CRUSH.local.md",
100− },
101− }
102−
103− schema["properties"].(map[string]any)["tui"] = map[string]any{
104− "type": "object",
105− "description": "Terminal User Interface configuration",
106− "properties": map[string]any{
107− "theme": map[string]any{
108− "type": "string",
109− "description": "TUI theme name",
110− "default": "crush",
111− "enum": []string{
112− "crush",
113− "catppuccin",
114− "dracula",
115− "flexoki",
116− "gruvbox",
117− "monokai",
118− "onedark",
119− "tokyonight",
120− "tron",
121− },
122− },
123− },
124− }
125−
126− // Add MCP servers
127− schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
128− "type": "object",
129− "description": "Model Control Protocol server configurations",
130− "additionalProperties": map[string]any{
131− "type": "object",
132− "description": "MCP server configuration",
133− "properties": map[string]any{
134− "command": map[string]any{
135− "type": "string",
136− "description": "Command to execute for the MCP server",
137− },
138− "env": map[string]any{
139− "type": "array",
140− "description": "Environment variables for the MCP server",
141− "items": map[string]any{
142− "type": "string",
143− },
144− },
145− "args": map[string]any{
146− "type": "array",
147− "description": "Command arguments for the MCP server",
148− "items": map[string]any{
149− "type": "string",
150− },
151− },
152− "type": map[string]any{
153− "type": "string",
154− "description": "Type of MCP server",
155− "enum": []string{"stdio", "sse"},
156− "default": "stdio",
157− },
158− "url": map[string]any{
159− "type": "string",
160− "description": "URL for SSE type MCP servers",
161− },
162− "headers": map[string]any{
163− "type": "object",
164− "description": "HTTP headers for SSE type MCP servers",
165− "additionalProperties": map[string]any{
166− "type": "string",
167− },
168− },
169− },
170− "required": []string{"command"},
171− },
172− }
173−
174− // Add providers
175− providerSchema := map[string]any{
176− "type": "object",
177− "description": "LLM provider configurations",
178− "additionalProperties": map[string]any{
179− "type": "object",
180− "description": "Provider configuration",
181− "properties": map[string]any{
182− "apiKey": map[string]any{
183− "type": "string",
184− "description": "API key for the provider",
185− },
186− "disabled": map[string]any{
187− "type": "boolean",
188− "description": "Whether the provider is disabled",
189− "default": false,
190− },
191− },
192− },
193− }
194−
195− providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
196− "type": "string",
197− "description": "Provider type",
198− "enum": []string{},
199− }
200−
201− schema["properties"].(map[string]any)["providers"] = providerSchema
202−
203− // Add agents
204− agentSchema := map[string]any{
205− "type": "object",
206− "description": "Agent configurations",
207− "additionalProperties": map[string]any{
208− "type": "object",
209− "description": "Agent configuration",
210− "properties": map[string]any{
211− "model": map[string]any{
212− "type": "string",
213− "description": "Model ID for the agent",
214− },
215− "maxTokens": map[string]any{
216− "type": "integer",
217− "description": "Maximum tokens for the agent",
218− "minimum": 1,
219− },
220− "reasoningEffort": map[string]any{
221− "type": "string",
222− "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
223− "enum": []string{"low", "medium", "high"},
224− },
225− },
226− "required": []string{"model"},
227− },
228− }
229−
230− // Add model enum
231− modelEnum := []string{}
232−
233− agentSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["model"].(map[string]any)["enum"] = modelEnum
234−
235− // Add specific agent properties
236− agentProperties := map[string]any{}
237− knownAgents := []string{
78+// GenerateSchema generates the complete JSON schema for the Crush configuration
79+func (g *SchemaGenerator) GenerateSchema() *JSONSchema {
80+ // Generate schema for the main Config struct
81+ configType := reflect.TypeOf(config.Config{})
82+ configSchema := g.generateTypeSchema(configType)
83+
84+ // Create the root schema
85+ schema := &JSONSchema{
86+ Schema: "http://json-schema.org/draft-07/schema#",
87+ Title: "Crush Configuration",
88+ Description: "Configuration schema for the Crush application",
89+ Type: configSchema.Type,
90+ Properties: configSchema.Properties,
91+ Required: configSchema.Required,
92+ Definitions: g.definitions,
93+ }
94+
95+ // Add custom enhancements
96+ g.enhanceSchema(schema)
97+
98+ return schema
99+}
100+
101+// generateTypeSchema generates a JSON schema for a given Go type
102+func (g *SchemaGenerator) generateTypeSchema(t reflect.Type) *JSONSchema {
103+ // Handle pointers
104+ if t.Kind() == reflect.Ptr {
105+ return g.generateTypeSchema(t.Elem())
106+ }
107+
108+ // Check if we've already processed this type
109+ if g.visited[t] {
110+ // Return a reference to avoid infinite recursion
111+ return &JSONSchema{
112+ Ref: fmt.Sprintf("#/definitions/%s", t.Name()),
113+ }
114+ }
115+
116+ switch t.Kind() {
117+ case reflect.String:
118+ return &JSONSchema{Type: "string"}
119+ case reflect.Bool:
120+ return &JSONSchema{Type: "boolean"}
121+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
122+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
123+ return &JSONSchema{Type: "integer"}
124+ case reflect.Float32, reflect.Float64:
125+ return &JSONSchema{Type: "number"}
126+ case reflect.Slice, reflect.Array:
127+ itemSchema := g.generateTypeSchema(t.Elem())
128+ return &JSONSchema{
129+ Type: "array",
130+ Items: itemSchema,
131+ }
132+ case reflect.Map:
133+ valueSchema := g.generateTypeSchema(t.Elem())
134+ return &JSONSchema{
135+ Type: "object",
136+ AdditionalProperties: valueSchema,
137+ }
138+ case reflect.Struct:
139+ return g.generateStructSchema(t)
140+ case reflect.Interface:
141+ // For interface{} types, allow any value
142+ return &JSONSchema{}
143+ default:
144+ // Fallback for unknown types
145+ return &JSONSchema{}
146+ }
147+}
148+
149+// generateStructSchema generates a JSON schema for a struct type
150+func (g *SchemaGenerator) generateStructSchema(t reflect.Type) *JSONSchema {
151+ // Mark as visited to prevent infinite recursion
152+ g.visited[t] = true
153+
154+ schema := &JSONSchema{
155+ Type: "object",
156+ Properties: make(map[string]*JSONSchema),
157+ }
158+
159+ var required []string
160+
161+ for i := range t.NumField() {
162+ field := t.Field(i)
163+
164+ // Skip unexported fields
165+ if !field.IsExported() {
166+ continue
167+ }
168+
169+ // Get JSON tag
170+ jsonTag := field.Tag.Get("json")
171+ if jsonTag == "-" {
172+ continue
173+ }
174+
175+ // Parse JSON tag
176+ jsonName, options := parseJSONTag(jsonTag)
177+ if jsonName == "" {
178+ jsonName = strings.ToLower(field.Name)
179+ }
180+
181+ // Generate field schema
182+ fieldSchema := g.generateTypeSchema(field.Type)
183+
184+ // Add description from field name if not present
185+ if fieldSchema.Description == "" {
186+ fieldSchema.Description = generateFieldDescription(field.Name, field.Type)
187+ }
188+
189+ // Check if field is required (not omitempty and not a pointer)
190+ if !slices.Contains(options, "omitempty") && field.Type.Kind() != reflect.Ptr {
191+ required = append(required, jsonName)
192+ }
193+
194+ schema.Properties[jsonName] = fieldSchema
195+ }
196+
197+ if len(required) > 0 {
198+ schema.Required = required
199+ }
200+
201+ // Store in definitions if it's a named type
202+ if t.Name() != "" {
203+ g.definitions[t.Name()] = schema
204+ }
205+
206+ return schema
207+}
208+
209+// parseJSONTag parses a JSON struct tag
210+func parseJSONTag(tag string) (name string, options []string) {
211+ if tag == "" {
212+ return "", nil
213+ }
214+
215+ parts := strings.Split(tag, ",")
216+ name = parts[0]
217+ if len(parts) > 1 {
218+ options = parts[1:]
219+ }
220+ return name, options
221+}
222+
223+// generateFieldDescription generates a description for a field based on its name and type
224+func generateFieldDescription(fieldName string, fieldType reflect.Type) string {
225+ // Convert camelCase to words
226+ words := camelCaseToWords(fieldName)
227+ description := strings.Join(words, " ")
228+
229+ // Add type-specific information
230+ switch fieldType.Kind() {
231+ case reflect.Bool:
232+ if !strings.Contains(strings.ToLower(description), "enable") &&
233+ !strings.Contains(strings.ToLower(description), "disable") {
234+ description = "Enable " + strings.ToLower(description)
235+ }
236+ case reflect.Slice:
237+ if !strings.HasSuffix(description, "s") {
238+ description = description + " list"
239+ }
240+ case reflect.Map:
241+ description = description + " configuration"
242+ }
243+
244+ return description
245+}
246+
247+// camelCaseToWords converts camelCase to separate words
248+func camelCaseToWords(s string) []string {
249+ var words []string
250+ var currentWord strings.Builder
251+
252+ for i, r := range s {
253+ if i > 0 && r >= 'A' && r <= 'Z' {
254+ if currentWord.Len() > 0 {
255+ words = append(words, currentWord.String())
256+ currentWord.Reset()
257+ }
258+ }
259+ currentWord.WriteRune(r)
260+ }
261+
262+ if currentWord.Len() > 0 {
263+ words = append(words, currentWord.String())
264+ }
265+
266+ return words
267+}
268+
269+// enhanceSchema adds custom enhancements to the generated schema
270+func (g *SchemaGenerator) enhanceSchema(schema *JSONSchema) {
271+ // Add provider enums
272+ g.addProviderEnums(schema)
273+
274+ // Add model enums
275+ g.addModelEnums(schema)
276+
277+ // Add agent enums
278+ g.addAgentEnums(schema)
279+
280+ // Add tool enums
281+ g.addToolEnums(schema)
282+
283+ // Add MCP type enums
284+ g.addMCPTypeEnums(schema)
285+
286+ // Add model type enums
287+ g.addModelTypeEnums(schema)
288+
289+ // Add default values
290+ g.addDefaultValues(schema)
291+
292+ // Add custom descriptions
293+ g.addCustomDescriptions(schema)
294+}
295+
296+// addProviderEnums adds provider enums to the schema
297+func (g *SchemaGenerator) addProviderEnums(schema *JSONSchema) {
298+ providers := config.Providers()
299+ var providerIDs []any
300+ for _, p := range providers {
301+ providerIDs = append(providerIDs, string(p.ID))
302+ }
303+
304+ // Add to PreferredModel provider field
305+ if preferredModelDef, exists := schema.Definitions["PreferredModel"]; exists {
306+ if providerProp, exists := preferredModelDef.Properties["provider"]; exists {
307+ providerProp.Enum = providerIDs
308+ }
309+ }
310+
311+ // Add to ProviderConfig ID field
312+ if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
313+ if idProp, exists := providerConfigDef.Properties["id"]; exists {
314+ idProp.Enum = providerIDs
315+ }
316+ }
317+}
318+
319+// addModelEnums adds model enums to the schema
320+func (g *SchemaGenerator) addModelEnums(schema *JSONSchema) {
321+ providers := config.Providers()
322+ var modelIDs []any
323+ for _, p := range providers {
324+ for _, m := range p.Models {
325+ modelIDs = append(modelIDs, m.ID)
326+ }
327+ }
328+
329+ // Add to PreferredModel model_id field
330+ if preferredModelDef, exists := schema.Definitions["PreferredModel"]; exists {
331+ if modelIDProp, exists := preferredModelDef.Properties["model_id"]; exists {
332+ modelIDProp.Enum = modelIDs
333+ }
334+ }
335+}
336+
337+// addAgentEnums adds agent ID enums to the schema
338+func (g *SchemaGenerator) addAgentEnums(schema *JSONSchema) {
339+ agentIDs := []any{
238340 string(config.AgentCoder),
239341 string(config.AgentTask),
240342 }
241343
242− for _, agentName := range knownAgents {
243− agentProperties[agentName] = map[string]any{
244− "$ref": "#/definitions/agent",
245− }
246− }
247−
248− // Create a combined schema that allows both specific agents and additional ones
249− combinedAgentSchema := map[string]any{
250− "type": "object",
251− "description": "Agent configurations",
252− "properties": agentProperties,
253− "additionalProperties": agentSchema["additionalProperties"],
254− }
255−
256− schema["properties"].(map[string]any)["agents"] = combinedAgentSchema
257− schema["definitions"] = map[string]any{
258− "agent": agentSchema["additionalProperties"],
259− }
260−
261− // Add LSP configuration
262− schema["properties"].(map[string]any)["lsp"] = map[string]any{
263− "type": "object",
264− "description": "Language Server Protocol configurations",
265− "additionalProperties": map[string]any{
266− "type": "object",
267− "description": "LSP configuration for a language",
268− "properties": map[string]any{
269− "disabled": map[string]any{
270− "type": "boolean",
271− "description": "Whether the LSP is disabled",
272− "default": false,
273− },
274− "command": map[string]any{
275− "type": "string",
276− "description": "Command to execute for the LSP server",
277− },
278− "args": map[string]any{
279− "type": "array",
280− "description": "Command arguments for the LSP server",
281− "items": map[string]any{
282− "type": "string",
283− },
284− },
285− "options": map[string]any{
286− "type": "object",
287− "description": "Additional options for the LSP server",
288− },
289− },
290− "required": []string{"command"},
291− },
344+ if agentDef, exists := schema.Definitions["Agent"]; exists {
345+ if idProp, exists := agentDef.Properties["id"]; exists {
346+ idProp.Enum = agentIDs
347+ }
292348 }
349+}
293350
294− return schema
351+// addToolEnums adds tool enums to the schema
352+func (g *SchemaGenerator) addToolEnums(schema *JSONSchema) {
353+ tools := []any{
354+ "bash", "edit", "fetch", "glob", "grep", "ls", "sourcegraph", "view", "write", "agent",
355+ }
356+
357+ if agentDef, exists := schema.Definitions["Agent"]; exists {
358+ if allowedToolsProp, exists := agentDef.Properties["allowed_tools"]; exists {
359+ if allowedToolsProp.Items != nil {
360+ allowedToolsProp.Items.Enum = tools
361+ }
362+ }
363+ }
364+}
365+
366+// addMCPTypeEnums adds MCP type enums to the schema
367+func (g *SchemaGenerator) addMCPTypeEnums(schema *JSONSchema) {
368+ mcpTypes := []any{
369+ string(config.MCPStdio),
370+ string(config.MCPSse),
371+ }
372+
373+ if mcpDef, exists := schema.Definitions["MCP"]; exists {
374+ if typeProp, exists := mcpDef.Properties["type"]; exists {
375+ typeProp.Enum = mcpTypes
376+ }
377+ }
378+}
379+
380+// addModelTypeEnums adds model type enums to the schema
381+func (g *SchemaGenerator) addModelTypeEnums(schema *JSONSchema) {
382+ modelTypes := []any{
383+ string(config.LargeModel),
384+ string(config.SmallModel),
385+ }
386+
387+ if agentDef, exists := schema.Definitions["Agent"]; exists {
388+ if modelProp, exists := agentDef.Properties["model"]; exists {
389+ modelProp.Enum = modelTypes
390+ }
391+ }
392+}
393+
394+// addDefaultValues adds default values to the schema
395+func (g *SchemaGenerator) addDefaultValues(schema *JSONSchema) {
396+ // Add default context paths
397+ if optionsDef, exists := schema.Definitions["Options"]; exists {
398+ if contextPathsProp, exists := optionsDef.Properties["context_paths"]; exists {
399+ contextPathsProp.Default = []any{
400+ ".github/copilot-instructions.md",
401+ ".cursorrules",
402+ ".cursor/rules/",
403+ "CLAUDE.md",
404+ "CLAUDE.local.md",
405+ "GEMINI.md",
406+ "gemini.md",
407+ "crush.md",
408+ "crush.local.md",
409+ "Crush.md",
410+ "Crush.local.md",
411+ "CRUSH.md",
412+ "CRUSH.local.md",
413+ }
414+ }
415+ if dataDirProp, exists := optionsDef.Properties["data_directory"]; exists {
416+ dataDirProp.Default = ".crush"
417+ }
418+ if debugProp, exists := optionsDef.Properties["debug"]; exists {
419+ debugProp.Default = false
420+ }
421+ if debugLSPProp, exists := optionsDef.Properties["debug_lsp"]; exists {
422+ debugLSPProp.Default = false
423+ }
424+ if disableAutoSummarizeProp, exists := optionsDef.Properties["disable_auto_summarize"]; exists {
425+ disableAutoSummarizeProp.Default = false
426+ }
427+ }
428+
429+ // Add default MCP type
430+ if mcpDef, exists := schema.Definitions["MCP"]; exists {
431+ if typeProp, exists := mcpDef.Properties["type"]; exists {
432+ typeProp.Default = string(config.MCPStdio)
433+ }
434+ }
435+
436+ // Add default TUI options
437+ if tuiOptionsDef, exists := schema.Definitions["TUIOptions"]; exists {
438+ if compactModeProp, exists := tuiOptionsDef.Properties["compact_mode"]; exists {
439+ compactModeProp.Default = false
440+ }
441+ }
442+
443+ // Add default provider disabled
444+ if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
445+ if disabledProp, exists := providerConfigDef.Properties["disabled"]; exists {
446+ disabledProp.Default = false
447+ }
448+ }
449+
450+ // Add default agent disabled
451+ if agentDef, exists := schema.Definitions["Agent"]; exists {
452+ if disabledProp, exists := agentDef.Properties["disabled"]; exists {
453+ disabledProp.Default = false
454+ }
455+ }
456+
457+ // Add default LSP disabled
458+ if lspConfigDef, exists := schema.Definitions["LSPConfig"]; exists {
459+ if disabledProp, exists := lspConfigDef.Properties["enabled"]; exists {
460+ disabledProp.Default = true
461+ }
462+ }
463+}
464+
465+// addCustomDescriptions adds custom descriptions to improve the schema
466+func (g *SchemaGenerator) addCustomDescriptions(schema *JSONSchema) {
467+ // Enhance main config descriptions
468+ if schema.Properties != nil {
469+ if modelsProp, exists := schema.Properties["models"]; exists {
470+ modelsProp.Description = "Preferred model configurations for large and small model types"
471+ }
472+ if providersProp, exists := schema.Properties["providers"]; exists {
473+ providersProp.Description = "LLM provider configurations"
474+ }
475+ if agentsProp, exists := schema.Properties["agents"]; exists {
476+ agentsProp.Description = "Agent configurations for different tasks"
477+ }
478+ if mcpProp, exists := schema.Properties["mcp"]; exists {
479+ mcpProp.Description = "Model Control Protocol server configurations"
480+ }
481+ if lspProp, exists := schema.Properties["lsp"]; exists {
482+ lspProp.Description = "Language Server Protocol configurations"
483+ }
484+ if optionsProp, exists := schema.Properties["options"]; exists {
485+ optionsProp.Description = "General application options and settings"
486+ }
487+ }
488+
489+ // Enhance specific field descriptions
490+ if providerConfigDef, exists := schema.Definitions["ProviderConfig"]; exists {
491+ if apiKeyProp, exists := providerConfigDef.Properties["api_key"]; exists {
492+ apiKeyProp.Description = "API key for authenticating with the provider"
493+ }
494+ if baseURLProp, exists := providerConfigDef.Properties["base_url"]; exists {
495+ baseURLProp.Description = "Base URL for the provider API (required for custom providers)"
496+ }
497+ if extraHeadersProp, exists := providerConfigDef.Properties["extra_headers"]; exists {
498+ extraHeadersProp.Description = "Additional HTTP headers to send with requests"
499+ }
500+ if extraParamsProp, exists := providerConfigDef.Properties["extra_params"]; exists {
501+ extraParamsProp.Description = "Additional provider-specific parameters"
502+ }
503+ }
504+
505+ if agentDef, exists := schema.Definitions["Agent"]; exists {
506+ if allowedToolsProp, exists := agentDef.Properties["allowed_tools"]; exists {
507+ allowedToolsProp.Description = "List of tools this agent is allowed to use (if nil, all tools are allowed)"
508+ }
509+ if allowedMCPProp, exists := agentDef.Properties["allowed_mcp"]; exists {
510+ allowedMCPProp.Description = "Map of MCP servers this agent can use and their allowed tools"
511+ }
512+ if allowedLSPProp, exists := agentDef.Properties["allowed_lsp"]; exists {
513+ allowedLSPProp.Description = "List of LSP servers this agent can use (if nil, all LSPs are allowed)"
514+ }
515+ if contextPathsProp, exists := agentDef.Properties["context_paths"]; exists {
516+ contextPathsProp.Description = "Custom context paths for this agent (additive to global context paths)"
517+ }
518+ }
519+
520+ if mcpDef, exists := schema.Definitions["MCP"]; exists {
521+ if commandProp, exists := mcpDef.Properties["command"]; exists {
522+ commandProp.Description = "Command to execute for stdio MCP servers"
523+ }
524+ if urlProp, exists := mcpDef.Properties["url"]; exists {
525+ urlProp.Description = "URL for SSE MCP servers"
526+ }
527+ if headersProp, exists := mcpDef.Properties["headers"]; exists {
528+ headersProp.Description = "HTTP headers for SSE MCP servers"
529+ }
530+ }
295531}
crush-schema.json+1425−303
@@ -1,383 +1,1505 @@
11{
22 "$schema": "http://json-schema.org/draft-07/schema#",
3− "definitions": {
4− "agent": {
5− "description": "Agent configuration",
6− "properties": {
7− "maxTokens": {
8− "description": "Maximum tokens for the agent",
9− "minimum": 1,
10− "type": "integer"
11− },
12− "model": {
13− "description": "Model ID for the agent",
14− "enum": [
15− "gpt-4.1",
16− "llama-3.3-70b-versatile",
17− "azure.gpt-4.1",
18− "openrouter.gpt-4o",
19− "openrouter.o1-mini",
20− "openrouter.claude-3-haiku",
21− "claude-3-opus",
22− "gpt-4o",
23− "gpt-4o-mini",
24− "o1",
25− "meta-llama/llama-4-maverick-17b-128e-instruct",
26− "azure.o3-mini",
27− "openrouter.gpt-4o-mini",
28− "openrouter.o1",
29− "claude-3.5-haiku",
30− "o4-mini",
31− "azure.gpt-4.1-mini",
32− "openrouter.o3",
33− "grok-3-beta",
34− "o3-mini",
35− "qwen-qwq",
36− "azure.o1",
37− "openrouter.gemini-2.5-flash",
38− "openrouter.gemini-2.5",
39− "o1-mini",
40− "azure.gpt-4o",
41− "openrouter.gpt-4.1-mini",
42− "openrouter.claude-3.5-sonnet",
43− "openrouter.o3-mini",
44− "gpt-4.1-mini",
45− "gpt-4.5-preview",
46− "gpt-4.1-nano",
47− "deepseek-r1-distill-llama-70b",
48− "azure.gpt-4o-mini",
49− "openrouter.gpt-4.1",
50− "bedrock.claude-3.7-sonnet",
51− "claude-3-haiku",
52− "o3",
53− "gemini-2.0-flash-lite",
54− "azure.o3",
55− "azure.gpt-4.5-preview",
56− "openrouter.claude-3-opus",
57− "grok-3-mini-fast-beta",
58− "claude-4-sonnet",
59− "azure.o4-mini",
60− "grok-3-fast-beta",
61− "claude-3.5-sonnet",
62− "azure.o1-mini",
63− "openrouter.claude-3.7-sonnet",
64− "openrouter.gpt-4.5-preview",
65− "grok-3-mini-beta",
66− "claude-3.7-sonnet",
67− "gemini-2.0-flash",
68− "openrouter.deepseek-r1-free",
69− "vertexai.gemini-2.5-flash",
70− "vertexai.gemini-2.5",
71− "o1-pro",
72− "gemini-2.5",
73− "meta-llama/llama-4-scout-17b-16e-instruct",
74− "azure.gpt-4.1-nano",
75− "openrouter.gpt-4.1-nano",
76− "gemini-2.5-flash",
77− "openrouter.o4-mini",
78− "openrouter.claude-3.5-haiku",
79− "claude-4-opus",
80− "openrouter.o1-pro"
81− ],
82− "type": "string"
83− },
84− "reasoningEffort": {
85− "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
86− "enum": ["low", "medium", "high"],
87− "type": "string"
88− }
89− },
90− "required": ["model"],
91− "type": "object"
92− }
93− },
3+ "title": "Crush Configuration",
944 "description": "Configuration schema for the Crush application",
5+ "type": "object",
956 "properties": {
967 "agents": {
8+ "description": "Agent configurations for different tasks",
9+ "type": "object",
9710 "additionalProperties": {
98− "description": "Agent configuration",
11+ "type": "object",
9912 "properties": {
100− "maxTokens": {
101− "description": "Maximum tokens for the agent",
102− "minimum": 1,
103− "type": "integer"
13+ "allowed_lsp": {
14+ "description": "List of LSP servers this agent can use (if nil, all LSPs are allowed)",
15+ "type": "array",
16+ "items": {
17+ "type": "string"
18+ }
19+ },
20+ "allowed_mcp": {
21+ "description": "Map of MCP servers this agent can use and their allowed tools",
22+ "type": "object",
23+ "additionalProperties": {
24+ "type": "array",
25+ "items": {
26+ "type": "string"
27+ }
28+ }
29+ },
30+ "allowed_tools": {
31+ "description": "List of tools this agent is allowed to use (if nil, all tools are allowed)",
32+ "type": "array",
33+ "items": {
34+ "type": "string",
35+ "enum": [
36+ "bash",
37+ "edit",
38+ "fetch",
39+ "glob",
40+ "grep",
41+ "ls",
42+ "sourcegraph",
43+ "view",
44+ "write",
45+ "agent"
46+ ]
47+ }
48+ },
49+ "context_paths": {
50+ "description": "Custom context paths for this agent (additive to global context paths)",
51+ "type": "array",
52+ "items": {
53+ "type": "string"
54+ }
55+ },
56+ "description": {
57+ "description": "Description",
58+ "type": "string"
59+ },
60+ "disabled": {
61+ "description": "Disabled",
62+ "type": "boolean",
63+ "default": false
64+ },
65+ "id": {
66+ "description": "I D",
67+ "type": "string",
68+ "enum": [
69+ "coder",
70+ "task"
71+ ]
10472 },
10573 "model": {
106− "description": "Model ID for the agent",
74+ "description": "Model",
75+ "type": "string",
10776 "enum": [
108− "gpt-4.1",
109− "llama-3.3-70b-versatile",
110− "azure.gpt-4.1",
111− "openrouter.gpt-4o",
112− "openrouter.o1-mini",
113− "openrouter.claude-3-haiku",
114− "claude-3-opus",
115− "gpt-4o",
116− "gpt-4o-mini",
117− "o1",
118− "meta-llama/llama-4-maverick-17b-128e-instruct",
119− "azure.o3-mini",
120− "openrouter.gpt-4o-mini",
121− "openrouter.o1",
122− "claude-3.5-haiku",
123− "o4-mini",
124− "azure.gpt-4.1-mini",
125− "openrouter.o3",
126− "grok-3-beta",
127− "o3-mini",
128− "qwen-qwq",
129− "azure.o1",
130− "openrouter.gemini-2.5-flash",
131− "openrouter.gemini-2.5",
132− "o1-mini",
133− "azure.gpt-4o",
134− "openrouter.gpt-4.1-mini",
135− "openrouter.claude-3.5-sonnet",
136− "openrouter.o3-mini",
137− "gpt-4.1-mini",
138− "gpt-4.5-preview",
139− "gpt-4.1-nano",
140− "deepseek-r1-distill-llama-70b",
141− "azure.gpt-4o-mini",
142− "openrouter.gpt-4.1",
143− "bedrock.claude-3.7-sonnet",
144− "claude-3-haiku",
145− "o3",
146− "gemini-2.0-flash-lite",
147− "azure.o3",
148− "azure.gpt-4.5-preview",
149− "openrouter.claude-3-opus",
150− "grok-3-mini-fast-beta",
151− "claude-4-sonnet",
152− "azure.o4-mini",
153− "grok-3-fast-beta",
154− "claude-3.5-sonnet",
155− "azure.o1-mini",
156− "openrouter.claude-3.7-sonnet",
157− "openrouter.gpt-4.5-preview",
158− "grok-3-mini-beta",
159− "claude-3.7-sonnet",
160− "gemini-2.0-flash",
161− "openrouter.deepseek-r1-free",
162− "vertexai.gemini-2.5-flash",
163− "vertexai.gemini-2.5",
164− "o1-pro",
165− "gemini-2.5",
166− "meta-llama/llama-4-scout-17b-16e-instruct",
167− "azure.gpt-4.1-nano",
168− "openrouter.gpt-4.1-nano",
169− "gemini-2.5-flash",
170− "openrouter.o4-mini",
171− "openrouter.claude-3.5-haiku",
172− "claude-4-opus",
173− "openrouter.o1-pro"
174− ],
175− "type": "string"
77+ "large",
78+ "small"
79+ ]
17680 },
177− "reasoningEffort": {
178− "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
179− "enum": ["low", "medium", "high"],
81+ "name": {
82+ "description": "Name",
18083 "type": "string"
18184 }
18285 },
183− "required": ["model"],
184− "type": "object"
185− },
186− "description": "Agent configurations",
187− "properties": {
188− "coder": {
189− "$ref": "#/definitions/agent"
190− },
191− "task": {
192− "$ref": "#/definitions/agent"
193− },
194− "title": {
195− "$ref": "#/definitions/agent"
196− }
197− },
198− "type": "object"
199− },
200− "contextPaths": {
201− "default": [
202− ".github/copilot-instructions.md",
203− ".cursorrules",
204− ".cursor/rules/",
205− "CLAUDE.md",
206− "CLAUDE.local.md",
207− "GEMINI.md",
208− "gemini.md",
209− "crush.md",
210− "crush.local.md",
211− "Crush.md",
212− "Crush.local.md",
213− "CRUSH.md",
214− "CRUSH.local.md"
215− ],
216− "description": "Context paths for the application",
217− "items": {
218− "type": "string"
219− },
220− "type": "array"
221− },
222− "data": {
223− "description": "Storage configuration",
224− "properties": {
225− "directory": {
226− "default": ".crush",
227− "description": "Directory where application data is stored",
228− "type": "string"
229− }
230− },
231− "required": ["directory"],
232− "type": "object"
233− },
234− "debug": {
235− "default": false,
236− "description": "Enable debug mode",
237− "type": "boolean"
238− },
239− "debugLSP": {
240− "default": false,
241− "description": "Enable LSP debug mode",
242− "type": "boolean"
86+ "required": [
87+ "id",
88+ "name",
89+ "disabled",
90+ "model",
91+ "allowed_tools",
92+ "allowed_mcp",
93+ "allowed_lsp",
94+ "context_paths"
95+ ]
96+ }
24397 },
24498 "lsp": {
99+ "description": "Language Server Protocol configurations",
100+ "type": "object",
245101 "additionalProperties": {
246− "description": "LSP configuration for a language",
102+ "type": "object",
247103 "properties": {
248104 "args": {
249− "description": "Command arguments for the LSP server",
105+ "description": "Args",
106+ "type": "array",
250107 "items": {
251108 "type": "string"
252− },
253− "type": "array"
109+ }
254110 },
255111 "command": {
256− "description": "Command to execute for the LSP server",
112+ "description": "Command",
257113 "type": "string"
258114 },
259− "disabled": {
260− "default": false,
261− "description": "Whether the LSP is disabled",
262− "type": "boolean"
115+ "enabled": {
116+ "description": "Disabled",
117+ "type": "boolean",
118+ "default": true
263119 },
264120 "options": {
265− "description": "Additional options for the LSP server",
266− "type": "object"
121+ "description": "Options"
267122 }
268123 },
269− "required": ["command"],
270− "type": "object"
271− },
272− "description": "Language Server Protocol configurations",
273− "type": "object"
124+ "required": [
125+ "enabled",
126+ "command",
127+ "args",
128+ "options"
129+ ]
130+ }
274131 },
275− "mcpServers": {
132+ "mcp": {
133+ "description": "Model Control Protocol server configurations",
134+ "type": "object",
276135 "additionalProperties": {
277− "description": "MCP server configuration",
136+ "type": "object",
278137 "properties": {
279138 "args": {
280− "description": "Command arguments for the MCP server",
139+ "description": "Args",
140+ "type": "array",
281141 "items": {
282142 "type": "string"
283− },
284− "type": "array"
143+ }
285144 },
286145 "command": {
287− "description": "Command to execute for the MCP server",
146+ "description": "Command to execute for stdio MCP servers",
288147 "type": "string"
289148 },
290149 "env": {
291− "description": "Environment variables for the MCP server",
150+ "description": "Env list",
151+ "type": "array",
292152 "items": {
293153 "type": "string"
294− },
295− "type": "array"
154+ }
296155 },
297156 "headers": {
157+ "description": "HTTP headers for SSE MCP servers",
158+ "type": "object",
298159 "additionalProperties": {
299160 "type": "string"
300− },
301− "description": "HTTP headers for SSE type MCP servers",
302− "type": "object"
161+ }
303162 },
304163 "type": {
305− "default": "stdio",
306− "description": "Type of MCP server",
307− "enum": ["stdio", "sse"],
308− "type": "string"
164+ "description": "Type",
165+ "type": "string",
166+ "enum": [
167+ "stdio",
168+ "sse"
169+ ],
170+ "default": "stdio"
309171 },
310172 "url": {
311− "description": "URL for SSE type MCP servers",
173+ "description": "URL for SSE MCP servers",
312174 "type": "string"
313175 }
314176 },
315− "required": ["command"],
316− "type": "object"
177+ "required": [
178+ "command",
179+ "env",
180+ "args",
181+ "type",
182+ "url",
183+ "headers"
184+ ]
185+ }
186+ },
187+ "models": {
188+ "description": "Preferred model configurations for large and small model types",
189+ "type": "object",
190+ "properties": {
191+ "large": {
192+ "description": "Large",
193+ "type": "object",
194+ "properties": {
195+ "max_tokens": {
196+ "description": "Max Tokens",
197+ "type": "integer"
198+ },
199+ "model_id": {
200+ "description": "Model I D",
201+ "type": "string",
202+ "enum": [
203+ "claude-3-opus",
204+ "claude-3-haiku",
205+ "claude-3-5-sonnet-20241022",
206+ "claude-3-5-haiku-20241022",
207+ "gpt-4",
208+ "gpt-3.5-turbo",
209+ "gpt-4-turbo",
210+ "gpt-4o",
211+ "gpt-4o-mini",
212+ "o1-preview",
213+ "o1-mini",
214+ "gemini-2.5-pro",
215+ "gemini-2.5-flash",
216+ "grok-beta",
217+ "anthropic/claude-3.5-sonnet",
218+ "anthropic/claude-3.5-haiku"
219+ ]
220+ },
221+ "provider": {
222+ "description": "Provider",
223+ "type": "string",
224+ "enum": [
225+ "anthropic",
226+ "openai",
227+ "gemini",
228+ "xai",
229+ "openrouter"
230+ ]
231+ },
232+ "reasoning_effort": {
233+ "description": "Reasoning Effort",
234+ "type": "string"
235+ },
236+ "think": {
237+ "description": "Enable think",
238+ "type": "boolean"
239+ }
240+ },
241+ "required": [
242+ "model_id",
243+ "provider"
244+ ]
245+ },
246+ "small": {
247+ "description": "Small",
248+ "$ref": "#/definitions/PreferredModel"
249+ }
317250 },
318− "description": "Model Control Protocol server configurations",
319− "type": "object"
251+ "required": [
252+ "large",
253+ "small"
254+ ]
255+ },
256+ "options": {
257+ "description": "General application options and settings",
258+ "type": "object",
259+ "properties": {
260+ "context_paths": {
261+ "description": "Context Paths",
262+ "type": "array",
263+ "items": {
264+ "type": "string"
265+ },
266+ "default": [
267+ ".github/copilot-instructions.md",
268+ ".cursorrules",
269+ ".cursor/rules/",
270+ "CLAUDE.md",
271+ "CLAUDE.local.md",
272+ "GEMINI.md",
273+ "gemini.md",
274+ "crush.md",
275+ "crush.local.md",
276+ "Crush.md",
277+ "Crush.local.md",
278+ "CRUSH.md",
279+ "CRUSH.local.md"
280+ ]
281+ },
282+ "data_directory": {
283+ "description": "Data Directory",
284+ "type": "string",
285+ "default": ".crush"
286+ },
287+ "debug": {
288+ "description": "Enable debug",
289+ "type": "boolean",
290+ "default": false
291+ },
292+ "debug_lsp": {
293+ "description": "Enable debug l s p",
294+ "type": "boolean",
295+ "default": false
296+ },
297+ "disable_auto_summarize": {
298+ "description": "Disable Auto Summarize",
299+ "type": "boolean",
300+ "default": false
301+ },
302+ "tui": {
303+ "description": "T U I",
304+ "type": "object",
305+ "properties": {
306+ "compact_mode": {
307+ "description": "Enable compact mode",
308+ "type": "boolean",
309+ "default": false
310+ }
311+ },
312+ "required": [
313+ "compact_mode"
314+ ]
315+ }
316+ },
317+ "required": [
318+ "context_paths",
319+ "tui",
320+ "debug",
321+ "debug_lsp",
322+ "disable_auto_summarize",
323+ "data_directory"
324+ ]
320325 },
321326 "providers": {
327+ "description": "LLM provider configurations",
328+ "type": "object",
322329 "additionalProperties": {
323− "description": "Provider configuration",
330+ "type": "object",
324331 "properties": {
325− "apiKey": {
326− "description": "API key for the provider",
332+ "api_key": {
333+ "description": "API key for authenticating with the provider",
334+ "type": "string"
335+ },
336+ "base_url": {
337+ "description": "Base URL for the provider API (required for custom providers)",
338+ "type": "string"
339+ },
340+ "default_large_model": {
341+ "description": "Default Large Model",
342+ "type": "string"
343+ },
344+ "default_small_model": {
345+ "description": "Default Small Model",
327346 "type": "string"
328347 },
329348 "disabled": {
330− "default": false,
331− "description": "Whether the provider is disabled",
332− "type": "boolean"
349+ "description": "Disabled",
350+ "type": "boolean",
351+ "default": false
352+ },
353+ "extra_headers": {
354+ "description": "Additional HTTP headers to send with requests",
355+ "type": "object",
356+ "additionalProperties": {
357+ "type": "string"
358+ }
333359 },
334− "provider": {
335− "description": "Provider type",
360+ "extra_params": {
361+ "description": "Additional provider-specific parameters",
362+ "type": "object",
363+ "additionalProperties": {
364+ "type": "string"
365+ }
366+ },
367+ "id": {
368+ "description": "I D",
369+ "type": "string",
336370 "enum": [
337371 "anthropic",
338372 "openai",
339373 "gemini",
340− "groq",
341− "openrouter",
342− "bedrock",
343− "azure",
344− "vertexai"
345− ],
374+ "xai",
375+ "openrouter"
376+ ]
377+ },
378+ "models": {
379+ "description": "Models",
380+ "type": "array",
381+ "items": {
382+ "type": "object",
383+ "properties": {
384+ "can_reason": {
385+ "description": "Enable can reason",
386+ "type": "boolean"
387+ },
388+ "context_window": {
389+ "description": "Context Window",
390+ "type": "integer"
391+ },
392+ "cost_per_1m_in": {
393+ "description": "Cost Per1 M In",
394+ "type": "number"
395+ },
396+ "cost_per_1m_in_cached": {
397+ "description": "Cost Per1 M In Cached",
398+ "type": "number"
399+ },
400+ "cost_per_1m_out": {
401+ "description": "Cost Per1 M Out",
402+ "type": "number"
403+ },
404+ "cost_per_1m_out_cached": {
405+ "description": "Cost Per1 M Out Cached",
406+ "type": "number"
407+ },
408+ "default_max_tokens": {
409+ "description": "Default Max Tokens",
410+ "type": "integer"
411+ },
412+ "has_reasoning_effort": {
413+ "description": "Enable has reasoning effort",
414+ "type": "boolean"
415+ },
416+ "id": {
417+ "description": "I D",
418+ "type": "string"
419+ },
420+ "model": {
421+ "description": "Name",
422+ "type": "string"
423+ },
424+ "reasoning_effort": {
425+ "description": "Reasoning Effort",
426+ "type": "string"
427+ },
428+ "supports_attachments": {
429+ "description": "Enable supports images",
430+ "type": "boolean"
431+ }
432+ },
433+ "required": [
434+ "id",
435+ "model",
436+ "cost_per_1m_in",
437+ "cost_per_1m_out",
438+ "cost_per_1m_in_cached",
439+ "cost_per_1m_out_cached",
440+ "context_window",
441+ "default_max_tokens",
442+ "can_reason",
443+ "reasoning_effort",
444+ "has_reasoning_effort",
445+ "supports_attachments"
446+ ]
447+ }
448+ },
449+ "provider_type": {
450+ "description": "Provider Type",
451+ "type": "string"
452+ }
453+ },
454+ "required": [
455+ "id",
456+ "provider_type",
457+ "disabled"
458+ ]
459+ }
460+ }
461+ },
462+ "required": [
463+ "models",
464+ "options"
465+ ],
466+ "definitions": {
467+ "Agent": {
468+ "type": "object",
469+ "properties": {
470+ "allowed_lsp": {
471+ "description": "List of LSP servers this agent can use (if nil, all LSPs are allowed)",
472+ "type": "array",
473+ "items": {
474+ "type": "string"
475+ }
476+ },
477+ "allowed_mcp": {
478+ "description": "Map of MCP servers this agent can use and their allowed tools",
479+ "type": "object",
480+ "additionalProperties": {
481+ "type": "array",
482+ "items": {
483+ "type": "string"
484+ }
485+ }
486+ },
487+ "allowed_tools": {
488+ "description": "List of tools this agent is allowed to use (if nil, all tools are allowed)",
489+ "type": "array",
490+ "items": {
491+ "type": "string",
492+ "enum": [
493+ "bash",
494+ "edit",
495+ "fetch",
496+ "glob",
497+ "grep",
498+ "ls",
499+ "sourcegraph",
500+ "view",
501+ "write",
502+ "agent"
503+ ]
504+ }
505+ },
506+ "context_paths": {
507+ "description": "Custom context paths for this agent (additive to global context paths)",
508+ "type": "array",
509+ "items": {
346510 "type": "string"
347511 }
348512 },
349− "type": "object"
513+ "description": {
514+ "description": "Description",
515+ "type": "string"
516+ },
517+ "disabled": {
518+ "description": "Disabled",
519+ "type": "boolean",
520+ "default": false
521+ },
522+ "id": {
523+ "description": "I D",
524+ "type": "string",
525+ "enum": [
526+ "coder",
527+ "task"
528+ ]
529+ },
530+ "model": {
531+ "description": "Model",
532+ "type": "string",
533+ "enum": [
534+ "large",
535+ "small"
536+ ]
537+ },
538+ "name": {
539+ "description": "Name",
540+ "type": "string"
541+ }
350542 },
351− "description": "LLM provider configurations",
352− "type": "object"
543+ "required": [
544+ "id",
545+ "name",
546+ "disabled",
547+ "model",
548+ "allowed_tools",
549+ "allowed_mcp",
550+ "allowed_lsp",
551+ "context_paths"
552+ ]
553+ },
554+ "Config": {
555+ "type": "object",
556+ "properties": {
557+ "agents": {
558+ "description": "Agent configurations for different tasks",
559+ "type": "object",
560+ "additionalProperties": {
561+ "type": "object",
562+ "properties": {
563+ "allowed_lsp": {
564+ "description": "List of LSP servers this agent can use (if nil, all LSPs are allowed)",
565+ "type": "array",
566+ "items": {
567+ "type": "string"
568+ }
569+ },
570+ "allowed_mcp": {
571+ "description": "Map of MCP servers this agent can use and their allowed tools",
572+ "type": "object",
573+ "additionalProperties": {
574+ "type": "array",
575+ "items": {
576+ "type": "string"
577+ }
578+ }
579+ },
580+ "allowed_tools": {
581+ "description": "List of tools this agent is allowed to use (if nil, all tools are allowed)",
582+ "type": "array",
583+ "items": {
584+ "type": "string",
585+ "enum": [
586+ "bash",
587+ "edit",
588+ "fetch",
589+ "glob",
590+ "grep",
591+ "ls",
592+ "sourcegraph",
593+ "view",
594+ "write",
595+ "agent"
596+ ]
597+ }
598+ },
599+ "context_paths": {
600+ "description": "Custom context paths for this agent (additive to global context paths)",
601+ "type": "array",
602+ "items": {
603+ "type": "string"
604+ }
605+ },
606+ "description": {
607+ "description": "Description",
608+ "type": "string"
609+ },
610+ "disabled": {
611+ "description": "Disabled",
612+ "type": "boolean",
613+ "default": false
614+ },
615+ "id": {
616+ "description": "I D",
617+ "type": "string",
618+ "enum": [
619+ "coder",
620+ "task"
621+ ]
622+ },
623+ "model": {
624+ "description": "Model",
625+ "type": "string",
626+ "enum": [
627+ "large",
628+ "small"
629+ ]
630+ },
631+ "name": {
632+ "description": "Name",
633+ "type": "string"
634+ }
635+ },
636+ "required": [
637+ "id",
638+ "name",
639+ "disabled",
640+ "model",
641+ "allowed_tools",
642+ "allowed_mcp",
643+ "allowed_lsp",
644+ "context_paths"
645+ ]
646+ }
647+ },
648+ "lsp": {
649+ "description": "Language Server Protocol configurations",
650+ "type": "object",
651+ "additionalProperties": {
652+ "type": "object",
653+ "properties": {
654+ "args": {
655+ "description": "Args",
656+ "type": "array",
657+ "items": {
658+ "type": "string"
659+ }
660+ },
661+ "command": {
662+ "description": "Command",
663+ "type": "string"
664+ },
665+ "enabled": {
666+ "description": "Disabled",
667+ "type": "boolean",
668+ "default": true
669+ },
670+ "options": {
671+ "description": "Options"
672+ }
673+ },
674+ "required": [
675+ "enabled",
676+ "command",
677+ "args",
678+ "options"
679+ ]
680+ }
681+ },
682+ "mcp": {
683+ "description": "Model Control Protocol server configurations",
684+ "type": "object",
685+ "additionalProperties": {
686+ "type": "object",
687+ "properties": {
688+ "args": {
689+ "description": "Args",
690+ "type": "array",
691+ "items": {
692+ "type": "string"
693+ }
694+ },
695+ "command": {
696+ "description": "Command to execute for stdio MCP servers",
697+ "type": "string"
698+ },
699+ "env": {
700+ "description": "Env list",
701+ "type": "array",
702+ "items": {
703+ "type": "string"
704+ }
705+ },
706+ "headers": {
707+ "description": "HTTP headers for SSE MCP servers",
708+ "type": "object",
709+ "additionalProperties": {
710+ "type": "string"
711+ }
712+ },
713+ "type": {
714+ "description": "Type",
715+ "type": "string",
716+ "enum": [
717+ "stdio",
718+ "sse"
719+ ],
720+ "default": "stdio"
721+ },
722+ "url": {
723+ "description": "URL for SSE MCP servers",
724+ "type": "string"
725+ }
726+ },
727+ "required": [
728+ "command",
729+ "env",
730+ "args",
731+ "type",
732+ "url",
733+ "headers"
734+ ]
735+ }
736+ },
737+ "models": {
738+ "description": "Preferred model configurations for large and small model types",
739+ "type": "object",
740+ "properties": {
741+ "large": {
742+ "description": "Large",
743+ "type": "object",
744+ "properties": {
745+ "max_tokens": {
746+ "description": "Max Tokens",
747+ "type": "integer"
748+ },
749+ "model_id": {
750+ "description": "Model I D",
751+ "type": "string",
752+ "enum": [
753+ "claude-3-opus",
754+ "claude-3-haiku",
755+ "claude-3-5-sonnet-20241022",
756+ "claude-3-5-haiku-20241022",
757+ "gpt-4",
758+ "gpt-3.5-turbo",
759+ "gpt-4-turbo",
760+ "gpt-4o",
761+ "gpt-4o-mini",
762+ "o1-preview",
763+ "o1-mini",
764+ "gemini-2.5-pro",
765+ "gemini-2.5-flash",
766+ "grok-beta",
767+ "anthropic/claude-3.5-sonnet",
768+ "anthropic/claude-3.5-haiku"
769+ ]
770+ },
771+ "provider": {
772+ "description": "Provider",
773+ "type": "string",
774+ "enum": [
775+ "anthropic",
776+ "openai",
777+ "gemini",
778+ "xai",
779+ "openrouter"
780+ ]
781+ },
782+ "reasoning_effort": {
783+ "description": "Reasoning Effort",
784+ "type": "string"
785+ },
786+ "think": {
787+ "description": "Enable think",
788+ "type": "boolean"
789+ }
790+ },
791+ "required": [
792+ "model_id",
793+ "provider"
794+ ]
795+ },
796+ "small": {
797+ "description": "Small",
798+ "$ref": "#/definitions/PreferredModel"
799+ }
800+ },
801+ "required": [
802+ "large",
803+ "small"
804+ ]
805+ },
806+ "options": {
807+ "description": "General application options and settings",
808+ "type": "object",
809+ "properties": {
810+ "context_paths": {
811+ "description": "Context Paths",
812+ "type": "array",
813+ "items": {
814+ "type": "string"
815+ },
816+ "default": [
817+ ".github/copilot-instructions.md",
818+ ".cursorrules",
819+ ".cursor/rules/",
820+ "CLAUDE.md",
821+ "CLAUDE.local.md",
822+ "GEMINI.md",
823+ "gemini.md",
824+ "crush.md",
825+ "crush.local.md",
826+ "Crush.md",
827+ "Crush.local.md",
828+ "CRUSH.md",
829+ "CRUSH.local.md"
830+ ]
831+ },
832+ "data_directory": {
833+ "description": "Data Directory",
834+ "type": "string",
835+ "default": ".crush"
836+ },
837+ "debug": {
838+ "description": "Enable debug",
839+ "type": "boolean",
840+ "default": false
841+ },
842+ "debug_lsp": {
843+ "description": "Enable debug l s p",
844+ "type": "boolean",
845+ "default": false
846+ },
847+ "disable_auto_summarize": {
848+ "description": "Disable Auto Summarize",
849+ "type": "boolean",
850+ "default": false
851+ },
852+ "tui": {
853+ "description": "T U I",
854+ "type": "object",
855+ "properties": {
856+ "compact_mode": {
857+ "description": "Enable compact mode",
858+ "type": "boolean",
859+ "default": false
860+ }
861+ },
862+ "required": [
863+ "compact_mode"
864+ ]
865+ }
866+ },
867+ "required": [
868+ "context_paths",
869+ "tui",
870+ "debug",
871+ "debug_lsp",
872+ "disable_auto_summarize",
873+ "data_directory"
874+ ]
875+ },
876+ "providers": {
877+ "description": "LLM provider configurations",
878+ "type": "object",
879+ "additionalProperties": {
880+ "type": "object",
881+ "properties": {
882+ "api_key": {
883+ "description": "API key for authenticating with the provider",
884+ "type": "string"
885+ },
886+ "base_url": {
887+ "description": "Base URL for the provider API (required for custom providers)",
888+ "type": "string"
889+ },
890+ "default_large_model": {
891+ "description": "Default Large Model",
892+ "type": "string"
893+ },
894+ "default_small_model": {
895+ "description": "Default Small Model",
896+ "type": "string"
897+ },
898+ "disabled": {
899+ "description": "Disabled",
900+ "type": "boolean",
901+ "default": false
902+ },
903+ "extra_headers": {
904+ "description": "Additional HTTP headers to send with requests",
905+ "type": "object",
906+ "additionalProperties": {
907+ "type": "string"
908+ }
909+ },
910+ "extra_params": {
911+ "description": "Additional provider-specific parameters",
912+ "type": "object",
913+ "additionalProperties": {
914+ "type": "string"
915+ }
916+ },
917+ "id": {
918+ "description": "I D",
919+ "type": "string",
920+ "enum": [
921+ "anthropic",
922+ "openai",
923+ "gemini",
924+ "xai",
925+ "openrouter"
926+ ]
927+ },
928+ "models": {
929+ "description": "Models",
930+ "type": "array",
931+ "items": {
932+ "type": "object",
933+ "properties": {
934+ "can_reason": {
935+ "description": "Enable can reason",
936+ "type": "boolean"
937+ },
938+ "context_window": {
939+ "description": "Context Window",
940+ "type": "integer"
941+ },
942+ "cost_per_1m_in": {
943+ "description": "Cost Per1 M In",
944+ "type": "number"
945+ },
946+ "cost_per_1m_in_cached": {
947+ "description": "Cost Per1 M In Cached",
948+ "type": "number"
949+ },
950+ "cost_per_1m_out": {
951+ "description": "Cost Per1 M Out",
952+ "type": "number"
953+ },
954+ "cost_per_1m_out_cached": {
955+ "description": "Cost Per1 M Out Cached",
956+ "type": "number"
957+ },
958+ "default_max_tokens": {
959+ "description": "Default Max Tokens",
960+ "type": "integer"
961+ },
962+ "has_reasoning_effort": {
963+ "description": "Enable has reasoning effort",
964+ "type": "boolean"
965+ },
966+ "id": {
967+ "description": "I D",
968+ "type": "string"
969+ },
970+ "model": {
971+ "description": "Name",
972+ "type": "string"
973+ },
974+ "reasoning_effort": {
975+ "description": "Reasoning Effort",
976+ "type": "string"
977+ },
978+ "supports_attachments": {
979+ "description": "Enable supports images",
980+ "type": "boolean"
981+ }
982+ },
983+ "required": [
984+ "id",
985+ "model",
986+ "cost_per_1m_in",
987+ "cost_per_1m_out",
988+ "cost_per_1m_in_cached",
989+ "cost_per_1m_out_cached",
990+ "context_window",
991+ "default_max_tokens",
992+ "can_reason",
993+ "reasoning_effort",
994+ "has_reasoning_effort",
995+ "supports_attachments"
996+ ]
997+ }
998+ },
999+ "provider_type": {
1000+ "description": "Provider Type",
1001+ "type": "string"
1002+ }
1003+ },
1004+ "required": [
1005+ "id",
1006+ "provider_type",
1007+ "disabled"
1008+ ]
1009+ }
1010+ }
1011+ },
1012+ "required": [
1013+ "models",
1014+ "options"
1015+ ]
3531016 },
354− "tui": {
355− "description": "Terminal User Interface configuration",
1017+ "LSPConfig": {
1018+ "type": "object",
3561019 "properties": {
357− "theme": {
358− "default": "crush",
359− "description": "TUI theme name",
1020+ "args": {
1021+ "description": "Args",
1022+ "type": "array",
1023+ "items": {
1024+ "type": "string"
1025+ }
1026+ },
1027+ "command": {
1028+ "description": "Command",
1029+ "type": "string"
1030+ },
1031+ "enabled": {
1032+ "description": "Disabled",
1033+ "type": "boolean",
1034+ "default": true
1035+ },
1036+ "options": {
1037+ "description": "Options"
1038+ }
1039+ },
1040+ "required": [
1041+ "enabled",
1042+ "command",
1043+ "args",
1044+ "options"
1045+ ]
1046+ },
1047+ "MCP": {
1048+ "type": "object",
1049+ "properties": {
1050+ "args": {
1051+ "description": "Args",
1052+ "type": "array",
1053+ "items": {
1054+ "type": "string"
1055+ }
1056+ },
1057+ "command": {
1058+ "description": "Command to execute for stdio MCP servers",
1059+ "type": "string"
1060+ },
1061+ "env": {
1062+ "description": "Env list",
1063+ "type": "array",
1064+ "items": {
1065+ "type": "string"
1066+ }
1067+ },
1068+ "headers": {
1069+ "description": "HTTP headers for SSE MCP servers",
1070+ "type": "object",
1071+ "additionalProperties": {
1072+ "type": "string"
1073+ }
1074+ },
1075+ "type": {
1076+ "description": "Type",
1077+ "type": "string",
3601078 "enum": [
361− "crush",
362− "catppuccin",
363− "dracula",
364− "flexoki",
365− "gruvbox",
366− "monokai",
367− "onedark",
368− "tokyonight",
369− "tron"
1079+ "stdio",
1080+ "sse"
3701081 ],
1082+ "default": "stdio"
1083+ },
1084+ "url": {
1085+ "description": "URL for SSE MCP servers",
1086+ "type": "string"
1087+ }
1088+ },
1089+ "required": [
1090+ "command",
1091+ "env",
1092+ "args",
1093+ "type",
1094+ "url",
1095+ "headers"
1096+ ]
1097+ },
1098+ "Model": {
1099+ "type": "object",
1100+ "properties": {
1101+ "can_reason": {
1102+ "description": "Enable can reason",
1103+ "type": "boolean"
1104+ },
1105+ "context_window": {
1106+ "description": "Context Window",
1107+ "type": "integer"
1108+ },
1109+ "cost_per_1m_in": {
1110+ "description": "Cost Per1 M In",
1111+ "type": "number"
1112+ },
1113+ "cost_per_1m_in_cached": {
1114+ "description": "Cost Per1 M In Cached",
1115+ "type": "number"
1116+ },
1117+ "cost_per_1m_out": {
1118+ "description": "Cost Per1 M Out",
1119+ "type": "number"
1120+ },
1121+ "cost_per_1m_out_cached": {
1122+ "description": "Cost Per1 M Out Cached",
1123+ "type": "number"
1124+ },
1125+ "default_max_tokens": {
1126+ "description": "Default Max Tokens",
1127+ "type": "integer"
1128+ },
1129+ "has_reasoning_effort": {
1130+ "description": "Enable has reasoning effort",
1131+ "type": "boolean"
1132+ },
1133+ "id": {
1134+ "description": "I D",
1135+ "type": "string"
1136+ },
1137+ "model": {
1138+ "description": "Name",
1139+ "type": "string"
1140+ },
1141+ "reasoning_effort": {
1142+ "description": "Reasoning Effort",
1143+ "type": "string"
1144+ },
1145+ "supports_attachments": {
1146+ "description": "Enable supports images",
1147+ "type": "boolean"
1148+ }
1149+ },
1150+ "required": [
1151+ "id",
1152+ "model",
1153+ "cost_per_1m_in",
1154+ "cost_per_1m_out",
1155+ "cost_per_1m_in_cached",
1156+ "cost_per_1m_out_cached",
1157+ "context_window",
1158+ "default_max_tokens",
1159+ "can_reason",
1160+ "reasoning_effort",
1161+ "has_reasoning_effort",
1162+ "supports_attachments"
1163+ ]
1164+ },
1165+ "Options": {
1166+ "description": "General application options and settings",
1167+ "type": "object",
1168+ "properties": {
1169+ "context_paths": {
1170+ "description": "Context Paths",
1171+ "type": "array",
1172+ "items": {
1173+ "type": "string"
1174+ },
1175+ "default": [
1176+ ".github/copilot-instructions.md",
1177+ ".cursorrules",
1178+ ".cursor/rules/",
1179+ "CLAUDE.md",
1180+ "CLAUDE.local.md",
1181+ "GEMINI.md",
1182+ "gemini.md",
1183+ "crush.md",
1184+ "crush.local.md",
1185+ "Crush.md",
1186+ "Crush.local.md",
1187+ "CRUSH.md",
1188+ "CRUSH.local.md"
1189+ ]
1190+ },
1191+ "data_directory": {
1192+ "description": "Data Directory",
1193+ "type": "string",
1194+ "default": ".crush"
1195+ },
1196+ "debug": {
1197+ "description": "Enable debug",
1198+ "type": "boolean",
1199+ "default": false
1200+ },
1201+ "debug_lsp": {
1202+ "description": "Enable debug l s p",
1203+ "type": "boolean",
1204+ "default": false
1205+ },
1206+ "disable_auto_summarize": {
1207+ "description": "Disable Auto Summarize",
1208+ "type": "boolean",
1209+ "default": false
1210+ },
1211+ "tui": {
1212+ "description": "T U I",
1213+ "type": "object",
1214+ "properties": {
1215+ "compact_mode": {
1216+ "description": "Enable compact mode",
1217+ "type": "boolean",
1218+ "default": false
1219+ }
1220+ },
1221+ "required": [
1222+ "compact_mode"
1223+ ]
1224+ }
1225+ },
1226+ "required": [
1227+ "context_paths",
1228+ "tui",
1229+ "debug",
1230+ "debug_lsp",
1231+ "disable_auto_summarize",
1232+ "data_directory"
1233+ ]
1234+ },
1235+ "PreferredModel": {
1236+ "description": "Large",
1237+ "type": "object",
1238+ "properties": {
1239+ "max_tokens": {
1240+ "description": "Max Tokens",
1241+ "type": "integer"
1242+ },
1243+ "model_id": {
1244+ "description": "Model I D",
1245+ "type": "string",
1246+ "enum": [
1247+ "claude-3-opus",
1248+ "claude-3-haiku",
1249+ "claude-3-5-sonnet-20241022",
1250+ "claude-3-5-haiku-20241022",
1251+ "gpt-4",
1252+ "gpt-3.5-turbo",
1253+ "gpt-4-turbo",
1254+ "gpt-4o",
1255+ "gpt-4o-mini",
1256+ "o1-preview",
1257+ "o1-mini",
1258+ "gemini-2.5-pro",
1259+ "gemini-2.5-flash",
1260+ "grok-beta",
1261+ "anthropic/claude-3.5-sonnet",
1262+ "anthropic/claude-3.5-haiku"
1263+ ]
1264+ },
1265+ "provider": {
1266+ "description": "Provider",
1267+ "type": "string",
1268+ "enum": [
1269+ "anthropic",
1270+ "openai",
1271+ "gemini",
1272+ "xai",
1273+ "openrouter"
1274+ ]
1275+ },
1276+ "reasoning_effort": {
1277+ "description": "Reasoning Effort",
1278+ "type": "string"
1279+ },
1280+ "think": {
1281+ "description": "Enable think",
1282+ "type": "boolean"
1283+ }
1284+ },
1285+ "required": [
1286+ "model_id",
1287+ "provider"
1288+ ]
1289+ },
1290+ "PreferredModels": {
1291+ "description": "Preferred model configurations for large and small model types",
1292+ "type": "object",
1293+ "properties": {
1294+ "large": {
1295+ "description": "Large",
1296+ "type": "object",
1297+ "properties": {
1298+ "max_tokens": {
1299+ "description": "Max Tokens",
1300+ "type": "integer"
1301+ },
1302+ "model_id": {
1303+ "description": "Model I D",
1304+ "type": "string",
1305+ "enum": [
1306+ "claude-3-opus",
1307+ "claude-3-haiku",
1308+ "claude-3-5-sonnet-20241022",
1309+ "claude-3-5-haiku-20241022",
1310+ "gpt-4",
1311+ "gpt-3.5-turbo",
1312+ "gpt-4-turbo",
1313+ "gpt-4o",
1314+ "gpt-4o-mini",
1315+ "o1-preview",
1316+ "o1-mini",
1317+ "gemini-2.5-pro",
1318+ "gemini-2.5-flash",
1319+ "grok-beta",
1320+ "anthropic/claude-3.5-sonnet",
1321+ "anthropic/claude-3.5-haiku"
1322+ ]
1323+ },
1324+ "provider": {
1325+ "description": "Provider",
1326+ "type": "string",
1327+ "enum": [
1328+ "anthropic",
1329+ "openai",
1330+ "gemini",
1331+ "xai",
1332+ "openrouter"
1333+ ]
1334+ },
1335+ "reasoning_effort": {
1336+ "description": "Reasoning Effort",
1337+ "type": "string"
1338+ },
1339+ "think": {
1340+ "description": "Enable think",
1341+ "type": "boolean"
1342+ }
1343+ },
1344+ "required": [
1345+ "model_id",
1346+ "provider"
1347+ ]
1348+ },
1349+ "small": {
1350+ "description": "Small",
1351+ "$ref": "#/definitions/PreferredModel"
1352+ }
1353+ },
1354+ "required": [
1355+ "large",
1356+ "small"
1357+ ]
1358+ },
1359+ "ProviderConfig": {
1360+ "type": "object",
1361+ "properties": {
1362+ "api_key": {
1363+ "description": "API key for authenticating with the provider",
1364+ "type": "string"
1365+ },
1366+ "base_url": {
1367+ "description": "Base URL for the provider API (required for custom providers)",
1368+ "type": "string"
1369+ },
1370+ "default_large_model": {
1371+ "description": "Default Large Model",
1372+ "type": "string"
1373+ },
1374+ "default_small_model": {
1375+ "description": "Default Small Model",
1376+ "type": "string"
1377+ },
1378+ "disabled": {
1379+ "description": "Disabled",
1380+ "type": "boolean",
1381+ "default": false
1382+ },
1383+ "extra_headers": {
1384+ "description": "Additional HTTP headers to send with requests",
1385+ "type": "object",
1386+ "additionalProperties": {
1387+ "type": "string"
1388+ }
1389+ },
1390+ "extra_params": {
1391+ "description": "Additional provider-specific parameters",
1392+ "type": "object",
1393+ "additionalProperties": {
1394+ "type": "string"
1395+ }
1396+ },
1397+ "id": {
1398+ "description": "I D",
1399+ "type": "string",
1400+ "enum": [
1401+ "anthropic",
1402+ "openai",
1403+ "gemini",
1404+ "xai",
1405+ "openrouter"
1406+ ]
1407+ },
1408+ "models": {
1409+ "description": "Models",
1410+ "type": "array",
1411+ "items": {
1412+ "type": "object",
1413+ "properties": {
1414+ "can_reason": {
1415+ "description": "Enable can reason",
1416+ "type": "boolean"
1417+ },
1418+ "context_window": {
1419+ "description": "Context Window",
1420+ "type": "integer"
1421+ },
1422+ "cost_per_1m_in": {
1423+ "description": "Cost Per1 M In",
1424+ "type": "number"
1425+ },
1426+ "cost_per_1m_in_cached": {
1427+ "description": "Cost Per1 M In Cached",
1428+ "type": "number"
1429+ },
1430+ "cost_per_1m_out": {
1431+ "description": "Cost Per1 M Out",
1432+ "type": "number"
1433+ },
1434+ "cost_per_1m_out_cached": {
1435+ "description": "Cost Per1 M Out Cached",
1436+ "type": "number"
1437+ },
1438+ "default_max_tokens": {
1439+ "description": "Default Max Tokens",
1440+ "type": "integer"
1441+ },
1442+ "has_reasoning_effort": {
1443+ "description": "Enable has reasoning effort",
1444+ "type": "boolean"
1445+ },
1446+ "id": {
1447+ "description": "I D",
1448+ "type": "string"
1449+ },
1450+ "model": {
1451+ "description": "Name",
1452+ "type": "string"
1453+ },
1454+ "reasoning_effort": {
1455+ "description": "Reasoning Effort",
1456+ "type": "string"
1457+ },
1458+ "supports_attachments": {
1459+ "description": "Enable supports images",
1460+ "type": "boolean"
1461+ }
1462+ },
1463+ "required": [
1464+ "id",
1465+ "model",
1466+ "cost_per_1m_in",
1467+ "cost_per_1m_out",
1468+ "cost_per_1m_in_cached",
1469+ "cost_per_1m_out_cached",
1470+ "context_window",
1471+ "default_max_tokens",
1472+ "can_reason",
1473+ "reasoning_effort",
1474+ "has_reasoning_effort",
1475+ "supports_attachments"
1476+ ]
1477+ }
1478+ },
1479+ "provider_type": {
1480+ "description": "Provider Type",
3711481 "type": "string"
3721482 }
3731483 },
374− "type": "object"
1484+ "required": [
1485+ "id",
1486+ "provider_type",
1487+ "disabled"
1488+ ]
3751489 },
376− "wd": {
377− "description": "Working directory for the application",
378− "type": "string"
1490+ "TUIOptions": {
1491+ "description": "T U I",
1492+ "type": "object",
1493+ "properties": {
1494+ "compact_mode": {
1495+ "description": "Enable compact mode",
1496+ "type": "boolean",
1497+ "default": false
1498+ }
1499+ },
1500+ "required": [
1501+ "compact_mode"
1502+ ]
3791503 }
380− },
381− "title": "Crush Configuration",
382− "type": "object"
1504+ }
3831505}
internal/config/config.go+56
@@ -732,6 +732,34 @@ func defaultConfigBasedOnEnv() *Config {
732732 "project": os.Getenv("GOOGLE_CLOUD_PROJECT"),
733733 "location": os.Getenv("GOOGLE_CLOUD_LOCATION"),
734734 }
735+ // Find the VertexAI provider definition to get default models
736+ for _, p := range providers {
737+ if p.ID == provider.InferenceProviderVertexAI {
738+ providerConfig.DefaultLargeModel = p.DefaultLargeModelID
739+ providerConfig.DefaultSmallModel = p.DefaultSmallModelID
740+ for _, model := range p.Models {
741+ configModel := Model{
742+ ID: model.ID,
743+ Name: model.Name,
744+ CostPer1MIn: model.CostPer1MIn,
745+ CostPer1MOut: model.CostPer1MOut,
746+ CostPer1MInCached: model.CostPer1MInCached,
747+ CostPer1MOutCached: model.CostPer1MOutCached,
748+ ContextWindow: model.ContextWindow,
749+ DefaultMaxTokens: model.DefaultMaxTokens,
750+ CanReason: model.CanReason,
751+ SupportsImages: model.SupportsImages,
752+ }
753+ // Set reasoning effort for reasoning models
754+ if model.HasReasoningEffort && model.DefaultReasoningEffort != "" {
755+ configModel.HasReasoningEffort = model.HasReasoningEffort
756+ configModel.ReasoningEffort = model.DefaultReasoningEffort
757+ }
758+ providerConfig.Models = append(providerConfig.Models, configModel)
759+ }
760+ break
761+ }
762+ }
735763 cfg.Providers[provider.InferenceProviderVertexAI] = providerConfig
736764 }
737765
@@ -743,6 +771,34 @@ func defaultConfigBasedOnEnv() *Config {
743771 if providerConfig.ExtraParams["region"] == "" {
744772 providerConfig.ExtraParams["region"] = os.Getenv("AWS_REGION")
745773 }
774+ // Find the Bedrock provider definition to get default models
775+ for _, p := range providers {
776+ if p.ID == provider.InferenceProviderBedrock {
777+ providerConfig.DefaultLargeModel = p.DefaultLargeModelID
778+ providerConfig.DefaultSmallModel = p.DefaultSmallModelID
779+ for _, model := range p.Models {
780+ configModel := Model{
781+ ID: model.ID,
782+ Name: model.Name,
783+ CostPer1MIn: model.CostPer1MIn,
784+ CostPer1MOut: model.CostPer1MOut,
785+ CostPer1MInCached: model.CostPer1MInCached,
786+ CostPer1MOutCached: model.CostPer1MOutCached,
787+ ContextWindow: model.ContextWindow,
788+ DefaultMaxTokens: model.DefaultMaxTokens,
789+ CanReason: model.CanReason,
790+ SupportsImages: model.SupportsImages,
791+ }
792+ // Set reasoning effort for reasoning models
793+ if model.HasReasoningEffort && model.DefaultReasoningEffort != "" {
794+ configModel.HasReasoningEffort = model.HasReasoningEffort
795+ configModel.ReasoningEffort = model.DefaultReasoningEffort
796+ }
797+ providerConfig.Models = append(providerConfig.Models, configModel)
798+ }
799+ break
800+ }
801+ }
746802 cfg.Providers[provider.InferenceProviderBedrock] = providerConfig
747803 }
748804 return cfg
internal/config/config_test.go+257−476
@@ -93,7 +93,7 @@ func TestInit_SingletonBehavior(t *testing.T) {
9393
9494 require.NoError(t, err1)
9595 require.NoError(t, err2)
96− assert.Same(t, cfg1, cfg2) // Should be the same instance
96+ assert.Same(t, cfg1, cfg2)
9797}
9898
9999func TestGet_BeforeInitialization(t *testing.T) {
@@ -124,7 +124,7 @@ func TestLoadConfig_NoConfigFiles(t *testing.T) {
124124 cfg, err := Init(cwdDir, false)
125125
126126 require.NoError(t, err)
127− assert.Len(t, cfg.Providers, 0) // No providers without env vars or config files
127+ assert.Len(t, cfg.Providers, 0)
128128 assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
129129}
130130
@@ -133,7 +133,6 @@ func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
133133 testConfigDir = t.TempDir()
134134 cwdDir := t.TempDir()
135135
136− // Create global config file
137136 globalConfig := Config{
138137 Providers: map[provider.InferenceProvider]ProviderConfig{
139138 provider.InferenceProviderOpenAI: {
@@ -187,7 +186,6 @@ func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
187186 testConfigDir = t.TempDir()
188187 cwdDir := t.TempDir()
189188
190− // Create local config file
191189 localConfig := Config{
192190 Providers: map[provider.InferenceProvider]ProviderConfig{
193191 provider.InferenceProviderAnthropic: {
@@ -239,7 +237,6 @@ func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
239237 testConfigDir = t.TempDir()
240238 cwdDir := t.TempDir()
241239
242− // Create global config
243240 globalConfig := Config{
244241 Providers: map[provider.InferenceProvider]ProviderConfig{
245242 provider.InferenceProviderOpenAI: {
@@ -279,7 +276,6 @@ func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
279276 require.NoError(t, err)
280277 require.NoError(t, os.WriteFile(configPath, data, 0o644))
281278
282− // Create local config that overrides and adds
283279 localConfig := Config{
284280 Providers: map[provider.InferenceProvider]ProviderConfig{
285281 provider.InferenceProviderOpenAI: {
@@ -327,14 +323,11 @@ func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
327323 require.NoError(t, err)
328324 assert.Len(t, cfg.Providers, 2)
329325
330− // Check that local config overrode global
331326 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
332327 assert.Equal(t, "local-key", openaiProvider.APIKey)
333328
334− // Check that local config added new provider
335329 assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
336330
337− // Check that context paths were merged
338331 assert.Contains(t, cfg.Options.ContextPaths, "global-context.md")
339332 assert.Contains(t, cfg.Options.ContextPaths, "local-context.md")
340333 assert.True(t, cfg.Options.TUI.CompactMode)
@@ -345,7 +338,6 @@ func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
345338 testConfigDir = t.TempDir()
346339 cwdDir := t.TempDir()
347340
348− // Create malformed global config
349341 configPath := filepath.Join(testConfigDir, "crush.json")
350342 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
351343 require.NoError(t, os.WriteFile(configPath, []byte(`{invalid json`), 0o644))
@@ -359,7 +351,6 @@ func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
359351 testConfigDir = t.TempDir()
360352 cwdDir := t.TempDir()
361353
362− // Create malformed local config
363354 localConfigPath := filepath.Join(cwdDir, "crush.json")
364355 require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
365356
@@ -409,7 +400,6 @@ func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
409400 testConfigDir = t.TempDir()
410401 cwdDir := t.TempDir()
411402
412− // Set all supported API keys
413403 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
414404 os.Setenv("OPENAI_API_KEY", "test-openai-key")
415405 os.Setenv("GEMINI_API_KEY", "test-gemini-key")
@@ -421,7 +411,6 @@ func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
421411 require.NoError(t, err)
422412 assert.Len(t, cfg.Providers, 5)
423413
424− // Verify each provider is configured correctly
425414 anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
426415 assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
427416 assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
@@ -449,7 +438,6 @@ func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
449438 testConfigDir = t.TempDir()
450439 cwdDir := t.TempDir()
451440
452− // Set only some API keys
453441 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
454442 os.Setenv("OPENAI_API_KEY", "test-openai-key")
455443
@@ -467,7 +455,6 @@ func TestEnvVars_VertexAIConfiguration(t *testing.T) {
467455 testConfigDir = t.TempDir()
468456 cwdDir := t.TempDir()
469457
470− // Set VertexAI environment variables
471458 os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
472459 os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
473460 os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
@@ -488,7 +475,6 @@ func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
488475 testConfigDir = t.TempDir()
489476 cwdDir := t.TempDir()
490477
491− // Set Google Cloud vars but not the use flag
492478 os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
493479 os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
494480
@@ -503,7 +489,6 @@ func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
503489 testConfigDir = t.TempDir()
504490 cwdDir := t.TempDir()
505491
506− // Set AWS credentials
507492 os.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
508493 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
509494 os.Setenv("AWS_DEFAULT_REGION", "us-east-1")
@@ -523,7 +508,6 @@ func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
523508 testConfigDir = t.TempDir()
524509 cwdDir := t.TempDir()
525510
526− // Set AWS profile
527511 os.Setenv("AWS_PROFILE", "test-profile")
528512 os.Setenv("AWS_REGION", "eu-west-1")
529513
@@ -541,7 +525,6 @@ func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
541525 testConfigDir = t.TempDir()
542526 cwdDir := t.TempDir()
543527
544− // Set AWS container credentials
545528 os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
546529 os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
547530
@@ -556,7 +539,6 @@ func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
556539 testConfigDir = t.TempDir()
557540 cwdDir := t.TempDir()
558541
559− // Set both region variables - AWS_DEFAULT_REGION should take priority
560542 os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
561543 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
562544 os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
@@ -574,7 +556,6 @@ func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
574556 testConfigDir = t.TempDir()
575557 cwdDir := t.TempDir()
576558
577− // Set only AWS_REGION (not AWS_DEFAULT_REGION)
578559 os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
579560 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
580561 os.Setenv("AWS_REGION", "us-east-1")
@@ -591,7 +572,6 @@ func TestEnvVars_NoAWSCredentials(t *testing.T) {
591572 testConfigDir = t.TempDir()
592573 cwdDir := t.TempDir()
593574
594− // Don't set any AWS credentials
595575 cfg, err := Init(cwdDir, false)
596576
597577 require.NoError(t, err)
@@ -603,15 +583,12 @@ func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
603583 testConfigDir = t.TempDir()
604584 cwdDir := t.TempDir()
605585
606− // Test that environment variables are properly resolved from provider definitions
607− // This test assumes the provider system uses $VARIABLE_NAME format
608586 os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
609587
610588 cfg, err := Init(cwdDir, false)
611589
612590 require.NoError(t, err)
613591 if len(cfg.Providers) > 0 {
614− // Verify that the environment variable was resolved
615592 if anthropicProvider, exists := cfg.Providers[provider.InferenceProviderAnthropic]; exists {
616593 assert.Equal(t, "resolved-anthropic-key", anthropicProvider.APIKey)
617594 }
@@ -623,11 +600,11 @@ func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
623600 testConfigDir = t.TempDir()
624601 cwdDir := t.TempDir()
625602
626− // Set multiple types of environment variables
627603 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic")
628604 os.Setenv("OPENAI_API_KEY", "test-openai")
629605 os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
630606 os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
607+ os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
631608 os.Setenv("AWS_ACCESS_KEY_ID", "test-aws-key")
632609 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-aws-secret")
633610 os.Setenv("AWS_DEFAULT_REGION", "us-west-1")
@@ -636,7 +613,6 @@ func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
636613
637614 require.NoError(t, err)
638615
639− // Should have API key providers + VertexAI + Bedrock
640616 expectedProviders := []provider.InferenceProvider{
641617 provider.InferenceProviderAnthropic,
642618 provider.InferenceProviderOpenAI,
@@ -696,14 +672,11 @@ func TestHasAWSCredentials_NoCredentials(t *testing.T) {
696672 assert.False(t, hasAWSCredentials())
697673}
698674
699−// Provider Configuration Tests
700−
701675func TestProviderMerging_GlobalToBase(t *testing.T) {
702676 reset()
703677 testConfigDir = t.TempDir()
704678 cwdDir := t.TempDir()
705679
706− // Create global config with provider
707680 globalConfig := Config{
708681 Providers: map[provider.InferenceProvider]ProviderConfig{
709682 provider.InferenceProviderOpenAI: {
@@ -719,6 +692,12 @@ func TestProviderMerging_GlobalToBase(t *testing.T) {
719692 ContextWindow: 8192,
720693 DefaultMaxTokens: 4096,
721694 },
695+ {
696+ ID: "gpt-3.5-turbo",
697+ Name: "GPT-3.5 Turbo",
698+ ContextWindow: 4096,
699+ DefaultMaxTokens: 2048,
700+ },
722701 },
723702 },
724703 },
@@ -739,7 +718,7 @@ func TestProviderMerging_GlobalToBase(t *testing.T) {
739718 assert.Equal(t, "global-openai-key", openaiProvider.APIKey)
740719 assert.Equal(t, "gpt-4", openaiProvider.DefaultLargeModel)
741720 assert.Equal(t, "gpt-3.5-turbo", openaiProvider.DefaultSmallModel)
742− assert.Len(t, openaiProvider.Models, 1)
721+ assert.Len(t, openaiProvider.Models, 2)
743722}
744723
745724func TestProviderMerging_LocalToBase(t *testing.T) {
@@ -747,7 +726,6 @@ func TestProviderMerging_LocalToBase(t *testing.T) {
747726 testConfigDir = t.TempDir()
748727 cwdDir := t.TempDir()
749728
750− // Create local config with provider
751729 localConfig := Config{
752730 Providers: map[provider.InferenceProvider]ProviderConfig{
753731 provider.InferenceProviderAnthropic: {
@@ -755,6 +733,25 @@ func TestProviderMerging_LocalToBase(t *testing.T) {
755733 APIKey: "local-anthropic-key",
756734 ProviderType: provider.TypeAnthropic,
757735 DefaultLargeModel: "claude-3-opus",
736+ DefaultSmallModel: "claude-3-haiku",
737+ Models: []Model{
738+ {
739+ ID: "claude-3-opus",
740+ Name: "Claude 3 Opus",
741+ ContextWindow: 200000,
742+ DefaultMaxTokens: 4096,
743+ CostPer1MIn: 15.0,
744+ CostPer1MOut: 75.0,
745+ },
746+ {
747+ ID: "claude-3-haiku",
748+ Name: "Claude 3 Haiku",
749+ ContextWindow: 200000,
750+ DefaultMaxTokens: 4096,
751+ CostPer1MIn: 0.25,
752+ CostPer1MOut: 1.25,
753+ },
754+ },
758755 },
759756 },
760757 }
@@ -772,6 +769,8 @@ func TestProviderMerging_LocalToBase(t *testing.T) {
772769 anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
773770 assert.Equal(t, "local-anthropic-key", anthropicProvider.APIKey)
774771 assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModel)
772+ assert.Equal(t, "claude-3-haiku", anthropicProvider.DefaultSmallModel)
773+ assert.Len(t, anthropicProvider.Models, 2)
775774}
776775
777776func TestProviderMerging_ConflictingSettings(t *testing.T) {
@@ -779,7 +778,6 @@ func TestProviderMerging_ConflictingSettings(t *testing.T) {
779778 testConfigDir = t.TempDir()
780779 cwdDir := t.TempDir()
781780
782− // Create global config
783781 globalConfig := Config{
784782 Providers: map[provider.InferenceProvider]ProviderConfig{
785783 provider.InferenceProviderOpenAI: {
@@ -788,6 +786,26 @@ func TestProviderMerging_ConflictingSettings(t *testing.T) {
788786 ProviderType: provider.TypeOpenAI,
789787 DefaultLargeModel: "gpt-4",
790788 DefaultSmallModel: "gpt-3.5-turbo",
789+ Models: []Model{
790+ {
791+ ID: "gpt-4",
792+ Name: "GPT-4",
793+ ContextWindow: 8192,
794+ DefaultMaxTokens: 4096,
795+ },
796+ {
797+ ID: "gpt-3.5-turbo",
798+ Name: "GPT-3.5 Turbo",
799+ ContextWindow: 4096,
800+ DefaultMaxTokens: 2048,
801+ },
802+ {
803+ ID: "gpt-4-turbo",
804+ Name: "GPT-4 Turbo",
805+ ContextWindow: 128000,
806+ DefaultMaxTokens: 4096,
807+ },
808+ },
791809 },
792810 },
793811 }
@@ -804,7 +822,6 @@ func TestProviderMerging_ConflictingSettings(t *testing.T) {
804822 provider.InferenceProviderOpenAI: {
805823 APIKey: "local-key",
806824 DefaultLargeModel: "gpt-4-turbo",
807− // Test disabled separately - don't disable here as it causes nil pointer
808825 },
809826 },
810827 }
@@ -819,11 +836,9 @@ func TestProviderMerging_ConflictingSettings(t *testing.T) {
819836 require.NoError(t, err)
820837
821838 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
822− // Local should override global
823839 assert.Equal(t, "local-key", openaiProvider.APIKey)
824840 assert.Equal(t, "gpt-4-turbo", openaiProvider.DefaultLargeModel)
825− assert.False(t, openaiProvider.Disabled) // Should not be disabled
826− // Global values should remain where not overridden
841+ assert.False(t, openaiProvider.Disabled)
827842 assert.Equal(t, "gpt-3.5-turbo", openaiProvider.DefaultSmallModel)
828843}
829844
@@ -834,22 +849,51 @@ func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
834849
835850 customProviderID := provider.InferenceProvider("custom-provider")
836851
837− // Create config with both known and custom providers
838852 globalConfig := Config{
839853 Providers: map[provider.InferenceProvider]ProviderConfig{
840− // Known provider - some fields should not be overrideable
841854 provider.InferenceProviderOpenAI: {
842− ID: provider.InferenceProviderOpenAI,
843− APIKey: "openai-key",
844− BaseURL: "should-not-override",
845− ProviderType: provider.TypeAnthropic, // Should not override
855+ ID: provider.InferenceProviderOpenAI,
856+ APIKey: "openai-key",
857+ BaseURL: "should-not-override",
858+ ProviderType: provider.TypeAnthropic,
859+ DefaultLargeModel: "gpt-4",
860+ DefaultSmallModel: "gpt-3.5-turbo",
861+ Models: []Model{
862+ {
863+ ID: "gpt-4",
864+ Name: "GPT-4",
865+ ContextWindow: 8192,
866+ DefaultMaxTokens: 4096,
867+ },
868+ {
869+ ID: "gpt-3.5-turbo",
870+ Name: "GPT-3.5 Turbo",
871+ ContextWindow: 4096,
872+ DefaultMaxTokens: 2048,
873+ },
874+ },
846875 },
847− // Custom provider - all fields should be configurable
848876 customProviderID: {
849− ID: customProviderID,
850− APIKey: "custom-key",
851− BaseURL: "https://custom.api.com",
852− ProviderType: provider.TypeOpenAI,
877+ ID: customProviderID,
878+ APIKey: "custom-key",
879+ BaseURL: "https://custom.api.com",
880+ ProviderType: provider.TypeOpenAI,
881+ DefaultLargeModel: "custom-large",
882+ DefaultSmallModel: "custom-small",
883+ Models: []Model{
884+ {
885+ ID: "custom-large",
886+ Name: "Custom Large",
887+ ContextWindow: 8192,
888+ DefaultMaxTokens: 4096,
889+ },
890+ {
891+ ID: "custom-small",
892+ Name: "Custom Small",
893+ ContextWindow: 4096,
894+ DefaultMaxTokens: 2048,
895+ },
896+ },
853897 },
854898 },
855899 }
@@ -882,14 +926,12 @@ func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
882926
883927 require.NoError(t, err)
884928
885− // Known provider should not have BaseURL/ProviderType overridden
886929 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
887930 assert.NotEqual(t, "https://should-not-change.com", openaiProvider.BaseURL)
888931 assert.NotEqual(t, provider.TypeGemini, openaiProvider.ProviderType)
889932
890− // Custom provider should have all fields configurable
891933 customProvider := cfg.Providers[customProviderID]
892− assert.Equal(t, "custom-key", customProvider.APIKey) // Should preserve from global
934+ assert.Equal(t, "custom-key", customProvider.APIKey)
893935 assert.Equal(t, "https://updated-custom.api.com", customProvider.BaseURL)
894936 assert.Equal(t, provider.TypeOpenAI, customProvider.ProviderType)
895937}
@@ -901,14 +943,12 @@ func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
901943
902944 customProviderID := provider.InferenceProvider("custom-provider")
903945
904− // Create config with custom provider missing BaseURL
905946 globalConfig := Config{
906947 Providers: map[provider.InferenceProvider]ProviderConfig{
907948 customProviderID: {
908949 ID: customProviderID,
909950 APIKey: "custom-key",
910951 ProviderType: provider.TypeOpenAI,
911− // Missing BaseURL
912952 },
913953 },
914954 }
@@ -922,7 +962,6 @@ func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
922962 cfg, err := Init(cwdDir, false)
923963
924964 require.NoError(t, err)
925− // Provider should be filtered out due to validation failure
926965 assert.NotContains(t, cfg.Providers, customProviderID)
927966}
928967
@@ -939,7 +978,6 @@ func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
939978 ID: customProviderID,
940979 BaseURL: "https://custom.api.com",
941980 ProviderType: provider.TypeOpenAI,
942− // Missing APIKey
943981 },
944982 },
945983 }
@@ -994,10 +1032,26 @@ func TestProviderValidation_KnownProviderValid(t *testing.T) {
9941032 globalConfig := Config{
9951033 Providers: map[provider.InferenceProvider]ProviderConfig{
9961034 provider.InferenceProviderOpenAI: {
997− ID: provider.InferenceProviderOpenAI,
998− APIKey: "openai-key",
999− ProviderType: provider.TypeOpenAI,
1000− // BaseURL not required for known providers
1035+ ID: provider.InferenceProviderOpenAI,
1036+ APIKey: "openai-key",
1037+ ProviderType: provider.TypeOpenAI,
1038+ DefaultLargeModel: "gpt-4",
1039+ DefaultSmallModel: "gpt-3.5-turbo",
1040+ Models: []Model{
1041+ {
1042+ ID: "gpt-4",
1043+ Name: "GPT-4",
1044+ ContextWindow: 8192,
1045+ DefaultMaxTokens: 4096,
1046+ },
1047+ {
1048+ ID: "gpt-3.5-turbo",
1049+ Name: "GPT-3.5 Turbo",
1050+ ContextWindow: 4096,
1051+ DefaultMaxTokens: 2048,
1052+ },
1053+ },
1054+
10011055 },
10021056 },
10031057 }
@@ -1022,10 +1076,48 @@ func TestProviderValidation_DisabledProvider(t *testing.T) {
10221076 globalConfig := Config{
10231077 Providers: map[provider.InferenceProvider]ProviderConfig{
10241078 provider.InferenceProviderOpenAI: {
1025− ID: provider.InferenceProviderOpenAI,
1026− APIKey: "openai-key",
1027− ProviderType: provider.TypeOpenAI,
1028− Disabled: true,
1079+ ID: provider.InferenceProviderOpenAI,
1080+ APIKey: "openai-key",
1081+ ProviderType: provider.TypeOpenAI,
1082+ Disabled: true,
1083+ DefaultLargeModel: "gpt-4",
1084+ DefaultSmallModel: "gpt-3.5-turbo",
1085+ Models: []Model{
1086+ {
1087+ ID: "gpt-4",
1088+ Name: "GPT-4",
1089+ ContextWindow: 8192,
1090+ DefaultMaxTokens: 4096,
1091+ },
1092+ {
1093+ ID: "gpt-3.5-turbo",
1094+ Name: "GPT-3.5 Turbo",
1095+ ContextWindow: 4096,
1096+ DefaultMaxTokens: 2048,
1097+ },
1098+ },
1099+ },
1100+ provider.InferenceProviderAnthropic: {
1101+ ID: provider.InferenceProviderAnthropic,
1102+ APIKey: "anthropic-key",
1103+ ProviderType: provider.TypeAnthropic,
1104+ Disabled: false, // This one is enabled
1105+ DefaultLargeModel: "claude-3-opus",
1106+ DefaultSmallModel: "claude-3-haiku",
1107+ Models: []Model{
1108+ {
1109+ ID: "claude-3-opus",
1110+ Name: "Claude 3 Opus",
1111+ ContextWindow: 200000,
1112+ DefaultMaxTokens: 4096,
1113+ },
1114+ {
1115+ ID: "claude-3-haiku",
1116+ Name: "Claude 3 Haiku",
1117+ ContextWindow: 200000,
1118+ DefaultMaxTokens: 4096,
1119+ },
1120+ },
10291121 },
10301122 },
10311123 }
@@ -1039,9 +1131,10 @@ func TestProviderValidation_DisabledProvider(t *testing.T) {
10391131 cfg, err := Init(cwdDir, false)
10401132
10411133 require.NoError(t, err)
1042− // Disabled providers should still be in the config but marked as disabled
10431134 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
10441135 assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
1136+ assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
1137+ assert.False(t, cfg.Providers[provider.InferenceProviderAnthropic].Disabled)
10451138}
10461139
10471140func TestProviderModels_AddingNewModels(t *testing.T) {
@@ -1052,9 +1145,11 @@ func TestProviderModels_AddingNewModels(t *testing.T) {
10521145 globalConfig := Config{
10531146 Providers: map[provider.InferenceProvider]ProviderConfig{
10541147 provider.InferenceProviderOpenAI: {
1055− ID: provider.InferenceProviderOpenAI,
1056− APIKey: "openai-key",
1057− ProviderType: provider.TypeOpenAI,
1148+ ID: provider.InferenceProviderOpenAI,
1149+ APIKey: "openai-key",
1150+ ProviderType: provider.TypeOpenAI,
1151+ DefaultLargeModel: "gpt-4",
1152+ DefaultSmallModel: "gpt-4-turbo",
10581153 Models: []Model{
10591154 {
10601155 ID: "gpt-4",
@@ -1098,7 +1193,7 @@ func TestProviderModels_AddingNewModels(t *testing.T) {
10981193 require.NoError(t, err)
10991194
11001195 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1101− assert.Len(t, openaiProvider.Models, 2) // Should have both models
1196+ assert.Len(t, openaiProvider.Models, 2)
11021197
11031198 modelIDs := make([]string, len(openaiProvider.Models))
11041199 for i, model := range openaiProvider.Models {
@@ -1116,9 +1211,11 @@ func TestProviderModels_DuplicateModelHandling(t *testing.T) {
11161211 globalConfig := Config{
11171212 Providers: map[provider.InferenceProvider]ProviderConfig{
11181213 provider.InferenceProviderOpenAI: {
1119− ID: provider.InferenceProviderOpenAI,
1120− APIKey: "openai-key",
1121− ProviderType: provider.TypeOpenAI,
1214+ ID: provider.InferenceProviderOpenAI,
1215+ APIKey: "openai-key",
1216+ ProviderType: provider.TypeOpenAI,
1217+ DefaultLargeModel: "gpt-4",
1218+ DefaultSmallModel: "gpt-4",
11221219 Models: []Model{
11231220 {
11241221 ID: "gpt-4",
@@ -1136,7 +1233,7 @@ func TestProviderModels_DuplicateModelHandling(t *testing.T) {
11361233 provider.InferenceProviderOpenAI: {
11371234 Models: []Model{
11381235 {
1139− ID: "gpt-4", // Same ID as global
1236+ ID: "gpt-4",
11401237 Name: "GPT-4 Updated",
11411238 ContextWindow: 16384,
11421239 DefaultMaxTokens: 8192,
@@ -1162,13 +1259,12 @@ func TestProviderModels_DuplicateModelHandling(t *testing.T) {
11621259 require.NoError(t, err)
11631260
11641261 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1165− assert.Len(t, openaiProvider.Models, 1) // Should not duplicate
1262+ assert.Len(t, openaiProvider.Models, 1)
11661263
1167− // Should keep the original model (global config)
11681264 model := openaiProvider.Models[0]
11691265 assert.Equal(t, "gpt-4", model.ID)
1170− assert.Equal(t, "GPT-4", model.Name) // Original name
1171− assert.Equal(t, int64(8192), model.ContextWindow) // Original context window
1266+ assert.Equal(t, "GPT-4", model.Name)
1267+ assert.Equal(t, int64(8192), model.ContextWindow)
11721268}
11731269
11741270func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
@@ -1179,9 +1275,11 @@ func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
11791275 globalConfig := Config{
11801276 Providers: map[provider.InferenceProvider]ProviderConfig{
11811277 provider.InferenceProviderOpenAI: {
1182− ID: provider.InferenceProviderOpenAI,
1183− APIKey: "openai-key",
1184− ProviderType: provider.TypeOpenAI,
1278+ ID: provider.InferenceProviderOpenAI,
1279+ APIKey: "openai-key",
1280+ ProviderType: provider.TypeOpenAI,
1281+ DefaultLargeModel: "gpt-4",
1282+ DefaultSmallModel: "gpt-4",
11851283 Models: []Model{
11861284 {
11871285 ID: "gpt-4",
@@ -1224,14 +1322,11 @@ func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
12241322 assert.True(t, model.SupportsImages)
12251323}
12261324
1227−// Agent Configuration Tests
1228−
12291325func TestDefaultAgents_CoderAgent(t *testing.T) {
12301326 reset()
12311327 testConfigDir = t.TempDir()
12321328 cwdDir := t.TempDir()
12331329
1234− // Set up a provider so we can test agent configuration
12351330 os.Setenv("ANTHROPIC_API_KEY", "test-key")
12361331
12371332 cfg, err := Init(cwdDir, false)
@@ -1246,7 +1341,6 @@ func TestDefaultAgents_CoderAgent(t *testing.T) {
12461341 assert.Equal(t, LargeModel, coderAgent.Model)
12471342 assert.False(t, coderAgent.Disabled)
12481343 assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
1249− // Coder agent should have all tools available (nil means all tools)
12501344 assert.Nil(t, coderAgent.AllowedTools)
12511345}
12521346
@@ -1255,7 +1349,6 @@ func TestDefaultAgents_TaskAgent(t *testing.T) {
12551349 testConfigDir = t.TempDir()
12561350 cwdDir := t.TempDir()
12571351
1258− // Set up a provider so we can test agent configuration
12591352 os.Setenv("ANTHROPIC_API_KEY", "test-key")
12601353
12611354 cfg, err := Init(cwdDir, false)
@@ -1271,11 +1364,9 @@ func TestDefaultAgents_TaskAgent(t *testing.T) {
12711364 assert.False(t, taskAgent.Disabled)
12721365 assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
12731366
1274− // Task agent should have restricted tools
12751367 expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
12761368 assert.Equal(t, expectedTools, taskAgent.AllowedTools)
12771369
1278− // Task agent should have no MCPs or LSPs by default
12791370 assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
12801371 assert.Equal(t, []string{}, taskAgent.AllowedLSP)
12811372}
@@ -1285,10 +1376,8 @@ func TestAgentMerging_CustomAgent(t *testing.T) {
12851376 testConfigDir = t.TempDir()
12861377 cwdDir := t.TempDir()
12871378
1288− // Set up a provider
12891379 os.Setenv("ANTHROPIC_API_KEY", "test-key")
12901380
1291− // Create config with custom agent
12921381 globalConfig := Config{
12931382 Agents: map[AgentID]Agent{
12941383 AgentID("custom-agent"): {
@@ -1302,6 +1391,23 @@ func TestAgentMerging_CustomAgent(t *testing.T) {
13021391 ContextPaths: []string{"custom-context.md"},
13031392 },
13041393 },
1394+ MCP: map[string]MCP{
1395+ "mcp1": {
1396+ Type: MCPStdio,
1397+ Command: "test-mcp-command",
1398+ Args: []string{"--test"},
1399+ },
1400+ },
1401+ LSP: map[string]LSPConfig{
1402+ "typescript": {
1403+ Command: "typescript-language-server",
1404+ Args: []string{"--stdio"},
1405+ },
1406+ "go": {
1407+ Command: "gopls",
1408+ Args: []string{},
1409+ },
1410+ },
13051411 }
13061412
13071413 configPath := filepath.Join(testConfigDir, "crush.json")
@@ -1314,7 +1420,6 @@ func TestAgentMerging_CustomAgent(t *testing.T) {
13141420
13151421 require.NoError(t, err)
13161422
1317− // Should have default agents plus custom agent
13181423 assert.Contains(t, cfg.Agents, AgentCoder)
13191424 assert.Contains(t, cfg.Agents, AgentTask)
13201425 assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
@@ -1326,7 +1431,6 @@ func TestAgentMerging_CustomAgent(t *testing.T) {
13261431 assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
13271432 assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
13281433 assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
1329− // Context paths should be additive (default + custom)
13301434 expectedContextPaths := append(defaultContextPaths, "custom-context.md")
13311435 assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
13321436}
@@ -1336,17 +1440,28 @@ func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
13361440 testConfigDir = t.TempDir()
13371441 cwdDir := t.TempDir()
13381442
1339− // Set up a provider
13401443 os.Setenv("ANTHROPIC_API_KEY", "test-key")
13411444
1342− // Create config that modifies the default coder agent
13431445 globalConfig := Config{
13441446 Agents: map[AgentID]Agent{
13451447 AgentCoder: {
1346− Model: SmallModel, // Change from default LargeModel
1448+ Model: SmallModel,
13471449 AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
13481450 AllowedLSP: []string{"typescript"},
1349− ContextPaths: []string{"coder-specific.md"}, // Should be additive
1451+ ContextPaths: []string{"coder-specific.md"},
1452+ },
1453+ },
1454+ MCP: map[string]MCP{
1455+ "mcp1": {
1456+ Type: MCPStdio,
1457+ Command: "test-mcp-command",
1458+ Args: []string{"--test"},
1459+ },
1460+ },
1461+ LSP: map[string]LSPConfig{
1462+ "typescript": {
1463+ Command: "typescript-language-server",
1464+ Args: []string{"--stdio"},
13501465 },
13511466 },
13521467 }
@@ -1362,16 +1477,13 @@ func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
13621477 require.NoError(t, err)
13631478
13641479 coderAgent := cfg.Agents[AgentCoder]
1365− // Should preserve default values for unspecified fields
13661480 assert.Equal(t, AgentCoder, coderAgent.ID)
13671481 assert.Equal(t, "Coder", coderAgent.Name)
13681482 assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
13691483
1370− // Context paths should be additive (default + custom)
13711484 expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
13721485 assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
13731486
1374− // Should update specified fields
13751487 assert.Equal(t, SmallModel, coderAgent.Model)
13761488 assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
13771489 assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
@@ -1382,22 +1494,31 @@ func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
13821494 testConfigDir = t.TempDir()
13831495 cwdDir := t.TempDir()
13841496
1385− // Set up a provider
13861497 os.Setenv("ANTHROPIC_API_KEY", "test-key")
13871498
1388− // Create config that modifies the default task agent
1389− // Note: Only model, MCP, and LSP should be configurable for known agents
13901499 globalConfig := Config{
13911500 Agents: map[AgentID]Agent{
13921501 AgentTask: {
1393− Model: SmallModel, // Should be updated
1394− AllowedMCP: map[string][]string{"search-mcp": nil}, // Should be updated
1395− AllowedLSP: []string{"python"}, // Should be updated
1396− // These should be ignored for known agents:
1397− Name: "Search Agent", // Should be ignored
1398− Description: "Custom search agent", // Should be ignored
1399− Disabled: true, // Should be ignored
1400− AllowedTools: []string{"glob", "grep", "view"}, // Should be ignored
1502+ Model: SmallModel,
1503+ AllowedMCP: map[string][]string{"search-mcp": nil},
1504+ AllowedLSP: []string{"python"},
1505+ Name: "Search Agent",
1506+ Description: "Custom search agent",
1507+ Disabled: true,
1508+ AllowedTools: []string{"glob", "grep", "view"},
1509+ },
1510+ },
1511+ MCP: map[string]MCP{
1512+ "search-mcp": {
1513+ Type: MCPStdio,
1514+ Command: "search-mcp-command",
1515+ Args: []string{"--search"},
1516+ },
1517+ },
1518+ LSP: map[string]LSPConfig{
1519+ "python": {
1520+ Command: "pylsp",
1521+ Args: []string{},
14011522 },
14021523 },
14031524 }
@@ -1413,13 +1534,11 @@ func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
14131534 require.NoError(t, err)
14141535
14151536 taskAgent := cfg.Agents[AgentTask]
1416− // Should preserve default values for protected fields
1417− assert.Equal(t, "Task", taskAgent.Name) // Should remain default
1418− assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description) // Should remain default
1419− assert.False(t, taskAgent.Disabled) // Should remain default
1420− assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools) // Should remain default
1537+ assert.Equal(t, "Task", taskAgent.Name)
1538+ assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1539+ assert.False(t, taskAgent.Disabled)
1540+ assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
14211541
1422− // Should update configurable fields
14231542 assert.Equal(t, SmallModel, taskAgent.Model)
14241543 assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
14251544 assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
@@ -1430,10 +1549,8 @@ func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
14301549 testConfigDir = t.TempDir()
14311550 cwdDir := t.TempDir()
14321551
1433− // Set up a provider
14341552 os.Setenv("ANTHROPIC_API_KEY", "test-key")
14351553
1436− // Create global config with custom agent
14371554 globalConfig := Config{
14381555 Agents: map[AgentID]Agent{
14391556 AgentID("test-agent"): {
@@ -1464,6 +1581,13 @@ func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
14641581 AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
14651582 },
14661583 },
1584+ MCP: map[string]MCP{
1585+ "local-mcp": {
1586+ Type: MCPStdio,
1587+ Command: "local-mcp-command",
1588+ Args: []string{"--local"},
1589+ },
1590+ },
14671591 }
14681592
14691593 localConfigPath := filepath.Join(cwdDir, "crush.json")
@@ -1476,7 +1600,6 @@ func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
14761600 require.NoError(t, err)
14771601
14781602 testAgent := cfg.Agents[AgentID("test-agent")]
1479− // Local should override global
14801603 assert.Equal(t, "Local Agent", testAgent.Name)
14811604 assert.Equal(t, "Local description", testAgent.Description)
14821605 assert.Equal(t, SmallModel, testAgent.Model)
@@ -1490,10 +1613,8 @@ func TestAgentModelTypeAssignment(t *testing.T) {
14901613 testConfigDir = t.TempDir()
14911614 cwdDir := t.TempDir()
14921615
1493− // Set up a provider
14941616 os.Setenv("ANTHROPIC_API_KEY", "test-key")
14951617
1496− // Create config with agents using different model types
14971618 globalConfig := Config{
14981619 Agents: map[AgentID]Agent{
14991620 AgentID("large-agent"): {
@@ -1509,7 +1630,6 @@ func TestAgentModelTypeAssignment(t *testing.T) {
15091630 AgentID("default-agent"): {
15101631 ID: AgentID("default-agent"),
15111632 Name: "Default Model Agent",
1512− // No model specified - should default to LargeModel
15131633 },
15141634 },
15151635 }
@@ -1526,7 +1646,7 @@ func TestAgentModelTypeAssignment(t *testing.T) {
15261646
15271647 assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
15281648 assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
1529− assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model) // Should default to LargeModel
1649+ assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model)
15301650}
15311651
15321652func TestAgentContextPathOverrides(t *testing.T) {
@@ -1534,10 +1654,8 @@ func TestAgentContextPathOverrides(t *testing.T) {
15341654 testConfigDir = t.TempDir()
15351655 cwdDir := t.TempDir()
15361656
1537− // Set up a provider
15381657 os.Setenv("ANTHROPIC_API_KEY", "test-key")
15391658
1540− // Create config with custom context paths
15411659 globalConfig := Config{
15421660 Options: Options{
15431661 ContextPaths: []string{"global-context.md", "shared-context.md"},
@@ -1551,7 +1669,6 @@ func TestAgentContextPathOverrides(t *testing.T) {
15511669 AgentID("default-context-agent"): {
15521670 ID: AgentID("default-context-agent"),
15531671 Name: "Default Context Agent",
1554− // No ContextPaths specified - should use global
15551672 },
15561673 },
15571674 }
@@ -1566,32 +1683,25 @@ func TestAgentContextPathOverrides(t *testing.T) {
15661683
15671684 require.NoError(t, err)
15681685
1569− // Agent with custom context paths should have default + global + custom paths (additive)
15701686 customAgent := cfg.Agents[AgentID("custom-context-agent")]
15711687 expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
15721688 assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
15731689
1574− // Agent without custom context paths should use global + defaults
15751690 defaultAgent := cfg.Agents[AgentID("default-context-agent")]
15761691 expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
15771692 assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
15781693
1579− // Default agents should also use the merged context paths
15801694 coderAgent := cfg.Agents[AgentCoder]
15811695 assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
15821696}
15831697
1584−// Options and Settings Tests
1585−
15861698func TestOptionsMerging_ContextPaths(t *testing.T) {
15871699 reset()
15881700 testConfigDir = t.TempDir()
15891701 cwdDir := t.TempDir()
15901702
1591− // Set up a provider
15921703 os.Setenv("ANTHROPIC_API_KEY", "test-key")
15931704
1594− // Create global config with context paths
15951705 globalConfig := Config{
15961706 Options: Options{
15971707 ContextPaths: []string{"global1.md", "global2.md"},
@@ -1604,7 +1714,6 @@ func TestOptionsMerging_ContextPaths(t *testing.T) {
16041714 require.NoError(t, err)
16051715 require.NoError(t, os.WriteFile(configPath, data, 0o644))
16061716
1607− // Create local config with additional context paths
16081717 localConfig := Config{
16091718 Options: Options{
16101719 ContextPaths: []string{"local1.md", "local2.md"},
@@ -1620,7 +1729,6 @@ func TestOptionsMerging_ContextPaths(t *testing.T) {
16201729
16211730 require.NoError(t, err)
16221731
1623− // Context paths should be merged: defaults + global + local
16241732 expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
16251733 assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
16261734}
@@ -1630,14 +1738,12 @@ func TestOptionsMerging_TUIOptions(t *testing.T) {
16301738 testConfigDir = t.TempDir()
16311739 cwdDir := t.TempDir()
16321740
1633− // Set up a provider
16341741 os.Setenv("ANTHROPIC_API_KEY", "test-key")
16351742
1636− // Create global config with TUI options
16371743 globalConfig := Config{
16381744 Options: Options{
16391745 TUI: TUIOptions{
1640− CompactMode: false, // Default value
1746+ CompactMode: false,
16411747 },
16421748 },
16431749 }
@@ -1648,7 +1754,6 @@ func TestOptionsMerging_TUIOptions(t *testing.T) {
16481754 require.NoError(t, err)
16491755 require.NoError(t, os.WriteFile(configPath, data, 0o644))
16501756
1651− // Create local config that enables compact mode
16521757 localConfig := Config{
16531758 Options: Options{
16541759 TUI: TUIOptions{
@@ -1666,7 +1771,6 @@ func TestOptionsMerging_TUIOptions(t *testing.T) {
16661771
16671772 require.NoError(t, err)
16681773
1669− // Local config should override global
16701774 assert.True(t, cfg.Options.TUI.CompactMode)
16711775}
16721776
@@ -1675,10 +1779,8 @@ func TestOptionsMerging_DebugFlags(t *testing.T) {
16751779 testConfigDir = t.TempDir()
16761780 cwdDir := t.TempDir()
16771781
1678− // Set up a provider
16791782 os.Setenv("ANTHROPIC_API_KEY", "test-key")
16801783
1681− // Create global config with debug flags
16821784 globalConfig := Config{
16831785 Options: Options{
16841786 Debug: false,
@@ -1693,7 +1795,6 @@ func TestOptionsMerging_DebugFlags(t *testing.T) {
16931795 require.NoError(t, err)
16941796 require.NoError(t, os.WriteFile(configPath, data, 0o644))
16951797
1696− // Create local config that enables debug flags
16971798 localConfig := Config{
16981799 Options: Options{
16991800 DebugLSP: true,
@@ -1710,10 +1811,9 @@ func TestOptionsMerging_DebugFlags(t *testing.T) {
17101811
17111812 require.NoError(t, err)
17121813
1713− // Local config should override global for boolean flags
1714− assert.False(t, cfg.Options.Debug) // Not set in local, remains global value
1715− assert.True(t, cfg.Options.DebugLSP) // Set to true in local
1716− assert.True(t, cfg.Options.DisableAutoSummarize) // Set to true in local
1814+ assert.False(t, cfg.Options.Debug)
1815+ assert.True(t, cfg.Options.DebugLSP)
1816+ assert.True(t, cfg.Options.DisableAutoSummarize)
17171817}
17181818
17191819func TestOptionsMerging_DataDirectory(t *testing.T) {
@@ -1721,10 +1821,8 @@ func TestOptionsMerging_DataDirectory(t *testing.T) {
17211821 testConfigDir = t.TempDir()
17221822 cwdDir := t.TempDir()
17231823
1724− // Set up a provider
17251824 os.Setenv("ANTHROPIC_API_KEY", "test-key")
17261825
1727− // Create global config with custom data directory
17281826 globalConfig := Config{
17291827 Options: Options{
17301828 DataDirectory: "global-data",
@@ -1737,7 +1835,6 @@ func TestOptionsMerging_DataDirectory(t *testing.T) {
17371835 require.NoError(t, err)
17381836 require.NoError(t, os.WriteFile(configPath, data, 0o644))
17391837
1740− // Create local config with different data directory
17411838 localConfig := Config{
17421839 Options: Options{
17431840 DataDirectory: "local-data",
@@ -1753,7 +1850,6 @@ func TestOptionsMerging_DataDirectory(t *testing.T) {
17531850
17541851 require.NoError(t, err)
17551852
1756− // Local config should override global
17571853 assert.Equal(t, "local-data", cfg.Options.DataDirectory)
17581854}
17591855
@@ -1762,15 +1858,12 @@ func TestOptionsMerging_DefaultValues(t *testing.T) {
17621858 testConfigDir = t.TempDir()
17631859 cwdDir := t.TempDir()
17641860
1765− // Set up a provider
17661861 os.Setenv("ANTHROPIC_API_KEY", "test-key")
17671862
1768− // No config files - should use defaults
17691863 cfg, err := Init(cwdDir, false)
17701864
17711865 require.NoError(t, err)
17721866
1773− // Should have default values
17741867 assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
17751868 assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
17761869 assert.False(t, cfg.Options.TUI.CompactMode)
@@ -1784,10 +1877,8 @@ func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
17841877 testConfigDir = t.TempDir()
17851878 cwdDir := t.TempDir()
17861879
1787− // Set up a provider
17881880 os.Setenv("ANTHROPIC_API_KEY", "test-key")
17891881
1790− // Create config with debug false
17911882 globalConfig := Config{
17921883 Options: Options{
17931884 Debug: false,
@@ -1800,7 +1891,6 @@ func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
18001891 require.NoError(t, err)
18011892 require.NoError(t, os.WriteFile(configPath, data, 0o644))
18021893
1803− // Init with debug=true should override config
18041894 cfg, err := Init(cwdDir, true)
18051895
18061896 require.NoError(t, err)
@@ -1895,85 +1985,20 @@ func TestModelSelection_PreferredModelSelection(t *testing.T) {
18951985 assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
18961986}
18971987
1898−func TestModelSelection_GetAgentModel(t *testing.T) {
1899− reset()
1900− testConfigDir = t.TempDir()
1901− cwdDir := t.TempDir()
1902−
1903− // Set up a provider with known models
1904− globalConfig := Config{
1905− Providers: map[provider.InferenceProvider]ProviderConfig{
1906− provider.InferenceProviderOpenAI: {
1907− ID: provider.InferenceProviderOpenAI,
1908− APIKey: "test-key",
1909− ProviderType: provider.TypeOpenAI,
1910− DefaultLargeModel: "gpt-4",
1911− DefaultSmallModel: "gpt-3.5-turbo",
1912− Models: []Model{
1913− {
1914− ID: "gpt-4",
1915− Name: "GPT-4",
1916− ContextWindow: 8192,
1917− DefaultMaxTokens: 4096,
1918− CanReason: true,
1919− SupportsImages: true,
1920− },
1921− {
1922− ID: "gpt-3.5-turbo",
1923− Name: "GPT-3.5 Turbo",
1924− ContextWindow: 4096,
1925− DefaultMaxTokens: 2048,
1926− CanReason: false,
1927− SupportsImages: false,
1928− },
1929− },
1930− },
1931− },
1932− }
1933−
1934− configPath := filepath.Join(testConfigDir, "crush.json")
1935− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1936− data, err := json.Marshal(globalConfig)
1937− require.NoError(t, err)
1938− require.NoError(t, os.WriteFile(configPath, data, 0o644))
1939−
1940− _, err = Init(cwdDir, false)
1941−
1942− require.NoError(t, err)
1943−
1944− // Test GetAgentModel for default agents
1945− coderModel := GetAgentModel(AgentCoder)
1946− assert.Equal(t, "gpt-4", coderModel.ID) // Coder uses LargeModel
1947− assert.Equal(t, "GPT-4", coderModel.Name)
1948− assert.True(t, coderModel.CanReason)
1949− assert.True(t, coderModel.SupportsImages)
1950−
1951− taskModel := GetAgentModel(AgentTask)
1952− assert.Equal(t, "gpt-4", taskModel.ID) // Task also uses LargeModel by default
1953− assert.Equal(t, "GPT-4", taskModel.Name)
1954−}
1955−
1956−func TestModelSelection_GetAgentModelWithCustomModelType(t *testing.T) {
1988+func TestValidation_InvalidModelReference(t *testing.T) {
19571989 reset()
19581990 testConfigDir = t.TempDir()
19591991 cwdDir := t.TempDir()
19601992
1961− // Set up provider and custom agent with SmallModel
19621993 globalConfig := Config{
19631994 Providers: map[provider.InferenceProvider]ProviderConfig{
19641995 provider.InferenceProviderOpenAI: {
19651996 ID: provider.InferenceProviderOpenAI,
19661997 APIKey: "test-key",
19671998 ProviderType: provider.TypeOpenAI,
1968− DefaultLargeModel: "gpt-4",
1999+ DefaultLargeModel: "non-existent-model",
19692000 DefaultSmallModel: "gpt-3.5-turbo",
19702001 Models: []Model{
1971− {
1972− ID: "gpt-4",
1973− Name: "GPT-4",
1974− ContextWindow: 8192,
1975− DefaultMaxTokens: 4096,
1976− },
19772002 {
19782003 ID: "gpt-3.5-turbo",
19792004 Name: "GPT-3.5 Turbo",
@@ -1983,54 +2008,6 @@ func TestModelSelection_GetAgentModelWithCustomModelType(t *testing.T) {
19832008 },
19842009 },
19852010 },
1986− Agents: map[AgentID]Agent{
1987− AgentID("small-agent"): {
1988− ID: AgentID("small-agent"),
1989− Name: "Small Agent",
1990− Model: SmallModel,
1991− },
1992− },
1993− }
1994−
1995− configPath := filepath.Join(testConfigDir, "crush.json")
1996− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1997− data, err := json.Marshal(globalConfig)
1998− require.NoError(t, err)
1999− require.NoError(t, os.WriteFile(configPath, data, 0o644))
2000−
2001− _, err = Init(cwdDir, false)
2002−
2003− require.NoError(t, err)
2004−
2005− // Test GetAgentModel for custom agent with SmallModel
2006− smallAgentModel := GetAgentModel(AgentID("small-agent"))
2007− assert.Equal(t, "gpt-3.5-turbo", smallAgentModel.ID)
2008− assert.Equal(t, "GPT-3.5 Turbo", smallAgentModel.Name)
2009−}
2010−
2011−func TestModelSelection_GetAgentProvider(t *testing.T) {
2012− reset()
2013− testConfigDir = t.TempDir()
2014− cwdDir := t.TempDir()
2015−
2016− // Set up multiple providers
2017− globalConfig := Config{
2018− Providers: map[provider.InferenceProvider]ProviderConfig{
2019− provider.InferenceProviderOpenAI: {
2020− ID: provider.InferenceProviderOpenAI,
2021− APIKey: "openai-key",
2022− ProviderType: provider.TypeOpenAI,
2023− DefaultLargeModel: "gpt-4",
2024− DefaultSmallModel: "gpt-3.5-turbo",
2025− },
2026− provider.InferenceProviderAnthropic: {
2027− ID: provider.InferenceProviderAnthropic,
2028− APIKey: "anthropic-key",
2029− ProviderType: provider.TypeAnthropic,
2030− DefaultLargeModel: "claude-3-opus",
2031− DefaultSmallModel: "claude-3-haiku",
2032− },
2033− },
20342011 }
20352012
20362013 configPath := filepath.Join(testConfigDir, "crush.json")
@@ -2040,27 +2017,18 @@ func TestModelSelection_GetAgentProvider(t *testing.T) {
20402017 require.NoError(t, os.WriteFile(configPath, data, 0o644))
20412018
20422019 _, err = Init(cwdDir, false)
2043−
2044− require.NoError(t, err)
2045−
2046− // Test GetAgentProvider
2047− coderProvider := GetAgentProvider(AgentCoder)
2048− assert.NotEmpty(t, coderProvider.ID)
2049− assert.NotEmpty(t, coderProvider.APIKey)
2050− assert.NotEmpty(t, coderProvider.ProviderType)
2020+ assert.Error(t, err)
20512021}
20522022
2053−func TestModelSelection_GetProviderModel(t *testing.T) {
2023+func TestValidation_EmptyAPIKey(t *testing.T) {
20542024 reset()
20552025 testConfigDir = t.TempDir()
20562026 cwdDir := t.TempDir()
20572027
2058− // Set up provider with specific models
20592028 globalConfig := Config{
20602029 Providers: map[provider.InferenceProvider]ProviderConfig{
20612030 provider.InferenceProviderOpenAI: {
20622031 ID: provider.InferenceProviderOpenAI,
2063− APIKey: "test-key",
20642032 ProviderType: provider.TypeOpenAI,
20652033 Models: []Model{
20662034 {
@@ -2068,16 +2036,6 @@ func TestModelSelection_GetProviderModel(t *testing.T) {
20682036 Name: "GPT-4",
20692037 ContextWindow: 8192,
20702038 DefaultMaxTokens: 4096,
2071− CostPer1MIn: 30.0,
2072− CostPer1MOut: 60.0,
2073− },
2074− {
2075− ID: "gpt-3.5-turbo",
2076− Name: "GPT-3.5 Turbo",
2077− ContextWindow: 4096,
2078− DefaultMaxTokens: 2048,
2079− CostPer1MIn: 1.5,
2080− CostPer1MOut: 2.0,
20812039 },
20822040 },
20832041 },
@@ -2091,107 +2049,22 @@ func TestModelSelection_GetProviderModel(t *testing.T) {
20912049 require.NoError(t, os.WriteFile(configPath, data, 0o644))
20922050
20932051 _, err = Init(cwdDir, false)
2094−
2095− require.NoError(t, err)
2096−
2097− // Test GetProviderModel
2098− gpt4Model := GetProviderModel(provider.InferenceProviderOpenAI, "gpt-4")
2099− assert.Equal(t, "gpt-4", gpt4Model.ID)
2100− assert.Equal(t, "GPT-4", gpt4Model.Name)
2101− assert.Equal(t, int64(8192), gpt4Model.ContextWindow)
2102− assert.Equal(t, 30.0, gpt4Model.CostPer1MIn)
2103−
2104− gpt35Model := GetProviderModel(provider.InferenceProviderOpenAI, "gpt-3.5-turbo")
2105− assert.Equal(t, "gpt-3.5-turbo", gpt35Model.ID)
2106− assert.Equal(t, "GPT-3.5 Turbo", gpt35Model.Name)
2107− assert.Equal(t, 1.5, gpt35Model.CostPer1MIn)
2108−
2109− // Test non-existent model
2110− nonExistentModel := GetProviderModel(provider.InferenceProviderOpenAI, "non-existent")
2111− assert.Empty(t, nonExistentModel.ID)
2052+ assert.Error(t, err)
21122053}
21132054
2114−func TestModelSelection_GetModel(t *testing.T) {
2055+func TestValidation_InvalidAgentModelType(t *testing.T) {
21152056 reset()
21162057 testConfigDir = t.TempDir()
21172058 cwdDir := t.TempDir()
21182059
2119− // Set up provider with models
2120− globalConfig := Config{
2121− Providers: map[provider.InferenceProvider]ProviderConfig{
2122− provider.InferenceProviderOpenAI: {
2123− ID: provider.InferenceProviderOpenAI,
2124− APIKey: "test-key",
2125− ProviderType: provider.TypeOpenAI,
2126− DefaultLargeModel: "gpt-4",
2127− DefaultSmallModel: "gpt-3.5-turbo",
2128− Models: []Model{
2129− {
2130− ID: "gpt-4",
2131− Name: "GPT-4",
2132− ContextWindow: 8192,
2133− DefaultMaxTokens: 4096,
2134− },
2135− {
2136− ID: "gpt-3.5-turbo",
2137− Name: "GPT-3.5 Turbo",
2138− ContextWindow: 4096,
2139− DefaultMaxTokens: 2048,
2140− },
2141− },
2142− },
2143− },
2144− }
2145−
2146− configPath := filepath.Join(testConfigDir, "crush.json")
2147− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2148− data, err := json.Marshal(globalConfig)
2149− require.NoError(t, err)
2150− require.NoError(t, os.WriteFile(configPath, data, 0o644))
2151−
2152− _, err = Init(cwdDir, false)
2153−
2154− require.NoError(t, err)
2155−
2156− // Test GetModel
2157− largeModel := GetModel(LargeModel)
2158− assert.Equal(t, "gpt-4", largeModel.ID)
2159− assert.Equal(t, "GPT-4", largeModel.Name)
2160−
2161− smallModel := GetModel(SmallModel)
2162− assert.Equal(t, "gpt-3.5-turbo", smallModel.ID)
2163− assert.Equal(t, "GPT-3.5 Turbo", smallModel.Name)
2164−}
2165−
2166−func TestModelSelection_UpdatePreferredModel(t *testing.T) {
2167− reset()
2168− testConfigDir = t.TempDir()
2169− cwdDir := t.TempDir()
2060+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
21702061
2171− // Set up multiple providers with OpenAI first to ensure it's selected initially
21722062 globalConfig := Config{
2173− Providers: map[provider.InferenceProvider]ProviderConfig{
2174− provider.InferenceProviderOpenAI: {
2175− ID: provider.InferenceProviderOpenAI,
2176− APIKey: "openai-key",
2177− ProviderType: provider.TypeOpenAI,
2178− DefaultLargeModel: "gpt-4",
2179− DefaultSmallModel: "gpt-3.5-turbo",
2180− Models: []Model{
2181− {ID: "gpt-4", Name: "GPT-4"},
2182− {ID: "gpt-3.5-turbo", Name: "GPT-3.5 Turbo"},
2183− },
2184− },
2185− provider.InferenceProviderAnthropic: {
2186− ID: provider.InferenceProviderAnthropic,
2187− APIKey: "anthropic-key",
2188− ProviderType: provider.TypeAnthropic,
2189− DefaultLargeModel: "claude-3-opus",
2190− DefaultSmallModel: "claude-3-haiku",
2191− Models: []Model{
2192− {ID: "claude-3-opus", Name: "Claude 3 Opus"},
2193− {ID: "claude-3-haiku", Name: "Claude 3 Haiku"},
2194− },
2063+ Agents: map[AgentID]Agent{
2064+ AgentID("invalid-agent"): {
2065+ ID: AgentID("invalid-agent"),
2066+ Name: "Invalid Agent",
2067+ Model: ModelType("invalid"),
21952068 },
21962069 },
21972070 }
@@ -2203,97 +2076,5 @@ func TestModelSelection_UpdatePreferredModel(t *testing.T) {
22032076 require.NoError(t, os.WriteFile(configPath, data, 0o644))
22042077
22052078 _, err = Init(cwdDir, false)
2206−
2207− require.NoError(t, err)
2208−
2209− // Get initial preferred models (should be OpenAI since it's listed first)
2210− initialLargeModel := GetModel(LargeModel)
2211− initialSmallModel := GetModel(SmallModel)
2212−
2213− // Verify initial models are OpenAI models
2214− assert.Equal(t, "claude-3-opus", initialLargeModel.ID)
2215− assert.Equal(t, "claude-3-haiku", initialSmallModel.ID)
2216−
2217− // Update preferred models to Anthropic
2218− newLargeModel := PreferredModel{
2219− ModelID: "gpt-4",
2220− Provider: provider.InferenceProviderOpenAI,
2221− }
2222− newSmallModel := PreferredModel{
2223− ModelID: "gpt-3.5-turbo",
2224− Provider: provider.InferenceProviderOpenAI,
2225− }
2226−
2227− err = UpdatePreferredModel(LargeModel, newLargeModel)
2228− require.NoError(t, err)
2229−
2230− err = UpdatePreferredModel(SmallModel, newSmallModel)
2231− require.NoError(t, err)
2232−
2233− // Verify models were updated
2234− updatedLargeModel := GetModel(LargeModel)
2235− assert.Equal(t, "gpt-4", updatedLargeModel.ID)
2236− assert.NotEqual(t, initialLargeModel.ID, updatedLargeModel.ID)
2237−
2238− updatedSmallModel := GetModel(SmallModel)
2239− assert.Equal(t, "gpt-3.5-turbo", updatedSmallModel.ID)
2240− assert.NotEqual(t, initialSmallModel.ID, updatedSmallModel.ID)
2241−}
2242−
2243−func TestModelSelection_InvalidModelType(t *testing.T) {
2244− reset()
2245− testConfigDir = t.TempDir()
2246− cwdDir := t.TempDir()
2247−
2248− // Set up a provider
2249− os.Setenv("ANTHROPIC_API_KEY", "test-key")
2250−
2251− _, err := Init(cwdDir, false)
2252− require.NoError(t, err)
2253−
2254− // Test UpdatePreferredModel with invalid model type
2255− invalidModel := PreferredModel{
2256− ModelID: "some-model",
2257− Provider: provider.InferenceProviderAnthropic,
2258− }
2259−
2260− err = UpdatePreferredModel(ModelType("invalid"), invalidModel)
22612079 assert.Error(t, err)
2262− assert.Contains(t, err.Error(), "unknown model type")
2263−}
2264−
2265−func TestModelSelection_NonExistentAgent(t *testing.T) {
2266− reset()
2267− testConfigDir = t.TempDir()
2268− cwdDir := t.TempDir()
2269−
2270− // Set up a provider
2271− os.Setenv("ANTHROPIC_API_KEY", "test-key")
2272−
2273− _, err := Init(cwdDir, false)
2274− require.NoError(t, err)
2275−
2276− // Test GetAgentModel with non-existent agent
2277− nonExistentModel := GetAgentModel(AgentID("non-existent"))
2278− assert.Empty(t, nonExistentModel.ID)
2279−
2280− // Test GetAgentProvider with non-existent agent
2281− nonExistentProvider := GetAgentProvider(AgentID("non-existent"))
2282− assert.Empty(t, nonExistentProvider.ID)
2283−}
2284−
2285−func TestModelSelection_NonExistentProvider(t *testing.T) {
2286− reset()
2287− testConfigDir = t.TempDir()
2288− cwdDir := t.TempDir()
2289−
2290− // Set up a provider
2291− os.Setenv("ANTHROPIC_API_KEY", "test-key")
2292−
2293− _, err := Init(cwdDir, false)
2294− require.NoError(t, err)
2295−
2296− // Test GetProviderModel with non-existent provider
2297− nonExistentModel := GetProviderModel(provider.InferenceProvider("non-existent"), "some-model")
2298− assert.Empty(t, nonExistentModel.ID)
22992080}
internal/config/provider_mock.go+171−135
@@ -6,6 +6,7 @@ import (
66
77// MockProviders returns a mock list of providers for testing.
88// This avoids making API calls during tests and provides consistent test data.
9+// Simplified version with only default models from each provider.
910func MockProviders() []provider.Provider {
1011 return []provider.Provider{
1112 {
@@ -14,43 +15,19 @@ func MockProviders() []provider.Provider {
1415 APIKey: "$ANTHROPIC_API_KEY",
1516 APIEndpoint: "$ANTHROPIC_API_ENDPOINT",
1617 Type: provider.TypeAnthropic,
17− DefaultLargeModelID: "claude-3-opus",
18− DefaultSmallModelID: "claude-3-haiku",
18+ DefaultLargeModelID: "claude-sonnet-4-20250514",
19+ DefaultSmallModelID: "claude-3-5-haiku-20241022",
1920 Models: []provider.Model{
2021 {
21− ID: "claude-3-opus",
22− Name: "Claude 3 Opus",
23− CostPer1MIn: 15.0,
24− CostPer1MOut: 75.0,
25− CostPer1MInCached: 18.75,
26− CostPer1MOutCached: 1.5,
27− ContextWindow: 200000,
28− DefaultMaxTokens: 4096,
29− CanReason: false,
30− SupportsImages: true,
31− },
32− {
33− ID: "claude-3-haiku",
34− Name: "Claude 3 Haiku",
35− CostPer1MIn: 0.25,
36− CostPer1MOut: 1.25,
37− CostPer1MInCached: 0.3,
38− CostPer1MOutCached: 0.03,
39− ContextWindow: 200000,
40− DefaultMaxTokens: 4096,
41− CanReason: false,
42− SupportsImages: true,
43− },
44− {
45− ID: "claude-3-5-sonnet-20241022",
46− Name: "Claude 3.5 Sonnet",
22+ ID: "claude-sonnet-4-20250514",
23+ Name: "Claude Sonnet 4",
4724 CostPer1MIn: 3.0,
4825 CostPer1MOut: 15.0,
4926 CostPer1MInCached: 3.75,
5027 CostPer1MOutCached: 0.3,
5128 ContextWindow: 200000,
52− DefaultMaxTokens: 8192,
53− CanReason: false,
29+ DefaultMaxTokens: 50000,
30+ CanReason: true,
5431 SupportsImages: true,
5532 },
5633 {
@@ -61,7 +38,7 @@ func MockProviders() []provider.Provider {
6138 CostPer1MInCached: 1.0,
6239 CostPer1MOutCached: 0.08,
6340 ContextWindow: 200000,
64− DefaultMaxTokens: 8192,
41+ DefaultMaxTokens: 5000,
6542 CanReason: false,
6643 SupportsImages: true,
6744 },
@@ -73,44 +50,22 @@ func MockProviders() []provider.Provider {
7350 APIKey: "$OPENAI_API_KEY",
7451 APIEndpoint: "$OPENAI_API_ENDPOINT",
7552 Type: provider.TypeOpenAI,
76− DefaultLargeModelID: "gpt-4",
77− DefaultSmallModelID: "gpt-3.5-turbo",
53+ DefaultLargeModelID: "codex-mini-latest",
54+ DefaultSmallModelID: "gpt-4o",
7855 Models: []provider.Model{
7956 {
80− ID: "gpt-4",
81− Name: "GPT-4",
82− CostPer1MIn: 30.0,
83− CostPer1MOut: 60.0,
84− CostPer1MInCached: 0.0,
85− CostPer1MOutCached: 0.0,
86− ContextWindow: 8192,
87− DefaultMaxTokens: 4096,
88− CanReason: false,
89− SupportsImages: false,
90− },
91− {
92− ID: "gpt-3.5-turbo",
93− Name: "GPT-3.5 Turbo",
94− CostPer1MIn: 1.0,
95− CostPer1MOut: 2.0,
96− CostPer1MInCached: 0.0,
97− CostPer1MOutCached: 0.0,
98− ContextWindow: 4096,
99− DefaultMaxTokens: 4096,
100− CanReason: false,
101− SupportsImages: false,
102− },
103− {
104− ID: "gpt-4-turbo",
105− Name: "GPT-4 Turbo",
106− CostPer1MIn: 10.0,
107− CostPer1MOut: 30.0,
108− CostPer1MInCached: 0.0,
109− CostPer1MOutCached: 0.0,
110− ContextWindow: 128000,
111− DefaultMaxTokens: 4096,
112− CanReason: false,
113− SupportsImages: true,
57+ ID: "codex-mini-latest",
58+ Name: "Codex Mini",
59+ CostPer1MIn: 1.5,
60+ CostPer1MOut: 6.0,
61+ CostPer1MInCached: 0.0,
62+ CostPer1MOutCached: 0.375,
63+ ContextWindow: 200000,
64+ DefaultMaxTokens: 50000,
65+ CanReason: true,
66+ HasReasoningEffort: true,
67+ DefaultReasoningEffort: "medium",
68+ SupportsImages: true,
11469 },
11570 {
11671 ID: "gpt-4o",
@@ -120,50 +75,10 @@ func MockProviders() []provider.Provider {
12075 CostPer1MInCached: 0.0,
12176 CostPer1MOutCached: 1.25,
12277 ContextWindow: 128000,
123− DefaultMaxTokens: 16384,
78+ DefaultMaxTokens: 20000,
12479 CanReason: false,
12580 SupportsImages: true,
12681 },
127− {
128− ID: "gpt-4o-mini",
129− Name: "GPT-4o-mini",
130− CostPer1MIn: 0.15,
131− CostPer1MOut: 0.6,
132− CostPer1MInCached: 0.0,
133− CostPer1MOutCached: 0.075,
134− ContextWindow: 128000,
135− DefaultMaxTokens: 16384,
136− CanReason: false,
137− SupportsImages: true,
138− },
139− {
140− ID: "o1-preview",
141− Name: "o1-preview",
142− CostPer1MIn: 15.0,
143− CostPer1MOut: 60.0,
144− CostPer1MInCached: 0.0,
145− CostPer1MOutCached: 0.0,
146− ContextWindow: 128000,
147− DefaultMaxTokens: 32768,
148− CanReason: true,
149− HasReasoningEffort: true,
150− DefaultReasoningEffort: "medium",
151− SupportsImages: true,
152− },
153− {
154− ID: "o1-mini",
155− Name: "o1-mini",
156− CostPer1MIn: 3.0,
157− CostPer1MOut: 12.0,
158− CostPer1MInCached: 0.0,
159− CostPer1MOutCached: 0.0,
160− ContextWindow: 128000,
161− DefaultMaxTokens: 65536,
162− CanReason: true,
163− HasReasoningEffort: true,
164− DefaultReasoningEffort: "medium",
165− SupportsImages: true,
166− },
16782 },
16883 },
16984 {
@@ -183,7 +98,7 @@ func MockProviders() []provider.Provider {
18398 CostPer1MInCached: 1.625,
18499 CostPer1MOutCached: 0.31,
185100 ContextWindow: 1048576,
186− DefaultMaxTokens: 65536,
101+ DefaultMaxTokens: 50000,
187102 CanReason: true,
188103 SupportsImages: true,
189104 },
@@ -195,7 +110,7 @@ func MockProviders() []provider.Provider {
195110 CostPer1MInCached: 0.3833,
196111 CostPer1MOutCached: 0.075,
197112 ContextWindow: 1048576,
198− DefaultMaxTokens: 65535,
113+ DefaultMaxTokens: 50000,
199114 CanReason: true,
200115 SupportsImages: true,
201116 },
@@ -207,18 +122,135 @@ func MockProviders() []provider.Provider {
207122 APIKey: "$XAI_API_KEY",
208123 APIEndpoint: "https://api.x.ai/v1",
209124 Type: provider.TypeXAI,
210− DefaultLargeModelID: "grok-beta",
211− DefaultSmallModelID: "grok-beta",
125+ DefaultLargeModelID: "grok-3",
126+ DefaultSmallModelID: "grok-3-mini",
212127 Models: []provider.Model{
213128 {
214− ID: "grok-beta",
215− Name: "Grok Beta",
216− CostPer1MIn: 5.0,
129+ ID: "grok-3",
130+ Name: "Grok 3",
131+ CostPer1MIn: 3.0,
217132 CostPer1MOut: 15.0,
133+ CostPer1MInCached: 0.0,
134+ CostPer1MOutCached: 0.75,
218135 ContextWindow: 131072,
219− DefaultMaxTokens: 4096,
136+ DefaultMaxTokens: 20000,
220137 CanReason: false,
221− SupportsImages: true,
138+ SupportsImages: false,
139+ },
140+ {
141+ ID: "grok-3-mini",
142+ Name: "Grok 3 Mini",
143+ CostPer1MIn: 0.3,
144+ CostPer1MOut: 0.5,
145+ CostPer1MInCached: 0.0,
146+ CostPer1MOutCached: 0.075,
147+ ContextWindow: 131072,
148+ DefaultMaxTokens: 20000,
149+ CanReason: true,
150+ SupportsImages: false,
151+ },
152+ },
153+ },
154+ {
155+ Name: "Azure OpenAI",
156+ ID: provider.InferenceProviderAzure,
157+ APIKey: "$AZURE_OPENAI_API_KEY",
158+ APIEndpoint: "$AZURE_OPENAI_API_ENDPOINT",
159+ Type: provider.TypeAzure,
160+ DefaultLargeModelID: "o4-mini",
161+ DefaultSmallModelID: "gpt-4o",
162+ Models: []provider.Model{
163+ {
164+ ID: "o4-mini",
165+ Name: "o4 Mini",
166+ CostPer1MIn: 1.1,
167+ CostPer1MOut: 4.4,
168+ CostPer1MInCached: 0.0,
169+ CostPer1MOutCached: 0.275,
170+ ContextWindow: 200000,
171+ DefaultMaxTokens: 50000,
172+ CanReason: true,
173+ HasReasoningEffort: false,
174+ DefaultReasoningEffort: "medium",
175+ SupportsImages: true,
176+ },
177+ {
178+ ID: "gpt-4o",
179+ Name: "GPT-4o",
180+ CostPer1MIn: 2.5,
181+ CostPer1MOut: 10.0,
182+ CostPer1MInCached: 0.0,
183+ CostPer1MOutCached: 1.25,
184+ ContextWindow: 128000,
185+ DefaultMaxTokens: 20000,
186+ CanReason: false,
187+ SupportsImages: true,
188+ },
189+ },
190+ },
191+ {
192+ Name: "AWS Bedrock",
193+ ID: provider.InferenceProviderBedrock,
194+ Type: provider.TypeBedrock,
195+ DefaultLargeModelID: "anthropic.claude-sonnet-4-20250514-v1:0",
196+ DefaultSmallModelID: "anthropic.claude-3-5-haiku-20241022-v1:0",
197+ Models: []provider.Model{
198+ {
199+ ID: "anthropic.claude-sonnet-4-20250514-v1:0",
200+ Name: "AWS Claude Sonnet 4",
201+ CostPer1MIn: 3.0,
202+ CostPer1MOut: 15.0,
203+ CostPer1MInCached: 3.75,
204+ CostPer1MOutCached: 0.3,
205+ ContextWindow: 200000,
206+ DefaultMaxTokens: 50000,
207+ CanReason: true,
208+ SupportsImages: true,
209+ },
210+ {
211+ ID: "anthropic.claude-3-5-haiku-20241022-v1:0",
212+ Name: "AWS Claude 3.5 Haiku",
213+ CostPer1MIn: 0.8,
214+ CostPer1MOut: 4.0,
215+ CostPer1MInCached: 1.0,
216+ CostPer1MOutCached: 0.08,
217+ ContextWindow: 200000,
218+ DefaultMaxTokens: 50000,
219+ CanReason: false,
220+ SupportsImages: true,
221+ },
222+ },
223+ },
224+ {
225+ Name: "Google Vertex AI",
226+ ID: provider.InferenceProviderVertexAI,
227+ Type: provider.TypeVertexAI,
228+ DefaultLargeModelID: "gemini-2.5-pro",
229+ DefaultSmallModelID: "gemini-2.5-flash",
230+ Models: []provider.Model{
231+ {
232+ ID: "gemini-2.5-pro",
233+ Name: "Gemini 2.5 Pro",
234+ CostPer1MIn: 1.25,
235+ CostPer1MOut: 10.0,
236+ CostPer1MInCached: 1.625,
237+ CostPer1MOutCached: 0.31,
238+ ContextWindow: 1048576,
239+ DefaultMaxTokens: 50000,
240+ CanReason: true,
241+ SupportsImages: true,
242+ },
243+ {
244+ ID: "gemini-2.5-flash",
245+ Name: "Gemini 2.5 Flash",
246+ CostPer1MIn: 0.3,
247+ CostPer1MOut: 2.5,
248+ CostPer1MInCached: 0.3833,
249+ CostPer1MOutCached: 0.075,
250+ ContextWindow: 1048576,
251+ DefaultMaxTokens: 50000,
252+ CanReason: true,
253+ SupportsImages: true,
222254 },
223255 },
224256 },
@@ -228,28 +260,32 @@ func MockProviders() []provider.Provider {
228260 APIKey: "$OPENROUTER_API_KEY",
229261 APIEndpoint: "https://openrouter.ai/api/v1",
230262 Type: provider.TypeOpenAI,
231− DefaultLargeModelID: "anthropic/claude-3.5-sonnet",
232− DefaultSmallModelID: "anthropic/claude-3.5-haiku",
263+ DefaultLargeModelID: "anthropic/claude-sonnet-4",
264+ DefaultSmallModelID: "anthropic/claude-haiku-3.5",
233265 Models: []provider.Model{
234266 {
235− ID: "anthropic/claude-3.5-sonnet",
236− Name: "Claude 3.5 Sonnet",
237− CostPer1MIn: 3.0,
238− CostPer1MOut: 15.0,
239− ContextWindow: 200000,
240− DefaultMaxTokens: 8192,
241− CanReason: false,
242− SupportsImages: true,
267+ ID: "anthropic/claude-sonnet-4",
268+ Name: "Anthropic: Claude Sonnet 4",
269+ CostPer1MIn: 3.0,
270+ CostPer1MOut: 15.0,
271+ CostPer1MInCached: 3.75,
272+ CostPer1MOutCached: 0.3,
273+ ContextWindow: 200000,
274+ DefaultMaxTokens: 32000,
275+ CanReason: true,
276+ SupportsImages: true,
243277 },
244278 {
245− ID: "anthropic/claude-3.5-haiku",
246− Name: "Claude 3.5 Haiku",
247− CostPer1MIn: 0.8,
248− CostPer1MOut: 4.0,
249− ContextWindow: 200000,
250− DefaultMaxTokens: 8192,
251− CanReason: false,
252− SupportsImages: true,
279+ ID: "anthropic/claude-haiku-3.5",
280+ Name: "Anthropic: Claude 3.5 Haiku",
281+ CostPer1MIn: 0.8,
282+ CostPer1MOut: 4.0,
283+ CostPer1MInCached: 1.0,
284+ CostPer1MOutCached: 0.08,
285+ ContextWindow: 200000,
286+ DefaultMaxTokens: 4096,
287+ CanReason: false,
288+ SupportsImages: true,
253289 },
254290 },
255291 },
internal/config/provider_test.go+11−210
@@ -1,7 +1,6 @@
11package config
22
33import (
4− "encoding/json"
54 "testing"
65
76 "github.com/charmbracelet/crush/internal/fur/provider"
@@ -9,8 +8,7 @@ import (
98 "github.com/stretchr/testify/require"
109)
1110
12−func TestMockProviders(t *testing.T) {
13− // Enable mock providers for testing
11+func TestProviders_MockEnabled(t *testing.T) {
1412 originalUseMock := UseMockProviders
1513 UseMockProviders = true
1614 defer func() {
@@ -18,94 +16,38 @@ func TestMockProviders(t *testing.T) {
1816 ResetProviders()
1917 }()
2018
21− // Reset providers to ensure we get fresh mock data
2219 ResetProviders()
23−
2420 providers := Providers()
25− require.NotEmpty(t, providers, "Mock providers should not be empty")
21+ require.NotEmpty(t, providers)
2622
27− // Verify we have the expected mock providers
2823 providerIDs := make(map[provider.InferenceProvider]bool)
2924 for _, p := range providers {
3025 providerIDs[p.ID] = true
3126 }
3227
33− assert.True(t, providerIDs[provider.InferenceProviderAnthropic], "Should have Anthropic provider")
34− assert.True(t, providerIDs[provider.InferenceProviderOpenAI], "Should have OpenAI provider")
35− assert.True(t, providerIDs[provider.InferenceProviderGemini], "Should have Gemini provider")
36−
37− // Verify Anthropic provider details
38− var anthropicProvider provider.Provider
39− for _, p := range providers {
40− if p.ID == provider.InferenceProviderAnthropic {
41− anthropicProvider = p
42− break
43− }
44− }
45−
46− assert.Equal(t, "Anthropic", anthropicProvider.Name)
47− assert.Equal(t, provider.TypeAnthropic, anthropicProvider.Type)
48− assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModelID)
49− assert.Equal(t, "claude-3-haiku", anthropicProvider.DefaultSmallModelID)
50− assert.Len(t, anthropicProvider.Models, 4, "Anthropic should have 4 models")
51−
52− // Verify model details
53− var opusModel provider.Model
54− for _, m := range anthropicProvider.Models {
55− if m.ID == "claude-3-opus" {
56− opusModel = m
57− break
58− }
59− }
60−
61− assert.Equal(t, "Claude 3 Opus", opusModel.Name)
62− assert.Equal(t, int64(200000), opusModel.ContextWindow)
63− assert.Equal(t, int64(4096), opusModel.DefaultMaxTokens)
64− assert.True(t, opusModel.SupportsImages)
65−}
66−
67−func TestProvidersWithoutMock(t *testing.T) {
68− // Ensure mock is disabled
69− originalUseMock := UseMockProviders
70− UseMockProviders = false
71− defer func() {
72− UseMockProviders = originalUseMock
73− ResetProviders()
74− }()
75−
76− // Reset providers to ensure we get fresh data
77− ResetProviders()
78−
79− // This will try to make an actual API call or use cached data
80− providers := Providers()
81−
82− // We can't guarantee what we'll get here since it depends on network/cache
83− // but we can at least verify the function doesn't panic
84− t.Logf("Got %d providers without mock", len(providers))
28+ assert.True(t, providerIDs[provider.InferenceProviderAnthropic])
29+ assert.True(t, providerIDs[provider.InferenceProviderOpenAI])
30+ assert.True(t, providerIDs[provider.InferenceProviderGemini])
8531}
8632
87−func TestResetProviders(t *testing.T) {
88− // Enable mock providers
33+func TestProviders_ResetFunctionality(t *testing.T) {
8934 UseMockProviders = true
9035 defer func() {
9136 UseMockProviders = false
9237 ResetProviders()
9338 }()
9439
95− // Get providers once
9640 providers1 := Providers()
9741 require.NotEmpty(t, providers1)
9842
99− // Reset and get again
10043 ResetProviders()
10144 providers2 := Providers()
10245 require.NotEmpty(t, providers2)
10346
104− // Should get the same mock data
10547 assert.Equal(t, len(providers1), len(providers2))
10648}
10749
108−func TestReasoningEffortSupport(t *testing.T) {
50+func TestProviders_ModelCapabilities(t *testing.T) {
10951 originalUseMock := UseMockProviders
11052 UseMockProviders = true
11153 defer func() {
@@ -125,156 +67,15 @@ func TestReasoningEffortSupport(t *testing.T) {
12567 }
12668 require.NotEmpty(t, openaiProvider.ID)
12769
128− var reasoningModel, nonReasoningModel provider.Model
70+ var foundReasoning, foundNonReasoning bool
12971 for _, model := range openaiProvider.Models {
13072 if model.CanReason && model.HasReasoningEffort {
131− reasoningModel = model
132− } else if !model.CanReason {
133− nonReasoningModel = model
134− }
135− }
136−
137− require.NotEmpty(t, reasoningModel.ID)
138− assert.Equal(t, "medium", reasoningModel.DefaultReasoningEffort)
139− assert.True(t, reasoningModel.HasReasoningEffort)
140−
141− require.NotEmpty(t, nonReasoningModel.ID)
142− assert.False(t, nonReasoningModel.HasReasoningEffort)
143− assert.Empty(t, nonReasoningModel.DefaultReasoningEffort)
144−}
145−
146−func TestReasoningEffortConfigTransfer(t *testing.T) {
147− originalUseMock := UseMockProviders
148− UseMockProviders = true
149− defer func() {
150− UseMockProviders = originalUseMock
151− ResetProviders()
152− }()
153−
154− ResetProviders()
155− t.Setenv("OPENAI_API_KEY", "test-openai-key")
156−
157− cfg, err := Init(t.TempDir(), false)
158− require.NoError(t, err)
159−
160− openaiProviderConfig, exists := cfg.Providers[provider.InferenceProviderOpenAI]
161− require.True(t, exists)
162−
163− var foundReasoning, foundNonReasoning bool
164− for _, model := range openaiProviderConfig.Models {
165− if model.CanReason && model.HasReasoningEffort && model.ReasoningEffort != "" {
166− assert.Equal(t, "medium", model.ReasoningEffort)
167− assert.True(t, model.HasReasoningEffort)
16873 foundReasoning = true
16974 } else if !model.CanReason {
170− assert.Empty(t, model.ReasoningEffort)
171− assert.False(t, model.HasReasoningEffort)
17275 foundNonReasoning = true
17376 }
17477 }
17578
176− assert.True(t, foundReasoning, "Should find at least one reasoning model")
177− assert.True(t, foundNonReasoning, "Should find at least one non-reasoning model")
178−}
179−
180−func TestNewProviders(t *testing.T) {
181− originalUseMock := UseMockProviders
182− UseMockProviders = true
183− defer func() {
184− UseMockProviders = originalUseMock
185− ResetProviders()
186− }()
187−
188− ResetProviders()
189− providers := Providers()
190− require.NotEmpty(t, providers)
191−
192− var xaiProvider, openRouterProvider provider.Provider
193− for _, p := range providers {
194− switch p.ID {
195− case provider.InferenceProviderXAI:
196− xaiProvider = p
197− case provider.InferenceProviderOpenRouter:
198− openRouterProvider = p
199− }
200− }
201−
202− require.NotEmpty(t, xaiProvider.ID)
203− assert.Equal(t, "xAI", xaiProvider.Name)
204− assert.Equal(t, "grok-beta", xaiProvider.DefaultLargeModelID)
205−
206− require.NotEmpty(t, openRouterProvider.ID)
207− assert.Equal(t, "OpenRouter", openRouterProvider.Name)
208− assert.Equal(t, "anthropic/claude-3.5-sonnet", openRouterProvider.DefaultLargeModelID)
209−}
210−
211−func TestO1ModelsInMockProvider(t *testing.T) {
212− originalUseMock := UseMockProviders
213− UseMockProviders = true
214− defer func() {
215− UseMockProviders = originalUseMock
216− ResetProviders()
217− }()
218−
219− ResetProviders()
220− providers := Providers()
221−
222− var openaiProvider provider.Provider
223− for _, p := range providers {
224− if p.ID == provider.InferenceProviderOpenAI {
225− openaiProvider = p
226− break
227− }
228− }
229− require.NotEmpty(t, openaiProvider.ID)
230−
231− modelTests := []struct {
232− id string
233− name string
234− }{
235− {"o1-preview", "o1-preview"},
236− {"o1-mini", "o1-mini"},
237− }
238−
239− for _, test := range modelTests {
240− var model provider.Model
241− var found bool
242− for _, m := range openaiProvider.Models {
243− if m.ID == test.id {
244− model = m
245− found = true
246− break
247− }
248− }
249− require.True(t, found, "Should find %s model", test.id)
250− assert.Equal(t, test.name, model.Name)
251− assert.True(t, model.CanReason)
252− assert.True(t, model.HasReasoningEffort)
253− assert.Equal(t, "medium", model.DefaultReasoningEffort)
254− }
255−}
256−
257−func TestPreferredModelReasoningEffort(t *testing.T) {
258− // Test that PreferredModel struct can hold reasoning effort
259− preferredModel := PreferredModel{
260− ModelID: "o1-preview",
261− Provider: provider.InferenceProviderOpenAI,
262− ReasoningEffort: "high",
263− }
264−
265− assert.Equal(t, "o1-preview", preferredModel.ModelID)
266− assert.Equal(t, provider.InferenceProviderOpenAI, preferredModel.Provider)
267− assert.Equal(t, "high", preferredModel.ReasoningEffort)
268−
269− // Test JSON marshaling/unmarshaling
270− jsonData, err := json.Marshal(preferredModel)
271− require.NoError(t, err)
272−
273− var unmarshaled PreferredModel
274− err = json.Unmarshal(jsonData, &unmarshaled)
275− require.NoError(t, err)
276−
277− assert.Equal(t, preferredModel.ModelID, unmarshaled.ModelID)
278− assert.Equal(t, preferredModel.Provider, unmarshaled.Provider)
279− assert.Equal(t, preferredModel.ReasoningEffort, unmarshaled.ReasoningEffort)
280−}
79+ assert.True(t, foundReasoning)
80+ assert.True(t, foundNonReasoning)
81+}
internal/config/shell.go−1
@@ -71,4 +71,3 @@ func resolveCommandAPIKey(command string) (string, error) {
7171 logging.Debug("Command executed successfully", "command", command, "result", result)
7272 return result, nil
7373}
74−