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