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