on main help sign in
chore: move fur structs, small provider changes
399e7f7Kujtim Hoxha
go.mod−3
@@ -2,8 +2,6 @@ module github.com/charmbracelet/crush
2
2
3go 1.24.3
3go 1.24.3
4
4
5−replace github.com/charmbracelet/fur => ../fur
6−
7require (
5require (
8 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
6 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
9 github.com/JohannesKaufmann/html-to-markdown v1.6.0
7 github.com/JohannesKaufmann/html-to-markdown v1.6.0
@@ -17,7 +15,6 @@ require (
17 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
15 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
18 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94
16 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94
19 github.com/charmbracelet/fang v0.1.0
17 github.com/charmbracelet/fang v0.1.0
20− github.com/charmbracelet/fur v0.0.0-00010101000000-000000000000
21 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
22 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
23 github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
20 github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
internal/config_v2/config.go+270−56
@@ -10,8 +10,8 @@ import (
10 "strings"
10 "strings"
11 "sync"
11 "sync"
12
12
13+ "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/logging"
14 "github.com/charmbracelet/crush/internal/logging"
14− "github.com/charmbracelet/fur/pkg/provider"
15)
15)
16
16
17const (
17const (
@@ -22,6 +22,29 @@ const (
22 MaxTokensFallbackDefault = 4096
22 MaxTokensFallbackDefault = 4096
23)
23)
24
24
25+var defaultContextPaths = []string{
26+ ".github/copilot-instructions.md",
27+ ".cursorrules",
28+ ".cursor/rules/",
29+ "CLAUDE.md",
30+ "CLAUDE.local.md",
31+ "crush.md",
32+ "crush.local.md",
33+ "Crush.md",
34+ "Crush.local.md",
35+ "CRUSH.md",
36+ "CRUSH.local.md",
37+}
38+
39+type AgentID string
40+
41+const (
42+ AgentCoder AgentID = "coder"
43+ AgentTask AgentID = "task"
44+ AgentTitle AgentID = "title"
45+ AgentSummarize AgentID = "summarize"
46+)
47+
25type Model struct {
48type Model struct {
26 ID string `json:"id"`
49 ID string `json:"id"`
27 Name string `json:"model"`
50 Name string `json:"model"`
@@ -43,40 +66,43 @@ type VertexAIOptions struct {
43}
66}
44
67
45type ProviderConfig struct {
68type ProviderConfig struct {
69+ ID provider.InferenceProvider `json:"id"`
46− BaseURL string `json:"base_url,omitempty"`
70+ BaseURL string `json:"base_url,omitempty"`
47− ProviderType provider.Type `json:"provider_type"`
71+ ProviderType provider.Type `json:"provider_type"`
48− APIKey string `json:"api_key,omitempty"`
72+ APIKey string `json:"api_key,omitempty"`
49− Disabled bool `json:"disabled"`
73+ Disabled bool `json:"disabled"`
50− ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
74+ ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
51 // used for e.x for vertex to set the project
75 // used for e.x for vertex to set the project
52 ExtraParams map[string]string `json:"extra_params,omitempty"`
76 ExtraParams map[string]string `json:"extra_params,omitempty"`
53
77
54− DefaultModel string `json:"default_model"`
78+ DefaultLargeModel string `json:"default_large_model,omitempty"`
79+ DefaultSmallModel string `json:"default_small_model,omitempty"`
80+
81+ Models []Model `json:"models,omitempty"`
55}
82}
56
83
57type Agent struct {
84type Agent struct {
58− Name string `json:"name"`
85+ Name string `json:"name"`
86+ Description string `json:"description,omitempty"`
59 // This is the id of the system prompt used by the agent
87 // This is the id of the system prompt used by the agent
60− // TODO: still needs to be implemented
61− PromptID string `json:"prompt_id"`
62− Disabled bool `json:"disabled"`
88+ Disabled bool `json:"disabled"`
63
89
64 Provider provider.InferenceProvider `json:"provider"`
90 Provider provider.InferenceProvider `json:"provider"`
65− Model Model `json:"model"`
91+ Model string `json:"model"`
66
92
67 // The available tools for the agent
93 // The available tools for the agent
68− // if this is empty, all tools are available
94+ // if this is nil, all tools are available
69 AllowedTools []string `json:"allowed_tools"`
95 AllowedTools []string `json:"allowed_tools"`
70
96
71 // this tells us which MCPs are available for this agent
97 // this tells us which MCPs are available for this agent
72 // if this is empty all mcps are available
98 // if this is empty all mcps are available
73− // the string array is the list of tools from the MCP the agent has available
99+ // the string array is the list of tools from the AllowedMCP the agent has available
74− // if the string array is empty, all tools from the MCP are available
100+ // if the string array is nil, all tools from the AllowedMCP are available
75− MCP map[string][]string `json:"mcp"`
101+ AllowedMCP map[string][]string `json:"allowed_mcp"`
76
102
77 // The list of LSPs that this agent can use
103 // The list of LSPs that this agent can use
78− // if this is empty, all LSPs are available
104+ // if this is nil, all LSPs are available
79− LSP []string `json:"lsp"`
105+ AllowedLSP []string `json:"allowed_lsp"`
80
106
81 // Overrides the context paths for this agent
107 // Overrides the context paths for this agent
82 ContextPaths []string `json:"context_paths"`
108 ContextPaths []string `json:"context_paths"`
@@ -125,7 +151,7 @@ type Config struct {
125 Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty"`
151 Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty"`
126
152
127 // List of configured agents
153 // List of configured agents
128− Agents map[string]Agent `json:"agents,omitempty"`
154+ Agents map[AgentID]Agent `json:"agents,omitempty"`
129
155
130 // List of configured MCPs
156 // List of configured MCPs
131 MCP map[string]MCP `json:"mcp,omitempty"`
157 MCP map[string]MCP `json:"mcp,omitempty"`
@@ -135,15 +161,13 @@ type Config struct {
135
161
136 // Miscellaneous options
162 // Miscellaneous options
137 Options Options `json:"options"`
163 Options Options `json:"options"`
138−
139− // Used to add models that are not already in the repository
140− Models map[provider.InferenceProvider][]provider.Model `json:"models,omitempty"`
141}
164}
142
165
143var (
166var (
144 instance *Config // The single instance of the Singleton
167 instance *Config // The single instance of the Singleton
145 cwd string
168 cwd string
146 once sync.Once // Ensures the initialization happens only once
169 once sync.Once // Ensures the initialization happens only once
170+
147)
171)
148
172
149func loadConfig(cwd string) (*Config, error) {
173func loadConfig(cwd string) (*Config, error) {
@@ -190,10 +214,73 @@ func loadConfig(cwd string) (*Config, error) {
190 }
214 }
191
215
192 // merge options
216 // merge options
193− cfg.Options = mergeOptions(cfg.Options, globalCfg.Options)
217+ mergeOptions(cfg, globalCfg, localConfig)
194− cfg.Options = mergeOptions(cfg.Options, localConfig.Options)
195
218
196 mergeProviderConfigs(cfg, globalCfg, localConfig)
219 mergeProviderConfigs(cfg, globalCfg, localConfig)
220+ // no providers found the app is not initialized yet
221+ if len(cfg.Providers) == 0 {
222+ return cfg, nil
223+ }
224+ preferredProvider := getPreferredProvider(cfg.Providers)
225+
226+ if preferredProvider == nil {
227+ return nil, errors.New("no valid providers configured")
228+ }
229+
230+ agents := map[AgentID]Agent{
231+ AgentCoder: {
232+ Name: "Coder",
233+ Description: "An agent that helps with executing coding tasks.",
234+ Provider: preferredProvider.ID,
235+ Model: preferredProvider.DefaultLargeModel,
236+ ContextPaths: cfg.Options.ContextPaths,
237+ // All tools allowed
238+ },
239+ AgentTask: {
240+ Name: "Task",
241+ Description: "An agent that helps with searching for context and finding implementation details.",
242+ Provider: preferredProvider.ID,
243+ Model: preferredProvider.DefaultLargeModel,
244+ ContextPaths: cfg.Options.ContextPaths,
245+ AllowedTools: []string{
246+ "glob",
247+ "grep",
248+ "ls",
249+ "sourcegraph",
250+ "view",
251+ },
252+ // NO MCPs or LSPs by default
253+ AllowedMCP: map[string][]string{},
254+ AllowedLSP: []string{},
255+ },
256+ AgentTitle: {
257+ Name: "Title",
258+ Description: "An agent that helps with generating titles for sessions.",
259+ Provider: preferredProvider.ID,
260+ Model: preferredProvider.DefaultSmallModel,
261+ ContextPaths: cfg.Options.ContextPaths,
262+ AllowedTools: []string{},
263+ // NO MCPs or LSPs by default
264+ AllowedMCP: map[string][]string{},
265+ AllowedLSP: []string{},
266+ },
267+ AgentSummarize: {
268+ Name: "Summarize",
269+ Description: "An agent that helps with summarizing sessions.",
270+ Provider: preferredProvider.ID,
271+ Model: preferredProvider.DefaultSmallModel,
272+ ContextPaths: cfg.Options.ContextPaths,
273+ AllowedTools: []string{},
274+ // NO MCPs or LSPs by default
275+ AllowedMCP: map[string][]string{},
276+ AllowedLSP: []string{},
277+ },
278+ }
279+ cfg.Agents = agents
280+ mergeAgents(cfg, globalCfg, localConfig)
281+ mergeMCPs(cfg, globalCfg, localConfig)
282+ mergeLSPs(cfg, globalCfg, localConfig)
283+
197 return cfg, nil
284 return cfg, nil
198}
285}
199
286
@@ -219,6 +306,22 @@ func GetConfig() *Config {
219 return instance
306 return instance
220}
307}
221
308
309+func getPreferredProvider(configuredProviders map[provider.InferenceProvider]ProviderConfig) *ProviderConfig {
310+ providers := Providers()
311+ for _, p := range providers {
312+ if providerConfig, ok := configuredProviders[p.ID]; ok && !providerConfig.Disabled {
313+ return &providerConfig
314+ }
315+ }
316+ // if none found return the first configured provider
317+ for _, providerConfig := range configuredProviders {
318+ if !providerConfig.Disabled {
319+ return &providerConfig
320+ }
321+ }
322+ return nil
323+}
324+
222func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
325func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
223 if other.APIKey != "" {
326 if other.APIKey != "" {
224 base.APIKey = other.APIKey
327 base.APIKey = other.APIKey
@@ -249,6 +352,26 @@ func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfi
249 base.Disabled = other.Disabled
352 base.Disabled = other.Disabled
250 }
353 }
251
354
355+ if other.DefaultLargeModel != "" {
356+ base.DefaultLargeModel = other.DefaultLargeModel
357+ }
358+ // Add new models if they don't exist
359+ if other.Models != nil {
360+ for _, model := range other.Models {
361+ // check if the model already exists
362+ exists := false
363+ for _, existingModel := range base.Models {
364+ if existingModel.ID == model.ID {
365+ exists = true
366+ break
367+ }
368+ }
369+ if !exists {
370+ base.Models = append(base.Models, model)
371+ }
372+ }
373+ }
374+
252 return base
375 return base
253}
376}
254
377
@@ -267,39 +390,114 @@ func validateProvider(p provider.InferenceProvider, providerConfig ProviderConfi
267 return nil
390 return nil
268}
391}
269
392
270−func mergeOptions(base, other Options) Options {
393+func mergeOptions(base, global, local *Config) {
271− result := base
394+ for _, cfg := range []*Config{global, local} {
395+ if cfg == nil {
396+ continue
397+ }
398+ baseOptions := base.Options
399+ other := cfg.Options
400+ if len(other.ContextPaths) > 0 {
401+ baseOptions.ContextPaths = append(baseOptions.ContextPaths, other.ContextPaths...)
402+ }
272
403
273− if len(other.ContextPaths) > 0 {
404+ if other.TUI.CompactMode {
274− base.ContextPaths = append(base.ContextPaths, other.ContextPaths...)
405+ baseOptions.TUI.CompactMode = other.TUI.CompactMode
275− }
406+ }
276
407
277− if other.TUI.CompactMode {
408+ if other.Debug {
278− result.TUI.CompactMode = other.TUI.CompactMode
409+ baseOptions.Debug = other.Debug
279− }
410+ }
280
411
281− if other.Debug {
412+ if other.DebugLSP {
282− result.Debug = other.Debug
413+ baseOptions.DebugLSP = other.DebugLSP
283− }
414+ }
284
415
285− if other.DebugLSP {
416+ if other.DisableAutoSummarize {
286− result.DebugLSP = other.DebugLSP
417+ baseOptions.DisableAutoSummarize = other.DisableAutoSummarize
418+ }
419+
420+ if other.DataDirectory != "" {
421+ baseOptions.DataDirectory = other.DataDirectory
422+ }
423+ base.Options = baseOptions
287 }
424 }
425+}
288
426
289− if other.DisableAutoSummarize {
427+func mergeAgents(base, global, local *Config) {
290− result.DisableAutoSummarize = other.DisableAutoSummarize
428+ for _, cfg := range []*Config{global, local} {
429+ if cfg == nil {
430+ continue
431+ }
432+ for agentID, globalAgent := range cfg.Agents {
433+ if _, ok := base.Agents[agentID]; !ok {
434+ base.Agents[agentID] = globalAgent
435+ } else {
436+ switch agentID {
437+ case AgentCoder:
438+ baseAgent := base.Agents[agentID]
439+ baseAgent.Model = globalAgent.Model
440+ baseAgent.Provider = globalAgent.Provider
441+ baseAgent.AllowedMCP = globalAgent.AllowedMCP
442+ baseAgent.AllowedLSP = globalAgent.AllowedLSP
443+ base.Agents[agentID] = baseAgent
444+ case AgentTask:
445+ baseAgent := base.Agents[agentID]
446+ baseAgent.Model = globalAgent.Model
447+ baseAgent.Provider = globalAgent.Provider
448+ base.Agents[agentID] = baseAgent
449+ case AgentTitle:
450+ baseAgent := base.Agents[agentID]
451+ baseAgent.Model = globalAgent.Model
452+ baseAgent.Provider = globalAgent.Provider
453+ base.Agents[agentID] = baseAgent
454+ case AgentSummarize:
455+ baseAgent := base.Agents[agentID]
456+ baseAgent.Model = globalAgent.Model
457+ baseAgent.Provider = globalAgent.Provider
458+ base.Agents[agentID] = baseAgent
459+ default:
460+ baseAgent := base.Agents[agentID]
461+ baseAgent.Name = globalAgent.Name
462+ baseAgent.Description = globalAgent.Description
463+ baseAgent.Disabled = globalAgent.Disabled
464+ baseAgent.Provider = globalAgent.Provider
465+ baseAgent.Model = globalAgent.Model
466+ baseAgent.AllowedTools = globalAgent.AllowedTools
467+ baseAgent.AllowedMCP = globalAgent.AllowedMCP
468+ baseAgent.AllowedLSP = globalAgent.AllowedLSP
469+ base.Agents[agentID] = baseAgent
470+
471+ }
472+ }
473+ }
291 }
474 }
475+}
292
476
293− if other.DataDirectory != "" {
477+func mergeMCPs(base, global, local *Config) {
294− result.DataDirectory = other.DataDirectory
478+ for _, cfg := range []*Config{global, local} {
479+ if cfg == nil {
480+ continue
481+ }
482+ maps.Copy(base.MCP, cfg.MCP)
295 }
483 }
484+}
296
485
297− return result
486+func mergeLSPs(base, global, local *Config) {
487+ for _, cfg := range []*Config{global, local} {
488+ if cfg == nil {
489+ continue
490+ }
491+ maps.Copy(base.LSP, cfg.LSP)
492+ }
298}
493}
299
494
300func mergeProviderConfigs(base, global, local *Config) {
495func mergeProviderConfigs(base, global, local *Config) {
496+ for _, cfg := range []*Config{global, local} {
301− if global != nil {
497+ if cfg == nil {
498+ continue
499+ }
302− for providerName, globalProvider := range global.Providers {
500+ for providerName, globalProvider := range cfg.Providers {
303 if _, ok := base.Providers[providerName]; !ok {
501 if _, ok := base.Providers[providerName]; !ok {
304 base.Providers[providerName] = globalProvider
502 base.Providers[providerName] = globalProvider
305 } else {
503 } else {
@@ -307,15 +505,6 @@ func mergeProviderConfigs(base, global, local *Config) {
307 }
505 }
308 }
506 }
309 }
507 }
310− if local != nil {
311− for providerName, localProvider := range local.Providers {
312− if _, ok := base.Providers[providerName]; !ok {
313− base.Providers[providerName] = localProvider
314− } else {
315− base.Providers[providerName] = mergeProviderConfig(providerName, base.Providers[providerName], localProvider)
316− }
317− }
318− }
319
508
320 finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
509 finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
321 for providerName, providerConfig := range base.Providers {
510 for providerName, providerConfig := range base.Providers {
@@ -328,30 +517,36 @@ func mergeProviderConfigs(base, global, local *Config) {
328 base.Providers = finalProviders
517 base.Providers = finalProviders
329}
518}
330
519
331−func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConfig {
520+func providerDefaultConfig(providerId provider.InferenceProvider) ProviderConfig {
332− switch providerName {
521+ switch providerId {
333 case provider.InferenceProviderAnthropic:
522 case provider.InferenceProviderAnthropic:
334 return ProviderConfig{
523 return ProviderConfig{
524+ ID: providerId,
335 ProviderType: provider.TypeAnthropic,
525 ProviderType: provider.TypeAnthropic,
336 }
526 }
337 case provider.InferenceProviderOpenAI:
527 case provider.InferenceProviderOpenAI:
338 return ProviderConfig{
528 return ProviderConfig{
529+ ID: providerId,
339 ProviderType: provider.TypeOpenAI,
530 ProviderType: provider.TypeOpenAI,
340 }
531 }
341 case provider.InferenceProviderGemini:
532 case provider.InferenceProviderGemini:
342 return ProviderConfig{
533 return ProviderConfig{
534+ ID: providerId,
343 ProviderType: provider.TypeGemini,
535 ProviderType: provider.TypeGemini,
344 }
536 }
345 case provider.InferenceProviderBedrock:
537 case provider.InferenceProviderBedrock:
346 return ProviderConfig{
538 return ProviderConfig{
539+ ID: providerId,
347 ProviderType: provider.TypeBedrock,
540 ProviderType: provider.TypeBedrock,
348 }
541 }
349 case provider.InferenceProviderAzure:
542 case provider.InferenceProviderAzure:
350 return ProviderConfig{
543 return ProviderConfig{
544+ ID: providerId,
351 ProviderType: provider.TypeAzure,
545 ProviderType: provider.TypeAzure,
352 }
546 }
353 case provider.InferenceProviderOpenRouter:
547 case provider.InferenceProviderOpenRouter:
354 return ProviderConfig{
548 return ProviderConfig{
549+ ID: providerId,
355 ProviderType: provider.TypeOpenAI,
550 ProviderType: provider.TypeOpenAI,
356 BaseURL: "https://openrouter.ai/api/v1",
551 BaseURL: "https://openrouter.ai/api/v1",
357 ExtraHeaders: map[string]string{
552 ExtraHeaders: map[string]string{
@@ -361,15 +556,18 @@ func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConf
361 }
556 }
362 case provider.InferenceProviderXAI:
557 case provider.InferenceProviderXAI:
363 return ProviderConfig{
558 return ProviderConfig{
559+ ID: providerId,
364 ProviderType: provider.TypeXAI,
560 ProviderType: provider.TypeXAI,
365 BaseURL: "https://api.x.ai/v1",
561 BaseURL: "https://api.x.ai/v1",
366 }
562 }
367 case provider.InferenceProviderVertexAI:
563 case provider.InferenceProviderVertexAI:
368 return ProviderConfig{
564 return ProviderConfig{
565+ ID: providerId,
369 ProviderType: provider.TypeVertexAI,
566 ProviderType: provider.TypeVertexAI,
370 }
567 }
371 default:
568 default:
372 return ProviderConfig{
569 return ProviderConfig{
570+ ID: providerId,
373 ProviderType: provider.TypeOpenAI,
571 ProviderType: provider.TypeOpenAI,
374 }
572 }
375 }
573 }
@@ -379,6 +577,7 @@ func defaultConfigBasedOnEnv() *Config {
379 cfg := &Config{
577 cfg := &Config{
380 Options: Options{
578 Options: Options{
381 DataDirectory: defaultDataDirectory,
579 DataDirectory: defaultDataDirectory,
580+ ContextPaths: defaultContextPaths,
382 },
581 },
383 Providers: make(map[provider.InferenceProvider]ProviderConfig),
582 Providers: make(map[provider.InferenceProvider]ProviderConfig),
384 }
583 }
@@ -391,7 +590,22 @@ func defaultConfigBasedOnEnv() *Config {
391 if apiKey := os.Getenv(envVar); apiKey != "" {
590 if apiKey := os.Getenv(envVar); apiKey != "" {
392 providerConfig := providerDefaultConfig(p.ID)
591 providerConfig := providerDefaultConfig(p.ID)
393 providerConfig.APIKey = apiKey
592 providerConfig.APIKey = apiKey
394− providerConfig.DefaultModel = p.DefaultModelID
593+ providerConfig.DefaultLargeModel = p.DefaultLargeModelID
594+ providerConfig.DefaultSmallModel = p.DefaultSmallModelID
595+ for _, model := range p.Models {
596+ providerConfig.Models = append(providerConfig.Models, Model{
597+ ID: model.ID,
598+ Name: model.Name,
599+ CostPer1MIn: model.CostPer1MIn,
600+ CostPer1MOut: model.CostPer1MOut,
601+ CostPer1MInCached: model.CostPer1MInCached,
602+ CostPer1MOutCached: model.CostPer1MOutCached,
603+ ContextWindow: model.ContextWindow,
604+ DefaultMaxTokens: model.DefaultMaxTokens,
605+ CanReason: model.CanReason,
606+ SupportsImages: model.SupportsImages,
607+ })
608+ }
395 cfg.Providers[p.ID] = providerConfig
609 cfg.Providers[p.ID] = providerConfig
396 }
610 }
397 }
611 }
internal/config_v2/config_test.go+3−1
@@ -1,6 +1,7 @@
1package configv2
1package configv2
2
2
3import (
3import (
4+ "encoding/json"
4 "fmt"
5 "fmt"
5 "os"
6 "os"
6 "testing"
7 "testing"
@@ -28,6 +29,7 @@ func TestConfigWithEnv(t *testing.T) {
28 os.Setenv("XAI_API_KEY", "test-xai-key")
29 os.Setenv("XAI_API_KEY", "test-xai-key")
29 os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
30 os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
30 cfg := InitConfig(cwdDir)
31 cfg := InitConfig(cwdDir)
32+ data, _ := json.MarshalIndent(cfg, "", " ")
31− fmt.Println(cfg)
33+ fmt.Println(string(data))
32 assert.Len(t, cfg.Providers, 5)
34 assert.Len(t, cfg.Providers, 5)
33}
35}
internal/config_v2/provider.go+2−2
@@ -6,8 +6,8 @@ import (
6 "path/filepath"
6 "path/filepath"
7 "sync"
7 "sync"
8
8
9− "github.com/charmbracelet/fur/pkg/client"
9+ "github.com/charmbracelet/crush/internal/fur/client"
10− "github.com/charmbracelet/fur/pkg/provider"
10+ "github.com/charmbracelet/crush/internal/fur/provider"
11)
11)
12
12
13var fur = client.New()
13var fur = client.New()
internal/fur/client/client.gonew file+63
@@ -0,0 +1,63 @@
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 = "http://localhost:8080"
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.gonew file+72
@@ -0,0 +1,72 @@
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+ TypeOpenRouter Type = "openrouter"
17+)
18+
19+// InferenceProvider represents the inference provider identifier.
20+type InferenceProvider string
21+
22+// All the inference providers supported by the system.
23+const (
24+ InferenceProviderOpenAI InferenceProvider = "openai"
25+ InferenceProviderAnthropic InferenceProvider = "anthropic"
26+ InferenceProviderGemini InferenceProvider = "gemini"
27+ InferenceProviderAzure InferenceProvider = "azure"
28+ InferenceProviderBedrock InferenceProvider = "bedrock"
29+ InferenceProviderVertexAI InferenceProvider = "vertexai"
30+ InferenceProviderXAI InferenceProvider = "xai"
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+ Name 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+ SupportsImages bool `json:"supports_attachments"`
58+}
59+
60+// KnownProviders returns all the known inference providers.
61+func KnownProviders() []InferenceProvider {
62+ return []InferenceProvider{
63+ InferenceProviderOpenAI,
64+ InferenceProviderAnthropic,
65+ InferenceProviderGemini,
66+ InferenceProviderAzure,
67+ InferenceProviderBedrock,
68+ InferenceProviderVertexAI,
69+ InferenceProviderXAI,
70+ InferenceProviderOpenRouter,
71+ }
72+}
internal/llm/agent/agent.go+9−15
@@ -734,21 +734,15 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error)
734 provider.WithSystemMessage(prompt.GetAgentPrompt(agentName, model.Provider)),
734 provider.WithSystemMessage(prompt.GetAgentPrompt(agentName, model.Provider)),
735 provider.WithMaxTokens(maxTokens),
735 provider.WithMaxTokens(maxTokens),
736 }
736 }
737+ // TODO: reimplement
737− if (model.Provider == models.ProviderOpenAI || model.Provider == models.ProviderLocal) && model.CanReason {
738+ // if model.Provider == models.ProviderOpenAI || model.Provider == models.ProviderLocal && model.CanReason {
738− opts = append(
739+ // opts = append(
739− opts,
740+ // opts,
740− provider.WithOpenAIOptions(
741+ // provider.WithOpenAIOptions(
741− provider.WithReasoningEffort(agentConfig.ReasoningEffort),
742+ // provider.WithReasoningEffort(agentConfig.ReasoningEffort),
742− ),
743− )
744− } else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder {
745− opts = append(
746− opts,
747− provider.WithAnthropicOptions(
748− provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn),
749− ),
743+ // ),
750− )
744+ // )
751− }
745+ // }
752 agentProvider, err := provider.NewProvider(
746 agentProvider, err := provider.NewProvider(
753 model.Provider,
747 model.Provider,
754 opts...,
748 opts...,
internal/llm/provider/anthropic.go+21−52
@@ -19,40 +19,25 @@ import (
19 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/message"
20)
20)
21
21
22−type anthropicOptions struct {
23− useBedrock bool
24− disableCache bool
25− shouldThink func(userMessage string) bool
26−}
27−
28−type AnthropicOption func(*anthropicOptions)
29−
30type anthropicClient struct {
22type anthropicClient struct {
31 providerOptions providerClientOptions
23 providerOptions providerClientOptions
32− options anthropicOptions
33 client anthropic.Client
24 client anthropic.Client
34}
25}
35
26
36type AnthropicClient ProviderClient
27type AnthropicClient ProviderClient
37
28
38−func newAnthropicClient(opts providerClientOptions) AnthropicClient {
29+func newAnthropicClient(opts providerClientOptions, useBedrock bool) AnthropicClient {
39− anthropicOpts := anthropicOptions{}
40− for _, o := range opts.anthropicOptions {
41− o(&anthropicOpts)
42− }
43−
44 anthropicClientOptions := []option.RequestOption{}
30 anthropicClientOptions := []option.RequestOption{}
45 if opts.apiKey != "" {
31 if opts.apiKey != "" {
46 anthropicClientOptions = append(anthropicClientOptions, option.WithAPIKey(opts.apiKey))
32 anthropicClientOptions = append(anthropicClientOptions, option.WithAPIKey(opts.apiKey))
47 }
33 }
48− if anthropicOpts.useBedrock {
34+ if useBedrock {
49 anthropicClientOptions = append(anthropicClientOptions, bedrock.WithLoadDefaultConfig(context.Background()))
35 anthropicClientOptions = append(anthropicClientOptions, bedrock.WithLoadDefaultConfig(context.Background()))
50 }
36 }
51
37
52 client := anthropic.NewClient(anthropicClientOptions...)
38 client := anthropic.NewClient(anthropicClientOptions...)
53 return &anthropicClient{
39 return &anthropicClient{
54 providerOptions: opts,
40 providerOptions: opts,
55− options: anthropicOpts,
56 client: client,
41 client: client,
57 }
42 }
58}
43}
@@ -66,7 +51,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
66 switch msg.Role {
51 switch msg.Role {
67 case message.User:
52 case message.User:
68 content := anthropic.NewTextBlock(msg.Content().String())
53 content := anthropic.NewTextBlock(msg.Content().String())
69− if cache && !a.options.disableCache {
54+ if cache && !a.providerOptions.disableCache {
70 content.OfText.CacheControl = anthropic.CacheControlEphemeralParam{
55 content.OfText.CacheControl = anthropic.CacheControlEphemeralParam{
71 Type: "ephemeral",
56 Type: "ephemeral",
72 }
57 }
@@ -84,7 +69,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
84 blocks := []anthropic.ContentBlockParamUnion{}
69 blocks := []anthropic.ContentBlockParamUnion{}
85 if msg.Content().String() != "" {
70 if msg.Content().String() != "" {
86 content := anthropic.NewTextBlock(msg.Content().String())
71 content := anthropic.NewTextBlock(msg.Content().String())
87− if cache && !a.options.disableCache {
72+ if cache && !a.providerOptions.disableCache {
88 content.OfText.CacheControl = anthropic.CacheControlEphemeralParam{
73 content.OfText.CacheControl = anthropic.CacheControlEphemeralParam{
89 Type: "ephemeral",
74 Type: "ephemeral",
90 }
75 }
@@ -132,7 +117,7 @@ func (a *anthropicClient) convertTools(tools []tools.BaseTool) []anthropic.ToolU
132 },
117 },
133 }
118 }
134
119
135− if i == len(tools)-1 && !a.options.disableCache {
120+ if i == len(tools)-1 && !a.providerOptions.disableCache {
136 toolParam.CacheControl = anthropic.CacheControlEphemeralParam{
121 toolParam.CacheControl = anthropic.CacheControlEphemeralParam{
137 Type: "ephemeral",
122 Type: "ephemeral",
138 }
123 }
@@ -161,21 +146,22 @@ func (a *anthropicClient) finishReason(reason string) message.FinishReason {
161
146
162func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, tools []anthropic.ToolUnionParam) anthropic.MessageNewParams {
147func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, tools []anthropic.ToolUnionParam) anthropic.MessageNewParams {
163 var thinkingParam anthropic.ThinkingConfigParamUnion
148 var thinkingParam anthropic.ThinkingConfigParamUnion
149+ // TODO: Implement a proper thinking function
164− lastMessage := messages[len(messages)-1]
150+ // lastMessage := messages[len(messages)-1]
165− isUser := lastMessage.Role == anthropic.MessageParamRoleUser
151+ // isUser := lastMessage.Role == anthropic.MessageParamRoleUser
166− messageContent := ""
152+ // messageContent := ""
167 temperature := anthropic.Float(0)
153 temperature := anthropic.Float(0)
168− if isUser {
154+ // if isUser {
169− for _, m := range lastMessage.Content {
155+ // for _, m := range lastMessage.Content {
170− if m.OfText != nil && m.OfText.Text != "" {
156+ // if m.OfText != nil && m.OfText.Text != "" {
171− messageContent = m.OfText.Text
157+ // messageContent = m.OfText.Text
172− }
158+ // }
173− }
159+ // }
174− if messageContent != "" && a.options.shouldThink != nil && a.options.shouldThink(messageContent) {
160+ // if messageContent != "" && a.shouldThink != nil && a.options.shouldThink(messageContent) {
175− thinkingParam = anthropic.ThinkingConfigParamOfEnabled(int64(float64(a.providerOptions.maxTokens) * 0.8))
161+ // thinkingParam = anthropic.ThinkingConfigParamOfEnabled(int64(float64(a.providerOptions.maxTokens) * 0.8))
176− temperature = anthropic.Float(1)
162+ // temperature = anthropic.Float(1)
177− }
163+ // }
178− }
164+ // }
179
165
180 return anthropic.MessageNewParams{
166 return anthropic.MessageNewParams{
181 Model: anthropic.Model(a.providerOptions.model.APIModel),
167 Model: anthropic.Model(a.providerOptions.model.APIModel),
@@ -439,24 +425,7 @@ func (a *anthropicClient) usage(msg anthropic.Message) TokenUsage {
439 }
425 }
440}
426}
441
427
442−func WithAnthropicBedrock(useBedrock bool) AnthropicOption {
428+// TODO: check if we need
443− return func(options *anthropicOptions) {
444− options.useBedrock = useBedrock
445− }
446−}
447−
448−func WithAnthropicDisableCache() AnthropicOption {
449− return func(options *anthropicOptions) {
450− options.disableCache = true
451− }
452−}
453−
454func DefaultShouldThinkFn(s string) bool {
429func DefaultShouldThinkFn(s string) bool {
455 return strings.Contains(strings.ToLower(s), "think")
430 return strings.Contains(strings.ToLower(s), "think")
456}
431}
457−
458−func WithAnthropicShouldThinkFn(fn func(string) bool) AnthropicOption {
459− return func(options *anthropicOptions) {
460− options.shouldThink = fn
461− }
462−}
internal/llm/provider/bedrock.go+3−16
@@ -11,22 +11,14 @@ import (
11 "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/message"
12)
12)
13
13
14−type bedrockOptions struct {
15− // Bedrock specific options can be added here
16−}
17−
18−type BedrockOption func(*bedrockOptions)
19−
20type bedrockClient struct {
14type bedrockClient struct {
21 providerOptions providerClientOptions
15 providerOptions providerClientOptions
22− options bedrockOptions
23 childProvider ProviderClient
16 childProvider ProviderClient
24}
17}
25
18
26type BedrockClient ProviderClient
19type BedrockClient ProviderClient
27
20
28func newBedrockClient(opts providerClientOptions) BedrockClient {
21func newBedrockClient(opts providerClientOptions) BedrockClient {
29− bedrockOpts := bedrockOptions{}
30 // Apply bedrock specific options if they are added in the future
22 // Apply bedrock specific options if they are added in the future
31
23
32 // Get AWS region from environment
24 // Get AWS region from environment
@@ -41,7 +33,6 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
41 if len(region) < 2 {
33 if len(region) < 2 {
42 return &bedrockClient{
34 return &bedrockClient{
43 providerOptions: opts,
35 providerOptions: opts,
44− options: bedrockOpts,
45 childProvider: nil, // Will cause an error when used
36 childProvider: nil, // Will cause an error when used
46 }
37 }
47 }
38 }
@@ -55,14 +46,11 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
55 if strings.Contains(string(opts.model.APIModel), "anthropic") {
46 if strings.Contains(string(opts.model.APIModel), "anthropic") {
56 // Create Anthropic client with Bedrock configuration
47 // Create Anthropic client with Bedrock configuration
57 anthropicOpts := opts
48 anthropicOpts := opts
58− anthropicOpts.anthropicOptions = append(anthropicOpts.anthropicOptions,
49+ // TODO: later find a way to check if the AWS account has caching enabled
59− WithAnthropicBedrock(true),
50+ opts.disableCache = true // Disable cache for Bedrock
60− WithAnthropicDisableCache(),
61− )
62 return &bedrockClient{
51 return &bedrockClient{
63 providerOptions: opts,
52 providerOptions: opts,
64− options: bedrockOpts,
65− childProvider: newAnthropicClient(anthropicOpts),
53+ childProvider: newAnthropicClient(anthropicOpts, true),
66 }
54 }
67 }
55 }
68
56
@@ -70,7 +58,6 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
70 // This will cause an error when used
58 // This will cause an error when used
71 return &bedrockClient{
59 return &bedrockClient{
72 providerOptions: opts,
60 providerOptions: opts,
73− options: bedrockOpts,
74 childProvider: nil,
61 childProvider: nil,
75 }
62 }
76}
63}
internal/llm/provider/gemini.go−19
@@ -17,26 +17,14 @@ import (
17 "google.golang.org/genai"
17 "google.golang.org/genai"
18)
18)
19
19
20−type geminiOptions struct {
21− disableCache bool
22−}
23−
24−type GeminiOption func(*geminiOptions)
25−
26type geminiClient struct {
20type geminiClient struct {
27 providerOptions providerClientOptions
21 providerOptions providerClientOptions
28− options geminiOptions
29 client *genai.Client
22 client *genai.Client
30}
23}
31
24
32type GeminiClient ProviderClient
25type GeminiClient ProviderClient
33
26
34func newGeminiClient(opts providerClientOptions) GeminiClient {
27func newGeminiClient(opts providerClientOptions) GeminiClient {
35− geminiOpts := geminiOptions{}
36− for _, o := range opts.geminiOptions {
37− o(&geminiOpts)
38− }
39−
40 client, err := genai.NewClient(context.Background(), &genai.ClientConfig{APIKey: opts.apiKey, Backend: genai.BackendGeminiAPI})
28 client, err := genai.NewClient(context.Background(), &genai.ClientConfig{APIKey: opts.apiKey, Backend: genai.BackendGeminiAPI})
41 if err != nil {
29 if err != nil {
42 logging.Error("Failed to create Gemini client", "error", err)
30 logging.Error("Failed to create Gemini client", "error", err)
@@ -45,7 +33,6 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
45
33
46 return &geminiClient{
34 return &geminiClient{
47 providerOptions: opts,
35 providerOptions: opts,
48− options: geminiOpts,
49 client: client,
36 client: client,
50 }
37 }
51}
38}
@@ -452,12 +439,6 @@ func (g *geminiClient) usage(resp *genai.GenerateContentResponse) TokenUsage {
452 }
439 }
453}
440}
454
441
455−func WithGeminiDisableCache() GeminiOption {
456− return func(options *geminiOptions) {
457− options.disableCache = true
458− }
459−}
460−
461// Helper functions
442// Helper functions
462func parseJsonToMap(jsonStr string) (map[string]any, error) {
443func parseJsonToMap(jsonStr string) (map[string]any, error) {
463 var result map[string]any
444 var result map[string]any
internal/llm/provider/openai.go+4−43
@@ -19,14 +19,9 @@ import (
19)
19)
20
20
21type openaiOptions struct {
21type openaiOptions struct {
22− baseURL string
23− disableCache bool
24 reasoningEffort string
22 reasoningEffort string
25− extraHeaders map[string]string
26}
23}
27
24
28−type OpenAIOption func(*openaiOptions)
29−
30type openaiClient struct {
25type openaiClient struct {
31 providerOptions providerClientOptions
26 providerOptions providerClientOptions
32 options openaiOptions
27 options openaiOptions
@@ -39,20 +34,17 @@ func newOpenAIClient(opts providerClientOptions) OpenAIClient {
39 openaiOpts := openaiOptions{
34 openaiOpts := openaiOptions{
40 reasoningEffort: "medium",
35 reasoningEffort: "medium",
41 }
36 }
42− for _, o := range opts.openaiOptions {
43− o(&openaiOpts)
44− }
45
37
46 openaiClientOptions := []option.RequestOption{}
38 openaiClientOptions := []option.RequestOption{}
47 if opts.apiKey != "" {
39 if opts.apiKey != "" {
48 openaiClientOptions = append(openaiClientOptions, option.WithAPIKey(opts.apiKey))
40 openaiClientOptions = append(openaiClientOptions, option.WithAPIKey(opts.apiKey))
49 }
41 }
50− if openaiOpts.baseURL != "" {
42+ if opts.baseURL != "" {
51− openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(openaiOpts.baseURL))
43+ openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(opts.baseURL))
52 }
44 }
53
45
54− if openaiOpts.extraHeaders != nil {
46+ if opts.extraHeaders != nil {
55− for key, value := range openaiOpts.extraHeaders {
47+ for key, value := range opts.extraHeaders {
56 openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
48 openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
57 }
49 }
58 }
50 }
@@ -392,34 +384,3 @@ func (o *openaiClient) usage(completion openai.ChatCompletion) TokenUsage {
392 CacheReadTokens: cachedTokens,
384 CacheReadTokens: cachedTokens,
393 }
385 }
394}
386}
395−
396−func WithOpenAIBaseURL(baseURL string) OpenAIOption {
397− return func(options *openaiOptions) {
398− options.baseURL = baseURL
399− }
400−}
401−
402−func WithOpenAIExtraHeaders(headers map[string]string) OpenAIOption {
403− return func(options *openaiOptions) {
404− options.extraHeaders = headers
405− }
406−}
407−
408−func WithOpenAIDisableCache() OpenAIOption {
409− return func(options *openaiOptions) {
410− options.disableCache = true
411− }
412−}
413−
414−func WithReasoningEffort(effort string) OpenAIOption {
415− return func(options *openaiOptions) {
416− defaultReasoningEffort := "medium"
417− switch effort {
418− case "low", "medium", "high":
419− defaultReasoningEffort = effort
420− default:
421− logging.Warn("Invalid reasoning effort, using default: medium")
422− }
423− options.reasoningEffort = defaultReasoningEffort
424− }
425−}
internal/llm/provider/provider.go+30−42
@@ -3,6 +3,7 @@ package provider
3import (
3import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6+ "maps"
6 "os"
7 "os"
7
8
8 "github.com/charmbracelet/crush/internal/llm/models"
9 "github.com/charmbracelet/crush/internal/llm/models"
@@ -59,15 +60,13 @@ type Provider interface {
59}
60}
60
61
61type providerClientOptions struct {
62type providerClientOptions struct {
63+ baseURL string
62 apiKey string
64 apiKey string
63 model models.Model
65 model models.Model
66+ disableCache bool
64 maxTokens int64
67 maxTokens int64
65 systemMessage string
68 systemMessage string
66−
69+ extraHeaders map[string]string
67− anthropicOptions []AnthropicOption
68− openaiOptions []OpenAIOption
69− geminiOptions []GeminiOption
70− bedrockOptions []BedrockOption
71}
70}
72
71
73type ProviderClientOption func(*providerClientOptions)
72type ProviderClientOption func(*providerClientOptions)
@@ -91,7 +90,7 @@ func NewProvider(providerName models.InferenceProvider, opts ...ProviderClientOp
91 case models.ProviderAnthropic:
90 case models.ProviderAnthropic:
92 return &baseProvider[AnthropicClient]{
91 return &baseProvider[AnthropicClient]{
93 options: clientOptions,
92 options: clientOptions,
94− client: newAnthropicClient(clientOptions),
93+ client: newAnthropicClient(clientOptions, false),
95 }, nil
94 }, nil
96 case models.ProviderOpenAI:
95 case models.ProviderOpenAI:
97 return &baseProvider[OpenAIClient]{
96 return &baseProvider[OpenAIClient]{
@@ -109,9 +108,7 @@ func NewProvider(providerName models.InferenceProvider, opts ...ProviderClientOp
109 client: newBedrockClient(clientOptions),
108 client: newBedrockClient(clientOptions),
110 }, nil
109 }, nil
111 case models.ProviderGROQ:
110 case models.ProviderGROQ:
112− clientOptions.openaiOptions = append(clientOptions.openaiOptions,
113− WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
111+ clientOptions.baseURL = "https://api.groq.com/openai/v1"
114− )
115 return &baseProvider[OpenAIClient]{
112 return &baseProvider[OpenAIClient]{
116 options: clientOptions,
113 options: clientOptions,
117 client: newOpenAIClient(clientOptions),
114 client: newOpenAIClient(clientOptions),
@@ -127,29 +124,23 @@ func NewProvider(providerName models.InferenceProvider, opts ...ProviderClientOp
127 client: newVertexAIClient(clientOptions),
124 client: newVertexAIClient(clientOptions),
128 }, nil
125 }, nil
129 case models.ProviderOpenRouter:
126 case models.ProviderOpenRouter:
130− clientOptions.openaiOptions = append(clientOptions.openaiOptions,
131− WithOpenAIBaseURL("https://openrouter.ai/api/v1"),
127+ clientOptions.baseURL = "https://openrouter.ai/api/v1"
132− WithOpenAIExtraHeaders(map[string]string{
128+ clientOptions.extraHeaders = map[string]string{
133− "HTTP-Referer": "crush.charm.land",
129+ "HTTP-Referer": "crush.charm.land",
134− "X-Title": "Crush",
130+ "X-Title": "Crush",
135− }),
131+ }
136− )
137 return &baseProvider[OpenAIClient]{
132 return &baseProvider[OpenAIClient]{
138 options: clientOptions,
133 options: clientOptions,
139 client: newOpenAIClient(clientOptions),
134 client: newOpenAIClient(clientOptions),
140 }, nil
135 }, nil
141 case models.ProviderXAI:
136 case models.ProviderXAI:
142− clientOptions.openaiOptions = append(clientOptions.openaiOptions,
143− WithOpenAIBaseURL("https://api.x.ai/v1"),
137+ clientOptions.baseURL = "https://api.x.ai/v1"
144− )
145 return &baseProvider[OpenAIClient]{
138 return &baseProvider[OpenAIClient]{
146 options: clientOptions,
139 options: clientOptions,
147 client: newOpenAIClient(clientOptions),
140 client: newOpenAIClient(clientOptions),
148 }, nil
141 }, nil
149 case models.ProviderLocal:
142 case models.ProviderLocal:
150− clientOptions.openaiOptions = append(clientOptions.openaiOptions,
151− WithOpenAIBaseURL(os.Getenv("LOCAL_ENDPOINT")),
143+ clientOptions.baseURL = os.Getenv("LOCAL_ENDPOINT")
152− )
153 return &baseProvider[OpenAIClient]{
144 return &baseProvider[OpenAIClient]{
154 options: clientOptions,
145 options: clientOptions,
155 client: newOpenAIClient(clientOptions),
146 client: newOpenAIClient(clientOptions),
@@ -186,50 +177,47 @@ func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message
186 return p.client.stream(ctx, messages, tools)
177 return p.client.stream(ctx, messages, tools)
187}
178}
188
179
189−func WithAPIKey(apiKey string) ProviderClientOption {
180+func WithBaseURL(baseURL string) ProviderClientOption {
190− return func(options *providerClientOptions) {
191− options.apiKey = apiKey
192− }
193−}
194−
195−func WithModel(model models.Model) ProviderClientOption {
196 return func(options *providerClientOptions) {
181 return func(options *providerClientOptions) {
197− options.model = model
182+ options.baseURL = baseURL
198 }
183 }
199}
184}
200
185
201−func WithMaxTokens(maxTokens int64) ProviderClientOption {
186+func WithAPIKey(apiKey string) ProviderClientOption {
202 return func(options *providerClientOptions) {
187 return func(options *providerClientOptions) {
203− options.maxTokens = maxTokens
188+ options.apiKey = apiKey
204 }
189 }
205}
190}
206
191
207−func WithSystemMessage(systemMessage string) ProviderClientOption {
192+func WithModel(model models.Model) ProviderClientOption {
208 return func(options *providerClientOptions) {
193 return func(options *providerClientOptions) {
209− options.systemMessage = systemMessage
194+ options.model = model
210 }
195 }
211}
196}
212
197
213−func WithAnthropicOptions(anthropicOptions ...AnthropicOption) ProviderClientOption {
198+func WithDisableCache(disableCache bool) ProviderClientOption {
214 return func(options *providerClientOptions) {
199 return func(options *providerClientOptions) {
215− options.anthropicOptions = anthropicOptions
200+ options.disableCache = disableCache
216 }
201 }
217}
202}
218
203
219−func WithOpenAIOptions(openaiOptions ...OpenAIOption) ProviderClientOption {
204+func WithExtraHeaders(extraHeaders map[string]string) ProviderClientOption {
220 return func(options *providerClientOptions) {
205 return func(options *providerClientOptions) {
221− options.openaiOptions = openaiOptions
206+ if options.extraHeaders == nil {
207+ options.extraHeaders = make(map[string]string)
208+ }
209+ maps.Copy(options.extraHeaders, extraHeaders)
222 }
210 }
223}
211}
224
212
225−func WithGeminiOptions(geminiOptions ...GeminiOption) ProviderClientOption {
213+func WithMaxTokens(maxTokens int64) ProviderClientOption {
226 return func(options *providerClientOptions) {
214 return func(options *providerClientOptions) {
227− options.geminiOptions = geminiOptions
215+ options.maxTokens = maxTokens
228 }
216 }
229}
217}
230
218
231−func WithBedrockOptions(bedrockOptions ...BedrockOption) ProviderClientOption {
219+func WithSystemMessage(systemMessage string) ProviderClientOption {
232 return func(options *providerClientOptions) {
220 return func(options *providerClientOptions) {
233− options.bedrockOptions = bedrockOptions
221+ options.systemMessage = systemMessage
234 }
222 }
235}
223}
internal/llm/provider/vertexai.go−6
@@ -11,11 +11,6 @@ import (
11type VertexAIClient ProviderClient
11type VertexAIClient ProviderClient
12
12
13func newVertexAIClient(opts providerClientOptions) VertexAIClient {
13func newVertexAIClient(opts providerClientOptions) VertexAIClient {
14− geminiOpts := geminiOptions{}
15− for _, o := range opts.geminiOptions {
16− o(&geminiOpts)
17− }
18−
19 client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
14 client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
20 Project: os.Getenv("GOOGLE_CLOUD_PROJECT"),
15 Project: os.Getenv("GOOGLE_CLOUD_PROJECT"),
21 Location: os.Getenv("GOOGLE_CLOUD_LOCATION"),
16 Location: os.Getenv("GOOGLE_CLOUD_LOCATION"),
@@ -28,7 +23,6 @@ func newVertexAIClient(opts providerClientOptions) VertexAIClient {
28
23
29 return &geminiClient{
24 return &geminiClient{
30 providerOptions: opts,
25 providerOptions: opts,
31− options: geminiOpts,
32 client: client,
26 client: client,
33 }
27 }
34}
28}