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, azure.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 openaicompat.Name:
254 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
255 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
256 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
257 }
258 parsed, err := openaicompat.ParseOptions(mergedOptions)
259 if err == nil {
260 options[openaicompat.Name] = parsed
261 }
262 }
263
264 return options
265}
266
267func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
268 modelOptions := getProviderOptions(model, cfg)
269 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
270 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
271 topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
272 freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
273 presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
274 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
275}
276
277func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent) (SessionAgent, error) {
278 large, small, err := c.buildAgentModels(ctx)
279 if err != nil {
280 return nil, err
281 }
282
283 systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
284 if err != nil {
285 return nil, err
286 }
287
288 largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
289 result := NewSessionAgent(SessionAgentOptions{
290 large,
291 small,
292 largeProviderCfg.SystemPromptPrefix,
293 systemPrompt,
294 c.cfg.Options.DisableAutoSummarize,
295 c.permissions.SkipRequests(),
296 c.sessions,
297 c.messages,
298 nil,
299 })
300 c.readyWg.Go(func() error {
301 tools, err := c.buildTools(ctx, agent)
302 if err != nil {
303 return err
304 }
305 result.SetTools(tools)
306 return nil
307 })
308
309 return result, nil
310}
311
312func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
313 var allTools []fantasy.AgentTool
314 if slices.Contains(agent.AllowedTools, AgentToolName) {
315 agentTool, err := c.agentTool(ctx)
316 if err != nil {
317 return nil, err
318 }
319 allTools = append(allTools, agentTool)
320 }
321
322 if slices.Contains(agent.AllowedTools, tools.AgenticFetchToolName) {
323 agenticFetchTool, err := c.agenticFetchTool(ctx, nil)
324 if err != nil {
325 return nil, err
326 }
327 allTools = append(allTools, agenticFetchTool)
328 }
329
330 allTools = append(allTools,
331 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
332 tools.NewJobOutputTool(),
333 tools.NewJobKillTool(),
334 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
335 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
336 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
337 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
338 tools.NewGlobTool(c.cfg.WorkingDir()),
339 tools.NewGrepTool(c.cfg.WorkingDir()),
340 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
341 tools.NewSourcegraphTool(nil),
342 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
343 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
344 )
345
346 if len(c.cfg.LSP) > 0 {
347 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
348 }
349
350 var filteredTools []fantasy.AgentTool
351 for _, tool := range allTools {
352 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
353 filteredTools = append(filteredTools, tool)
354 }
355 }
356
357 for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) {
358 if agent.AllowedMCP == nil {
359 // No MCP restrictions
360 filteredTools = append(filteredTools, tool)
361 continue
362 }
363 if len(agent.AllowedMCP) == 0 {
364 // No MCPs allowed
365 slog.Debug("no MCPs allowed", "tool", tool.Name(), "agent", agent.Name)
366 break
367 }
368
369 for mcp, tools := range agent.AllowedMCP {
370 if mcp != tool.MCP() {
371 continue
372 }
373 if len(tools) == 0 || slices.Contains(tools, tool.MCPToolName()) {
374 filteredTools = append(filteredTools, tool)
375 }
376 }
377 slog.Debug("MCP not allowed", "tool", tool.Name(), "agent", agent.Name)
378 }
379 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
380 return strings.Compare(a.Info().Name, b.Info().Name)
381 })
382 return filteredTools, nil
383}
384
385// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
386func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
387 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
388 if !ok {
389 return Model{}, Model{}, errors.New("large model not selected")
390 }
391 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
392 if !ok {
393 return Model{}, Model{}, errors.New("small model not selected")
394 }
395
396 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
397 if !ok {
398 return Model{}, Model{}, errors.New("large model provider not configured")
399 }
400
401 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
402 if err != nil {
403 return Model{}, Model{}, err
404 }
405
406 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
407 if !ok {
408 return Model{}, Model{}, errors.New("large model provider not configured")
409 }
410
411 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
412 if err != nil {
413 return Model{}, Model{}, err
414 }
415
416 var largeCatwalkModel *catwalk.Model
417 var smallCatwalkModel *catwalk.Model
418
419 for _, m := range largeProviderCfg.Models {
420 if m.ID == largeModelCfg.Model {
421 largeCatwalkModel = &m
422 }
423 }
424 for _, m := range smallProviderCfg.Models {
425 if m.ID == smallModelCfg.Model {
426 smallCatwalkModel = &m
427 }
428 }
429
430 if largeCatwalkModel == nil {
431 return Model{}, Model{}, errors.New("large model not found in provider config")
432 }
433
434 if smallCatwalkModel == nil {
435 return Model{}, Model{}, errors.New("snall model not found in provider config")
436 }
437
438 largeModelID := largeModelCfg.Model
439 smallModelID := smallModelCfg.Model
440
441 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
442 largeModelID += ":exacto"
443 }
444
445 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
446 smallModelID += ":exacto"
447 }
448
449 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
450 if err != nil {
451 return Model{}, Model{}, err
452 }
453 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
454 if err != nil {
455 return Model{}, Model{}, err
456 }
457
458 return Model{
459 Model: largeModel,
460 CatwalkCfg: *largeCatwalkModel,
461 ModelCfg: largeModelCfg,
462 }, Model{
463 Model: smallModel,
464 CatwalkCfg: *smallCatwalkModel,
465 ModelCfg: smallModelCfg,
466 }, nil
467}
468
469func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
470 hasBearerAuth := false
471 for key := range headers {
472 if strings.ToLower(key) == "authorization" {
473 hasBearerAuth = true
474 break
475 }
476 }
477
478 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
479
480 var opts []anthropic.Option
481 if apiKey != "" && !hasBearerAuth {
482 if isBearerToken {
483 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
484 headers["Authorization"] = apiKey
485 apiKey = "" // clear apiKey to avoid using X-Api-Key header
486 }
487 }
488
489 if apiKey != "" {
490 // Use standard X-Api-Key header
491 opts = append(opts, anthropic.WithAPIKey(apiKey))
492 }
493
494 if len(headers) > 0 {
495 opts = append(opts, anthropic.WithHeaders(headers))
496 }
497
498 if baseURL != "" {
499 opts = append(opts, anthropic.WithBaseURL(baseURL))
500 }
501
502 if c.cfg.Options.Debug {
503 httpClient := log.NewHTTPClient()
504 opts = append(opts, anthropic.WithHTTPClient(httpClient))
505 }
506
507 return anthropic.New(opts...)
508}
509
510func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
511 opts := []openai.Option{
512 openai.WithAPIKey(apiKey),
513 openai.WithUseResponsesAPI(),
514 }
515 if c.cfg.Options.Debug {
516 httpClient := log.NewHTTPClient()
517 opts = append(opts, openai.WithHTTPClient(httpClient))
518 }
519 if len(headers) > 0 {
520 opts = append(opts, openai.WithHeaders(headers))
521 }
522 if baseURL != "" {
523 opts = append(opts, openai.WithBaseURL(baseURL))
524 }
525 return openai.New(opts...)
526}
527
528func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
529 opts := []openrouter.Option{
530 openrouter.WithAPIKey(apiKey),
531 }
532 if c.cfg.Options.Debug {
533 httpClient := log.NewHTTPClient()
534 opts = append(opts, openrouter.WithHTTPClient(httpClient))
535 }
536 if len(headers) > 0 {
537 opts = append(opts, openrouter.WithHeaders(headers))
538 }
539 return openrouter.New(opts...)
540}
541
542func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
543 opts := []openaicompat.Option{
544 openaicompat.WithBaseURL(baseURL),
545 openaicompat.WithAPIKey(apiKey),
546 }
547 if c.cfg.Options.Debug {
548 httpClient := log.NewHTTPClient()
549 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
550 }
551 if len(headers) > 0 {
552 opts = append(opts, openaicompat.WithHeaders(headers))
553 }
554
555 for extraKey, extraValue := range extraBody {
556 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
557 }
558
559 return openaicompat.New(opts...)
560}
561
562func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
563 opts := []azure.Option{
564 azure.WithBaseURL(baseURL),
565 azure.WithAPIKey(apiKey),
566 azure.WithUseResponsesAPI(),
567 }
568 if c.cfg.Options.Debug {
569 httpClient := log.NewHTTPClient()
570 opts = append(opts, azure.WithHTTPClient(httpClient))
571 }
572 if options == nil {
573 options = make(map[string]string)
574 }
575 if apiVersion, ok := options["apiVersion"]; ok {
576 opts = append(opts, azure.WithAPIVersion(apiVersion))
577 }
578 if len(headers) > 0 {
579 opts = append(opts, azure.WithHeaders(headers))
580 }
581
582 return azure.New(opts...)
583}
584
585func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
586 var opts []bedrock.Option
587 if c.cfg.Options.Debug {
588 httpClient := log.NewHTTPClient()
589 opts = append(opts, bedrock.WithHTTPClient(httpClient))
590 }
591 if len(headers) > 0 {
592 opts = append(opts, bedrock.WithHeaders(headers))
593 }
594 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
595 if bearerToken != "" {
596 opts = append(opts, bedrock.WithAPIKey(bearerToken))
597 }
598 return bedrock.New(opts...)
599}
600
601func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
602 opts := []google.Option{
603 google.WithBaseURL(baseURL),
604 google.WithGeminiAPIKey(apiKey),
605 }
606 if c.cfg.Options.Debug {
607 httpClient := log.NewHTTPClient()
608 opts = append(opts, google.WithHTTPClient(httpClient))
609 }
610 if len(headers) > 0 {
611 opts = append(opts, google.WithHeaders(headers))
612 }
613 return google.New(opts...)
614}
615
616func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
617 opts := []google.Option{}
618 if c.cfg.Options.Debug {
619 httpClient := log.NewHTTPClient()
620 opts = append(opts, google.WithHTTPClient(httpClient))
621 }
622 if len(headers) > 0 {
623 opts = append(opts, google.WithHeaders(headers))
624 }
625
626 project := options["project"]
627 location := options["location"]
628
629 opts = append(opts, google.WithVertex(project, location))
630
631 return google.New(opts...)
632}
633
634func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
635 if model.Think {
636 return true
637 }
638
639 if model.ProviderOptions == nil {
640 return false
641 }
642
643 opts, err := anthropic.ParseOptions(model.ProviderOptions)
644 if err != nil {
645 return false
646 }
647 if opts.Thinking != nil {
648 return true
649 }
650 return false
651}
652
653func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
654 headers := maps.Clone(providerCfg.ExtraHeaders)
655 if headers == nil {
656 headers = make(map[string]string)
657 }
658
659 // handle special headers for anthropic
660 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
661 if v, ok := headers["anthropic-beta"]; ok {
662 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
663 } else {
664 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
665 }
666 }
667
668 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
669 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
670
671 switch providerCfg.Type {
672 case openai.Name:
673 return c.buildOpenaiProvider(baseURL, apiKey, headers)
674 case anthropic.Name:
675 return c.buildAnthropicProvider(baseURL, apiKey, headers)
676 case openrouter.Name:
677 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
678 case azure.Name:
679 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
680 case bedrock.Name:
681 return c.buildBedrockProvider(headers)
682 case google.Name:
683 return c.buildGoogleProvider(baseURL, apiKey, headers)
684 case "google-vertex":
685 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
686 case openaicompat.Name:
687 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
688 default:
689 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
690 }
691}
692
693func isExactoSupported(modelID string) bool {
694 supportedModels := []string{
695 "moonshotai/kimi-k2-0905",
696 "deepseek/deepseek-v3.1-terminus",
697 "z-ai/glm-4.6",
698 "openai/gpt-oss-120b",
699 "qwen/qwen3-coder",
700 }
701 return slices.Contains(supportedModels, modelID)
702}
703
704func (c *coordinator) Cancel(sessionID string) {
705 c.currentAgent.Cancel(sessionID)
706}
707
708func (c *coordinator) CancelAll() {
709 c.currentAgent.CancelAll()
710}
711
712func (c *coordinator) ClearQueue(sessionID string) {
713 c.currentAgent.ClearQueue(sessionID)
714}
715
716func (c *coordinator) IsBusy() bool {
717 return c.currentAgent.IsBusy()
718}
719
720func (c *coordinator) IsSessionBusy(sessionID string) bool {
721 return c.currentAgent.IsSessionBusy(sessionID)
722}
723
724func (c *coordinator) Model() Model {
725 return c.currentAgent.Model()
726}
727
728func (c *coordinator) UpdateModels(ctx context.Context) error {
729 // build the models again so we make sure we get the latest config
730 large, small, err := c.buildAgentModels(ctx)
731 if err != nil {
732 return err
733 }
734 c.currentAgent.SetModels(large, small)
735
736 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
737 if !ok {
738 return errors.New("coder agent not configured")
739 }
740
741 tools, err := c.buildTools(ctx, agentCfg)
742 if err != nil {
743 return err
744 }
745 c.currentAgent.SetTools(tools)
746 return nil
747}
748
749func (c *coordinator) QueuedPrompts(sessionID string) int {
750 return c.currentAgent.QueuedPrompts(sessionID)
751}
752
753func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
754 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
755 if !ok {
756 return errors.New("model provider not configured")
757 }
758 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
759}