1package agent
2
3import (
4 "bytes"
5 "cmp"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "log/slog"
12 "maps"
13 "os"
14 "slices"
15 "strings"
16
17 "charm.land/fantasy"
18 "github.com/charmbracelet/catwalk/pkg/catwalk"
19 "github.com/charmbracelet/crush/internal/agent/prompt"
20 "github.com/charmbracelet/crush/internal/agent/tools"
21 "github.com/charmbracelet/crush/internal/config"
22 "github.com/charmbracelet/crush/internal/csync"
23 "github.com/charmbracelet/crush/internal/history"
24 "github.com/charmbracelet/crush/internal/log"
25 "github.com/charmbracelet/crush/internal/lsp"
26 "github.com/charmbracelet/crush/internal/message"
27 "github.com/charmbracelet/crush/internal/permission"
28 "github.com/charmbracelet/crush/internal/session"
29 "golang.org/x/sync/errgroup"
30
31 "charm.land/fantasy/providers/anthropic"
32 "charm.land/fantasy/providers/azure"
33 "charm.land/fantasy/providers/bedrock"
34 "charm.land/fantasy/providers/google"
35 "charm.land/fantasy/providers/openai"
36 "charm.land/fantasy/providers/openaicompat"
37 "charm.land/fantasy/providers/openrouter"
38 openaisdk "github.com/openai/openai-go/v2/option"
39 "github.com/qjebbs/go-jsons"
40)
41
42type Coordinator interface {
43 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
44 // SetMainAgent(string)
45 Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
46 Cancel(sessionID string)
47 CancelAll()
48 IsSessionBusy(sessionID string) bool
49 IsBusy() bool
50 QueuedPrompts(sessionID string) int
51 ClearQueue(sessionID string)
52 Summarize(context.Context, string) error
53 Model() Model
54 UpdateModels(ctx context.Context) error
55}
56
57type coordinator struct {
58 cfg *config.Config
59 sessions session.Service
60 messages message.Service
61 permissions permission.Service
62 history history.Service
63 lspClients *csync.Map[string, *lsp.Client]
64
65 currentAgent SessionAgent
66 agents map[string]SessionAgent
67
68 readyWg errgroup.Group
69}
70
71func NewCoordinator(
72 ctx context.Context,
73 cfg *config.Config,
74 sessions session.Service,
75 messages message.Service,
76 permissions permission.Service,
77 history history.Service,
78 lspClients *csync.Map[string, *lsp.Client],
79) (Coordinator, error) {
80 c := &coordinator{
81 cfg: cfg,
82 sessions: sessions,
83 messages: messages,
84 permissions: permissions,
85 history: history,
86 lspClients: lspClients,
87 agents: make(map[string]SessionAgent),
88 }
89
90 agentCfg, ok := cfg.Agents[config.AgentCoder]
91 if !ok {
92 return nil, errors.New("coder agent not configured")
93 }
94
95 // TODO: make this dynamic when we support multiple agents
96 prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
97 if err != nil {
98 return nil, err
99 }
100
101 agent, err := c.buildAgent(ctx, prompt, agentCfg)
102 if err != nil {
103 return nil, err
104 }
105 c.currentAgent = agent
106 c.agents[config.AgentCoder] = agent
107 return c, nil
108}
109
110// Run implements Coordinator.
111func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
112 if err := c.readyWg.Wait(); err != nil {
113 return nil, err
114 }
115
116 model := c.currentAgent.Model()
117 maxTokens := model.CatwalkCfg.DefaultMaxTokens
118 if model.ModelCfg.MaxTokens != 0 {
119 maxTokens = model.ModelCfg.MaxTokens
120 }
121
122 if !model.CatwalkCfg.SupportsImages && attachments != nil {
123 attachments = nil
124 }
125
126 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
127 if !ok {
128 return nil, errors.New("model provider not configured")
129 }
130
131 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
132
133 return c.currentAgent.Run(ctx, SessionAgentCall{
134 SessionID: sessionID,
135 Prompt: prompt,
136 Attachments: attachments,
137 MaxOutputTokens: maxTokens,
138 ProviderOptions: mergedOptions,
139 Temperature: temp,
140 TopP: topP,
141 TopK: topK,
142 FrequencyPenalty: freqPenalty,
143 PresencePenalty: presPenalty,
144 })
145}
146
147func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
148 options := fantasy.ProviderOptions{}
149
150 cfgOpts := []byte("{}")
151 providerCfgOpts := []byte("{}")
152 catwalkOpts := []byte("{}")
153
154 if model.ModelCfg.ProviderOptions != nil {
155 data, err := json.Marshal(model.ModelCfg.ProviderOptions)
156 if err == nil {
157 cfgOpts = data
158 }
159 }
160
161 if providerCfg.ProviderOptions != nil {
162 data, err := json.Marshal(providerCfg.ProviderOptions)
163 if err == nil {
164 providerCfgOpts = data
165 }
166 }
167
168 if model.CatwalkCfg.Options.ProviderOptions != nil {
169 data, err := json.Marshal(model.CatwalkCfg.Options.ProviderOptions)
170 if err == nil {
171 catwalkOpts = data
172 }
173 }
174
175 readers := []io.Reader{
176 bytes.NewReader(catwalkOpts),
177 bytes.NewReader(providerCfgOpts),
178 bytes.NewReader(cfgOpts),
179 }
180
181 got, err := jsons.Merge(readers)
182 if err != nil {
183 slog.Error("Could not merge call config", "err", err)
184 return options
185 }
186
187 mergedOptions := make(map[string]any)
188
189 err = json.Unmarshal([]byte(got), &mergedOptions)
190 if err != nil {
191 slog.Error("Could not create config for call", "err", err)
192 return options
193 }
194
195 switch providerCfg.Type {
196 case openai.Name:
197 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
198 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
199 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
200 }
201 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
202 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
203 mergedOptions["reasoning_summary"] = "auto"
204 mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
205 }
206 parsed, err := openai.ParseResponsesOptions(mergedOptions)
207 if err == nil {
208 options[openai.Name] = parsed
209 }
210 } else {
211 parsed, err := openai.ParseOptions(mergedOptions)
212 if err == nil {
213 options[openai.Name] = parsed
214 }
215 }
216 case anthropic.Name:
217 _, hasThink := mergedOptions["thinking"]
218 if !hasThink && model.ModelCfg.Think {
219 mergedOptions["thinking"] = map[string]any{
220 // TODO: kujtim see if we need to make this dynamic
221 "budget_tokens": 2000,
222 }
223 }
224 parsed, err := anthropic.ParseOptions(mergedOptions)
225 if err == nil {
226 options[anthropic.Name] = parsed
227 }
228
229 case openrouter.Name:
230 _, hasReasoning := mergedOptions["reasoning"]
231 if !hasReasoning && model.ModelCfg.ReasoningEffort != "" {
232 mergedOptions["reasoning"] = map[string]any{
233 "enabled": true,
234 "effort": model.ModelCfg.ReasoningEffort,
235 }
236 }
237 parsed, err := openrouter.ParseOptions(mergedOptions)
238 if err == nil {
239 options[openrouter.Name] = parsed
240 }
241 case google.Name:
242 _, hasReasoning := mergedOptions["thinking_config"]
243 if !hasReasoning {
244 mergedOptions["thinking_config"] = map[string]any{
245 "thinking_budget": 2000,
246 "include_thoughts": true,
247 }
248 }
249 parsed, err := google.ParseOptions(mergedOptions)
250 if err == nil {
251 options[google.Name] = parsed
252 }
253 case azure.Name:
254 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
255 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
256 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
257 }
258 // azure uses the same options as openaicompat
259 parsed, err := openaicompat.ParseOptions(mergedOptions)
260 if err == nil {
261 options[azure.Name] = parsed
262 }
263 case openaicompat.Name:
264 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
265 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
266 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
267 }
268 parsed, err := openaicompat.ParseOptions(mergedOptions)
269 if err == nil {
270 options[openaicompat.Name] = parsed
271 }
272 }
273
274 return options
275}
276
277func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
278 modelOptions := getProviderOptions(model, cfg)
279 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
280 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
281 topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
282 freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
283 presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
284 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
285}
286
287func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent) (SessionAgent, error) {
288 large, small, err := c.buildAgentModels(ctx)
289 if err != nil {
290 return nil, err
291 }
292
293 systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
294 if err != nil {
295 return nil, err
296 }
297
298 largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
299 result := NewSessionAgent(SessionAgentOptions{
300 large,
301 small,
302 largeProviderCfg.SystemPromptPrefix,
303 systemPrompt,
304 c.cfg.Options.DisableAutoSummarize,
305 c.permissions.SkipRequests(),
306 c.sessions,
307 c.messages,
308 nil,
309 })
310 c.readyWg.Go(func() error {
311 tools, err := c.buildTools(ctx, agent)
312 if err != nil {
313 return err
314 }
315 result.SetTools(tools)
316 return nil
317 })
318
319 return result, nil
320}
321
322func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
323 var allTools []fantasy.AgentTool
324 if slices.Contains(agent.AllowedTools, AgentToolName) {
325 agentTool, err := c.agentTool(ctx)
326 if err != nil {
327 return nil, err
328 }
329 allTools = append(allTools, agentTool)
330 }
331
332 if slices.Contains(agent.AllowedTools, tools.AgenticFetchToolName) {
333 agenticFetchTool, err := c.agenticFetchTool(ctx, nil)
334 if err != nil {
335 return nil, err
336 }
337 allTools = append(allTools, agenticFetchTool)
338 }
339
340 allTools = append(allTools,
341 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
342 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
343 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
344 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
345 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
346 tools.NewGlobTool(c.cfg.WorkingDir()),
347 tools.NewGrepTool(c.cfg.WorkingDir()),
348 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
349 tools.NewSourcegraphTool(nil),
350 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
351 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
352 )
353
354 if len(c.cfg.LSP) > 0 {
355 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
356 }
357
358 var filteredTools []fantasy.AgentTool
359 for _, tool := range allTools {
360 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
361 filteredTools = append(filteredTools, tool)
362 }
363 }
364
365 mcpTools := tools.GetMCPTools(context.Background(), c.permissions, c.cfg)
366
367 for _, mcpTool := range mcpTools {
368 if agent.AllowedMCP == nil {
369 // No MCP restrictions
370 filteredTools = append(filteredTools, mcpTool)
371 } else if len(agent.AllowedMCP) == 0 {
372 // no mcps allowed
373 break
374 }
375
376 for mcp, tools := range agent.AllowedMCP {
377 if mcp == mcpTool.MCP() {
378 if len(tools) == 0 {
379 filteredTools = append(filteredTools, mcpTool)
380 }
381 for _, t := range tools {
382 if t == mcpTool.MCPToolName() {
383 filteredTools = append(filteredTools, mcpTool)
384 }
385 }
386 break
387 }
388 }
389 }
390 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
391 return strings.Compare(a.Info().Name, b.Info().Name)
392 })
393 return filteredTools, nil
394}
395
396// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
397func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
398 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
399 if !ok {
400 return Model{}, Model{}, errors.New("large model not selected")
401 }
402 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
403 if !ok {
404 return Model{}, Model{}, errors.New("small model not selected")
405 }
406
407 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
408 if !ok {
409 return Model{}, Model{}, errors.New("large model provider not configured")
410 }
411
412 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
413 if err != nil {
414 return Model{}, Model{}, err
415 }
416
417 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
418 if !ok {
419 return Model{}, Model{}, errors.New("large model provider not configured")
420 }
421
422 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
423 if err != nil {
424 return Model{}, Model{}, err
425 }
426
427 var largeCatwalkModel *catwalk.Model
428 var smallCatwalkModel *catwalk.Model
429
430 for _, m := range largeProviderCfg.Models {
431 if m.ID == largeModelCfg.Model {
432 largeCatwalkModel = &m
433 }
434 }
435 for _, m := range smallProviderCfg.Models {
436 if m.ID == smallModelCfg.Model {
437 smallCatwalkModel = &m
438 }
439 }
440
441 if largeCatwalkModel == nil {
442 return Model{}, Model{}, errors.New("large model not found in provider config")
443 }
444
445 if smallCatwalkModel == nil {
446 return Model{}, Model{}, errors.New("snall model not found in provider config")
447 }
448
449 largeModelID := largeModelCfg.Model
450 smallModelID := smallModelCfg.Model
451
452 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
453 largeModelID += ":exacto"
454 }
455
456 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
457 smallModelID += ":exacto"
458 }
459
460 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
461 if err != nil {
462 return Model{}, Model{}, err
463 }
464 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
465 if err != nil {
466 return Model{}, Model{}, err
467 }
468
469 return Model{
470 Model: largeModel,
471 CatwalkCfg: *largeCatwalkModel,
472 ModelCfg: largeModelCfg,
473 }, Model{
474 Model: smallModel,
475 CatwalkCfg: *smallCatwalkModel,
476 ModelCfg: smallModelCfg,
477 }, nil
478}
479
480func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
481 hasBearerAuth := false
482 for key := range headers {
483 if strings.ToLower(key) == "authorization" {
484 hasBearerAuth = true
485 break
486 }
487 }
488
489 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
490
491 var opts []anthropic.Option
492 if apiKey != "" && !hasBearerAuth {
493 if isBearerToken {
494 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
495 headers["Authorization"] = apiKey
496 apiKey = "" // clear apiKey to avoid using X-Api-Key header
497 }
498 }
499
500 if apiKey != "" {
501 // Use standard X-Api-Key header
502 opts = append(opts, anthropic.WithAPIKey(apiKey))
503 }
504
505 if len(headers) > 0 {
506 opts = append(opts, anthropic.WithHeaders(headers))
507 }
508
509 if baseURL != "" {
510 opts = append(opts, anthropic.WithBaseURL(baseURL))
511 }
512
513 if c.cfg.Options.Debug {
514 httpClient := log.NewHTTPClient()
515 opts = append(opts, anthropic.WithHTTPClient(httpClient))
516 }
517
518 return anthropic.New(opts...)
519}
520
521func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
522 opts := []openai.Option{
523 openai.WithAPIKey(apiKey),
524 openai.WithUseResponsesAPI(),
525 }
526 if c.cfg.Options.Debug {
527 httpClient := log.NewHTTPClient()
528 opts = append(opts, openai.WithHTTPClient(httpClient))
529 }
530 if len(headers) > 0 {
531 opts = append(opts, openai.WithHeaders(headers))
532 }
533 if baseURL != "" {
534 opts = append(opts, openai.WithBaseURL(baseURL))
535 }
536 return openai.New(opts...)
537}
538
539func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
540 opts := []openrouter.Option{
541 openrouter.WithAPIKey(apiKey),
542 }
543 if c.cfg.Options.Debug {
544 httpClient := log.NewHTTPClient()
545 opts = append(opts, openrouter.WithHTTPClient(httpClient))
546 }
547 if len(headers) > 0 {
548 opts = append(opts, openrouter.WithHeaders(headers))
549 }
550 return openrouter.New(opts...)
551}
552
553func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
554 opts := []openaicompat.Option{
555 openaicompat.WithBaseURL(baseURL),
556 openaicompat.WithAPIKey(apiKey),
557 }
558 if c.cfg.Options.Debug {
559 httpClient := log.NewHTTPClient()
560 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
561 }
562 if len(headers) > 0 {
563 opts = append(opts, openaicompat.WithHeaders(headers))
564 }
565
566 for extraKey, extraValue := range extraBody {
567 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
568 }
569
570 return openaicompat.New(opts...)
571}
572
573func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
574 opts := []azure.Option{
575 azure.WithBaseURL(baseURL),
576 azure.WithAPIKey(apiKey),
577 }
578 if c.cfg.Options.Debug {
579 httpClient := log.NewHTTPClient()
580 opts = append(opts, azure.WithHTTPClient(httpClient))
581 }
582 if options == nil {
583 options = make(map[string]string)
584 }
585 if apiVersion, ok := options["apiVersion"]; ok {
586 opts = append(opts, azure.WithAPIVersion(apiVersion))
587 }
588 if len(headers) > 0 {
589 opts = append(opts, azure.WithHeaders(headers))
590 }
591
592 return azure.New(opts...)
593}
594
595func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
596 var opts []bedrock.Option
597 if c.cfg.Options.Debug {
598 httpClient := log.NewHTTPClient()
599 opts = append(opts, bedrock.WithHTTPClient(httpClient))
600 }
601 if len(headers) > 0 {
602 opts = append(opts, bedrock.WithHeaders(headers))
603 }
604 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
605 if bearerToken != "" {
606 opts = append(opts, bedrock.WithAPIKey(bearerToken))
607 }
608 return bedrock.New(opts...)
609}
610
611func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
612 opts := []google.Option{
613 google.WithBaseURL(baseURL),
614 google.WithGeminiAPIKey(apiKey),
615 }
616 if c.cfg.Options.Debug {
617 httpClient := log.NewHTTPClient()
618 opts = append(opts, google.WithHTTPClient(httpClient))
619 }
620 if len(headers) > 0 {
621 opts = append(opts, google.WithHeaders(headers))
622 }
623 return google.New(opts...)
624}
625
626func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
627 opts := []google.Option{}
628 if c.cfg.Options.Debug {
629 httpClient := log.NewHTTPClient()
630 opts = append(opts, google.WithHTTPClient(httpClient))
631 }
632 if len(headers) > 0 {
633 opts = append(opts, google.WithHeaders(headers))
634 }
635
636 project := options["project"]
637 location := options["location"]
638
639 opts = append(opts, google.WithVertex(project, location))
640
641 return google.New(opts...)
642}
643
644func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
645 if model.Think {
646 return true
647 }
648
649 if model.ProviderOptions == nil {
650 return false
651 }
652
653 opts, err := anthropic.ParseOptions(model.ProviderOptions)
654 if err != nil {
655 return false
656 }
657 if opts.Thinking != nil {
658 return true
659 }
660 return false
661}
662
663func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
664 headers := maps.Clone(providerCfg.ExtraHeaders)
665
666 // handle special headers for anthropic
667 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
668 if v, ok := headers["anthropic-beta"]; ok {
669 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
670 } else {
671 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
672 }
673 }
674
675 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
676 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
677
678 switch providerCfg.Type {
679 case openai.Name:
680 return c.buildOpenaiProvider(baseURL, apiKey, headers)
681 case anthropic.Name:
682 return c.buildAnthropicProvider(baseURL, apiKey, headers)
683 case openrouter.Name:
684 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
685 case azure.Name:
686 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
687 case bedrock.Name:
688 return c.buildBedrockProvider(headers)
689 case google.Name:
690 return c.buildGoogleProvider(baseURL, apiKey, headers)
691 case "google-vertex":
692 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
693 case openaicompat.Name:
694 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
695 default:
696 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
697 }
698}
699
700func isExactoSupported(modelID string) bool {
701 supportedModels := []string{
702 "moonshotai/kimi-k2-0905",
703 "deepseek/deepseek-v3.1-terminus",
704 "z-ai/glm-4.6",
705 "openai/gpt-oss-120b",
706 "qwen/qwen3-coder",
707 }
708 return slices.Contains(supportedModels, modelID)
709}
710
711func (c *coordinator) Cancel(sessionID string) {
712 c.currentAgent.Cancel(sessionID)
713}
714
715func (c *coordinator) CancelAll() {
716 c.currentAgent.CancelAll()
717}
718
719func (c *coordinator) ClearQueue(sessionID string) {
720 c.currentAgent.ClearQueue(sessionID)
721}
722
723func (c *coordinator) IsBusy() bool {
724 return c.currentAgent.IsBusy()
725}
726
727func (c *coordinator) IsSessionBusy(sessionID string) bool {
728 return c.currentAgent.IsSessionBusy(sessionID)
729}
730
731func (c *coordinator) Model() Model {
732 return c.currentAgent.Model()
733}
734
735func (c *coordinator) UpdateModels(ctx context.Context) error {
736 // build the models again so we make sure we get the latest config
737 large, small, err := c.buildAgentModels(ctx)
738 if err != nil {
739 return err
740 }
741 c.currentAgent.SetModels(large, small)
742
743 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
744 if !ok {
745 return errors.New("coder agent not configured")
746 }
747
748 tools, err := c.buildTools(ctx, agentCfg)
749 if err != nil {
750 return err
751 }
752 c.currentAgent.SetTools(tools)
753 return nil
754}
755
756func (c *coordinator) QueuedPrompts(sessionID string) int {
757 return c.currentAgent.QueuedPrompts(sessionID)
758}
759
760func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
761 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
762 if !ok {
763 return errors.New("model provider not configured")
764 }
765 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
766}