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