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 allTools = append(allTools,
333 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
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 mcpTools := tools.GetMCPTools(context.Background(), c.permissions, c.cfg)
358
359 for _, mcpTool := range mcpTools {
360 if agent.AllowedMCP == nil {
361 // No MCP restrictions
362 filteredTools = append(filteredTools, mcpTool)
363 } else if len(agent.AllowedMCP) == 0 {
364 // no mcps allowed
365 break
366 }
367
368 for mcp, tools := range agent.AllowedMCP {
369 if mcp == mcpTool.MCP() {
370 if len(tools) == 0 {
371 filteredTools = append(filteredTools, mcpTool)
372 }
373 for _, t := range tools {
374 if t == mcpTool.MCPToolName() {
375 filteredTools = append(filteredTools, mcpTool)
376 }
377 }
378 break
379 }
380 }
381 }
382 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
383 return strings.Compare(a.Info().Name, b.Info().Name)
384 })
385 return filteredTools, nil
386}
387
388// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
389func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
390 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
391 if !ok {
392 return Model{}, Model{}, errors.New("large model not selected")
393 }
394 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
395 if !ok {
396 return Model{}, Model{}, errors.New("small model not selected")
397 }
398
399 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
400 if !ok {
401 return Model{}, Model{}, errors.New("large model provider not configured")
402 }
403
404 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
405 if err != nil {
406 return Model{}, Model{}, err
407 }
408
409 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
410 if !ok {
411 return Model{}, Model{}, errors.New("large model provider not configured")
412 }
413
414 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
415 if err != nil {
416 return Model{}, Model{}, err
417 }
418
419 var largeCatwalkModel *catwalk.Model
420 var smallCatwalkModel *catwalk.Model
421
422 for _, m := range largeProviderCfg.Models {
423 if m.ID == largeModelCfg.Model {
424 largeCatwalkModel = &m
425 }
426 }
427 for _, m := range smallProviderCfg.Models {
428 if m.ID == smallModelCfg.Model {
429 smallCatwalkModel = &m
430 }
431 }
432
433 if largeCatwalkModel == nil {
434 return Model{}, Model{}, errors.New("large model not found in provider config")
435 }
436
437 if smallCatwalkModel == nil {
438 return Model{}, Model{}, errors.New("snall model not found in provider config")
439 }
440
441 largeModelID := largeModelCfg.Model
442 smallModelID := smallModelCfg.Model
443
444 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
445 largeModelID += ":exacto"
446 }
447
448 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
449 smallModelID += ":exacto"
450 }
451
452 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
453 if err != nil {
454 return Model{}, Model{}, err
455 }
456 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
457 if err != nil {
458 return Model{}, Model{}, err
459 }
460
461 return Model{
462 Model: largeModel,
463 CatwalkCfg: *largeCatwalkModel,
464 ModelCfg: largeModelCfg,
465 }, Model{
466 Model: smallModel,
467 CatwalkCfg: *smallCatwalkModel,
468 ModelCfg: smallModelCfg,
469 }, nil
470}
471
472func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
473 hasBearerAuth := false
474 for key := range headers {
475 if strings.ToLower(key) == "authorization" {
476 hasBearerAuth = true
477 break
478 }
479 }
480
481 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
482
483 var opts []anthropic.Option
484 if apiKey != "" && !hasBearerAuth {
485 if isBearerToken {
486 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
487 headers["Authorization"] = apiKey
488 apiKey = "" // clear apiKey to avoid using X-Api-Key header
489 }
490 }
491
492 if apiKey != "" {
493 // Use standard X-Api-Key header
494 opts = append(opts, anthropic.WithAPIKey(apiKey))
495 }
496
497 if len(headers) > 0 {
498 opts = append(opts, anthropic.WithHeaders(headers))
499 }
500
501 if baseURL != "" {
502 opts = append(opts, anthropic.WithBaseURL(baseURL))
503 }
504
505 if c.cfg.Options.Debug {
506 httpClient := log.NewHTTPClient()
507 opts = append(opts, anthropic.WithHTTPClient(httpClient))
508 }
509
510 return anthropic.New(opts...)
511}
512
513func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
514 opts := []openai.Option{
515 openai.WithAPIKey(apiKey),
516 openai.WithUseResponsesAPI(),
517 }
518 if c.cfg.Options.Debug {
519 httpClient := log.NewHTTPClient()
520 opts = append(opts, openai.WithHTTPClient(httpClient))
521 }
522 if len(headers) > 0 {
523 opts = append(opts, openai.WithHeaders(headers))
524 }
525 if baseURL != "" {
526 opts = append(opts, openai.WithBaseURL(baseURL))
527 }
528 return openai.New(opts...)
529}
530
531func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
532 opts := []openrouter.Option{
533 openrouter.WithAPIKey(apiKey),
534 }
535 if c.cfg.Options.Debug {
536 httpClient := log.NewHTTPClient()
537 opts = append(opts, openrouter.WithHTTPClient(httpClient))
538 }
539 if len(headers) > 0 {
540 opts = append(opts, openrouter.WithHeaders(headers))
541 }
542 return openrouter.New(opts...)
543}
544
545func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
546 opts := []openaicompat.Option{
547 openaicompat.WithBaseURL(baseURL),
548 openaicompat.WithAPIKey(apiKey),
549 }
550 if c.cfg.Options.Debug {
551 httpClient := log.NewHTTPClient()
552 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
553 }
554 if len(headers) > 0 {
555 opts = append(opts, openaicompat.WithHeaders(headers))
556 }
557
558 for extraKey, extraValue := range extraBody {
559 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
560 }
561
562 return openaicompat.New(opts...)
563}
564
565func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
566 opts := []azure.Option{
567 azure.WithBaseURL(baseURL),
568 azure.WithAPIKey(apiKey),
569 }
570 if c.cfg.Options.Debug {
571 httpClient := log.NewHTTPClient()
572 opts = append(opts, azure.WithHTTPClient(httpClient))
573 }
574 if options == nil {
575 options = make(map[string]string)
576 }
577 if apiVersion, ok := options["apiVersion"]; ok {
578 opts = append(opts, azure.WithAPIVersion(apiVersion))
579 }
580 if len(headers) > 0 {
581 opts = append(opts, azure.WithHeaders(headers))
582 }
583
584 return azure.New(opts...)
585}
586
587func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
588 var opts []bedrock.Option
589 if c.cfg.Options.Debug {
590 httpClient := log.NewHTTPClient()
591 opts = append(opts, bedrock.WithHTTPClient(httpClient))
592 }
593 if len(headers) > 0 {
594 opts = append(opts, bedrock.WithHeaders(headers))
595 }
596 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
597 if bearerToken != "" {
598 opts = append(opts, bedrock.WithAPIKey(bearerToken))
599 }
600 return bedrock.New(opts...)
601}
602
603func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
604 opts := []google.Option{
605 google.WithBaseURL(baseURL),
606 google.WithGeminiAPIKey(apiKey),
607 }
608 if c.cfg.Options.Debug {
609 httpClient := log.NewHTTPClient()
610 opts = append(opts, google.WithHTTPClient(httpClient))
611 }
612 if len(headers) > 0 {
613 opts = append(opts, google.WithHeaders(headers))
614 }
615 return google.New(opts...)
616}
617
618func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
619 opts := []google.Option{}
620 if c.cfg.Options.Debug {
621 httpClient := log.NewHTTPClient()
622 opts = append(opts, google.WithHTTPClient(httpClient))
623 }
624 if len(headers) > 0 {
625 opts = append(opts, google.WithHeaders(headers))
626 }
627
628 project := options["project"]
629 location := options["location"]
630
631 opts = append(opts, google.WithVertex(project, location))
632
633 return google.New(opts...)
634}
635
636func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
637 if model.Think {
638 return true
639 }
640
641 if model.ProviderOptions == nil {
642 return false
643 }
644
645 opts, err := anthropic.ParseOptions(model.ProviderOptions)
646 if err != nil {
647 return false
648 }
649 if opts.Thinking != nil {
650 return true
651 }
652 return false
653}
654
655func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
656 headers := maps.Clone(providerCfg.ExtraHeaders)
657
658 // handle special headers for anthropic
659 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
660 if v, ok := headers["anthropic-beta"]; ok {
661 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
662 } else {
663 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
664 }
665 }
666
667 // TODO: make sure we have
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}