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