on main help sign in
Merge pull request #292 from charmbracelet/catwalk
506c3b6Kujtim Hoxha
README.md+4−4
@@ -43,7 +43,7 @@ rm -rf ./crush
43
43
44Then, run Crush by typing `crush`.
44Then, run Crush by typing `crush`.
45
45
46−***
46+---
47
47
48</details>
48</details>
49
49
@@ -108,7 +108,7 @@ Crush supports Model Context Protocol (MCP) servers through three transport type
108 "mcp": {
108 "mcp": {
109 "filesystem": {
109 "filesystem": {
110 "type": "stdio",
110 "type": "stdio",
111− "command": "node",
111+ "command": "node",
112 "args": ["/path/to/mcp-server.js"],
112 "args": ["/path/to/mcp-server.js"],
113 "env": {
113 "env": {
114 "NODE_ENV": "production"
114 "NODE_ENV": "production"
@@ -143,7 +143,7 @@ crush -d
143# View last 1000 lines
143# View last 1000 lines
144crush logs
144crush logs
145
145
146−# Follow logs in real-time
146+# Follow logs in real-time
147crush logs -f
147crush logs -f
148
148
149# Show last 500 lines
149# Show last 500 lines
@@ -174,7 +174,7 @@ Crush supports all OpenAI-compatible APIs. Here's an example configuration for D
174 "models": [
174 "models": [
175 {
175 {
176 "id": "deepseek-chat",
176 "id": "deepseek-chat",
177− "model": "Deepseek V3",
177+ "name": "Deepseek V3",
178 "cost_per_1m_in": 0.27,
178 "cost_per_1m_in": 0.27,
179 "cost_per_1m_out": 1.1,
179 "cost_per_1m_out": 1.1,
180 "cost_per_1m_in_cached": 0.07,
180 "cost_per_1m_in_cached": 0.07,
go.mod+1
@@ -13,6 +13,7 @@ require (
13 github.com/charlievieth/fastwalk v1.0.11
13 github.com/charlievieth/fastwalk v1.0.11
14 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
14 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
15 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac
15 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac
16+ github.com/charmbracelet/catwalk v0.3.1
16 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
17 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
17 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
18 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
go.sum+2−2
@@ -72,6 +72,8 @@ github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
72github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
72github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
73github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac h1:murtkvFYxZ/73vk4Z/tpE4biB+WDZcFmmBp8je/yV6M=
73github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac h1:murtkvFYxZ/73vk4Z/tpE4biB+WDZcFmmBp8je/yV6M=
74github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac/go.mod h1:m240IQxo1/eDQ7klblSzOCAUyc3LddHcV3Rc/YEGAgw=
74github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac/go.mod h1:m240IQxo1/eDQ7klblSzOCAUyc3LddHcV3Rc/YEGAgw=
75+github.com/charmbracelet/catwalk v0.3.1 h1:MkGWspcMyE659zDkqS+9wsaCMTKRFEDBFY2A2sap6+U=
76+github.com/charmbracelet/catwalk v0.3.1/go.mod h1:gUUCqqZ8bk4D7ZzGTu3I77k7cC2x4exRuJBN1H2u2pc=
75github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
77github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
76github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
78github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
77github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
79github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
@@ -82,8 +84,6 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
82github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112/go.mod h1:BXY7j7rZgAprFwzNcO698++5KTd6GKI6lU83Pr4o0r0=
84github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112/go.mod h1:BXY7j7rZgAprFwzNcO698++5KTd6GKI6lU83Pr4o0r0=
83github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
85github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
84github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
86github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
85−github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42 h1:Zqw2oP9Wo8VzMijVJbtIJcAaZviYyU07stvmCFCfn0Y=
86−github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
87github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1 h1:tsw1mOuIEIKlmm614bXctvJ3aavaFhyPG+y+wrKtuKQ=
87github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1 h1:tsw1mOuIEIKlmm614bXctvJ3aavaFhyPG+y+wrKtuKQ=
88github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
88github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
89github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
89github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
internal/config/config.go+12−12
@@ -9,8 +9,8 @@ import (
9 "strings"
9 "strings"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/env"
13 "github.com/charmbracelet/crush/internal/env"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/tidwall/sjson"
14 "github.com/tidwall/sjson"
15 "golang.org/x/exp/slog"
15 "golang.org/x/exp/slog"
16)
16)
@@ -70,7 +70,7 @@ type ProviderConfig struct {
70 // The provider's API endpoint.
70 // The provider's API endpoint.
71 BaseURL string `json:"base_url,omitempty"`
71 BaseURL string `json:"base_url,omitempty"`
72 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
72 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
73− Type provider.Type `json:"type,omitempty"`
73+ Type catwalk.Type `json:"type,omitempty"`
74 // The provider's API key.
74 // The provider's API key.
75 APIKey string `json:"api_key,omitempty"`
75 APIKey string `json:"api_key,omitempty"`
76 // Marks the provider as disabled.
76 // Marks the provider as disabled.
@@ -85,7 +85,7 @@ type ProviderConfig struct {
85 ExtraParams map[string]string `json:"-"`
85 ExtraParams map[string]string `json:"-"`
86
86
87 // The provider models
87 // The provider models
88− Models []provider.Model `json:"models,omitempty"`
88+ Models []catwalk.Model `json:"models,omitempty"`
89}
89}
90
90
91type MCPType string
91type MCPType string
@@ -250,8 +250,8 @@ type Config struct {
250 Agents map[string]Agent `json:"-"`
250 Agents map[string]Agent `json:"-"`
251 // TODO: find a better way to do this this should probably not be part of the config
251 // TODO: find a better way to do this this should probably not be part of the config
252 resolver VariableResolver
252 resolver VariableResolver
253− dataConfigDir string `json:"-"`
253+ dataConfigDir string `json:"-"`
254− knownProviders []provider.Provider `json:"-"`
254+ knownProviders []catwalk.Provider `json:"-"`
255}
255}
256
256
257func (c *Config) WorkingDir() string {
257func (c *Config) WorkingDir() string {
@@ -273,7 +273,7 @@ func (c *Config) IsConfigured() bool {
273 return len(c.EnabledProviders()) > 0
273 return len(c.EnabledProviders()) > 0
274}
274}
275
275
276−func (c *Config) GetModel(provider, model string) *provider.Model {
276+func (c *Config) GetModel(provider, model string) *catwalk.Model {
277 if providerConfig, ok := c.Providers[provider]; ok {
277 if providerConfig, ok := c.Providers[provider]; ok {
278 for _, m := range providerConfig.Models {
278 for _, m := range providerConfig.Models {
279 if m.ID == model {
279 if m.ID == model {
@@ -295,7 +295,7 @@ func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfi
295 return nil
295 return nil
296}
296}
297
297
298−func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
298+func (c *Config) GetModelByType(modelType SelectedModelType) *catwalk.Model {
299 model, ok := c.Models[modelType]
299 model, ok := c.Models[modelType]
300 if !ok {
300 if !ok {
301 return nil
301 return nil
@@ -303,7 +303,7 @@ func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
303 return c.GetModel(model.Provider, model.Model)
303 return c.GetModel(model.Provider, model.Model)
304}
304}
305
305
306−func (c *Config) LargeModel() *provider.Model {
306+func (c *Config) LargeModel() *catwalk.Model {
307 model, ok := c.Models[SelectedModelTypeLarge]
307 model, ok := c.Models[SelectedModelTypeLarge]
308 if !ok {
308 if !ok {
309 return nil
309 return nil
@@ -311,7 +311,7 @@ func (c *Config) LargeModel() *provider.Model {
311 return c.GetModel(model.Provider, model.Model)
311 return c.GetModel(model.Provider, model.Model)
312}
312}
313
313
314−func (c *Config) SmallModel() *provider.Model {
314+func (c *Config) SmallModel() *catwalk.Model {
315 model, ok := c.Models[SelectedModelTypeSmall]
315 model, ok := c.Models[SelectedModelTypeSmall]
316 if !ok {
316 if !ok {
317 return nil
317 return nil
@@ -381,7 +381,7 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
381 return nil
381 return nil
382 }
382 }
383
383
384− var foundProvider *provider.Provider
384+ var foundProvider *catwalk.Provider
385 for _, p := range c.knownProviders {
385 for _, p := range c.knownProviders {
386 if string(p.ID) == providerID {
386 if string(p.ID) == providerID {
387 foundProvider = &p
387 foundProvider = &p
@@ -450,14 +450,14 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
450 headers := make(map[string]string)
450 headers := make(map[string]string)
451 apiKey, _ := resolver.ResolveValue(c.APIKey)
451 apiKey, _ := resolver.ResolveValue(c.APIKey)
452 switch c.Type {
452 switch c.Type {
453− case provider.TypeOpenAI:
453+ case catwalk.TypeOpenAI:
454 baseURL, _ := resolver.ResolveValue(c.BaseURL)
454 baseURL, _ := resolver.ResolveValue(c.BaseURL)
455 if baseURL == "" {
455 if baseURL == "" {
456 baseURL = "https://api.openai.com/v1"
456 baseURL = "https://api.openai.com/v1"
457 }
457 }
458 testURL = baseURL + "/models"
458 testURL = baseURL + "/models"
459 headers["Authorization"] = "Bearer " + apiKey
459 headers["Authorization"] = "Bearer " + apiKey
460− case provider.TypeAnthropic:
460+ case catwalk.TypeAnthropic:
461 baseURL, _ := resolver.ResolveValue(c.BaseURL)
461 baseURL, _ := resolver.ResolveValue(c.BaseURL)
462 if baseURL == "" {
462 if baseURL == "" {
463 baseURL = "https://api.anthropic.com/v1"
463 baseURL = "https://api.anthropic.com/v1"
internal/config/load.go+19−18
@@ -11,13 +11,14 @@ import (
11 "strings"
11 "strings"
12 "sync"
12 "sync"
13
13
14+ "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/env"
15 "github.com/charmbracelet/crush/internal/env"
15− "github.com/charmbracelet/crush/internal/fur/client"
16− "github.com/charmbracelet/crush/internal/fur/provider"
17 "github.com/charmbracelet/crush/internal/log"
16 "github.com/charmbracelet/crush/internal/log"
18 "golang.org/x/exp/slog"
17 "golang.org/x/exp/slog"
19)
18)
20
19
20+const catwalkURL = "https://catwalk.charm.sh"
21+
21// LoadReader config via io.Reader.
22// LoadReader config via io.Reader.
22func LoadReader(fd io.Reader) (*Config, error) {
23func LoadReader(fd io.Reader) (*Config, error) {
23 data, err := io.ReadAll(fd)
24 data, err := io.ReadAll(fd)
@@ -61,8 +62,8 @@ func Load(workingDir string, debug bool) (*Config, error) {
61 cfg.Options.Debug,
62 cfg.Options.Debug,
62 )
63 )
63
64
64− // Load known providers, this loads the config from fur
65+ // Load known providers, this loads the config from catwalk
65− providers, err := LoadProviders(client.New())
66+ providers, err := LoadProviders(catwalk.NewWithURL(catwalkURL))
66 if err != nil || len(providers) == 0 {
67 if err != nil || len(providers) == 0 {
67 return nil, fmt.Errorf("failed to load providers: %w", err)
68 return nil, fmt.Errorf("failed to load providers: %w", err)
68 }
69 }
@@ -81,7 +82,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
81 var wg sync.WaitGroup
82 var wg sync.WaitGroup
82
83
83 for _, p := range cfg.Providers {
84 for _, p := range cfg.Providers {
84− if p.Type == provider.TypeOpenAI || p.Type == provider.TypeAnthropic {
85+ if p.Type == catwalk.TypeOpenAI || p.Type == catwalk.TypeAnthropic {
85 wg.Add(1)
86 wg.Add(1)
86 go func(provider ProviderConfig) {
87 go func(provider ProviderConfig) {
87 defer wg.Done()
88 defer wg.Done()
@@ -117,7 +118,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
117 return cfg, nil
118 return cfg, nil
118}
119}
119
120
120−func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []provider.Provider) error {
121+func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []catwalk.Provider) error {
121 knownProviderNames := make(map[string]bool)
122 knownProviderNames := make(map[string]bool)
122 for _, p := range knownProviders {
123 for _, p := range knownProviders {
123 knownProviderNames[string(p.ID)] = true
124 knownProviderNames[string(p.ID)] = true
@@ -136,7 +137,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
136 p.APIKey = config.APIKey
137 p.APIKey = config.APIKey
137 }
138 }
138 if len(config.Models) > 0 {
139 if len(config.Models) > 0 {
139− models := []provider.Model{}
140+ models := []catwalk.Model{}
140 seen := make(map[string]bool)
141 seen := make(map[string]bool)
141
142
142 for _, model := range config.Models {
143 for _, model := range config.Models {
@@ -144,8 +145,8 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
144 continue
145 continue
145 }
146 }
146 seen[model.ID] = true
147 seen[model.ID] = true
147− if model.Model == "" {
148+ if model.Name == "" {
148− model.Model = model.ID
149+ model.Name = model.ID
149 }
150 }
150 models = append(models, model)
151 models = append(models, model)
151 }
152 }
@@ -154,8 +155,8 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
154 continue
155 continue
155 }
156 }
156 seen[model.ID] = true
157 seen[model.ID] = true
157− if model.Model == "" {
158+ if model.Name == "" {
158− model.Model = model.ID
159+ model.Name = model.ID
159 }
160 }
160 models = append(models, model)
161 models = append(models, model)
161 }
162 }
@@ -178,7 +179,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
178
179
179 switch p.ID {
180 switch p.ID {
180 // Handle specific providers that require additional configuration
181 // Handle specific providers that require additional configuration
181− case provider.InferenceProviderVertexAI:
182+ case catwalk.InferenceProviderVertexAI:
182 if !hasVertexCredentials(env) {
183 if !hasVertexCredentials(env) {
183 if configExists {
184 if configExists {
184 slog.Warn("Skipping Vertex AI provider due to missing credentials")
185 slog.Warn("Skipping Vertex AI provider due to missing credentials")
@@ -188,7 +189,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
188 }
189 }
189 prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
190 prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
190 prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
191 prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
191− case provider.InferenceProviderAzure:
192+ case catwalk.InferenceProviderAzure:
192 endpoint, err := resolver.ResolveValue(p.APIEndpoint)
193 endpoint, err := resolver.ResolveValue(p.APIEndpoint)
193 if err != nil || endpoint == "" {
194 if err != nil || endpoint == "" {
194 if configExists {
195 if configExists {
@@ -199,7 +200,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
199 }
200 }
200 prepared.BaseURL = endpoint
201 prepared.BaseURL = endpoint
201 prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
202 prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
202− case provider.InferenceProviderBedrock:
203+ case catwalk.InferenceProviderBedrock:
203 if !hasAWSCredentials(env) {
204 if !hasAWSCredentials(env) {
204 if configExists {
205 if configExists {
205 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
206 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
@@ -239,7 +240,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
239 }
240 }
240 // default to OpenAI if not set
241 // default to OpenAI if not set
241 if providerConfig.Type == "" {
242 if providerConfig.Type == "" {
242− providerConfig.Type = provider.TypeOpenAI
243+ providerConfig.Type = catwalk.TypeOpenAI
243 }
244 }
244
245
245 if providerConfig.Disable {
246 if providerConfig.Disable {
@@ -260,7 +261,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
260 delete(c.Providers, id)
261 delete(c.Providers, id)
261 continue
262 continue
262 }
263 }
263− if providerConfig.Type != provider.TypeOpenAI {
264+ if providerConfig.Type != catwalk.TypeOpenAI {
264 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
265 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
265 delete(c.Providers, id)
266 delete(c.Providers, id)
266 continue
267 continue
@@ -315,7 +316,7 @@ func (c *Config) setDefaults(workingDir string) {
315 c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
316 c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
316}
317}
317
318
318−func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
319+func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
319 if len(knownProviders) == 0 && len(c.Providers) == 0 {
320 if len(knownProviders) == 0 && len(c.Providers) == 0 {
320 err = fmt.Errorf("no providers configured, please configure at least one provider")
321 err = fmt.Errorf("no providers configured, please configure at least one provider")
321 return
322 return
@@ -384,7 +385,7 @@ func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (larg
384 return
385 return
385}
386}
386
387
387−func (c *Config) configureSelectedModels(knownProviders []provider.Provider) error {
388+func (c *Config) configureSelectedModels(knownProviders []catwalk.Provider) error {
388 defaultLarge, defaultSmall, err := c.defaultModelSelection(knownProviders)
389 defaultLarge, defaultSmall, err := c.defaultModelSelection(knownProviders)
389 if err != nil {
390 if err != nil {
390 return fmt.Errorf("failed to select default models: %w", err)
391 return fmt.Errorf("failed to select default models: %w", err)
internal/config/load_test.go+80−80
@@ -8,8 +8,8 @@ import (
8 "strings"
8 "strings"
9 "testing"
9 "testing"
10
10
11+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/crush/internal/env"
12 "github.com/charmbracelet/crush/internal/env"
12− "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/assert"
14)
14)
15
15
@@ -54,12 +54,12 @@ func TestConfig_setDefaults(t *testing.T) {
54}
54}
55
55
56func TestConfig_configureProviders(t *testing.T) {
56func TestConfig_configureProviders(t *testing.T) {
57− knownProviders := []provider.Provider{
57+ knownProviders := []catwalk.Provider{
58 {
58 {
59 ID: "openai",
59 ID: "openai",
60 APIKey: "$OPENAI_API_KEY",
60 APIKey: "$OPENAI_API_KEY",
61 APIEndpoint: "https://api.openai.com/v1",
61 APIEndpoint: "https://api.openai.com/v1",
62− Models: []provider.Model{{
62+ Models: []catwalk.Model{{
63 ID: "test-model",
63 ID: "test-model",
64 }},
64 }},
65 },
65 },
@@ -80,12 +80,12 @@ func TestConfig_configureProviders(t *testing.T) {
80}
80}
81
81
82func TestConfig_configureProvidersWithOverride(t *testing.T) {
82func TestConfig_configureProvidersWithOverride(t *testing.T) {
83− knownProviders := []provider.Provider{
83+ knownProviders := []catwalk.Provider{
84 {
84 {
85 ID: "openai",
85 ID: "openai",
86 APIKey: "$OPENAI_API_KEY",
86 APIKey: "$OPENAI_API_KEY",
87 APIEndpoint: "https://api.openai.com/v1",
87 APIEndpoint: "https://api.openai.com/v1",
88− Models: []provider.Model{{
88+ Models: []catwalk.Model{{
89 ID: "test-model",
89 ID: "test-model",
90 }},
90 }},
91 },
91 },
@@ -96,10 +96,10 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
96 "openai": {
96 "openai": {
97 APIKey: "xyz",
97 APIKey: "xyz",
98 BaseURL: "https://api.openai.com/v2",
98 BaseURL: "https://api.openai.com/v2",
99− Models: []provider.Model{
99+ Models: []catwalk.Model{
100 {
100 {
101− ID: "test-model",
101+ ID: "test-model",
102− Model: "Updated",
102+ Name: "Updated",
103 },
103 },
104 {
104 {
105 ID: "another-model",
105 ID: "another-model",
@@ -122,16 +122,16 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
122 assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
122 assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
123 assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
123 assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
124 assert.Len(t, cfg.Providers["openai"].Models, 2)
124 assert.Len(t, cfg.Providers["openai"].Models, 2)
125− assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Model)
125+ assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Name)
126}
126}
127
127
128func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
128func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
129− knownProviders := []provider.Provider{
129+ knownProviders := []catwalk.Provider{
130 {
130 {
131 ID: "openai",
131 ID: "openai",
132 APIKey: "$OPENAI_API_KEY",
132 APIKey: "$OPENAI_API_KEY",
133 APIEndpoint: "https://api.openai.com/v1",
133 APIEndpoint: "https://api.openai.com/v1",
134− Models: []provider.Model{{
134+ Models: []catwalk.Model{{
135 ID: "test-model",
135 ID: "test-model",
136 }},
136 }},
137 },
137 },
@@ -142,7 +142,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
142 "custom": {
142 "custom": {
143 APIKey: "xyz",
143 APIKey: "xyz",
144 BaseURL: "https://api.someendpoint.com/v2",
144 BaseURL: "https://api.someendpoint.com/v2",
145− Models: []provider.Model{
145+ Models: []catwalk.Model{
146 {
146 {
147 ID: "test-model",
147 ID: "test-model",
148 },
148 },
@@ -172,12 +172,12 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
172}
172}
173
173
174func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
174func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
175− knownProviders := []provider.Provider{
175+ knownProviders := []catwalk.Provider{
176 {
176 {
177− ID: provider.InferenceProviderBedrock,
177+ ID: catwalk.InferenceProviderBedrock,
178 APIKey: "",
178 APIKey: "",
179 APIEndpoint: "",
179 APIEndpoint: "",
180− Models: []provider.Model{{
180+ Models: []catwalk.Model{{
181 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
181 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
182 }},
182 }},
183 },
183 },
@@ -201,12 +201,12 @@ func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
201}
201}
202
202
203func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
203func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
204− knownProviders := []provider.Provider{
204+ knownProviders := []catwalk.Provider{
205 {
205 {
206− ID: provider.InferenceProviderBedrock,
206+ ID: catwalk.InferenceProviderBedrock,
207 APIKey: "",
207 APIKey: "",
208 APIEndpoint: "",
208 APIEndpoint: "",
209− Models: []provider.Model{{
209+ Models: []catwalk.Model{{
210 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
210 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
211 }},
211 }},
212 },
212 },
@@ -223,12 +223,12 @@ func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
223}
223}
224
224
225func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
225func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
226− knownProviders := []provider.Provider{
226+ knownProviders := []catwalk.Provider{
227 {
227 {
228− ID: provider.InferenceProviderBedrock,
228+ ID: catwalk.InferenceProviderBedrock,
229 APIKey: "",
229 APIKey: "",
230 APIEndpoint: "",
230 APIEndpoint: "",
231− Models: []provider.Model{{
231+ Models: []catwalk.Model{{
232 ID: "some-random-model",
232 ID: "some-random-model",
233 }},
233 }},
234 },
234 },
@@ -246,12 +246,12 @@ func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
246}
246}
247
247
248func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
248func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
249− knownProviders := []provider.Provider{
249+ knownProviders := []catwalk.Provider{
250 {
250 {
251− ID: provider.InferenceProviderVertexAI,
251+ ID: catwalk.InferenceProviderVertexAI,
252 APIKey: "",
252 APIKey: "",
253 APIEndpoint: "",
253 APIEndpoint: "",
254− Models: []provider.Model{{
254+ Models: []catwalk.Model{{
255 ID: "gemini-pro",
255 ID: "gemini-pro",
256 }},
256 }},
257 },
257 },
@@ -278,12 +278,12 @@ func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
278}
278}
279
279
280func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
280func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
281− knownProviders := []provider.Provider{
281+ knownProviders := []catwalk.Provider{
282 {
282 {
283− ID: provider.InferenceProviderVertexAI,
283+ ID: catwalk.InferenceProviderVertexAI,
284 APIKey: "",
284 APIKey: "",
285 APIEndpoint: "",
285 APIEndpoint: "",
286− Models: []provider.Model{{
286+ Models: []catwalk.Model{{
287 ID: "gemini-pro",
287 ID: "gemini-pro",
288 }},
288 }},
289 },
289 },
@@ -304,12 +304,12 @@ func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
304}
304}
305
305
306func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
306func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
307− knownProviders := []provider.Provider{
307+ knownProviders := []catwalk.Provider{
308 {
308 {
309− ID: provider.InferenceProviderVertexAI,
309+ ID: catwalk.InferenceProviderVertexAI,
310 APIKey: "",
310 APIKey: "",
311 APIEndpoint: "",
311 APIEndpoint: "",
312− Models: []provider.Model{{
312+ Models: []catwalk.Model{{
313 ID: "gemini-pro",
313 ID: "gemini-pro",
314 }},
314 }},
315 },
315 },
@@ -329,12 +329,12 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
329}
329}
330
330
331func TestConfig_configureProvidersSetProviderID(t *testing.T) {
331func TestConfig_configureProvidersSetProviderID(t *testing.T) {
332− knownProviders := []provider.Provider{
332+ knownProviders := []catwalk.Provider{
333 {
333 {
334 ID: "openai",
334 ID: "openai",
335 APIKey: "$OPENAI_API_KEY",
335 APIKey: "$OPENAI_API_KEY",
336 APIEndpoint: "https://api.openai.com/v1",
336 APIEndpoint: "https://api.openai.com/v1",
337− Models: []provider.Model{{
337+ Models: []catwalk.Model{{
338 ID: "test-model",
338 ID: "test-model",
339 }},
339 }},
340 },
340 },
@@ -450,12 +450,12 @@ func TestConfig_IsConfigured(t *testing.T) {
450}
450}
451
451
452func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
452func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
453− knownProviders := []provider.Provider{
453+ knownProviders := []catwalk.Provider{
454 {
454 {
455 ID: "openai",
455 ID: "openai",
456 APIKey: "$OPENAI_API_KEY",
456 APIKey: "$OPENAI_API_KEY",
457 APIEndpoint: "https://api.openai.com/v1",
457 APIEndpoint: "https://api.openai.com/v1",
458− Models: []provider.Model{{
458+ Models: []catwalk.Model{{
459 ID: "test-model",
459 ID: "test-model",
460 }},
460 }},
461 },
461 },
@@ -489,7 +489,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
489 Providers: map[string]ProviderConfig{
489 Providers: map[string]ProviderConfig{
490 "custom": {
490 "custom": {
491 BaseURL: "https://api.custom.com/v1",
491 BaseURL: "https://api.custom.com/v1",
492− Models: []provider.Model{{
492+ Models: []catwalk.Model{{
493 ID: "test-model",
493 ID: "test-model",
494 }},
494 }},
495 },
495 },
@@ -502,7 +502,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
502
502
503 env := env.NewFromMap(map[string]string{})
503 env := env.NewFromMap(map[string]string{})
504 resolver := NewEnvironmentVariableResolver(env)
504 resolver := NewEnvironmentVariableResolver(env)
505− err := cfg.configureProviders(env, resolver, []provider.Provider{})
505+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
506 assert.NoError(t, err)
506 assert.NoError(t, err)
507
507
508 assert.Len(t, cfg.Providers, 1)
508 assert.Len(t, cfg.Providers, 1)
@@ -515,7 +515,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
515 Providers: map[string]ProviderConfig{
515 Providers: map[string]ProviderConfig{
516 "custom": {
516 "custom": {
517 APIKey: "test-key",
517 APIKey: "test-key",
518− Models: []provider.Model{{
518+ Models: []catwalk.Model{{
519 ID: "test-model",
519 ID: "test-model",
520 }},
520 }},
521 },
521 },
@@ -525,7 +525,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
525
525
526 env := env.NewFromMap(map[string]string{})
526 env := env.NewFromMap(map[string]string{})
527 resolver := NewEnvironmentVariableResolver(env)
527 resolver := NewEnvironmentVariableResolver(env)
528− err := cfg.configureProviders(env, resolver, []provider.Provider{})
528+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
529 assert.NoError(t, err)
529 assert.NoError(t, err)
530
530
531 assert.Len(t, cfg.Providers, 0)
531 assert.Len(t, cfg.Providers, 0)
@@ -539,7 +539,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
539 "custom": {
539 "custom": {
540 APIKey: "test-key",
540 APIKey: "test-key",
541 BaseURL: "https://api.custom.com/v1",
541 BaseURL: "https://api.custom.com/v1",
542− Models: []provider.Model{},
542+ Models: []catwalk.Model{},
543 },
543 },
544 },
544 },
545 }
545 }
@@ -547,7 +547,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
547
547
548 env := env.NewFromMap(map[string]string{})
548 env := env.NewFromMap(map[string]string{})
549 resolver := NewEnvironmentVariableResolver(env)
549 resolver := NewEnvironmentVariableResolver(env)
550− err := cfg.configureProviders(env, resolver, []provider.Provider{})
550+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
551 assert.NoError(t, err)
551 assert.NoError(t, err)
552
552
553 assert.Len(t, cfg.Providers, 0)
553 assert.Len(t, cfg.Providers, 0)
@@ -562,7 +562,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
562 APIKey: "test-key",
562 APIKey: "test-key",
563 BaseURL: "https://api.custom.com/v1",
563 BaseURL: "https://api.custom.com/v1",
564 Type: "unsupported",
564 Type: "unsupported",
565− Models: []provider.Model{{
565+ Models: []catwalk.Model{{
566 ID: "test-model",
566 ID: "test-model",
567 }},
567 }},
568 },
568 },
@@ -572,7 +572,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
572
572
573 env := env.NewFromMap(map[string]string{})
573 env := env.NewFromMap(map[string]string{})
574 resolver := NewEnvironmentVariableResolver(env)
574 resolver := NewEnvironmentVariableResolver(env)
575− err := cfg.configureProviders(env, resolver, []provider.Provider{})
575+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
576 assert.NoError(t, err)
576 assert.NoError(t, err)
577
577
578 assert.Len(t, cfg.Providers, 0)
578 assert.Len(t, cfg.Providers, 0)
@@ -586,8 +586,8 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
586 "custom": {
586 "custom": {
587 APIKey: "test-key",
587 APIKey: "test-key",
588 BaseURL: "https://api.custom.com/v1",
588 BaseURL: "https://api.custom.com/v1",
589− Type: provider.TypeOpenAI,
589+ Type: catwalk.TypeOpenAI,
590− Models: []provider.Model{{
590+ Models: []catwalk.Model{{
591 ID: "test-model",
591 ID: "test-model",
592 }},
592 }},
593 },
593 },
@@ -597,7 +597,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
597
597
598 env := env.NewFromMap(map[string]string{})
598 env := env.NewFromMap(map[string]string{})
599 resolver := NewEnvironmentVariableResolver(env)
599 resolver := NewEnvironmentVariableResolver(env)
600− err := cfg.configureProviders(env, resolver, []provider.Provider{})
600+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
601 assert.NoError(t, err)
601 assert.NoError(t, err)
602
602
603 assert.Len(t, cfg.Providers, 1)
603 assert.Len(t, cfg.Providers, 1)
@@ -614,9 +614,9 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
614 "custom": {
614 "custom": {
615 APIKey: "test-key",
615 APIKey: "test-key",
616 BaseURL: "https://api.custom.com/v1",
616 BaseURL: "https://api.custom.com/v1",
617− Type: provider.TypeOpenAI,
617+ Type: catwalk.TypeOpenAI,
618 Disable: true,
618 Disable: true,
619− Models: []provider.Model{{
619+ Models: []catwalk.Model{{
620 ID: "test-model",
620 ID: "test-model",
621 }},
621 }},
622 },
622 },
@@ -626,7 +626,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
626
626
627 env := env.NewFromMap(map[string]string{})
627 env := env.NewFromMap(map[string]string{})
628 resolver := NewEnvironmentVariableResolver(env)
628 resolver := NewEnvironmentVariableResolver(env)
629− err := cfg.configureProviders(env, resolver, []provider.Provider{})
629+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
630 assert.NoError(t, err)
630 assert.NoError(t, err)
631
631
632 assert.Len(t, cfg.Providers, 0)
632 assert.Len(t, cfg.Providers, 0)
@@ -637,12 +637,12 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
637
637
638func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
638func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
639 t.Run("VertexAI provider removed when credentials missing with existing config", func(t *testing.T) {
639 t.Run("VertexAI provider removed when credentials missing with existing config", func(t *testing.T) {
640− knownProviders := []provider.Provider{
640+ knownProviders := []catwalk.Provider{
641 {
641 {
642− ID: provider.InferenceProviderVertexAI,
642+ ID: catwalk.InferenceProviderVertexAI,
643 APIKey: "",
643 APIKey: "",
644 APIEndpoint: "",
644 APIEndpoint: "",
645− Models: []provider.Model{{
645+ Models: []catwalk.Model{{
646 ID: "gemini-pro",
646 ID: "gemini-pro",
647 }},
647 }},
648 },
648 },
@@ -670,12 +670,12 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
670 })
670 })
671
671
672 t.Run("Bedrock provider removed when AWS credentials missing with existing config", func(t *testing.T) {
672 t.Run("Bedrock provider removed when AWS credentials missing with existing config", func(t *testing.T) {
673− knownProviders := []provider.Provider{
673+ knownProviders := []catwalk.Provider{
674 {
674 {
675− ID: provider.InferenceProviderBedrock,
675+ ID: catwalk.InferenceProviderBedrock,
676 APIKey: "",
676 APIKey: "",
677 APIEndpoint: "",
677 APIEndpoint: "",
678− Models: []provider.Model{{
678+ Models: []catwalk.Model{{
679 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
679 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
680 }},
680 }},
681 },
681 },
@@ -701,12 +701,12 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
701 })
701 })
702
702
703 t.Run("provider removed when API key missing with existing config", func(t *testing.T) {
703 t.Run("provider removed when API key missing with existing config", func(t *testing.T) {
704− knownProviders := []provider.Provider{
704+ knownProviders := []catwalk.Provider{
705 {
705 {
706 ID: "openai",
706 ID: "openai",
707 APIKey: "$MISSING_API_KEY",
707 APIKey: "$MISSING_API_KEY",
708 APIEndpoint: "https://api.openai.com/v1",
708 APIEndpoint: "https://api.openai.com/v1",
709− Models: []provider.Model{{
709+ Models: []catwalk.Model{{
710 ID: "test-model",
710 ID: "test-model",
711 }},
711 }},
712 },
712 },
@@ -732,12 +732,12 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
732 })
732 })
733
733
734 t.Run("known provider should still be added if the endpoint is missing the client will use default endpoints", func(t *testing.T) {
734 t.Run("known provider should still be added if the endpoint is missing the client will use default endpoints", func(t *testing.T) {
735− knownProviders := []provider.Provider{
735+ knownProviders := []catwalk.Provider{
736 {
736 {
737 ID: "openai",
737 ID: "openai",
738 APIKey: "$OPENAI_API_KEY",
738 APIKey: "$OPENAI_API_KEY",
739 APIEndpoint: "$MISSING_ENDPOINT",
739 APIEndpoint: "$MISSING_ENDPOINT",
740− Models: []provider.Model{{
740+ Models: []catwalk.Model{{
741 ID: "test-model",
741 ID: "test-model",
742 }},
742 }},
743 },
743 },
@@ -767,13 +767,13 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
767
767
768func TestConfig_defaultModelSelection(t *testing.T) {
768func TestConfig_defaultModelSelection(t *testing.T) {
769 t.Run("default behavior uses the default models for given provider", func(t *testing.T) {
769 t.Run("default behavior uses the default models for given provider", func(t *testing.T) {
770− knownProviders := []provider.Provider{
770+ knownProviders := []catwalk.Provider{
771 {
771 {
772 ID: "openai",
772 ID: "openai",
773 APIKey: "abc",
773 APIKey: "abc",
774 DefaultLargeModelID: "large-model",
774 DefaultLargeModelID: "large-model",
775 DefaultSmallModelID: "small-model",
775 DefaultSmallModelID: "small-model",
776− Models: []provider.Model{
776+ Models: []catwalk.Model{
777 {
777 {
778 ID: "large-model",
778 ID: "large-model",
779 DefaultMaxTokens: 1000,
779 DefaultMaxTokens: 1000,
@@ -803,13 +803,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
803 assert.Equal(t, int64(500), small.MaxTokens)
803 assert.Equal(t, int64(500), small.MaxTokens)
804 })
804 })
805 t.Run("should error if no providers configured", func(t *testing.T) {
805 t.Run("should error if no providers configured", func(t *testing.T) {
806− knownProviders := []provider.Provider{
806+ knownProviders := []catwalk.Provider{
807 {
807 {
808 ID: "openai",
808 ID: "openai",
809 APIKey: "$MISSING_KEY",
809 APIKey: "$MISSING_KEY",
810 DefaultLargeModelID: "large-model",
810 DefaultLargeModelID: "large-model",
811 DefaultSmallModelID: "small-model",
811 DefaultSmallModelID: "small-model",
812− Models: []provider.Model{
812+ Models: []catwalk.Model{
813 {
813 {
814 ID: "large-model",
814 ID: "large-model",
815 DefaultMaxTokens: 1000,
815 DefaultMaxTokens: 1000,
@@ -833,13 +833,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
833 assert.Error(t, err)
833 assert.Error(t, err)
834 })
834 })
835 t.Run("should error if model is missing", func(t *testing.T) {
835 t.Run("should error if model is missing", func(t *testing.T) {
836− knownProviders := []provider.Provider{
836+ knownProviders := []catwalk.Provider{
837 {
837 {
838 ID: "openai",
838 ID: "openai",
839 APIKey: "abc",
839 APIKey: "abc",
840 DefaultLargeModelID: "large-model",
840 DefaultLargeModelID: "large-model",
841 DefaultSmallModelID: "small-model",
841 DefaultSmallModelID: "small-model",
842− Models: []provider.Model{
842+ Models: []catwalk.Model{
843 {
843 {
844 ID: "not-large-model",
844 ID: "not-large-model",
845 DefaultMaxTokens: 1000,
845 DefaultMaxTokens: 1000,
@@ -863,13 +863,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
863 })
863 })
864
864
865 t.Run("should configure the default models with a custom provider", func(t *testing.T) {
865 t.Run("should configure the default models with a custom provider", func(t *testing.T) {
866− knownProviders := []provider.Provider{
866+ knownProviders := []catwalk.Provider{
867 {
867 {
868 ID: "openai",
868 ID: "openai",
869 APIKey: "$MISSING", // will not be included in the config
869 APIKey: "$MISSING", // will not be included in the config
870 DefaultLargeModelID: "large-model",
870 DefaultLargeModelID: "large-model",
871 DefaultSmallModelID: "small-model",
871 DefaultSmallModelID: "small-model",
872− Models: []provider.Model{
872+ Models: []catwalk.Model{
873 {
873 {
874 ID: "not-large-model",
874 ID: "not-large-model",
875 DefaultMaxTokens: 1000,
875 DefaultMaxTokens: 1000,
@@ -887,7 +887,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
887 "custom": {
887 "custom": {
888 APIKey: "test-key",
888 APIKey: "test-key",
889 BaseURL: "https://api.custom.com/v1",
889 BaseURL: "https://api.custom.com/v1",
890− Models: []provider.Model{
890+ Models: []catwalk.Model{
891 {
891 {
892 ID: "model",
892 ID: "model",
893 DefaultMaxTokens: 600,
893 DefaultMaxTokens: 600,
@@ -912,13 +912,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
912 })
912 })
913
913
914 t.Run("should fail if no model configured", func(t *testing.T) {
914 t.Run("should fail if no model configured", func(t *testing.T) {
915− knownProviders := []provider.Provider{
915+ knownProviders := []catwalk.Provider{
916 {
916 {
917 ID: "openai",
917 ID: "openai",
918 APIKey: "$MISSING", // will not be included in the config
918 APIKey: "$MISSING", // will not be included in the config
919 DefaultLargeModelID: "large-model",
919 DefaultLargeModelID: "large-model",
920 DefaultSmallModelID: "small-model",
920 DefaultSmallModelID: "small-model",
921− Models: []provider.Model{
921+ Models: []catwalk.Model{
922 {
922 {
923 ID: "not-large-model",
923 ID: "not-large-model",
924 DefaultMaxTokens: 1000,
924 DefaultMaxTokens: 1000,
@@ -936,7 +936,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
936 "custom": {
936 "custom": {
937 APIKey: "test-key",
937 APIKey: "test-key",
938 BaseURL: "https://api.custom.com/v1",
938 BaseURL: "https://api.custom.com/v1",
939− Models: []provider.Model{},
939+ Models: []catwalk.Model{},
940 },
940 },
941 },
941 },
942 }
942 }
@@ -949,13 +949,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
949 assert.Error(t, err)
949 assert.Error(t, err)
950 })
950 })
951 t.Run("should use the default provider first", func(t *testing.T) {
951 t.Run("should use the default provider first", func(t *testing.T) {
952− knownProviders := []provider.Provider{
952+ knownProviders := []catwalk.Provider{
953 {
953 {
954 ID: "openai",
954 ID: "openai",
955 APIKey: "set",
955 APIKey: "set",
956 DefaultLargeModelID: "large-model",
956 DefaultLargeModelID: "large-model",
957 DefaultSmallModelID: "small-model",
957 DefaultSmallModelID: "small-model",
958− Models: []provider.Model{
958+ Models: []catwalk.Model{
959 {
959 {
960 ID: "large-model",
960 ID: "large-model",
961 DefaultMaxTokens: 1000,
961 DefaultMaxTokens: 1000,
@@ -973,7 +973,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
973 "custom": {
973 "custom": {
974 APIKey: "test-key",
974 APIKey: "test-key",
975 BaseURL: "https://api.custom.com/v1",
975 BaseURL: "https://api.custom.com/v1",
976− Models: []provider.Model{
976+ Models: []catwalk.Model{
977 {
977 {
978 ID: "large-model",
978 ID: "large-model",
979 DefaultMaxTokens: 1000,
979 DefaultMaxTokens: 1000,
@@ -1000,13 +1000,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
1000
1000
1001func TestConfig_configureSelectedModels(t *testing.T) {
1001func TestConfig_configureSelectedModels(t *testing.T) {
1002 t.Run("should override defaults", func(t *testing.T) {
1002 t.Run("should override defaults", func(t *testing.T) {
1003− knownProviders := []provider.Provider{
1003+ knownProviders := []catwalk.Provider{
1004 {
1004 {
1005 ID: "openai",
1005 ID: "openai",
1006 APIKey: "abc",
1006 APIKey: "abc",
1007 DefaultLargeModelID: "large-model",
1007 DefaultLargeModelID: "large-model",
1008 DefaultSmallModelID: "small-model",
1008 DefaultSmallModelID: "small-model",
1009− Models: []provider.Model{
1009+ Models: []catwalk.Model{
1010 {
1010 {
1011 ID: "larger-model",
1011 ID: "larger-model",
1012 DefaultMaxTokens: 2000,
1012 DefaultMaxTokens: 2000,
@@ -1048,13 +1048,13 @@ func TestConfig_configureSelectedModels(t *testing.T) {
1048 assert.Equal(t, int64(500), small.MaxTokens)
1048 assert.Equal(t, int64(500), small.MaxTokens)
1049 })
1049 })
1050 t.Run("should be possible to use multiple providers", func(t *testing.T) {
1050 t.Run("should be possible to use multiple providers", func(t *testing.T) {
1051− knownProviders := []provider.Provider{
1051+ knownProviders := []catwalk.Provider{
1052 {
1052 {
1053 ID: "openai",
1053 ID: "openai",
1054 APIKey: "abc",
1054 APIKey: "abc",
1055 DefaultLargeModelID: "large-model",
1055 DefaultLargeModelID: "large-model",
1056 DefaultSmallModelID: "small-model",
1056 DefaultSmallModelID: "small-model",
1057− Models: []provider.Model{
1057+ Models: []catwalk.Model{
1058 {
1058 {
1059 ID: "large-model",
1059 ID: "large-model",
1060 DefaultMaxTokens: 1000,
1060 DefaultMaxTokens: 1000,
@@ -1070,7 +1070,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
1070 APIKey: "abc",
1070 APIKey: "abc",
1071 DefaultLargeModelID: "a-large-model",
1071 DefaultLargeModelID: "a-large-model",
1072 DefaultSmallModelID: "a-small-model",
1072 DefaultSmallModelID: "a-small-model",
1073− Models: []provider.Model{
1073+ Models: []catwalk.Model{
1074 {
1074 {
1075 ID: "a-large-model",
1075 ID: "a-large-model",
1076 DefaultMaxTokens: 1000,
1076 DefaultMaxTokens: 1000,
@@ -1111,13 +1111,13 @@ func TestConfig_configureSelectedModels(t *testing.T) {
1111 })
1111 })
1112
1112
1113 t.Run("should override the max tokens only", func(t *testing.T) {
1113 t.Run("should override the max tokens only", func(t *testing.T) {
1114− knownProviders := []provider.Provider{
1114+ knownProviders := []catwalk.Provider{
1115 {
1115 {
1116 ID: "openai",
1116 ID: "openai",
1117 APIKey: "abc",
1117 APIKey: "abc",
1118 DefaultLargeModelID: "large-model",
1118 DefaultLargeModelID: "large-model",
1119 DefaultSmallModelID: "small-model",
1119 DefaultSmallModelID: "small-model",
1120− Models: []provider.Model{
1120+ Models: []catwalk.Model{
1121 {
1121 {
1122 ID: "large-model",
1122 ID: "large-model",
1123 DefaultMaxTokens: 1000,
1123 DefaultMaxTokens: 1000,
internal/config/provider.go+10−11
@@ -7,17 +7,16 @@ import (
7 "runtime"
7 "runtime"
8 "sync"
8 "sync"
9
9
10− "github.com/charmbracelet/crush/internal/fur/client"
10+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11− "github.com/charmbracelet/crush/internal/fur/provider"
12)
11)
13
12
14type ProviderClient interface {
13type ProviderClient interface {
15− GetProviders() ([]provider.Provider, error)
14+ GetProviders() ([]catwalk.Provider, error)
16}
15}
17
16
18var (
17var (
19 providerOnce sync.Once
18 providerOnce sync.Once
20− providerList []provider.Provider
19+ providerList []catwalk.Provider
21)
20)
22
21
23// file to cache provider data
22// file to cache provider data
@@ -41,7 +40,7 @@ func providerCacheFileData() string {
41 return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
40 return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
42}
41}
43
42
44−func saveProvidersInCache(path string, providers []provider.Provider) error {
43+func saveProvidersInCache(path string, providers []catwalk.Provider) error {
45 dir := filepath.Dir(path)
44 dir := filepath.Dir(path)
46 if err := os.MkdirAll(dir, 0o755); err != nil {
45 if err := os.MkdirAll(dir, 0o755); err != nil {
47 return err
46 return err
@@ -55,18 +54,18 @@ func saveProvidersInCache(path string, providers []provider.Provider) error {
55 return os.WriteFile(path, data, 0o644)
54 return os.WriteFile(path, data, 0o644)
56}
55}
57
56
58−func loadProvidersFromCache(path string) ([]provider.Provider, error) {
57+func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
59 data, err := os.ReadFile(path)
58 data, err := os.ReadFile(path)
60 if err != nil {
59 if err != nil {
61 return nil, err
60 return nil, err
62 }
61 }
63
62
64− var providers []provider.Provider
63+ var providers []catwalk.Provider
65 err = json.Unmarshal(data, &providers)
64 err = json.Unmarshal(data, &providers)
66 return providers, err
65 return providers, err
67}
66}
68
67
69−func loadProviders(path string, client ProviderClient) ([]provider.Provider, error) {
68+func loadProviders(path string, client ProviderClient) ([]catwalk.Provider, error) {
70 providers, err := client.GetProviders()
69 providers, err := client.GetProviders()
71 if err != nil {
70 if err != nil {
72 fallbackToCache, err := loadProvidersFromCache(path)
71 fallbackToCache, err := loadProvidersFromCache(path)
@@ -82,11 +81,11 @@ func loadProviders(path string, client ProviderClient) ([]provider.Provider, err
82 return providers, nil
81 return providers, nil
83}
82}
84
83
85−func Providers() ([]provider.Provider, error) {
84+func Providers() ([]catwalk.Provider, error) {
86− return LoadProviders(client.New())
85+ return LoadProviders(catwalk.NewWithURL(catwalkURL))
87}
86}
88
87
89−func LoadProviders(client ProviderClient) ([]provider.Provider, error) {
88+func LoadProviders(client ProviderClient) ([]catwalk.Provider, error) {
90 var err error
89 var err error
91 providerOnce.Do(func() {
90 providerOnce.Do(func() {
92 providerList, err = loadProviders(providerCacheFileData(), client)
91 providerList, err = loadProviders(providerCacheFileData(), client)
internal/config/provider_test.go+4−4
@@ -6,7 +6,7 @@ import (
6 "os"
6 "os"
7 "testing"
7 "testing"
8
8
9− "github.com/charmbracelet/crush/internal/fur/provider"
9+ "github.com/charmbracelet/catwalk/pkg/catwalk"
10 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/assert"
11)
11)
12
12
@@ -14,11 +14,11 @@ type mockProviderClient struct {
14 shouldFail bool
14 shouldFail bool
15}
15}
16
16
17−func (m *mockProviderClient) GetProviders() ([]provider.Provider, error) {
17+func (m *mockProviderClient) GetProviders() ([]catwalk.Provider, error) {
18 if m.shouldFail {
18 if m.shouldFail {
19 return nil, errors.New("failed to load providers")
19 return nil, errors.New("failed to load providers")
20 }
20 }
21− return []provider.Provider{
21+ return []catwalk.Provider{
22 {
22 {
23 Name: "Mock",
23 Name: "Mock",
24 },
24 },
@@ -43,7 +43,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
43 client := &mockProviderClient{shouldFail: true}
43 client := &mockProviderClient{shouldFail: true}
44 tmpPath := t.TempDir() + "/providers.json"
44 tmpPath := t.TempDir() + "/providers.json"
45 // store providers to a temporary file
45 // store providers to a temporary file
46− oldProviders := []provider.Provider{
46+ oldProviders := []catwalk.Provider{
47 {
47 {
48 Name: "OldProvider",
48 Name: "OldProvider",
49 },
49 },
internal/fur/client/client.godeleted−63
@@ -1,63 +0,0 @@
1−// Package client provides a client for interacting with the fur service.
2−package client
3−
4−import (
5− "encoding/json"
6− "fmt"
7− "net/http"
8− "os"
9−
10− "github.com/charmbracelet/crush/internal/fur/provider"
11−)
12−
13−const defaultURL = "https://fur.charm.sh"
14−
15−// Client represents a client for the fur service.
16−type Client struct {
17− baseURL string
18− httpClient *http.Client
19−}
20−
21−// New creates a new client instance
22−// Uses FUR_URL environment variable or falls back to localhost:8080.
23−func New() *Client {
24− baseURL := os.Getenv("FUR_URL")
25− if baseURL == "" {
26− baseURL = defaultURL
27− }
28−
29− return &Client{
30− baseURL: baseURL,
31− httpClient: &http.Client{},
32− }
33−}
34−
35−// NewWithURL creates a new client with a specific URL.
36−func NewWithURL(url string) *Client {
37− return &Client{
38− baseURL: url,
39− httpClient: &http.Client{},
40− }
41−}
42−
43−// GetProviders retrieves all available providers from the service.
44−func (c *Client) GetProviders() ([]provider.Provider, error) {
45− url := fmt.Sprintf("%s/providers", c.baseURL)
46−
47− resp, err := c.httpClient.Get(url) //nolint:noctx
48− if err != nil {
49− return nil, fmt.Errorf("failed to make request: %w", err)
50− }
51− defer resp.Body.Close() //nolint:errcheck
52−
53− if resp.StatusCode != http.StatusOK {
54− return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
55− }
56−
57− var providers []provider.Provider
58− if err := json.NewDecoder(resp.Body).Decode(&providers); err != nil {
59− return nil, fmt.Errorf("failed to decode response: %w", err)
60− }
61−
62− return providers, nil
63−}
internal/fur/provider/provider.godeleted−75
@@ -1,75 +0,0 @@
1−// Package provider provides types and constants for AI providers.
2−package provider
3−
4−// Type represents the type of AI provider.
5−type Type string
6−
7−// All the supported AI provider types.
8−const (
9− TypeOpenAI Type = "openai"
10− TypeAnthropic Type = "anthropic"
11− TypeGemini Type = "gemini"
12− TypeAzure Type = "azure"
13− TypeBedrock Type = "bedrock"
14− TypeVertexAI Type = "vertexai"
15− TypeXAI Type = "xai"
16−)
17−
18−// InferenceProvider represents the inference provider identifier.
19−type InferenceProvider string
20−
21−// All the inference providers supported by the system.
22−const (
23− InferenceProviderOpenAI InferenceProvider = "openai"
24− InferenceProviderAnthropic InferenceProvider = "anthropic"
25− InferenceProviderGemini InferenceProvider = "gemini"
26− InferenceProviderAzure InferenceProvider = "azure"
27− InferenceProviderBedrock InferenceProvider = "bedrock"
28− InferenceProviderVertexAI InferenceProvider = "vertexai"
29− InferenceProviderXAI InferenceProvider = "xai"
30− InferenceProviderGROQ InferenceProvider = "groq"
31− InferenceProviderOpenRouter InferenceProvider = "openrouter"
32−)
33−
34−// Provider represents an AI provider configuration.
35−type Provider struct {
36− Name string `json:"name"`
37− ID InferenceProvider `json:"id"`
38− APIKey string `json:"api_key,omitempty"`
39− APIEndpoint string `json:"api_endpoint,omitempty"`
40− Type Type `json:"type,omitempty"`
41− DefaultLargeModelID string `json:"default_large_model_id,omitempty"`
42− DefaultSmallModelID string `json:"default_small_model_id,omitempty"`
43− Models []Model `json:"models,omitempty"`
44−}
45−
46−// Model represents an AI model configuration.
47−type Model struct {
48− ID string `json:"id"`
49− Model string `json:"model"`
50− CostPer1MIn float64 `json:"cost_per_1m_in"`
51− CostPer1MOut float64 `json:"cost_per_1m_out"`
52− CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
53− CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
54− ContextWindow int64 `json:"context_window"`
55− DefaultMaxTokens int64 `json:"default_max_tokens"`
56− CanReason bool `json:"can_reason"`
57− HasReasoningEffort bool `json:"has_reasoning_efforts"`
58− DefaultReasoningEffort string `json:"default_reasoning_effort,omitempty"`
59− SupportsImages bool `json:"supports_attachments"`
60−}
61−
62−// KnownProviders returns all the known inference providers.
63−func KnownProviders() []InferenceProvider {
64− return []InferenceProvider{
65− InferenceProviderOpenAI,
66− InferenceProviderAnthropic,
67− InferenceProviderGemini,
68− InferenceProviderAzure,
69− InferenceProviderBedrock,
70− InferenceProviderVertexAI,
71− InferenceProviderXAI,
72− InferenceProviderGROQ,
73− InferenceProviderOpenRouter,
74− }
75−}
internal/llm/agent/agent.go+4−4
@@ -10,8 +10,8 @@ import (
10 "sync"
10 "sync"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14− fur "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
17 "github.com/charmbracelet/crush/internal/llm/provider"
17 "github.com/charmbracelet/crush/internal/llm/provider"
@@ -52,7 +52,7 @@ type AgentEvent struct {
52
52
53type Service interface {
53type Service interface {
54 pubsub.Suscriber[AgentEvent]
54 pubsub.Suscriber[AgentEvent]
55− Model() fur.Model
55+ Model() catwalk.Model
56 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
56 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
57 Cancel(sessionID string)
57 Cancel(sessionID string)
58 CancelAll()
58 CancelAll()
@@ -219,7 +219,7 @@ func NewAgent(
219 return agent, nil
219 return agent, nil
220}
220}
221
221
222−func (a *agent) Model() fur.Model {
222+func (a *agent) Model() catwalk.Model {
223 return *config.Get().GetModelByType(a.agentCfg.Model)
223 return *config.Get().GetModelByType(a.agentCfg.Model)
224}
224}
225
225
@@ -638,7 +638,7 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
638 return nil
638 return nil
639}
639}
640
640
641−func (a *agent) TrackUsage(ctx context.Context, sessionID string, model fur.Model, usage provider.TokenUsage) error {
641+func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk.Model, usage provider.TokenUsage) error {
642 sess, err := a.sessions.Get(ctx, sessionID)
642 sess, err := a.sessions.Get(ctx, sessionID)
643 if err != nil {
643 if err != nil {
644 return fmt.Errorf("failed to get session: %w", err)
644 return fmt.Errorf("failed to get session: %w", err)
internal/llm/prompt/coder.go+3−3
@@ -9,17 +9,17 @@ import (
9 "runtime"
9 "runtime"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15)
15)
16
16
17func CoderPrompt(p string, contextFiles ...string) string {
17func CoderPrompt(p string, contextFiles ...string) string {
18 var basePrompt string
18 var basePrompt string
19 switch p {
19 switch p {
20− case string(provider.InferenceProviderOpenAI):
20+ case string(catwalk.InferenceProviderOpenAI):
21 basePrompt = baseOpenAICoderPrompt
21 basePrompt = baseOpenAICoderPrompt
22− case string(provider.InferenceProviderGemini), string(provider.InferenceProviderVertexAI):
22+ case string(catwalk.InferenceProviderGemini), string(catwalk.InferenceProviderVertexAI):
23 basePrompt = baseGeminiCoderPrompt
23 basePrompt = baseGeminiCoderPrompt
24 default:
24 default:
25 basePrompt = baseAnthropicCoderPrompt
25 basePrompt = baseAnthropicCoderPrompt
internal/llm/provider/anthropic.go+3−3
@@ -15,8 +15,8 @@ import (
15 "github.com/anthropics/anthropic-sdk-go"
15 "github.com/anthropics/anthropic-sdk-go"
16 "github.com/anthropics/anthropic-sdk-go/bedrock"
16 "github.com/anthropics/anthropic-sdk-go/bedrock"
17 "github.com/anthropics/anthropic-sdk-go/option"
17 "github.com/anthropics/anthropic-sdk-go/option"
18+ "github.com/charmbracelet/catwalk/pkg/catwalk"
18 "github.com/charmbracelet/crush/internal/config"
19 "github.com/charmbracelet/crush/internal/config"
19− "github.com/charmbracelet/crush/internal/fur/provider"
20 "github.com/charmbracelet/crush/internal/llm/tools"
20 "github.com/charmbracelet/crush/internal/llm/tools"
21 "github.com/charmbracelet/crush/internal/message"
21 "github.com/charmbracelet/crush/internal/message"
22)
22)
@@ -71,7 +71,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
71 var contentBlocks []anthropic.ContentBlockParamUnion
71 var contentBlocks []anthropic.ContentBlockParamUnion
72 contentBlocks = append(contentBlocks, content)
72 contentBlocks = append(contentBlocks, content)
73 for _, binaryContent := range msg.BinaryContent() {
73 for _, binaryContent := range msg.BinaryContent() {
74− base64Image := binaryContent.String(provider.InferenceProviderAnthropic)
74+ base64Image := binaryContent.String(catwalk.InferenceProviderAnthropic)
75 imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
75 imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
76 contentBlocks = append(contentBlocks, imageBlock)
76 contentBlocks = append(contentBlocks, imageBlock)
77 }
77 }
@@ -529,6 +529,6 @@ func (a *anthropicClient) usage(msg anthropic.Message) TokenUsage {
529 }
529 }
530}
530}
531
531
532−func (a *anthropicClient) Model() provider.Model {
532+func (a *anthropicClient) Model() catwalk.Model {
533 return a.providerOptions.model(a.providerOptions.modelType)
533 return a.providerOptions.model(a.providerOptions.modelType)
534}
534}
internal/llm/provider/bedrock.go+3−3
@@ -6,8 +6,8 @@ import (
6 "fmt"
6 "fmt"
7 "strings"
7 "strings"
8
8
9+ "github.com/charmbracelet/catwalk/pkg/catwalk"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
10− "github.com/charmbracelet/crush/internal/fur/provider"
11 "github.com/charmbracelet/crush/internal/llm/tools"
11 "github.com/charmbracelet/crush/internal/llm/tools"
12 "github.com/charmbracelet/crush/internal/message"
12 "github.com/charmbracelet/crush/internal/message"
13)
13)
@@ -32,7 +32,7 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
32 }
32 }
33 }
33 }
34
34
35− opts.model = func(modelType config.SelectedModelType) provider.Model {
35+ opts.model = func(modelType config.SelectedModelType) catwalk.Model {
36 model := config.Get().GetModelByType(modelType)
36 model := config.Get().GetModelByType(modelType)
37
37
38 // Prefix the model name with region
38 // Prefix the model name with region
@@ -88,6 +88,6 @@ func (b *bedrockClient) stream(ctx context.Context, messages []message.Message,
88 return b.childProvider.stream(ctx, messages, tools)
88 return b.childProvider.stream(ctx, messages, tools)
89}
89}
90
90
91−func (b *bedrockClient) Model() provider.Model {
91+func (b *bedrockClient) Model() catwalk.Model {
92 return b.providerOptions.model(b.providerOptions.modelType)
92 return b.providerOptions.model(b.providerOptions.modelType)
93}
93}
internal/llm/provider/gemini.go+2−2
@@ -10,8 +10,8 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/llm/tools"
16 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/message"
17 "github.com/google/uuid"
17 "github.com/google/uuid"
@@ -463,7 +463,7 @@ func (g *geminiClient) usage(resp *genai.GenerateContentResponse) TokenUsage {
463 }
463 }
464}
464}
465
465
466−func (g *geminiClient) Model() provider.Model {
466+func (g *geminiClient) Model() catwalk.Model {
467 return g.providerOptions.model(g.providerOptions.modelType)
467 return g.providerOptions.model(g.providerOptions.modelType)
468}
468}
469
469
internal/llm/provider/openai.go+3−3
@@ -9,8 +9,8 @@ import (
9 "log/slog"
9 "log/slog"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/message"
16 "github.com/openai/openai-go"
16 "github.com/openai/openai-go"
@@ -66,7 +66,7 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
66 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
66 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
67 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
67 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
68 for _, binaryContent := range msg.BinaryContent() {
68 for _, binaryContent := range msg.BinaryContent() {
69− imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(provider.InferenceProviderOpenAI)}
69+ imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(catwalk.InferenceProviderOpenAI)}
70 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
70 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
71
71
72 content = append(content, openai.ChatCompletionContentPartUnionParam{OfImageURL: &imageBlock})
72 content = append(content, openai.ChatCompletionContentPartUnionParam{OfImageURL: &imageBlock})
@@ -486,6 +486,6 @@ func (o *openaiClient) usage(completion openai.ChatCompletion) TokenUsage {
486 }
486 }
487}
487}
488
488
489−func (o *openaiClient) Model() provider.Model {
489+func (o *openaiClient) Model() catwalk.Model {
490 return o.providerOptions.model(o.providerOptions.modelType)
490 return o.providerOptions.model(o.providerOptions.modelType)
491}
491}
internal/llm/provider/openai_test.go+5−5
@@ -9,8 +9,8 @@ import (
9 "testing"
9 "testing"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/message"
16 "github.com/openai/openai-go"
16 "github.com/openai/openai-go"
@@ -55,10 +55,10 @@ func TestOpenAIClientStreamChoices(t *testing.T) {
55 modelType: config.SelectedModelTypeLarge,
55 modelType: config.SelectedModelTypeLarge,
56 apiKey: "test-key",
56 apiKey: "test-key",
57 systemMessage: "test",
57 systemMessage: "test",
58− model: func(config.SelectedModelType) provider.Model {
58+ model: func(config.SelectedModelType) catwalk.Model {
59− return provider.Model{
59+ return catwalk.Model{
60− ID: "test-model",
60+ ID: "test-model",
61− Model: "test-model",
61+ Name: "test-model",
62 }
62 }
63 },
63 },
64 },
64 },
internal/llm/provider/provider.go+13−13
@@ -4,8 +4,8 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6
6
7+ "github.com/charmbracelet/catwalk/pkg/catwalk"
7 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
8− "github.com/charmbracelet/crush/internal/fur/provider"
9 "github.com/charmbracelet/crush/internal/llm/tools"
9 "github.com/charmbracelet/crush/internal/llm/tools"
10 "github.com/charmbracelet/crush/internal/message"
10 "github.com/charmbracelet/crush/internal/message"
11)
11)
@@ -57,7 +57,7 @@ type Provider interface {
57
57
58 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
58 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
59
59
60− Model() provider.Model
60+ Model() catwalk.Model
61}
61}
62
62
63type providerClientOptions struct {
63type providerClientOptions struct {
@@ -65,7 +65,7 @@ type providerClientOptions struct {
65 config config.ProviderConfig
65 config config.ProviderConfig
66 apiKey string
66 apiKey string
67 modelType config.SelectedModelType
67 modelType config.SelectedModelType
68− model func(config.SelectedModelType) provider.Model
68+ model func(config.SelectedModelType) catwalk.Model
69 disableCache bool
69 disableCache bool
70 systemMessage string
70 systemMessage string
71 maxTokens int64
71 maxTokens int64
@@ -80,7 +80,7 @@ type ProviderClient interface {
80 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
80 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
81 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
81 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
82
82
83− Model() provider.Model
83+ Model() catwalk.Model
84}
84}
85
85
86type baseProvider[C ProviderClient] struct {
86type baseProvider[C ProviderClient] struct {
@@ -109,7 +109,7 @@ func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message
109 return p.client.stream(ctx, messages, tools)
109 return p.client.stream(ctx, messages, tools)
110}
110}
111
111
112−func (p *baseProvider[C]) Model() provider.Model {
112+func (p *baseProvider[C]) Model() catwalk.Model {
113 return p.client.Model()
113 return p.client.Model()
114}
114}
115
115
@@ -149,7 +149,7 @@ func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provi
149 apiKey: resolvedAPIKey,
149 apiKey: resolvedAPIKey,
150 extraHeaders: cfg.ExtraHeaders,
150 extraHeaders: cfg.ExtraHeaders,
151 extraBody: cfg.ExtraBody,
151 extraBody: cfg.ExtraBody,
152− model: func(tp config.SelectedModelType) provider.Model {
152+ model: func(tp config.SelectedModelType) catwalk.Model {
153 return *config.Get().GetModelByType(tp)
153 return *config.Get().GetModelByType(tp)
154 },
154 },
155 }
155 }
@@ -157,37 +157,37 @@ func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provi
157 o(&clientOptions)
157 o(&clientOptions)
158 }
158 }
159 switch cfg.Type {
159 switch cfg.Type {
160− case provider.TypeAnthropic:
160+ case catwalk.TypeAnthropic:
161 return &baseProvider[AnthropicClient]{
161 return &baseProvider[AnthropicClient]{
162 options: clientOptions,
162 options: clientOptions,
163 client: newAnthropicClient(clientOptions, false),
163 client: newAnthropicClient(clientOptions, false),
164 }, nil
164 }, nil
165− case provider.TypeOpenAI:
165+ case catwalk.TypeOpenAI:
166 return &baseProvider[OpenAIClient]{
166 return &baseProvider[OpenAIClient]{
167 options: clientOptions,
167 options: clientOptions,
168 client: newOpenAIClient(clientOptions),
168 client: newOpenAIClient(clientOptions),
169 }, nil
169 }, nil
170− case provider.TypeGemini:
170+ case catwalk.TypeGemini:
171 return &baseProvider[GeminiClient]{
171 return &baseProvider[GeminiClient]{
172 options: clientOptions,
172 options: clientOptions,
173 client: newGeminiClient(clientOptions),
173 client: newGeminiClient(clientOptions),
174 }, nil
174 }, nil
175− case provider.TypeBedrock:
175+ case catwalk.TypeBedrock:
176 return &baseProvider[BedrockClient]{
176 return &baseProvider[BedrockClient]{
177 options: clientOptions,
177 options: clientOptions,
178 client: newBedrockClient(clientOptions),
178 client: newBedrockClient(clientOptions),
179 }, nil
179 }, nil
180− case provider.TypeAzure:
180+ case catwalk.TypeAzure:
181 return &baseProvider[AzureClient]{
181 return &baseProvider[AzureClient]{
182 options: clientOptions,
182 options: clientOptions,
183 client: newAzureClient(clientOptions),
183 client: newAzureClient(clientOptions),
184 }, nil
184 }, nil
185− case provider.TypeVertexAI:
185+ case catwalk.TypeVertexAI:
186 return &baseProvider[VertexAIClient]{
186 return &baseProvider[VertexAIClient]{
187 options: clientOptions,
187 options: clientOptions,
188 client: newVertexAIClient(clientOptions),
188 client: newVertexAIClient(clientOptions),
189 }, nil
189 }, nil
190− case provider.TypeXAI:
190+ case catwalk.TypeXAI:
191 clientOptions.baseURL = "https://api.x.ai/v1"
191 clientOptions.baseURL = "https://api.x.ai/v1"
192 return &baseProvider[OpenAIClient]{
192 return &baseProvider[OpenAIClient]{
193 options: clientOptions,
193 options: clientOptions,
internal/message/content.go+3−3
@@ -5,7 +5,7 @@ import (
5 "slices"
5 "slices"
6 "time"
6 "time"
7
7
8− "github.com/charmbracelet/crush/internal/fur/provider"
8+ "github.com/charmbracelet/catwalk/pkg/catwalk"
9)
9)
10
10
11type MessageRole string
11type MessageRole string
@@ -74,9 +74,9 @@ type BinaryContent struct {
74 Data []byte
74 Data []byte
75}
75}
76
76
77−func (bc BinaryContent) String(p provider.InferenceProvider) string {
77+func (bc BinaryContent) String(p catwalk.InferenceProvider) string {
78 base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
78 base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
79− if p == provider.InferenceProviderOpenAI {
79+ if p == catwalk.InferenceProviderOpenAI {
80 return "data:" + bc.MIMEType + ";base64," + base64Encoded
80 return "data:" + bc.MIMEType + ";base64," + base64Encoded
81 }
81 }
82 return base64Encoded
82 return base64Encoded
internal/tui/components/chat/messages/messages.go+4−4
@@ -8,11 +8,11 @@ import (
8
8
9 "github.com/charmbracelet/bubbles/v2/viewport"
9 "github.com/charmbracelet/bubbles/v2/viewport"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/x/ansi"
13 "github.com/charmbracelet/x/ansi"
13
14
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/config"
15− "github.com/charmbracelet/crush/internal/fur/provider"
16 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/message"
17 "github.com/charmbracelet/crush/internal/tui/components/anim"
17 "github.com/charmbracelet/crush/internal/tui/components/anim"
18 "github.com/charmbracelet/crush/internal/tui/components/core"
18 "github.com/charmbracelet/crush/internal/tui/components/core"
@@ -369,11 +369,11 @@ func (m *assistantSectionModel) View() string {
369 model := config.Get().GetModel(m.message.Provider, m.message.Model)
369 model := config.Get().GetModel(m.message.Provider, m.message.Model)
370 if model == nil {
370 if model == nil {
371 // This means the model is not configured anymore
371 // This means the model is not configured anymore
372− model = &provider.Model{
372+ model = &catwalk.Model{
373− Model: "Unknown Model",
373+ Name: "Unknown Model",
374 }
374 }
375 }
375 }
376− modelFormatted := t.S().Muted.Render(model.Model)
376+ modelFormatted := t.S().Muted.Render(model.Name)
377 assistant := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
377 assistant := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
378 return t.S().Base.PaddingLeft(2).Render(
378 return t.S().Base.PaddingLeft(2).Render(
379 core.Section(assistant, m.width-2),
379 core.Section(assistant, m.width-2),
internal/tui/components/chat/sidebar/sidebar.go+4−4
@@ -9,10 +9,10 @@ import (
9 "sync"
9 "sync"
10
10
11 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/fur/provider"
16 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/lsp"
18 "github.com/charmbracelet/crush/internal/lsp/protocol"
18 "github.com/charmbracelet/crush/internal/lsp/protocol"
@@ -897,7 +897,7 @@ func (s *sidebarCmp) currentModelBlock() string {
897 t := styles.CurrentTheme()
897 t := styles.CurrentTheme()
898
898
899 modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
899 modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
900− modelName := t.S().Text.Render(model.Model)
900+ modelName := t.S().Text.Render(model.Name)
901 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
901 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
902 parts := []string{
902 parts := []string{
903 modelInfo,
903 modelInfo,
@@ -905,14 +905,14 @@ func (s *sidebarCmp) currentModelBlock() string {
905 if model.CanReason {
905 if model.CanReason {
906 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
906 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
907 switch modelProvider.Type {
907 switch modelProvider.Type {
908− case provider.TypeOpenAI:
908+ case catwalk.TypeOpenAI:
909 reasoningEffort := model.DefaultReasoningEffort
909 reasoningEffort := model.DefaultReasoningEffort
910 if selectedModel.ReasoningEffort != "" {
910 if selectedModel.ReasoningEffort != "" {
911 reasoningEffort = selectedModel.ReasoningEffort
911 reasoningEffort = selectedModel.ReasoningEffort
912 }
912 }
913 formatter := cases.Title(language.English, cases.NoLower)
913 formatter := cases.Title(language.English, cases.NoLower)
914 parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
914 parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
915− case provider.TypeAnthropic:
915+ case catwalk.TypeAnthropic:
916 formatter := cases.Title(language.English, cases.NoLower)
916 formatter := cases.Title(language.English, cases.NoLower)
917 if selectedModel.Think {
917 if selectedModel.Think {
918 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking on")))
918 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking on")))
internal/tui/components/chat/splash/splash.go+3−3
@@ -10,8 +10,8 @@ import (
10 "github.com/charmbracelet/bubbles/v2/key"
10 "github.com/charmbracelet/bubbles/v2/key"
11 "github.com/charmbracelet/bubbles/v2/spinner"
11 "github.com/charmbracelet/bubbles/v2/spinner"
12 tea "github.com/charmbracelet/bubbletea/v2"
12 tea "github.com/charmbracelet/bubbletea/v2"
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/internal/llm/prompt"
15 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/tui/components/chat"
16 "github.com/charmbracelet/crush/internal/tui/components/chat"
17 "github.com/charmbracelet/crush/internal/tui/components/completions"
17 "github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -109,7 +109,7 @@ func (s *splashCmp) SetOnboarding(onboarding bool) {
109 if err != nil {
109 if err != nil {
110 return
110 return
111 }
111 }
112− filteredProviders := []provider.Provider{}
112+ filteredProviders := []catwalk.Provider{}
113 simpleProviders := []string{
113 simpleProviders := []string{
114 "anthropic",
114 "anthropic",
115 "openai",
115 "openai",
@@ -407,7 +407,7 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
407 return nil
407 return nil
408}
408}
409
409
410−func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
410+func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
411 providers, err := config.Providers()
411 providers, err := config.Providers()
412 if err != nil {
412 if err != nil {
413 return nil, err
413 return nil, err
internal/tui/components/dialogs/commands/commands.go+2−2
@@ -4,10 +4,10 @@ import (
4 "github.com/charmbracelet/bubbles/v2/help"
4 "github.com/charmbracelet/bubbles/v2/help"
5 "github.com/charmbracelet/bubbles/v2/key"
5 "github.com/charmbracelet/bubbles/v2/key"
6 tea "github.com/charmbracelet/bubbletea/v2"
6 tea "github.com/charmbracelet/bubbletea/v2"
7+ "github.com/charmbracelet/catwalk/pkg/catwalk"
7 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/charmbracelet/lipgloss/v2"
8
9
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
10− "github.com/charmbracelet/crush/internal/fur/provider"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -270,7 +270,7 @@ func (c *commandDialogCmp) defaultCommands() []Command {
270 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
270 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
271 model := cfg.GetModelByType(agentCfg.Model)
271 model := cfg.GetModelByType(agentCfg.Model)
272 if providerCfg != nil && model != nil &&
272 if providerCfg != nil && model != nil &&
273− providerCfg.Type == provider.TypeAnthropic && model.CanReason {
273+ providerCfg.Type == catwalk.TypeAnthropic && model.CanReason {
274 selectedModel := cfg.Models[agentCfg.Model]
274 selectedModel := cfg.Models[agentCfg.Model]
275 status := "Enable"
275 status := "Enable"
276 if selectedModel.Think {
276 if selectedModel.Think {
internal/tui/components/dialogs/models/list.go+11−11
@@ -5,8 +5,8 @@ import (
5 "slices"
5 "slices"
6
6
7 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
8+ "github.com/charmbracelet/catwalk/pkg/catwalk"
8 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/config"
9− "github.com/charmbracelet/crush/internal/fur/provider"
10 "github.com/charmbracelet/crush/internal/tui/components/completions"
10 "github.com/charmbracelet/crush/internal/tui/components/completions"
11 "github.com/charmbracelet/crush/internal/tui/components/core/list"
11 "github.com/charmbracelet/crush/internal/tui/components/core/list"
12 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
12 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
@@ -18,7 +18,7 @@ import (
18type ModelListComponent struct {
18type ModelListComponent struct {
19 list list.ListModel
19 list list.ListModel
20 modelType int
20 modelType int
21− providers []provider.Provider
21+ providers []catwalk.Provider
22}
22}
23
23
24func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputPlaceholder string) *ModelListComponent {
24func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputPlaceholder string) *ModelListComponent {
@@ -109,19 +109,19 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
109 }
109 }
110
110
111 // Check if this provider is not in the known providers list
111 // Check if this provider is not in the known providers list
112− if !slices.ContainsFunc(knownProviders, func(p provider.Provider) bool { return p.ID == provider.InferenceProvider(providerID) }) {
112+ if !slices.ContainsFunc(knownProviders, func(p catwalk.Provider) bool { return p.ID == catwalk.InferenceProvider(providerID) }) {
113 // Convert config provider to provider.Provider format
113 // Convert config provider to provider.Provider format
114− configProvider := provider.Provider{
114+ configProvider := catwalk.Provider{
115 Name: providerConfig.Name,
115 Name: providerConfig.Name,
116− ID: provider.InferenceProvider(providerID),
116+ ID: catwalk.InferenceProvider(providerID),
117− Models: make([]provider.Model, len(providerConfig.Models)),
117+ Models: make([]catwalk.Model, len(providerConfig.Models)),
118 }
118 }
119
119
120 // Convert models
120 // Convert models
121 for i, model := range providerConfig.Models {
121 for i, model := range providerConfig.Models {
122− configProvider.Models[i] = provider.Model{
122+ configProvider.Models[i] = catwalk.Model{
123 ID: model.ID,
123 ID: model.ID,
124− Model: model.Model,
124+ Name: model.Name,
125 CostPer1MIn: model.CostPer1MIn,
125 CostPer1MIn: model.CostPer1MIn,
126 CostPer1MOut: model.CostPer1MOut,
126 CostPer1MOut: model.CostPer1MOut,
127 CostPer1MInCached: model.CostPer1MInCached,
127 CostPer1MInCached: model.CostPer1MInCached,
@@ -144,7 +144,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
144 section.SetInfo(configured)
144 section.SetInfo(configured)
145 modelItems = append(modelItems, section)
145 modelItems = append(modelItems, section)
146 for _, model := range configProvider.Models {
146 for _, model := range configProvider.Models {
147− modelItems = append(modelItems, completions.NewCompletionItem(model.Model, ModelOption{
147+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
148 Provider: configProvider,
148 Provider: configProvider,
149 Model: model,
149 Model: model,
150 }))
150 }))
@@ -179,7 +179,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
179 }
179 }
180 modelItems = append(modelItems, section)
180 modelItems = append(modelItems, section)
181 for _, model := range provider.Models {
181 for _, model := range provider.Models {
182− modelItems = append(modelItems, completions.NewCompletionItem(model.Model, ModelOption{
182+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
183 Provider: provider,
183 Provider: provider,
184 Model: model,
184 Model: model,
185 }))
185 }))
@@ -201,6 +201,6 @@ func (m *ModelListComponent) SetInputPlaceholder(placeholder string) {
201 m.list.SetFilterPlaceholder(placeholder)
201 m.list.SetFilterPlaceholder(placeholder)
202}
202}
203
203
204−func (m *ModelListComponent) SetProviders(providers []provider.Provider) {
204+func (m *ModelListComponent) SetProviders(providers []catwalk.Provider) {
205 m.providers = providers
205 m.providers = providers
206}
206}
internal/tui/components/dialogs/models/models.go+4−4
@@ -8,8 +8,8 @@ import (
8 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/spinner"
9 "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
15 "github.com/charmbracelet/crush/internal/tui/components/core/list"
15 "github.com/charmbracelet/crush/internal/tui/components/core/list"
@@ -48,8 +48,8 @@ type ModelDialog interface {
48}
48}
49
49
50type ModelOption struct {
50type ModelOption struct {
51− Provider provider.Provider
51+ Provider catwalk.Provider
52− Model provider.Model
52+ Model catwalk.Model
53}
53}
54
54
55type modelDialogCmp struct {
55type modelDialogCmp struct {
@@ -363,7 +363,7 @@ func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
363 return false
363 return false
364}
364}
365
365
366−func (m *modelDialogCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
366+func (m *modelDialogCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
367 providers, err := config.Providers()
367 providers, err := config.Providers()
368 if err != nil {
368 if err != nil {
369 return nil, err
369 return nil, err
internal/tui/page/chat/chat.go+1−1
@@ -279,7 +279,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
279 if model.SupportsImages {
279 if model.SupportsImages {
280 return p, util.CmdHandler(OpenFilePickerMsg{})
280 return p, util.CmdHandler(OpenFilePickerMsg{})
281 } else {
281 } else {
282− return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Model)
282+ return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
283 }
283 }
284 case key.Matches(msg, p.keyMap.Tab):
284 case key.Matches(msg, p.keyMap.Tab):
285 if p.session.ID == "" {
285 if p.session.ID == "" {