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.Warn("MCPs not allowed")
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 }
378 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
379 return strings.Compare(a.Info().Name, b.Info().Name)
380 })
381 return filteredTools, nil
382}
383
384// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
385func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
386 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
387 if !ok {
388 return Model{}, Model{}, errors.New("large model not selected")
389 }
390 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
391 if !ok {
392 return Model{}, Model{}, errors.New("small model not selected")
393 }
394
395 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
396 if !ok {
397 return Model{}, Model{}, errors.New("large model provider not configured")
398 }
399
400 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
401 if err != nil {
402 return Model{}, Model{}, err
403 }
404
405 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
406 if !ok {
407 return Model{}, Model{}, errors.New("large model provider not configured")
408 }
409
410 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
411 if err != nil {
412 return Model{}, Model{}, err
413 }
414
415 var largeCatwalkModel *catwalk.Model
416 var smallCatwalkModel *catwalk.Model
417
418 for _, m := range largeProviderCfg.Models {
419 if m.ID == largeModelCfg.Model {
420 largeCatwalkModel = &m
421 }
422 }
423 for _, m := range smallProviderCfg.Models {
424 if m.ID == smallModelCfg.Model {
425 smallCatwalkModel = &m
426 }
427 }
428
429 if largeCatwalkModel == nil {
430 return Model{}, Model{}, errors.New("large model not found in provider config")
431 }
432
433 if smallCatwalkModel == nil {
434 return Model{}, Model{}, errors.New("snall model not found in provider config")
435 }
436
437 largeModelID := largeModelCfg.Model
438 smallModelID := smallModelCfg.Model
439
440 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
441 largeModelID += ":exacto"
442 }
443
444 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
445 smallModelID += ":exacto"
446 }
447
448 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
449 if err != nil {
450 return Model{}, Model{}, err
451 }
452 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
453 if err != nil {
454 return Model{}, Model{}, err
455 }
456
457 return Model{
458 Model: largeModel,
459 CatwalkCfg: *largeCatwalkModel,
460 ModelCfg: largeModelCfg,
461 }, Model{
462 Model: smallModel,
463 CatwalkCfg: *smallCatwalkModel,
464 ModelCfg: smallModelCfg,
465 }, nil
466}
467
468func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
469 hasBearerAuth := false
470 for key := range headers {
471 if strings.ToLower(key) == "authorization" {
472 hasBearerAuth = true
473 break
474 }
475 }
476
477 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
478
479 var opts []anthropic.Option
480 if apiKey != "" && !hasBearerAuth {
481 if isBearerToken {
482 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
483 headers["Authorization"] = apiKey
484 apiKey = "" // clear apiKey to avoid using X-Api-Key header
485 }
486 }
487
488 if apiKey != "" {
489 // Use standard X-Api-Key header
490 opts = append(opts, anthropic.WithAPIKey(apiKey))
491 }
492
493 if len(headers) > 0 {
494 opts = append(opts, anthropic.WithHeaders(headers))
495 }
496
497 if baseURL != "" {
498 opts = append(opts, anthropic.WithBaseURL(baseURL))
499 }
500
501 if c.cfg.Options.Debug {
502 httpClient := log.NewHTTPClient()
503 opts = append(opts, anthropic.WithHTTPClient(httpClient))
504 }
505
506 return anthropic.New(opts...)
507}
508
509func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
510 opts := []openai.Option{
511 openai.WithAPIKey(apiKey),
512 openai.WithUseResponsesAPI(),
513 }
514 if c.cfg.Options.Debug {
515 httpClient := log.NewHTTPClient()
516 opts = append(opts, openai.WithHTTPClient(httpClient))
517 }
518 if len(headers) > 0 {
519 opts = append(opts, openai.WithHeaders(headers))
520 }
521 if baseURL != "" {
522 opts = append(opts, openai.WithBaseURL(baseURL))
523 }
524 return openai.New(opts...)
525}
526
527func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
528 opts := []openrouter.Option{
529 openrouter.WithAPIKey(apiKey),
530 }
531 if c.cfg.Options.Debug {
532 httpClient := log.NewHTTPClient()
533 opts = append(opts, openrouter.WithHTTPClient(httpClient))
534 }
535 if len(headers) > 0 {
536 opts = append(opts, openrouter.WithHeaders(headers))
537 }
538 return openrouter.New(opts...)
539}
540
541func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
542 opts := []openaicompat.Option{
543 openaicompat.WithBaseURL(baseURL),
544 openaicompat.WithAPIKey(apiKey),
545 }
546 if c.cfg.Options.Debug {
547 httpClient := log.NewHTTPClient()
548 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
549 }
550 if len(headers) > 0 {
551 opts = append(opts, openaicompat.WithHeaders(headers))
552 }
553
554 for extraKey, extraValue := range extraBody {
555 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
556 }
557
558 return openaicompat.New(opts...)
559}
560
561func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
562 opts := []azure.Option{
563 azure.WithBaseURL(baseURL),
564 azure.WithAPIKey(apiKey),
565 azure.WithUseResponsesAPI(),
566 }
567 if c.cfg.Options.Debug {
568 httpClient := log.NewHTTPClient()
569 opts = append(opts, azure.WithHTTPClient(httpClient))
570 }
571 if options == nil {
572 options = make(map[string]string)
573 }
574 if apiVersion, ok := options["apiVersion"]; ok {
575 opts = append(opts, azure.WithAPIVersion(apiVersion))
576 }
577 if len(headers) > 0 {
578 opts = append(opts, azure.WithHeaders(headers))
579 }
580
581 return azure.New(opts...)
582}
583
584func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
585 var opts []bedrock.Option
586 if c.cfg.Options.Debug {
587 httpClient := log.NewHTTPClient()
588 opts = append(opts, bedrock.WithHTTPClient(httpClient))
589 }
590 if len(headers) > 0 {
591 opts = append(opts, bedrock.WithHeaders(headers))
592 }
593 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
594 if bearerToken != "" {
595 opts = append(opts, bedrock.WithAPIKey(bearerToken))
596 }
597 return bedrock.New(opts...)
598}
599
600func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
601 opts := []google.Option{
602 google.WithBaseURL(baseURL),
603 google.WithGeminiAPIKey(apiKey),
604 }
605 if c.cfg.Options.Debug {
606 httpClient := log.NewHTTPClient()
607 opts = append(opts, google.WithHTTPClient(httpClient))
608 }
609 if len(headers) > 0 {
610 opts = append(opts, google.WithHeaders(headers))
611 }
612 return google.New(opts...)
613}
614
615func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
616 opts := []google.Option{}
617 if c.cfg.Options.Debug {
618 httpClient := log.NewHTTPClient()
619 opts = append(opts, google.WithHTTPClient(httpClient))
620 }
621 if len(headers) > 0 {
622 opts = append(opts, google.WithHeaders(headers))
623 }
624
625 project := options["project"]
626 location := options["location"]
627
628 opts = append(opts, google.WithVertex(project, location))
629
630 return google.New(opts...)
631}
632
633func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
634 if model.Think {
635 return true
636 }
637
638 if model.ProviderOptions == nil {
639 return false
640 }
641
642 opts, err := anthropic.ParseOptions(model.ProviderOptions)
643 if err != nil {
644 return false
645 }
646 if opts.Thinking != nil {
647 return true
648 }
649 return false
650}
651
652func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
653 headers := maps.Clone(providerCfg.ExtraHeaders)
654 if headers == nil {
655 headers = make(map[string]string)
656 }
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 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
668 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
669
670 switch providerCfg.Type {
671 case openai.Name:
672 return c.buildOpenaiProvider(baseURL, apiKey, headers)
673 case anthropic.Name:
674 return c.buildAnthropicProvider(baseURL, apiKey, headers)
675 case openrouter.Name:
676 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
677 case azure.Name:
678 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
679 case bedrock.Name:
680 return c.buildBedrockProvider(headers)
681 case google.Name:
682 return c.buildGoogleProvider(baseURL, apiKey, headers)
683 case "google-vertex":
684 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
685 case openaicompat.Name:
686 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
687 default:
688 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
689 }
690}
691
692func isExactoSupported(modelID string) bool {
693 supportedModels := []string{
694 "moonshotai/kimi-k2-0905",
695 "deepseek/deepseek-v3.1-terminus",
696 "z-ai/glm-4.6",
697 "openai/gpt-oss-120b",
698 "qwen/qwen3-coder",
699 }
700 return slices.Contains(supportedModels, modelID)
701}
702
703func (c *coordinator) Cancel(sessionID string) {
704 c.currentAgent.Cancel(sessionID)
705}
706
707func (c *coordinator) CancelAll() {
708 c.currentAgent.CancelAll()
709}
710
711func (c *coordinator) ClearQueue(sessionID string) {
712 c.currentAgent.ClearQueue(sessionID)
713}
714
715func (c *coordinator) IsBusy() bool {
716 return c.currentAgent.IsBusy()
717}
718
719func (c *coordinator) IsSessionBusy(sessionID string) bool {
720 return c.currentAgent.IsSessionBusy(sessionID)
721}
722
723func (c *coordinator) Model() Model {
724 return c.currentAgent.Model()
725}
726
727func (c *coordinator) UpdateModels(ctx context.Context) error {
728 // build the models again so we make sure we get the latest config
729 large, small, err := c.buildAgentModels(ctx)
730 if err != nil {
731 return err
732 }
733 c.currentAgent.SetModels(large, small)
734
735 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
736 if !ok {
737 return errors.New("coder agent not configured")
738 }
739
740 tools, err := c.buildTools(ctx, agentCfg)
741 if err != nil {
742 return err
743 }
744 c.currentAgent.SetTools(tools)
745 return nil
746}
747
748func (c *coordinator) QueuedPrompts(sessionID string) int {
749 return c.currentAgent.QueuedPrompts(sessionID)
750}
751
752func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
753 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
754 if !ok {
755 return errors.New("model provider not configured")
756 }
757 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
758}