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