on main help sign in
chore: change to the small/large model conceptg
5721e7bKujtim Hoxha
internal/app/app.go+3−3
@@ -9,7 +9,7 @@ import (
9 "sync"
9 "sync"
10 "time"
10 "time"
11
11
12− configv2 "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/db"
13 "github.com/charmbracelet/crush/internal/db"
14 "github.com/charmbracelet/crush/internal/format"
14 "github.com/charmbracelet/crush/internal/format"
15 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/history"
@@ -55,9 +55,9 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
55 // Initialize LSP clients in the background
55 // Initialize LSP clients in the background
56 go app.initLSPClients(ctx)
56 go app.initLSPClients(ctx)
57
57
58− cfg := configv2.Get()
58+ cfg := config.Get()
59
59
60− coderAgentCfg := cfg.Agents[configv2.AgentCoder]
60+ coderAgentCfg := cfg.Agents[config.AgentCoder]
61 if coderAgentCfg.ID == "" {
61 if coderAgentCfg.ID == "" {
62 return nil, fmt.Errorf("coder agent configuration is missing")
62 return nil, fmt.Errorf("coder agent configuration is missing")
63 }
63 }
internal/config/config.go+95−16
@@ -47,6 +47,13 @@ const (
47 AgentTask AgentID = "task"
47 AgentTask AgentID = "task"
48)
48)
49
49
50+type ModelType string
51+
52+const (
53+ LargeModel ModelType = "large"
54+ SmallModel ModelType = "small"
55+)
56+
50type Model struct {
57type Model struct {
51 ID string `json:"id"`
58 ID string `json:"id"`
52 Name string `json:"model"`
59 Name string `json:"model"`
@@ -90,8 +97,7 @@ type Agent struct {
90 // This is the id of the system prompt used by the agent
97 // This is the id of the system prompt used by the agent
91 Disabled bool `json:"disabled"`
98 Disabled bool `json:"disabled"`
92
99
93− Provider provider.InferenceProvider `json:"provider"`
94− Model string `json:"model"`
100+ Model ModelType `json:"model"`
95
101
96 // The available tools for the agent
102 // The available tools for the agent
97 // if this is nil, all tools are available
103 // if this is nil, all tools are available
@@ -291,8 +297,7 @@ func loadConfig(cwd string, debug bool) (*Config, error) {
291 ID: AgentCoder,
297 ID: AgentCoder,
292 Name: "Coder",
298 Name: "Coder",
293 Description: "An agent that helps with executing coding tasks.",
299 Description: "An agent that helps with executing coding tasks.",
294− Provider: cfg.Models.Large.Provider,
300+ Model: LargeModel,
295− Model: cfg.Models.Large.ModelID,
296 ContextPaths: cfg.Options.ContextPaths,
301 ContextPaths: cfg.Options.ContextPaths,
297 // All tools allowed
302 // All tools allowed
298 },
303 },
@@ -300,8 +305,7 @@ func loadConfig(cwd string, debug bool) (*Config, error) {
300 ID: AgentTask,
305 ID: AgentTask,
301 Name: "Task",
306 Name: "Task",
302 Description: "An agent that helps with searching for context and finding implementation details.",
307 Description: "An agent that helps with searching for context and finding implementation details.",
303− Provider: cfg.Models.Large.Provider,
308+ Model: LargeModel,
304− Model: cfg.Models.Large.ModelID,
305 ContextPaths: cfg.Options.ContextPaths,
309 ContextPaths: cfg.Options.ContextPaths,
306 AllowedTools: []string{
310 AllowedTools: []string{
307 "glob",
311 "glob",
@@ -490,9 +494,8 @@ func mergeAgents(base, global, local *Config) {
490 switch agentID {
494 switch agentID {
491 case AgentCoder:
495 case AgentCoder:
492 baseAgent := base.Agents[agentID]
496 baseAgent := base.Agents[agentID]
493− if newAgent.Model != "" && newAgent.Provider != "" {
497+ if newAgent.Model != "" {
494 baseAgent.Model = newAgent.Model
498 baseAgent.Model = newAgent.Model
495− baseAgent.Provider = newAgent.Provider
496 }
499 }
497 baseAgent.AllowedMCP = newAgent.AllowedMCP
500 baseAgent.AllowedMCP = newAgent.AllowedMCP
498 baseAgent.AllowedLSP = newAgent.AllowedLSP
501 baseAgent.AllowedLSP = newAgent.AllowedLSP
@@ -502,9 +505,8 @@ func mergeAgents(base, global, local *Config) {
502 baseAgent.Name = newAgent.Name
505 baseAgent.Name = newAgent.Name
503 baseAgent.Description = newAgent.Description
506 baseAgent.Description = newAgent.Description
504 baseAgent.Disabled = newAgent.Disabled
507 baseAgent.Disabled = newAgent.Disabled
505− if newAgent.Model == "" || newAgent.Provider == "" {
508+ if newAgent.Model == "" {
506− baseAgent.Provider = base.Models.Large.Provider
509+ baseAgent.Model = LargeModel
507− baseAgent.Model = base.Models.Large.ModelID
508 }
510 }
509 baseAgent.AllowedTools = newAgent.AllowedTools
511 baseAgent.AllowedTools = newAgent.AllowedTools
510 baseAgent.AllowedMCP = newAgent.AllowedMCP
512 baseAgent.AllowedMCP = newAgent.AllowedMCP
@@ -709,6 +711,8 @@ func WorkingDirectory() string {
709 return cwd
711 return cwd
710}
712}
711
713
714+// TODO: Handle error state
715+
712func GetAgentModel(agentID AgentID) Model {
716func GetAgentModel(agentID AgentID) Model {
713 cfg := Get()
717 cfg := Get()
714 agent, ok := cfg.Agents[agentID]
718 agent, ok := cfg.Agents[agentID]
@@ -717,15 +721,25 @@ func GetAgentModel(agentID AgentID) Model {
717 return Model{}
721 return Model{}
718 }
722 }
719
723
724+ var model PreferredModel
725+ switch agent.Model {
726+ case LargeModel:
727+ model = cfg.Models.Large
728+ case SmallModel:
729+ model = cfg.Models.Small
730+ default:
731+ logging.Warn("Unknown model type for agent", "agent_id", agentID, "model_type", agent.Model)
732+ model = cfg.Models.Large // Fallback to large model
733+ }
720− providerConfig, ok := cfg.Providers[agent.Provider]
734+ providerConfig, ok := cfg.Providers[model.Provider]
721 if !ok {
735 if !ok {
722− logging.Error("Provider not found for agent", "agent_id", agentID, "provider", agent.Provider)
736+ logging.Error("Provider not found for agent", "agent_id", agentID, "provider", model.Provider)
723 return Model{}
737 return Model{}
724 }
738 }
725
739
726− for _, model := range providerConfig.Models {
740+ for _, m := range providerConfig.Models {
727− if model.ID == agent.Model {
741+ if m.ID == model.ModelID {
728− return model
742+ return m
729 }
743 }
730 }
744 }
731
745
@@ -733,6 +747,34 @@ func GetAgentModel(agentID AgentID) Model {
733 return Model{}
747 return Model{}
734}
748}
735
749
750+func GetAgentProvider(agentID AgentID) ProviderConfig {
751+ cfg := Get()
752+ agent, ok := cfg.Agents[agentID]
753+ if !ok {
754+ logging.Error("Agent not found", "agent_id", agentID)
755+ return ProviderConfig{}
756+ }
757+
758+ var model PreferredModel
759+ switch agent.Model {
760+ case LargeModel:
761+ model = cfg.Models.Large
762+ case SmallModel:
763+ model = cfg.Models.Small
764+ default:
765+ logging.Warn("Unknown model type for agent", "agent_id", agentID, "model_type", agent.Model)
766+ model = cfg.Models.Large // Fallback to large model
767+ }
768+
769+ providerConfig, ok := cfg.Providers[model.Provider]
770+ if !ok {
771+ logging.Error("Provider not found for agent", "agent_id", agentID, "provider", model.Provider)
772+ return ProviderConfig{}
773+ }
774+
775+ return providerConfig
776+}
777+
736func GetProviderModel(provider provider.InferenceProvider, modelID string) Model {
778func GetProviderModel(provider provider.InferenceProvider, modelID string) Model {
737 cfg := Get()
779 cfg := Get()
738 providerConfig, ok := cfg.Providers[provider]
780 providerConfig, ok := cfg.Providers[provider]
@@ -750,3 +792,40 @@ func GetProviderModel(provider provider.InferenceProvider, modelID string) Model
750 logging.Error("Model not found for provider", "provider", provider, "model_id", modelID)
792 logging.Error("Model not found for provider", "provider", provider, "model_id", modelID)
751 return Model{}
793 return Model{}
752}
794}
795+
796+func GetModel(modelType ModelType) Model {
797+ cfg := Get()
798+ var model PreferredModel
799+ switch modelType {
800+ case LargeModel:
801+ model = cfg.Models.Large
802+ case SmallModel:
803+ model = cfg.Models.Small
804+ default:
805+ model = cfg.Models.Large // Fallback to large model
806+ }
807+ providerConfig, ok := cfg.Providers[model.Provider]
808+ if !ok {
809+ return Model{}
810+ }
811+
812+ for _, m := range providerConfig.Models {
813+ if m.ID == model.ModelID {
814+ return m
815+ }
816+ }
817+ return Model{}
818+}
819+
820+func UpdatePreferredModel(modelType ModelType, model PreferredModel) error {
821+ cfg := Get()
822+ switch modelType {
823+ case LargeModel:
824+ cfg.Models.Large = model
825+ case SmallModel:
826+ cfg.Models.Small = model
827+ default:
828+ return fmt.Errorf("unknown model type: %s", modelType)
829+ }
830+ return nil
831+}
internal/llm/agent/agent.go+25−84
@@ -9,7 +9,7 @@ import (
9 "sync"
9 "sync"
10 "time"
10 "time"
11
11
12− configv2 "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/history"
13 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/llm/prompt"
14 "github.com/charmbracelet/crush/internal/llm/prompt"
15 "github.com/charmbracelet/crush/internal/llm/provider"
15 "github.com/charmbracelet/crush/internal/llm/provider"
@@ -49,19 +49,18 @@ type AgentEvent struct {
49
49
50type Service interface {
50type Service interface {
51 pubsub.Suscriber[AgentEvent]
51 pubsub.Suscriber[AgentEvent]
52− Model() configv2.Model
52+ Model() config.Model
53 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
53 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
54 Cancel(sessionID string)
54 Cancel(sessionID string)
55 CancelAll()
55 CancelAll()
56 IsSessionBusy(sessionID string) bool
56 IsSessionBusy(sessionID string) bool
57 IsBusy() bool
57 IsBusy() bool
58− Update(model configv2.PreferredModel) (configv2.Model, error)
59 Summarize(ctx context.Context, sessionID string) error
58 Summarize(ctx context.Context, sessionID string) error
60}
59}
61
60
62type agent struct {
61type agent struct {
63 *pubsub.Broker[AgentEvent]
62 *pubsub.Broker[AgentEvent]
64− agentCfg configv2.Agent
63+ agentCfg config.Agent
65 sessions session.Service
64 sessions session.Service
66 messages message.Service
65 messages message.Service
67
66
@@ -76,13 +75,13 @@ type agent struct {
76 activeRequests sync.Map
75 activeRequests sync.Map
77}
76}
78
77
79−var agentPromptMap = map[configv2.AgentID]prompt.PromptID{
78+var agentPromptMap = map[config.AgentID]prompt.PromptID{
80− configv2.AgentCoder: prompt.PromptCoder,
79+ config.AgentCoder: prompt.PromptCoder,
81− configv2.AgentTask: prompt.PromptTask,
80+ config.AgentTask: prompt.PromptTask,
82}
81}
83
82
84func NewAgent(
83func NewAgent(
85− agentCfg configv2.Agent,
84+ agentCfg config.Agent,
86 // These services are needed in the tools
85 // These services are needed in the tools
87 permissions permission.Service,
86 permissions permission.Service,
88 sessions session.Service,
87 sessions session.Service,
@@ -91,7 +90,7 @@ func NewAgent(
91 lspClients map[string]*lsp.Client,
90 lspClients map[string]*lsp.Client,
92) (Service, error) {
91) (Service, error) {
93 ctx := context.Background()
92 ctx := context.Background()
94− cfg := configv2.Get()
93+ cfg := config.Get()
95 otherTools := GetMcpTools(ctx, permissions)
94 otherTools := GetMcpTools(ctx, permissions)
96 if len(lspClients) > 0 {
95 if len(lspClients) > 0 {
97 otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients))
96 otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients))
@@ -109,8 +108,8 @@ func NewAgent(
109 tools.NewWriteTool(lspClients, permissions, history),
108 tools.NewWriteTool(lspClients, permissions, history),
110 }
109 }
111
110
112− if agentCfg.ID == configv2.AgentCoder {
111+ if agentCfg.ID == config.AgentCoder {
113− taskAgentCfg := configv2.Get().Agents[configv2.AgentTask]
112+ taskAgentCfg := config.Get().Agents[config.AgentTask]
114 if taskAgentCfg.ID == "" {
113 if taskAgentCfg.ID == "" {
115 return nil, fmt.Errorf("task agent not found in config")
114 return nil, fmt.Errorf("task agent not found in config")
116 }
115 }
@@ -130,26 +129,14 @@ func NewAgent(
130 }
129 }
131
130
132 allTools = append(allTools, otherTools...)
131 allTools = append(allTools, otherTools...)
133− var providerCfg configv2.ProviderConfig
132+ providerCfg := config.GetAgentProvider(agentCfg.ID)
134− for _, p := range cfg.Providers {
135− if p.ID == agentCfg.Provider {
136− providerCfg = p
137− break
138− }
139− }
140 if providerCfg.ID == "" {
133 if providerCfg.ID == "" {
141− return nil, fmt.Errorf("provider %s not found in config", agentCfg.Provider)
134+ return nil, fmt.Errorf("provider for agent %s not found in config", agentCfg.Name)
142 }
135 }
136+ model := config.GetAgentModel(agentCfg.ID)
143
137
144− var model configv2.Model
145− for _, m := range providerCfg.Models {
146− if m.ID == agentCfg.Model {
147− model = m
148− break
149− }
150− }
151 if model.ID == "" {
138 if model.ID == "" {
152− return nil, fmt.Errorf("model %s not found in provider %s", agentCfg.Model, agentCfg.Provider)
139+ return nil, fmt.Errorf("model not found for agent %s", agentCfg.Name)
153 }
140 }
154
141
155 promptID := agentPromptMap[agentCfg.ID]
142 promptID := agentPromptMap[agentCfg.ID]
@@ -157,7 +144,7 @@ func NewAgent(
157 promptID = prompt.PromptDefault
144 promptID = prompt.PromptDefault
158 }
145 }
159 opts := []provider.ProviderClientOption{
146 opts := []provider.ProviderClientOption{
160− provider.WithModel(model),
147+ provider.WithModel(agentCfg.Model),
161 provider.WithSystemMessage(prompt.GetPrompt(promptID, providerCfg.ID)),
148 provider.WithSystemMessage(prompt.GetPrompt(promptID, providerCfg.ID)),
162 provider.WithMaxTokens(model.DefaultMaxTokens),
149 provider.WithMaxTokens(model.DefaultMaxTokens),
163 }
150 }
@@ -167,9 +154,9 @@ func NewAgent(
167 }
154 }
168
155
169 smallModelCfg := cfg.Models.Small
156 smallModelCfg := cfg.Models.Small
170− var smallModel configv2.Model
157+ var smallModel config.Model
171
158
172− var smallModelProviderCfg configv2.ProviderConfig
159+ var smallModelProviderCfg config.ProviderConfig
173 if smallModelCfg.Provider == providerCfg.ID {
160 if smallModelCfg.Provider == providerCfg.ID {
174 smallModelProviderCfg = providerCfg
161 smallModelProviderCfg = providerCfg
175 } else {
162 } else {
@@ -194,7 +181,7 @@ func NewAgent(
194 }
181 }
195
182
196 titleOpts := []provider.ProviderClientOption{
183 titleOpts := []provider.ProviderClientOption{
197− provider.WithModel(smallModel),
184+ provider.WithModel(config.SmallModel),
198 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptTitle, smallModelProviderCfg.ID)),
185 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptTitle, smallModelProviderCfg.ID)),
199 provider.WithMaxTokens(40),
186 provider.WithMaxTokens(40),
200 }
187 }
@@ -203,7 +190,7 @@ func NewAgent(
203 return nil, err
190 return nil, err
204 }
191 }
205 summarizeOpts := []provider.ProviderClientOption{
192 summarizeOpts := []provider.ProviderClientOption{
206− provider.WithModel(smallModel),
193+ provider.WithModel(config.SmallModel),
207 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptSummarizer, smallModelProviderCfg.ID)),
194 provider.WithSystemMessage(prompt.GetPrompt(prompt.PromptSummarizer, smallModelProviderCfg.ID)),
208 provider.WithMaxTokens(smallModel.DefaultMaxTokens),
195 provider.WithMaxTokens(smallModel.DefaultMaxTokens),
209 }
196 }
@@ -240,8 +227,8 @@ func NewAgent(
240 return agent, nil
227 return agent, nil
241}
228}
242
229
243−func (a *agent) Model() configv2.Model {
230+func (a *agent) Model() config.Model {
244− return a.provider.Model()
231+ return config.GetAgentModel(a.agentCfg.ID)
245}
232}
246
233
247func (a *agent) Cancel(sessionID string) {
234func (a *agent) Cancel(sessionID string) {
@@ -336,7 +323,7 @@ func (a *agent) err(err error) AgentEvent {
336}
323}
337
324
338func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) {
325func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) {
339− if !a.provider.Model().SupportsImages && attachments != nil {
326+ if !a.Model().SupportsImages && attachments != nil {
340 attachments = nil
327 attachments = nil
341 }
328 }
342 events := make(chan AgentEvent)
329 events := make(chan AgentEvent)
@@ -458,7 +445,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
458 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
445 assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
459 Role: message.Assistant,
446 Role: message.Assistant,
460 Parts: []message.ContentPart{},
447 Parts: []message.ContentPart{},
461− Model: a.provider.Model().ID,
448+ Model: a.Model().ID,
462 Provider: a.providerID,
449 Provider: a.providerID,
463 })
450 })
464 if err != nil {
451 if err != nil {
@@ -609,13 +596,13 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
609 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
596 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
610 return fmt.Errorf("failed to update message: %w", err)
597 return fmt.Errorf("failed to update message: %w", err)
611 }
598 }
612− return a.TrackUsage(ctx, sessionID, a.provider.Model(), event.Response.Usage)
599+ return a.TrackUsage(ctx, sessionID, a.Model(), event.Response.Usage)
613 }
600 }
614
601
615 return nil
602 return nil
616}
603}
617
604
618−func (a *agent) TrackUsage(ctx context.Context, sessionID string, model configv2.Model, usage provider.TokenUsage) error {
605+func (a *agent) TrackUsage(ctx context.Context, sessionID string, model config.Model, usage provider.TokenUsage) error {
619 sess, err := a.sessions.Get(ctx, sessionID)
606 sess, err := a.sessions.Get(ctx, sessionID)
620 if err != nil {
607 if err != nil {
621 return fmt.Errorf("failed to get session: %w", err)
608 return fmt.Errorf("failed to get session: %w", err)
@@ -637,52 +624,6 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model configv2
637 return nil
624 return nil
638}
625}
639
626
640−func (a *agent) Update(modelCfg configv2.PreferredModel) (configv2.Model, error) {
641− if a.IsBusy() {
642− return configv2.Model{}, fmt.Errorf("cannot change model while processing requests")
643− }
644−
645− cfg := configv2.Get()
646− var providerCfg configv2.ProviderConfig
647− for _, p := range cfg.Providers {
648− if p.ID == modelCfg.Provider {
649− providerCfg = p
650− break
651− }
652− }
653− if providerCfg.ID == "" {
654− return configv2.Model{}, fmt.Errorf("provider %s not found in config", modelCfg.Provider)
655− }
656−
657− var model configv2.Model
658− for _, m := range providerCfg.Models {
659− if m.ID == modelCfg.ModelID {
660− model = m
661− break
662− }
663− }
664− if model.ID == "" {
665− return configv2.Model{}, fmt.Errorf("model %s not found in provider %s", modelCfg.ModelID, modelCfg.Provider)
666− }
667−
668− promptID := agentPromptMap[a.agentCfg.ID]
669− if promptID == "" {
670− promptID = prompt.PromptDefault
671− }
672− opts := []provider.ProviderClientOption{
673− provider.WithModel(model),
674− provider.WithSystemMessage(prompt.GetPrompt(promptID, providerCfg.ID)),
675− provider.WithMaxTokens(model.DefaultMaxTokens),
676− }
677− agentProvider, err := provider.NewProviderV2(providerCfg, opts...)
678− if err != nil {
679− return configv2.Model{}, err
680− }
681− a.provider = agentProvider
682−
683− return a.provider.Model(), nil
684−}
685−
686func (a *agent) Summarize(ctx context.Context, sessionID string) error {
627func (a *agent) Summarize(ctx context.Context, sessionID string) error {
687 if a.summarizeProvider == nil {
628 if a.summarizeProvider == nil {
688 return fmt.Errorf("summarize provider not available")
629 return fmt.Errorf("summarize provider not available")
internal/llm/provider/anthropic.go+6−1
@@ -145,6 +145,7 @@ func (a *anthropicClient) finishReason(reason string) message.FinishReason {
145}
145}
146
146
147func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, tools []anthropic.ToolUnionParam) anthropic.MessageNewParams {
147func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, tools []anthropic.ToolUnionParam) anthropic.MessageNewParams {
148+ model := a.providerOptions.model(a.providerOptions.modelType)
148 var thinkingParam anthropic.ThinkingConfigParamUnion
149 var thinkingParam anthropic.ThinkingConfigParamUnion
149 // TODO: Implement a proper thinking function
150 // TODO: Implement a proper thinking function
150 // lastMessage := messages[len(messages)-1]
151 // lastMessage := messages[len(messages)-1]
@@ -164,7 +165,7 @@ func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, to
164 // }
165 // }
165
166
166 return anthropic.MessageNewParams{
167 return anthropic.MessageNewParams{
167− Model: anthropic.Model(a.providerOptions.model.ID),
168+ Model: anthropic.Model(model.ID),
168 MaxTokens: a.providerOptions.maxTokens,
169 MaxTokens: a.providerOptions.maxTokens,
169 Temperature: temperature,
170 Temperature: temperature,
170 Messages: messages,
171 Messages: messages,
@@ -425,6 +426,10 @@ func (a *anthropicClient) usage(msg anthropic.Message) TokenUsage {
425 }
426 }
426}
427}
427
428
429+func (a *anthropicClient) Model() config.Model {
430+ return a.providerOptions.model(a.providerOptions.modelType)
431+}
432+
428// TODO: check if we need
433// TODO: check if we need
429func DefaultShouldThinkFn(s string) bool {
434func DefaultShouldThinkFn(s string) bool {
430 return strings.Contains(strings.ToLower(s), "think")
435 return strings.Contains(strings.ToLower(s), "think")
internal/llm/provider/bedrock.go+17−5
@@ -6,6 +6,7 @@ import (
6 "fmt"
6 "fmt"
7 "strings"
7 "strings"
8
8
9+ "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/llm/tools"
10 "github.com/charmbracelet/crush/internal/llm/tools"
10 "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/message"
11)
12)
@@ -30,13 +31,20 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
30 }
31 }
31 }
32 }
32
33
34+ opts.model = func(modelType config.ModelType) config.Model {
35+ model := config.GetModel(modelType)
36+
33− // Prefix the model name with region
37+ // Prefix the model name with region
34− regionPrefix := region[:2]
38+ regionPrefix := region[:2]
35− modelName := opts.model.ID
39+ modelName := model.ID
36− opts.model.ID = fmt.Sprintf("%s.%s", regionPrefix, modelName)
40+ model.ID = fmt.Sprintf("%s.%s", regionPrefix, modelName)
41+ return model
42+ }
43+
44+ model := opts.model(opts.modelType)
37
45
38 // Determine which provider to use based on the model
46 // Determine which provider to use based on the model
39− if strings.Contains(string(opts.model.ID), "anthropic") {
47+ if strings.Contains(string(model.ID), "anthropic") {
40 // Create Anthropic client with Bedrock configuration
48 // Create Anthropic client with Bedrock configuration
41 anthropicOpts := opts
49 anthropicOpts := opts
42 // TODO: later find a way to check if the AWS account has caching enabled
50 // TODO: later find a way to check if the AWS account has caching enabled
@@ -78,3 +86,7 @@ func (b *bedrockClient) stream(ctx context.Context, messages []message.Message,
78
86
79 return b.childProvider.stream(ctx, messages, tools)
87 return b.childProvider.stream(ctx, messages, tools)
80}
88}
89+
90+func (b *bedrockClient) Model() config.Model {
91+ return b.providerOptions.model(b.providerOptions.modelType)
92+}
internal/llm/provider/gemini.go+8−2
@@ -173,7 +173,8 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
173 if len(tools) > 0 {
173 if len(tools) > 0 {
174 config.Tools = g.convertTools(tools)
174 config.Tools = g.convertTools(tools)
175 }
175 }
176+ model := g.providerOptions.model(g.providerOptions.modelType)
176− chat, _ := g.client.Chats.Create(ctx, g.providerOptions.model.ID, config, history)
177+ chat, _ := g.client.Chats.Create(ctx, model.ID, config, history)
177
178
178 attempts := 0
179 attempts := 0
179 for {
180 for {
@@ -261,7 +262,8 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
261 if len(tools) > 0 {
262 if len(tools) > 0 {
262 config.Tools = g.convertTools(tools)
263 config.Tools = g.convertTools(tools)
263 }
264 }
265+ model := g.providerOptions.model(g.providerOptions.modelType)
264− chat, _ := g.client.Chats.Create(ctx, g.providerOptions.model.ID, config, history)
266+ chat, _ := g.client.Chats.Create(ctx, model.ID, config, history)
265
267
266 attempts := 0
268 attempts := 0
267 eventChan := make(chan ProviderEvent)
269 eventChan := make(chan ProviderEvent)
@@ -439,6 +441,10 @@ func (g *geminiClient) usage(resp *genai.GenerateContentResponse) TokenUsage {
439 }
441 }
440}
442}
441
443
444+func (g *geminiClient) Model() config.Model {
445+ return g.providerOptions.model(g.providerOptions.modelType)
446+}
447+
442// Helper functions
448// Helper functions
443func parseJsonToMap(jsonStr string) (map[string]any, error) {
449func parseJsonToMap(jsonStr string) (map[string]any, error) {
444 var result map[string]any
450 var result map[string]any
internal/llm/provider/openai.go+7−3
@@ -152,13 +152,13 @@ func (o *openaiClient) finishReason(reason string) message.FinishReason {
152}
152}
153
153
154func (o *openaiClient) preparedParams(messages []openai.ChatCompletionMessageParamUnion, tools []openai.ChatCompletionToolParam) openai.ChatCompletionNewParams {
154func (o *openaiClient) preparedParams(messages []openai.ChatCompletionMessageParamUnion, tools []openai.ChatCompletionToolParam) openai.ChatCompletionNewParams {
155+ model := o.providerOptions.model(o.providerOptions.modelType)
155 params := openai.ChatCompletionNewParams{
156 params := openai.ChatCompletionNewParams{
156− Model: openai.ChatModel(o.providerOptions.model.ID),
157+ Model: openai.ChatModel(model.ID),
157 Messages: messages,
158 Messages: messages,
158 Tools: tools,
159 Tools: tools,
159 }
160 }
160−
161− if o.providerOptions.model.CanReason {
161+ if model.CanReason {
162 params.MaxCompletionTokens = openai.Int(o.providerOptions.maxTokens)
162 params.MaxCompletionTokens = openai.Int(o.providerOptions.maxTokens)
163 switch o.options.reasoningEffort {
163 switch o.options.reasoningEffort {
164 case "low":
164 case "low":
@@ -384,3 +384,7 @@ func (o *openaiClient) usage(completion openai.ChatCompletion) TokenUsage {
384 CacheReadTokens: cachedTokens,
384 CacheReadTokens: cachedTokens,
385 }
385 }
386}
386}
387+
388+func (a *openaiClient) Model() config.Model {
389+ return a.providerOptions.model(a.providerOptions.modelType)
390+}
internal/llm/provider/provider.go+16−10
@@ -4,7 +4,7 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6
6
7− configv2 "github.com/charmbracelet/crush/internal/config"
7+ "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/fur/provider"
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"
@@ -55,13 +55,14 @@ type Provider interface {
55
55
56 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
56 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
57
57
58− Model() configv2.Model
58+ Model() config.Model
59}
59}
60
60
61type providerClientOptions struct {
61type providerClientOptions struct {
62 baseURL string
62 baseURL string
63 apiKey string
63 apiKey string
64− model configv2.Model
64+ modelType config.ModelType
65+ model func(config.ModelType) config.Model
65 disableCache bool
66 disableCache bool
66 maxTokens int64
67 maxTokens int64
67 systemMessage string
68 systemMessage string
@@ -74,6 +75,8 @@ type ProviderClientOption func(*providerClientOptions)
74type ProviderClient interface {
75type ProviderClient interface {
75 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
76 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
76 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
77 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
78+
79+ Model() config.Model
77}
80}
78
81
79type baseProvider[C ProviderClient] struct {
82type baseProvider[C ProviderClient] struct {
@@ -97,18 +100,18 @@ func (p *baseProvider[C]) SendMessages(ctx context.Context, messages []message.M
97 return p.client.send(ctx, messages, tools)
100 return p.client.send(ctx, messages, tools)
98}
101}
99
102
100−func (p *baseProvider[C]) Model() configv2.Model {
101− return p.options.model
102−}
103−
104func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
103func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
105 messages = p.cleanMessages(messages)
104 messages = p.cleanMessages(messages)
106 return p.client.stream(ctx, messages, tools)
105 return p.client.stream(ctx, messages, tools)
107}
106}
108
107
108+func (p *baseProvider[C]) Model() config.Model {
109+ return p.client.Model()
110+}
111+
109−func WithModel(model configv2.Model) ProviderClientOption {
112+func WithModel(model config.ModelType) ProviderClientOption {
110 return func(options *providerClientOptions) {
113 return func(options *providerClientOptions) {
111− options.model = model
114+ options.modelType = model
112 }
115 }
113}
116}
114
117
@@ -130,11 +133,14 @@ func WithSystemMessage(systemMessage string) ProviderClientOption {
130 }
133 }
131}
134}
132
135
133−func NewProviderV2(cfg configv2.ProviderConfig, opts ...ProviderClientOption) (Provider, error) {
136+func NewProviderV2(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provider, error) {
134 clientOptions := providerClientOptions{
137 clientOptions := providerClientOptions{
135 baseURL: cfg.BaseURL,
138 baseURL: cfg.BaseURL,
136 apiKey: cfg.APIKey,
139 apiKey: cfg.APIKey,
137 extraHeaders: cfg.ExtraHeaders,
140 extraHeaders: cfg.ExtraHeaders,
141+ model: func(tp config.ModelType) config.Model {
142+ return config.GetModel(tp)
143+ },
138 }
144 }
139 for _, o := range opts {
145 for _, o := range opts {
140 o(&clientOptions)
146 o(&clientOptions)
internal/tui/components/dialogs/init/init.go+3−3
@@ -5,7 +5,7 @@ import (
5 tea "github.com/charmbracelet/bubbletea/v2"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7
7
8− configv2 "github.com/charmbracelet/crush/internal/config"
8+ "github.com/charmbracelet/crush/internal/config"
9 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
9 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
10 "github.com/charmbracelet/crush/internal/tui/components/core"
10 "github.com/charmbracelet/crush/internal/tui/components/core"
11 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
11 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -184,7 +184,7 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
184Add the .crush directory to the .gitignore file if it's not already there.`
184Add the .crush directory to the .gitignore file if it's not already there.`
185
185
186 // Mark the project as initialized
186 // Mark the project as initialized
187− if err := configv2.MarkProjectInitialized(); err != nil {
187+ if err := config.MarkProjectInitialized(); err != nil {
188 return util.ReportError(err)
188 return util.ReportError(err)
189 }
189 }
190
190
@@ -196,7 +196,7 @@ Add the .crush directory to the .gitignore file if it's not already there.`
196 )
196 )
197 } else {
197 } else {
198 // Mark the project as initialized without running the command
198 // Mark the project as initialized without running the command
199− if err := configv2.MarkProjectInitialized(); err != nil {
199+ if err := config.MarkProjectInitialized(); err != nil {
200 return util.ReportError(err)
200 return util.ReportError(err)
201 }
201 }
202 }
202 }
internal/tui/components/dialogs/models/models.go+7−7
@@ -4,7 +4,7 @@ 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− configv2 "github.com/charmbracelet/crush/internal/config"
7+ "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/fur/provider"
8 "github.com/charmbracelet/crush/internal/fur/provider"
9 "github.com/charmbracelet/crush/internal/tui/components/completions"
9 "github.com/charmbracelet/crush/internal/tui/components/completions"
10 "github.com/charmbracelet/crush/internal/tui/components/core"
10 "github.com/charmbracelet/crush/internal/tui/components/core"
@@ -24,7 +24,7 @@ const (
24
24
25// ModelSelectedMsg is sent when a model is selected
25// ModelSelectedMsg is sent when a model is selected
26type ModelSelectedMsg struct {
26type ModelSelectedMsg struct {
27− Model configv2.PreferredModel
27+ Model config.PreferredModel
28}
28}
29
29
30// CloseModelDialogMsg is sent when a model is selected
30// CloseModelDialogMsg is sent when a model is selected
@@ -84,12 +84,12 @@ func NewModelDialogCmp() ModelDialog {
84}
84}
85
85
86func (m *modelDialogCmp) Init() tea.Cmd {
86func (m *modelDialogCmp) Init() tea.Cmd {
87− providers := configv2.Providers()
87+ providers := config.Providers()
88− cfg := configv2.Get()
89
88
90− coderAgent := cfg.Agents[configv2.AgentCoder]
91 modelItems := []util.Model{}
89 modelItems := []util.Model{}
92 selectIndex := 0
90 selectIndex := 0
91+ agentModel := config.GetAgentModel(config.AgentCoder)
92+ agentProvider := config.GetAgentProvider(config.AgentCoder)
93 for _, provider := range providers {
93 for _, provider := range providers {
94 name := provider.Name
94 name := provider.Name
95 if name == "" {
95 if name == "" {
@@ -97,7 +97,7 @@ func (m *modelDialogCmp) Init() tea.Cmd {
97 }
97 }
98 modelItems = append(modelItems, commands.NewItemSection(name))
98 modelItems = append(modelItems, commands.NewItemSection(name))
99 for _, model := range provider.Models {
99 for _, model := range provider.Models {
100− if model.ID == coderAgent.Model && provider.ID == coderAgent.Provider {
100+ if model.ID == agentModel.ID && provider.ID == agentProvider.ID {
101 selectIndex = len(modelItems) // Set the selected index to the current model
101 selectIndex = len(modelItems) // Set the selected index to the current model
102 }
102 }
103 modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
103 modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
@@ -128,7 +128,7 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
128
128
129 return m, tea.Sequence(
129 return m, tea.Sequence(
130 util.CmdHandler(dialogs.CloseDialogMsg{}),
130 util.CmdHandler(dialogs.CloseDialogMsg{}),
131− util.CmdHandler(ModelSelectedMsg{Model: configv2.PreferredModel{
131+ util.CmdHandler(ModelSelectedMsg{Model: config.PreferredModel{
132 ModelID: selectedItem.Model.ID,
132 ModelID: selectedItem.Model.ID,
133 Provider: selectedItem.Provider.ID,
133 Provider: selectedItem.Provider.ID,
134 }}),
134 }}),
internal/tui/tui.go+3−8
@@ -8,7 +8,6 @@ import (
8 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/app"
9 "github.com/charmbracelet/crush/internal/app"
10 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
11− configv2 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/llm/agent"
11 "github.com/charmbracelet/crush/internal/llm/agent"
13 "github.com/charmbracelet/crush/internal/logging"
12 "github.com/charmbracelet/crush/internal/logging"
14 "github.com/charmbracelet/crush/internal/permission"
13 "github.com/charmbracelet/crush/internal/permission"
@@ -70,7 +69,7 @@ func (a appModel) Init() tea.Cmd {
70
69
71 // Check if we should show the init dialog
70 // Check if we should show the init dialog
72 cmds = append(cmds, func() tea.Msg {
71 cmds = append(cmds, func() tea.Msg {
73− shouldShow, err := configv2.ProjectNeedsInitialization()
72+ shouldShow, err := config.ProjectNeedsInitialization()
74 if err != nil {
73 if err != nil {
75 return util.InfoMsg{
74 return util.InfoMsg{
76 Type: util.InfoTypeError,
75 Type: util.InfoTypeError,
@@ -173,12 +172,8 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
173
172
174 // Model Switch
173 // Model Switch
175 case models.ModelSelectedMsg:
174 case models.ModelSelectedMsg:
176− model, err := a.app.CoderAgent.Update(msg.Model)
175+ config.UpdatePreferredModel(config.LargeModel, msg.Model)
177− if err != nil {
178− return a, util.ReportError(err)
179− }
180−
181− return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name))
176+ return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", msg.Model.ModelID))
182
177
183 // File Picker
178 // File Picker
184 case chat.OpenFilePickerMsg:
179 case chat.OpenFilePickerMsg: