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.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.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
338 tools.NewGlobTool(c.cfg.WorkingDir()),
339 tools.NewGrepTool(c.cfg.WorkingDir()),
340 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
341 tools.NewSourcegraphTool(nil),
342 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
343 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
344 )
345
346 if len(c.cfg.LSP) > 0 {
347 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
348 }
349
350 var filteredTools []fantasy.AgentTool
351 for _, tool := range allTools {
352 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
353 filteredTools = append(filteredTools, tool)
354 }
355 }
356
357 mcpTools := tools.GetMCPTools(context.Background(), c.permissions, c.cfg)
358
359 for _, mcpTool := range mcpTools {
360 if agent.AllowedMCP == nil {
361 // No MCP restrictions
362 filteredTools = append(filteredTools, mcpTool)
363 } else if len(agent.AllowedMCP) == 0 {
364 // no mcps allowed
365 break
366 }
367
368 for mcp, tools := range agent.AllowedMCP {
369 if mcp == mcpTool.MCP() {
370 if len(tools) == 0 {
371 filteredTools = append(filteredTools, mcpTool)
372 }
373 for _, t := range tools {
374 if t == mcpTool.MCPToolName() {
375 filteredTools = append(filteredTools, mcpTool)
376 }
377 }
378 break
379 }
380 }
381 }
382 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
383 return strings.Compare(a.Info().Name, b.Info().Name)
384 })
385 return filteredTools, nil
386}
387
388// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
389func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
390 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
391 if !ok {
392 return Model{}, Model{}, errors.New("large model not selected")
393 }
394 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
395 if !ok {
396 return Model{}, Model{}, errors.New("small model not selected")
397 }
398
399 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
400 if !ok {
401 return Model{}, Model{}, errors.New("large model provider not configured")
402 }
403
404 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
405 if err != nil {
406 return Model{}, Model{}, err
407 }
408
409 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
410 if !ok {
411 return Model{}, Model{}, errors.New("large model provider not configured")
412 }
413
414 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
415 if err != nil {
416 return Model{}, Model{}, err
417 }
418
419 var largeCatwalkModel *catwalk.Model
420 var smallCatwalkModel *catwalk.Model
421
422 for _, m := range largeProviderCfg.Models {
423 if m.ID == largeModelCfg.Model {
424 largeCatwalkModel = &m
425 }
426 }
427 for _, m := range smallProviderCfg.Models {
428 if m.ID == smallModelCfg.Model {
429 smallCatwalkModel = &m
430 }
431 }
432
433 if largeCatwalkModel == nil {
434 return Model{}, Model{}, errors.New("large model not found in provider config")
435 }
436
437 if smallCatwalkModel == nil {
438 return Model{}, Model{}, errors.New("snall model not found in provider config")
439 }
440
441 largeModelID := largeModelCfg.Model
442 smallModelID := smallModelCfg.Model
443
444 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
445 largeModelID += ":exacto"
446 }
447
448 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
449 smallModelID += ":exacto"
450 }
451
452 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
453 if err != nil {
454 return Model{}, Model{}, err
455 }
456 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
457 if err != nil {
458 return Model{}, Model{}, err
459 }
460
461 return Model{
462 Model: largeModel,
463 CatwalkCfg: *largeCatwalkModel,
464 ModelCfg: largeModelCfg,
465 }, Model{
466 Model: smallModel,
467 CatwalkCfg: *smallCatwalkModel,
468 ModelCfg: smallModelCfg,
469 }, nil
470}
471
472func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
473 hasBearerAuth := false
474 for key := range headers {
475 if strings.ToLower(key) == "authorization" {
476 hasBearerAuth = true
477 break
478 }
479 }
480
481 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
482
483 var opts []anthropic.Option
484 if apiKey != "" && !hasBearerAuth {
485 if isBearerToken {
486 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
487 headers["Authorization"] = apiKey
488 apiKey = "" // clear apiKey to avoid using X-Api-Key header
489 }
490 }
491
492 if apiKey != "" {
493 // Use standard X-Api-Key header
494 opts = append(opts, anthropic.WithAPIKey(apiKey))
495 }
496
497 if len(headers) > 0 {
498 opts = append(opts, anthropic.WithHeaders(headers))
499 }
500
501 if baseURL != "" {
502 opts = append(opts, anthropic.WithBaseURL(baseURL))
503 }
504
505 if c.cfg.Options.Debug {
506 httpClient := log.NewHTTPClient()
507 opts = append(opts, anthropic.WithHTTPClient(httpClient))
508 }
509
510 return anthropic.New(opts...)
511}
512
513func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
514 opts := []openai.Option{
515 openai.WithAPIKey(apiKey),
516 openai.WithUseResponsesAPI(),
517 }
518 if c.cfg.Options.Debug {
519 httpClient := log.NewHTTPClient()
520 opts = append(opts, openai.WithHTTPClient(httpClient))
521 }
522 if len(headers) > 0 {
523 opts = append(opts, openai.WithHeaders(headers))
524 }
525 if baseURL != "" {
526 opts = append(opts, openai.WithBaseURL(baseURL))
527 }
528 return openai.New(opts...)
529}
530
531func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
532 opts := []openrouter.Option{
533 openrouter.WithAPIKey(apiKey),
534 }
535 if c.cfg.Options.Debug {
536 httpClient := log.NewHTTPClient()
537 opts = append(opts, openrouter.WithHTTPClient(httpClient))
538 }
539 if len(headers) > 0 {
540 opts = append(opts, openrouter.WithHeaders(headers))
541 }
542 return openrouter.New(opts...)
543}
544
545func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
546 opts := []openaicompat.Option{
547 openaicompat.WithBaseURL(baseURL),
548 openaicompat.WithAPIKey(apiKey),
549 }
550 if c.cfg.Options.Debug {
551 httpClient := log.NewHTTPClient()
552 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
553 }
554 if len(headers) > 0 {
555 opts = append(opts, openaicompat.WithHeaders(headers))
556 }
557
558 for extraKey, extraValue := range extraBody {
559 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
560 }
561
562 return openaicompat.New(opts...)
563}
564
565func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
566 opts := []azure.Option{
567 azure.WithBaseURL(baseURL),
568 azure.WithAPIKey(apiKey),
569 }
570 if c.cfg.Options.Debug {
571 httpClient := log.NewHTTPClient()
572 opts = append(opts, azure.WithHTTPClient(httpClient))
573 }
574 if options == nil {
575 options = make(map[string]string)
576 }
577 if apiVersion, ok := options["apiVersion"]; ok {
578 opts = append(opts, azure.WithAPIVersion(apiVersion))
579 }
580 if len(headers) > 0 {
581 opts = append(opts, azure.WithHeaders(headers))
582 }
583
584 return azure.New(opts...)
585}
586
587func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
588 var opts []bedrock.Option
589 if c.cfg.Options.Debug {
590 httpClient := log.NewHTTPClient()
591 opts = append(opts, bedrock.WithHTTPClient(httpClient))
592 }
593 if len(headers) > 0 {
594 opts = append(opts, bedrock.WithHeaders(headers))
595 }
596 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
597 if bearerToken != "" {
598 opts = append(opts, bedrock.WithAPIKey(bearerToken))
599 }
600 return bedrock.New(opts...)
601}
602
603func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
604 opts := []google.Option{
605 google.WithBaseURL(baseURL),
606 google.WithGeminiAPIKey(apiKey),
607 }
608 if c.cfg.Options.Debug {
609 httpClient := log.NewHTTPClient()
610 opts = append(opts, google.WithHTTPClient(httpClient))
611 }
612 if len(headers) > 0 {
613 opts = append(opts, google.WithHeaders(headers))
614 }
615 return google.New(opts...)
616}
617
618func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
619 opts := []google.Option{}
620 if c.cfg.Options.Debug {
621 httpClient := log.NewHTTPClient()
622 opts = append(opts, google.WithHTTPClient(httpClient))
623 }
624 if len(headers) > 0 {
625 opts = append(opts, google.WithHeaders(headers))
626 }
627
628 project := options["project"]
629 location := options["location"]
630
631 opts = append(opts, google.WithVertex(project, location))
632
633 return google.New(opts...)
634}
635
636func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
637 if model.Think {
638 return true
639 }
640
641 if model.ProviderOptions == nil {
642 return false
643 }
644
645 opts, err := anthropic.ParseOptions(model.ProviderOptions)
646 if err != nil {
647 return false
648 }
649 if opts.Thinking != nil {
650 return true
651 }
652 return false
653}
654
655func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
656 headers := maps.Clone(providerCfg.ExtraHeaders)
657
658 // handle special headers for anthropic
659 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
660 if v, ok := headers["anthropic-beta"]; ok {
661 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
662 } else {
663 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
664 }
665 }
666
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}