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