1package agent
2
3import (
4 "bytes"
5 "cmp"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "log/slog"
12 "maps"
13 "os"
14 "slices"
15 "strings"
16
17 "charm.land/fantasy"
18 "github.com/charmbracelet/catwalk/pkg/catwalk"
19 "github.com/charmbracelet/crush/internal/agent/prompt"
20 "github.com/charmbracelet/crush/internal/agent/tools"
21 "github.com/charmbracelet/crush/internal/config"
22 "github.com/charmbracelet/crush/internal/csync"
23 "github.com/charmbracelet/crush/internal/history"
24 "github.com/charmbracelet/crush/internal/hooks"
25 "github.com/charmbracelet/crush/internal/log"
26 "github.com/charmbracelet/crush/internal/lsp"
27 "github.com/charmbracelet/crush/internal/message"
28 "github.com/charmbracelet/crush/internal/permission"
29 "github.com/charmbracelet/crush/internal/session"
30
31 "charm.land/fantasy/providers/anthropic"
32 "charm.land/fantasy/providers/azure"
33 "charm.land/fantasy/providers/bedrock"
34 "charm.land/fantasy/providers/google"
35 "charm.land/fantasy/providers/openai"
36 "charm.land/fantasy/providers/openaicompat"
37 "charm.land/fantasy/providers/openrouter"
38 openaisdk "github.com/openai/openai-go/v2/option"
39 "github.com/qjebbs/go-jsons"
40)
41
42type Coordinator interface {
43 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
44 // SetMainAgent(string)
45 Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
46 Cancel(sessionID string)
47 CancelAll()
48 IsSessionBusy(sessionID string) bool
49 IsBusy() bool
50 QueuedPrompts(sessionID string) int
51 ClearQueue(sessionID string)
52 Summarize(context.Context, string) error
53 Model() Model
54 UpdateModels(ctx context.Context) error
55}
56
57type coordinator struct {
58 cfg *config.Config
59 sessions session.Service
60 messages message.Service
61 permissions permission.Service
62 history history.Service
63 lspClients *csync.Map[string, *lsp.Client]
64 hooks *hooks.Executor
65
66 currentAgent SessionAgent
67 agents map[string]SessionAgent
68}
69
70func NewCoordinator(
71 ctx context.Context,
72 cfg *config.Config,
73 sessions session.Service,
74 messages message.Service,
75 permissions permission.Service,
76 history history.Service,
77 lspClients *csync.Map[string, *lsp.Client],
78) (Coordinator, error) {
79 // Initialize hooks executor
80 hooksExecutor := hooks.NewExecutor(cfg.Hooks, cfg.WorkingDir())
81
82 c := &coordinator{
83 cfg: cfg,
84 sessions: sessions,
85 messages: messages,
86 permissions: permissions,
87 history: history,
88 lspClients: lspClients,
89 hooks: hooksExecutor,
90 agents: make(map[string]SessionAgent),
91 }
92
93 agentCfg, ok := cfg.Agents[config.AgentCoder]
94 if !ok {
95 return nil, errors.New("coder agent not configured")
96 }
97
98 // TODO: make this dynamic when we support multiple agents
99 prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
100 if err != nil {
101 return nil, err
102 }
103
104 agent, err := c.buildAgent(ctx, prompt, agentCfg)
105 if err != nil {
106 return nil, err
107 }
108 c.currentAgent = agent
109 c.agents[config.AgentCoder] = agent
110 return c, nil
111}
112
113// Run implements Coordinator.
114func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
115 model := c.currentAgent.Model()
116 maxTokens := model.CatwalkCfg.DefaultMaxTokens
117 if model.ModelCfg.MaxTokens != 0 {
118 maxTokens = model.ModelCfg.MaxTokens
119 }
120
121 if !model.CatwalkCfg.SupportsImages && attachments != nil {
122 attachments = nil
123 }
124
125 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
126 if !ok {
127 return nil, errors.New("model provider not configured")
128 }
129
130 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
131
132 return c.currentAgent.Run(ctx, SessionAgentCall{
133 SessionID: sessionID,
134 Prompt: prompt,
135 Attachments: attachments,
136 MaxOutputTokens: maxTokens,
137 ProviderOptions: mergedOptions,
138 Temperature: temp,
139 TopP: topP,
140 TopK: topK,
141 FrequencyPenalty: freqPenalty,
142 PresencePenalty: presPenalty,
143 })
144}
145
146func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
147 options := fantasy.ProviderOptions{}
148
149 cfgOpts := []byte("{}")
150 providerCfgOpts := []byte("{}")
151 catwalkOpts := []byte("{}")
152
153 if model.ModelCfg.ProviderOptions != nil {
154 data, err := json.Marshal(model.ModelCfg.ProviderOptions)
155 if err == nil {
156 cfgOpts = data
157 }
158 }
159
160 if providerCfg.ProviderOptions != nil {
161 data, err := json.Marshal(providerCfg.ProviderOptions)
162 if err == nil {
163 providerCfgOpts = data
164 }
165 }
166
167 if model.CatwalkCfg.Options.ProviderOptions != nil {
168 data, err := json.Marshal(model.CatwalkCfg.Options.ProviderOptions)
169 if err == nil {
170 catwalkOpts = data
171 }
172 }
173
174 readers := []io.Reader{
175 bytes.NewReader(catwalkOpts),
176 bytes.NewReader(providerCfgOpts),
177 bytes.NewReader(cfgOpts),
178 }
179
180 got, err := jsons.Merge(readers)
181 if err != nil {
182 slog.Error("Could not merge call config", "err", err)
183 return options
184 }
185
186 mergedOptions := make(map[string]any)
187
188 err = json.Unmarshal([]byte(got), &mergedOptions)
189 if err != nil {
190 slog.Error("Could not create config for call", "err", err)
191 return options
192 }
193
194 switch providerCfg.Type {
195 case openai.Name:
196 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
197 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
198 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
199 }
200 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
201 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
202 mergedOptions["reasoning_summary"] = "auto"
203 mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
204 }
205 parsed, err := openai.ParseResponsesOptions(mergedOptions)
206 if err == nil {
207 options[openai.Name] = parsed
208 }
209 } else {
210 parsed, err := openai.ParseOptions(mergedOptions)
211 if err == nil {
212 options[openai.Name] = parsed
213 }
214 }
215 case anthropic.Name:
216 _, hasThink := mergedOptions["thinking"]
217 if !hasThink && model.ModelCfg.Think {
218 mergedOptions["thinking"] = map[string]any{
219 // TODO: kujtim see if we need to make this dynamic
220 "budget_tokens": 2000,
221 }
222 }
223 parsed, err := anthropic.ParseOptions(mergedOptions)
224 if err == nil {
225 options[anthropic.Name] = parsed
226 }
227
228 case openrouter.Name:
229 _, hasReasoning := mergedOptions["reasoning"]
230 if !hasReasoning && model.ModelCfg.ReasoningEffort != "" {
231 mergedOptions["reasoning"] = map[string]any{
232 "enabled": true,
233 "effort": model.ModelCfg.ReasoningEffort,
234 }
235 }
236 parsed, err := openrouter.ParseOptions(mergedOptions)
237 if err == nil {
238 options[openrouter.Name] = parsed
239 }
240 case google.Name:
241 _, hasReasoning := mergedOptions["thinking_config"]
242 if !hasReasoning {
243 mergedOptions["thinking_config"] = map[string]any{
244 "thinking_budget": 2000,
245 "include_thoughts": true,
246 }
247 }
248 parsed, err := google.ParseOptions(mergedOptions)
249 if err == nil {
250 options[google.Name] = parsed
251 }
252 case azure.Name:
253 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
254 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
255 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
256 }
257 // azure uses the same options as openaicompat
258 parsed, err := openaicompat.ParseOptions(mergedOptions)
259 if err == nil {
260 options[azure.Name] = parsed
261 }
262 case openaicompat.Name:
263 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
264 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
265 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
266 }
267 parsed, err := openaicompat.ParseOptions(mergedOptions)
268 if err == nil {
269 options[openaicompat.Name] = parsed
270 }
271 }
272
273 return options
274}
275
276func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
277 modelOptions := getProviderOptions(model, cfg)
278 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
279 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
280 topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
281 freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
282 presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
283 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
284}
285
286func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent) (SessionAgent, error) {
287 large, small, err := c.buildAgentModels(ctx)
288 if err != nil {
289 return nil, err
290 }
291
292 systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
293 if err != nil {
294 return nil, err
295 }
296
297 largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
298 result := NewSessionAgent(SessionAgentOptions{
299 LargeModel: large,
300 SmallModel: small,
301 SystemPromptPrefix: largeProviderCfg.SystemPromptPrefix,
302 SystemPrompt: systemPrompt,
303 DisableAutoSummarize: c.cfg.Options.DisableAutoSummarize,
304 IsYolo: c.permissions.SkipRequests(),
305 Sessions: c.sessions,
306 Messages: c.messages,
307 Tools: nil,
308 Hooks: c.hooks,
309 })
310 go func() {
311 tools, err := c.buildTools(ctx, agent)
312 if err != nil {
313 slog.Error("could not init agent tools", "err", err)
314 return
315 }
316 result.SetTools(tools)
317 }()
318 return result, nil
319}
320
321func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
322 var allTools []fantasy.AgentTool
323 if slices.Contains(agent.AllowedTools, AgentToolName) {
324 agentTool, err := c.agentTool(ctx)
325 if err != nil {
326 return nil, err
327 }
328 allTools = append(allTools, agentTool)
329 }
330
331 allTools = append(allTools,
332 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
333 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
334 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
335 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
336 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
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}