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