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