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