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