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/agent/tools/mcp"
22 "github.com/charmbracelet/crush/internal/config"
23 "github.com/charmbracelet/crush/internal/csync"
24 "github.com/charmbracelet/crush/internal/history"
25 "github.com/charmbracelet/crush/internal/log"
26 "github.com/charmbracelet/crush/internal/lsp"
27 "github.com/charmbracelet/crush/internal/message"
28 "github.com/charmbracelet/crush/internal/permission"
29 "github.com/charmbracelet/crush/internal/session"
30 "golang.org/x/sync/errgroup"
31
32 "charm.land/fantasy/providers/anthropic"
33 "charm.land/fantasy/providers/azure"
34 "charm.land/fantasy/providers/bedrock"
35 "charm.land/fantasy/providers/google"
36 "charm.land/fantasy/providers/openai"
37 "charm.land/fantasy/providers/openaicompat"
38 "charm.land/fantasy/providers/openrouter"
39 openaisdk "github.com/openai/openai-go/v2/option"
40 "github.com/qjebbs/go-jsons"
41)
42
43type Coordinator interface {
44 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
45 // SetMainAgent(string)
46 Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
47 Cancel(sessionID string)
48 CancelAll()
49 IsSessionBusy(sessionID string) bool
50 IsBusy() bool
51 QueuedPrompts(sessionID string) int
52 ClearQueue(sessionID string)
53 Summarize(context.Context, string) error
54 Model() Model
55 UpdateModels(ctx context.Context) error
56}
57
58type coordinator struct {
59 cfg *config.Config
60 sessions session.Service
61 messages message.Service
62 permissions permission.Service
63 history history.Service
64 lspClients *csync.Map[string, *lsp.Client]
65
66 currentAgent SessionAgent
67 agents map[string]SessionAgent
68
69 readyWg errgroup.Group
70}
71
72func NewCoordinator(
73 ctx context.Context,
74 cfg *config.Config,
75 sessions session.Service,
76 messages message.Service,
77 permissions permission.Service,
78 history history.Service,
79 lspClients *csync.Map[string, *lsp.Client],
80) (Coordinator, error) {
81 c := &coordinator{
82 cfg: cfg,
83 sessions: sessions,
84 messages: messages,
85 permissions: permissions,
86 history: history,
87 lspClients: lspClients,
88 agents: make(map[string]SessionAgent),
89 }
90
91 agentCfg, ok := cfg.Agents[config.AgentCoder]
92 if !ok {
93 return nil, errors.New("coder agent not configured")
94 }
95
96 // TODO: make this dynamic when we support multiple agents
97 prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
98 if err != nil {
99 return nil, err
100 }
101
102 agent, err := c.buildAgent(ctx, prompt, agentCfg)
103 if err != nil {
104 return nil, err
105 }
106 c.currentAgent = agent
107 c.agents[config.AgentCoder] = agent
108 return c, nil
109}
110
111// Run implements Coordinator.
112func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
113 if err := c.readyWg.Wait(); err != nil {
114 return nil, err
115 }
116
117 model := c.currentAgent.Model()
118 maxTokens := model.CatwalkCfg.DefaultMaxTokens
119 if model.ModelCfg.MaxTokens != 0 {
120 maxTokens = model.ModelCfg.MaxTokens
121 }
122
123 if !model.CatwalkCfg.SupportsImages && attachments != nil {
124 attachments = nil
125 }
126
127 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
128 if !ok {
129 return nil, errors.New("model provider not configured")
130 }
131
132 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
133
134 return c.currentAgent.Run(ctx, SessionAgentCall{
135 SessionID: sessionID,
136 Prompt: prompt,
137 Attachments: attachments,
138 MaxOutputTokens: maxTokens,
139 ProviderOptions: mergedOptions,
140 Temperature: temp,
141 TopP: topP,
142 TopK: topK,
143 FrequencyPenalty: freqPenalty,
144 PresencePenalty: presPenalty,
145 })
146}
147
148func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
149 options := fantasy.ProviderOptions{}
150
151 cfgOpts := []byte("{}")
152 providerCfgOpts := []byte("{}")
153 catwalkOpts := []byte("{}")
154
155 if model.ModelCfg.ProviderOptions != nil {
156 data, err := json.Marshal(model.ModelCfg.ProviderOptions)
157 if err == nil {
158 cfgOpts = data
159 }
160 }
161
162 if providerCfg.ProviderOptions != nil {
163 data, err := json.Marshal(providerCfg.ProviderOptions)
164 if err == nil {
165 providerCfgOpts = data
166 }
167 }
168
169 if model.CatwalkCfg.Options.ProviderOptions != nil {
170 data, err := json.Marshal(model.CatwalkCfg.Options.ProviderOptions)
171 if err == nil {
172 catwalkOpts = data
173 }
174 }
175
176 readers := []io.Reader{
177 bytes.NewReader(catwalkOpts),
178 bytes.NewReader(providerCfgOpts),
179 bytes.NewReader(cfgOpts),
180 }
181
182 got, err := jsons.Merge(readers)
183 if err != nil {
184 slog.Error("Could not merge call config", "err", err)
185 return options
186 }
187
188 mergedOptions := make(map[string]any)
189
190 err = json.Unmarshal([]byte(got), &mergedOptions)
191 if err != nil {
192 slog.Error("Could not create config for call", "err", err)
193 return options
194 }
195
196 switch providerCfg.Type {
197 case openai.Name, azure.Name:
198 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
199 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
200 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
201 }
202 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
203 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
204 mergedOptions["reasoning_summary"] = "auto"
205 mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
206 }
207 parsed, err := openai.ParseResponsesOptions(mergedOptions)
208 if err == nil {
209 options[openai.Name] = parsed
210 }
211 } else {
212 parsed, err := openai.ParseOptions(mergedOptions)
213 if err == nil {
214 options[openai.Name] = parsed
215 }
216 }
217 case anthropic.Name:
218 _, hasThink := mergedOptions["thinking"]
219 if !hasThink && model.ModelCfg.Think {
220 mergedOptions["thinking"] = map[string]any{
221 // TODO: kujtim see if we need to make this dynamic
222 "budget_tokens": 2000,
223 }
224 }
225 parsed, err := anthropic.ParseOptions(mergedOptions)
226 if err == nil {
227 options[anthropic.Name] = parsed
228 }
229
230 case openrouter.Name:
231 _, hasReasoning := mergedOptions["reasoning"]
232 if !hasReasoning && model.ModelCfg.ReasoningEffort != "" {
233 mergedOptions["reasoning"] = map[string]any{
234 "enabled": true,
235 "effort": model.ModelCfg.ReasoningEffort,
236 }
237 }
238 parsed, err := openrouter.ParseOptions(mergedOptions)
239 if err == nil {
240 options[openrouter.Name] = parsed
241 }
242 case google.Name:
243 _, hasReasoning := mergedOptions["thinking_config"]
244 if !hasReasoning {
245 mergedOptions["thinking_config"] = map[string]any{
246 "thinking_budget": 2000,
247 "include_thoughts": true,
248 }
249 }
250 parsed, err := google.ParseOptions(mergedOptions)
251 if err == nil {
252 options[google.Name] = parsed
253 }
254 case openaicompat.Name:
255 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
256 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
257 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
258 }
259 parsed, err := openaicompat.ParseOptions(mergedOptions)
260 if err == nil {
261 options[openaicompat.Name] = parsed
262 }
263 }
264
265 return options
266}
267
268func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
269 modelOptions := getProviderOptions(model, cfg)
270 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
271 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
272 topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
273 freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
274 presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
275 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
276}
277
278func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent) (SessionAgent, error) {
279 large, small, err := c.buildAgentModels(ctx)
280 if err != nil {
281 return nil, err
282 }
283
284 systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
285 if err != nil {
286 return nil, err
287 }
288
289 largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
290 result := NewSessionAgent(SessionAgentOptions{
291 large,
292 small,
293 largeProviderCfg.SystemPromptPrefix,
294 systemPrompt,
295 c.cfg.Options.DisableAutoSummarize,
296 c.permissions.SkipRequests(),
297 c.sessions,
298 c.messages,
299 nil,
300 })
301 c.readyWg.Go(func() error {
302 tools, err := c.buildTools(ctx, agent)
303 if err != nil {
304 return err
305 }
306 result.SetTools(tools)
307 return nil
308 })
309
310 return result, nil
311}
312
313func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
314 var allTools []fantasy.AgentTool
315 if slices.Contains(agent.AllowedTools, AgentToolName) {
316 agentTool, err := c.agentTool(ctx)
317 if err != nil {
318 return nil, err
319 }
320 allTools = append(allTools, agentTool)
321 }
322
323 allTools = append(allTools,
324 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
325 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
326 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
327 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
328 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
329 tools.NewGlobTool(c.cfg.WorkingDir()),
330 tools.NewGrepTool(c.cfg.WorkingDir()),
331 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
332 tools.NewSourcegraphTool(nil),
333 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
334 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
335 )
336
337 if len(c.cfg.LSP) > 0 {
338 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
339 }
340
341 var filteredTools []fantasy.AgentTool
342 for _, tool := range allTools {
343 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
344 filteredTools = append(filteredTools, tool)
345 }
346 }
347
348 for tool := range mcp.GetMCPTools() {
349 if agent.AllowedMCP == nil {
350 // No MCP restrictions
351 filteredTools = append(filteredTools, tool)
352 } else if len(agent.AllowedMCP) == 0 {
353 // no mcps allowed
354 break
355 }
356
357 for mcp, tools := range agent.AllowedMCP {
358 if mcp == tool.MCP() {
359 if len(tools) == 0 {
360 filteredTools = append(filteredTools, tool)
361 }
362 for _, t := range tools {
363 if t == tool.MCPToolName() {
364 filteredTools = append(filteredTools, tool)
365 }
366 }
367 break
368 }
369 }
370 }
371 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
372 return strings.Compare(a.Info().Name, b.Info().Name)
373 })
374 return filteredTools, nil
375}
376
377// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
378func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
379 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
380 if !ok {
381 return Model{}, Model{}, errors.New("large model not selected")
382 }
383 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
384 if !ok {
385 return Model{}, Model{}, errors.New("small model not selected")
386 }
387
388 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
389 if !ok {
390 return Model{}, Model{}, errors.New("large model provider not configured")
391 }
392
393 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
394 if err != nil {
395 return Model{}, Model{}, err
396 }
397
398 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
399 if !ok {
400 return Model{}, Model{}, errors.New("large model provider not configured")
401 }
402
403 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
404 if err != nil {
405 return Model{}, Model{}, err
406 }
407
408 var largeCatwalkModel *catwalk.Model
409 var smallCatwalkModel *catwalk.Model
410
411 for _, m := range largeProviderCfg.Models {
412 if m.ID == largeModelCfg.Model {
413 largeCatwalkModel = &m
414 }
415 }
416 for _, m := range smallProviderCfg.Models {
417 if m.ID == smallModelCfg.Model {
418 smallCatwalkModel = &m
419 }
420 }
421
422 if largeCatwalkModel == nil {
423 return Model{}, Model{}, errors.New("large model not found in provider config")
424 }
425
426 if smallCatwalkModel == nil {
427 return Model{}, Model{}, errors.New("snall model not found in provider config")
428 }
429
430 largeModelID := largeModelCfg.Model
431 smallModelID := smallModelCfg.Model
432
433 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
434 largeModelID += ":exacto"
435 }
436
437 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
438 smallModelID += ":exacto"
439 }
440
441 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
442 if err != nil {
443 return Model{}, Model{}, err
444 }
445 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
446 if err != nil {
447 return Model{}, Model{}, err
448 }
449
450 return Model{
451 Model: largeModel,
452 CatwalkCfg: *largeCatwalkModel,
453 ModelCfg: largeModelCfg,
454 }, Model{
455 Model: smallModel,
456 CatwalkCfg: *smallCatwalkModel,
457 ModelCfg: smallModelCfg,
458 }, nil
459}
460
461func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
462 hasBearerAuth := false
463 for key := range headers {
464 if strings.ToLower(key) == "authorization" {
465 hasBearerAuth = true
466 break
467 }
468 }
469
470 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
471
472 var opts []anthropic.Option
473 if apiKey != "" && !hasBearerAuth {
474 if isBearerToken {
475 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
476 headers["Authorization"] = apiKey
477 apiKey = "" // clear apiKey to avoid using X-Api-Key header
478 }
479 }
480
481 if apiKey != "" {
482 // Use standard X-Api-Key header
483 opts = append(opts, anthropic.WithAPIKey(apiKey))
484 }
485
486 if len(headers) > 0 {
487 opts = append(opts, anthropic.WithHeaders(headers))
488 }
489
490 if baseURL != "" {
491 opts = append(opts, anthropic.WithBaseURL(baseURL))
492 }
493
494 if c.cfg.Options.Debug {
495 httpClient := log.NewHTTPClient()
496 opts = append(opts, anthropic.WithHTTPClient(httpClient))
497 }
498
499 return anthropic.New(opts...)
500}
501
502func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
503 opts := []openai.Option{
504 openai.WithAPIKey(apiKey),
505 openai.WithUseResponsesAPI(),
506 }
507 if c.cfg.Options.Debug {
508 httpClient := log.NewHTTPClient()
509 opts = append(opts, openai.WithHTTPClient(httpClient))
510 }
511 if len(headers) > 0 {
512 opts = append(opts, openai.WithHeaders(headers))
513 }
514 if baseURL != "" {
515 opts = append(opts, openai.WithBaseURL(baseURL))
516 }
517 return openai.New(opts...)
518}
519
520func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
521 opts := []openrouter.Option{
522 openrouter.WithAPIKey(apiKey),
523 }
524 if c.cfg.Options.Debug {
525 httpClient := log.NewHTTPClient()
526 opts = append(opts, openrouter.WithHTTPClient(httpClient))
527 }
528 if len(headers) > 0 {
529 opts = append(opts, openrouter.WithHeaders(headers))
530 }
531 return openrouter.New(opts...)
532}
533
534func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
535 opts := []openaicompat.Option{
536 openaicompat.WithBaseURL(baseURL),
537 openaicompat.WithAPIKey(apiKey),
538 }
539 if c.cfg.Options.Debug {
540 httpClient := log.NewHTTPClient()
541 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
542 }
543 if len(headers) > 0 {
544 opts = append(opts, openaicompat.WithHeaders(headers))
545 }
546
547 for extraKey, extraValue := range extraBody {
548 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
549 }
550
551 return openaicompat.New(opts...)
552}
553
554func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
555 opts := []azure.Option{
556 azure.WithBaseURL(baseURL),
557 azure.WithAPIKey(apiKey),
558 azure.WithUseResponsesAPI(),
559 }
560 if c.cfg.Options.Debug {
561 httpClient := log.NewHTTPClient()
562 opts = append(opts, azure.WithHTTPClient(httpClient))
563 }
564 if options == nil {
565 options = make(map[string]string)
566 }
567 if apiVersion, ok := options["apiVersion"]; ok {
568 opts = append(opts, azure.WithAPIVersion(apiVersion))
569 }
570 if len(headers) > 0 {
571 opts = append(opts, azure.WithHeaders(headers))
572 }
573
574 return azure.New(opts...)
575}
576
577func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
578 var opts []bedrock.Option
579 if c.cfg.Options.Debug {
580 httpClient := log.NewHTTPClient()
581 opts = append(opts, bedrock.WithHTTPClient(httpClient))
582 }
583 if len(headers) > 0 {
584 opts = append(opts, bedrock.WithHeaders(headers))
585 }
586 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
587 if bearerToken != "" {
588 opts = append(opts, bedrock.WithAPIKey(bearerToken))
589 }
590 return bedrock.New(opts...)
591}
592
593func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
594 opts := []google.Option{
595 google.WithBaseURL(baseURL),
596 google.WithGeminiAPIKey(apiKey),
597 }
598 if c.cfg.Options.Debug {
599 httpClient := log.NewHTTPClient()
600 opts = append(opts, google.WithHTTPClient(httpClient))
601 }
602 if len(headers) > 0 {
603 opts = append(opts, google.WithHeaders(headers))
604 }
605 return google.New(opts...)
606}
607
608func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
609 opts := []google.Option{}
610 if c.cfg.Options.Debug {
611 httpClient := log.NewHTTPClient()
612 opts = append(opts, google.WithHTTPClient(httpClient))
613 }
614 if len(headers) > 0 {
615 opts = append(opts, google.WithHeaders(headers))
616 }
617
618 project := options["project"]
619 location := options["location"]
620
621 opts = append(opts, google.WithVertex(project, location))
622
623 return google.New(opts...)
624}
625
626func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
627 if model.Think {
628 return true
629 }
630
631 if model.ProviderOptions == nil {
632 return false
633 }
634
635 opts, err := anthropic.ParseOptions(model.ProviderOptions)
636 if err != nil {
637 return false
638 }
639 if opts.Thinking != nil {
640 return true
641 }
642 return false
643}
644
645func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
646 headers := maps.Clone(providerCfg.ExtraHeaders)
647 if headers == nil {
648 headers = make(map[string]string)
649 }
650
651 // handle special headers for anthropic
652 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
653 if v, ok := headers["anthropic-beta"]; ok {
654 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
655 } else {
656 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
657 }
658 }
659
660 // TODO: make sure we have
661 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
662 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
663
664 switch providerCfg.Type {
665 case openai.Name:
666 return c.buildOpenaiProvider(baseURL, apiKey, headers)
667 case anthropic.Name:
668 return c.buildAnthropicProvider(baseURL, apiKey, headers)
669 case openrouter.Name:
670 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
671 case azure.Name:
672 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
673 case bedrock.Name:
674 return c.buildBedrockProvider(headers)
675 case google.Name:
676 return c.buildGoogleProvider(baseURL, apiKey, headers)
677 case "google-vertex":
678 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
679 case openaicompat.Name:
680 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
681 default:
682 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
683 }
684}
685
686func isExactoSupported(modelID string) bool {
687 supportedModels := []string{
688 "moonshotai/kimi-k2-0905",
689 "deepseek/deepseek-v3.1-terminus",
690 "z-ai/glm-4.6",
691 "openai/gpt-oss-120b",
692 "qwen/qwen3-coder",
693 }
694 return slices.Contains(supportedModels, modelID)
695}
696
697func (c *coordinator) Cancel(sessionID string) {
698 c.currentAgent.Cancel(sessionID)
699}
700
701func (c *coordinator) CancelAll() {
702 c.currentAgent.CancelAll()
703}
704
705func (c *coordinator) ClearQueue(sessionID string) {
706 c.currentAgent.ClearQueue(sessionID)
707}
708
709func (c *coordinator) IsBusy() bool {
710 return c.currentAgent.IsBusy()
711}
712
713func (c *coordinator) IsSessionBusy(sessionID string) bool {
714 return c.currentAgent.IsSessionBusy(sessionID)
715}
716
717func (c *coordinator) Model() Model {
718 return c.currentAgent.Model()
719}
720
721func (c *coordinator) UpdateModels(ctx context.Context) error {
722 // build the models again so we make sure we get the latest config
723 large, small, err := c.buildAgentModels(ctx)
724 if err != nil {
725 return err
726 }
727 c.currentAgent.SetModels(large, small)
728
729 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
730 if !ok {
731 return errors.New("coder agent not configured")
732 }
733
734 tools, err := c.buildTools(ctx, agentCfg)
735 if err != nil {
736 return err
737 }
738 c.currentAgent.SetTools(tools)
739 return nil
740}
741
742func (c *coordinator) QueuedPrompts(sessionID string) int {
743 return c.currentAgent.QueuedPrompts(sessionID)
744}
745
746func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
747 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
748 if !ok {
749 return errors.New("model provider not configured")
750 }
751 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
752}