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 "golang.org/x/sync/errgroup"
31
32 "charm.land/fantasy/providers/anthropic"
33 "charm.land/fantasy/providers/azure"
34 "charm.land/fantasy/providers/bedrock"
35 "charm.land/fantasy/providers/google"
36 "charm.land/fantasy/providers/openai"
37 "charm.land/fantasy/providers/openaicompat"
38 "charm.land/fantasy/providers/openrouter"
39 openaisdk "github.com/openai/openai-go/v2/option"
40 "github.com/qjebbs/go-jsons"
41)
42
43type Coordinator interface {
44 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
45 // SetMainAgent(string)
46 Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
47 Cancel(sessionID string)
48 CancelAll()
49 IsSessionBusy(sessionID string) bool
50 IsBusy() bool
51 QueuedPrompts(sessionID string) int
52 ClearQueue(sessionID string)
53 Summarize(context.Context, string) error
54 Model() Model
55 UpdateModels(ctx context.Context) error
56}
57
58type coordinator struct {
59 cfg *config.Config
60 sessions session.Service
61 messages message.Service
62 permissions permission.Service
63 history history.Service
64 hooks hooks.Service
65 lspClients *csync.Map[string, *lsp.Client]
66
67 currentAgent SessionAgent
68 agents map[string]SessionAgent
69
70 readyWg errgroup.Group
71}
72
73func NewCoordinator(
74 ctx context.Context,
75 cfg *config.Config,
76 sessions session.Service,
77 messages message.Service,
78 permissions permission.Service,
79 history history.Service,
80 lspClients *csync.Map[string, *lsp.Client],
81 hooks hooks.Service,
82) (Coordinator, error) {
83 c := &coordinator{
84 cfg: cfg,
85 sessions: sessions,
86 messages: messages,
87 permissions: permissions,
88 history: history,
89 lspClients: lspClients,
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, hooks)
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 if err := c.readyWg.Wait(); err != nil {
116 return nil, err
117 }
118
119 model := c.currentAgent.Model()
120 maxTokens := model.CatwalkCfg.DefaultMaxTokens
121 if model.ModelCfg.MaxTokens != 0 {
122 maxTokens = model.ModelCfg.MaxTokens
123 }
124
125 if !model.CatwalkCfg.SupportsImages && attachments != nil {
126 attachments = nil
127 }
128
129 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
130 if !ok {
131 return nil, errors.New("model provider not configured")
132 }
133
134 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
135
136 return c.currentAgent.Run(ctx, SessionAgentCall{
137 SessionID: sessionID,
138 Prompt: prompt,
139 Attachments: attachments,
140 MaxOutputTokens: maxTokens,
141 ProviderOptions: mergedOptions,
142 Temperature: temp,
143 TopP: topP,
144 TopK: topK,
145 FrequencyPenalty: freqPenalty,
146 PresencePenalty: presPenalty,
147 })
148}
149
150func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
151 options := fantasy.ProviderOptions{}
152
153 cfgOpts := []byte("{}")
154 providerCfgOpts := []byte("{}")
155 catwalkOpts := []byte("{}")
156
157 if model.ModelCfg.ProviderOptions != nil {
158 data, err := json.Marshal(model.ModelCfg.ProviderOptions)
159 if err == nil {
160 cfgOpts = data
161 }
162 }
163
164 if providerCfg.ProviderOptions != nil {
165 data, err := json.Marshal(providerCfg.ProviderOptions)
166 if err == nil {
167 providerCfgOpts = data
168 }
169 }
170
171 if model.CatwalkCfg.Options.ProviderOptions != nil {
172 data, err := json.Marshal(model.CatwalkCfg.Options.ProviderOptions)
173 if err == nil {
174 catwalkOpts = data
175 }
176 }
177
178 readers := []io.Reader{
179 bytes.NewReader(catwalkOpts),
180 bytes.NewReader(providerCfgOpts),
181 bytes.NewReader(cfgOpts),
182 }
183
184 got, err := jsons.Merge(readers)
185 if err != nil {
186 slog.Error("Could not merge call config", "err", err)
187 return options
188 }
189
190 mergedOptions := make(map[string]any)
191
192 err = json.Unmarshal([]byte(got), &mergedOptions)
193 if err != nil {
194 slog.Error("Could not create config for call", "err", err)
195 return options
196 }
197
198 switch providerCfg.Type {
199 case openai.Name, azure.Name:
200 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
201 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
202 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
203 }
204 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
205 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
206 mergedOptions["reasoning_summary"] = "auto"
207 mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
208 }
209 parsed, err := openai.ParseResponsesOptions(mergedOptions)
210 if err == nil {
211 options[openai.Name] = parsed
212 }
213 } else {
214 parsed, err := openai.ParseOptions(mergedOptions)
215 if err == nil {
216 options[openai.Name] = parsed
217 }
218 }
219 case anthropic.Name:
220 _, hasThink := mergedOptions["thinking"]
221 if !hasThink && model.ModelCfg.Think {
222 mergedOptions["thinking"] = map[string]any{
223 // TODO: kujtim see if we need to make this dynamic
224 "budget_tokens": 2000,
225 }
226 }
227 parsed, err := anthropic.ParseOptions(mergedOptions)
228 if err == nil {
229 options[anthropic.Name] = parsed
230 }
231
232 case openrouter.Name:
233 _, hasReasoning := mergedOptions["reasoning"]
234 if !hasReasoning && model.ModelCfg.ReasoningEffort != "" {
235 mergedOptions["reasoning"] = map[string]any{
236 "enabled": true,
237 "effort": model.ModelCfg.ReasoningEffort,
238 }
239 }
240 parsed, err := openrouter.ParseOptions(mergedOptions)
241 if err == nil {
242 options[openrouter.Name] = parsed
243 }
244 case google.Name:
245 _, hasReasoning := mergedOptions["thinking_config"]
246 if !hasReasoning {
247 mergedOptions["thinking_config"] = map[string]any{
248 "thinking_budget": 2000,
249 "include_thoughts": true,
250 }
251 }
252 parsed, err := google.ParseOptions(mergedOptions)
253 if err == nil {
254 options[google.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, hooks hooks.Service) (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
293 result := NewSessionAgent(SessionAgentOptions{
294 large,
295 small,
296 largeProviderCfg.SystemPromptPrefix,
297 systemPrompt,
298 c.cfg.Options.DisableAutoSummarize,
299 c.permissions.SkipRequests(),
300 c.sessions,
301 c.messages,
302 hooks,
303 nil,
304 })
305 c.readyWg.Go(func() error {
306 tools, err := c.buildTools(ctx, agent)
307 if err != nil {
308 return err
309 }
310 result.SetTools(tools)
311 return nil
312 })
313
314 return result, nil
315}
316
317func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
318 var allTools []fantasy.AgentTool
319 if slices.Contains(agent.AllowedTools, AgentToolName) {
320 agentTool, err := c.agentTool(ctx)
321 if err != nil {
322 return nil, err
323 }
324 allTools = append(allTools, agentTool)
325 }
326
327 if slices.Contains(agent.AllowedTools, tools.AgenticFetchToolName) {
328 agenticFetchTool, err := c.agenticFetchTool(ctx, nil)
329 if err != nil {
330 return nil, err
331 }
332 allTools = append(allTools, agenticFetchTool)
333 }
334
335 // Get the model name for the agent
336 modelName := ""
337 if modelCfg, ok := c.cfg.Models[agent.Model]; ok {
338 if model := c.cfg.GetModel(modelCfg.Provider, modelCfg.Model); model != nil {
339 modelName = model.Name
340 }
341 }
342
343 allTools = append(allTools,
344 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution, modelName),
345 tools.NewJobOutputTool(),
346 tools.NewJobKillTool(),
347 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
348 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
349 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
350 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
351 tools.NewGlobTool(c.cfg.WorkingDir()),
352 tools.NewGrepTool(c.cfg.WorkingDir()),
353 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
354 tools.NewSourcegraphTool(nil),
355 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
356 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
357 )
358
359 if len(c.cfg.LSP) > 0 {
360 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
361 }
362
363 var filteredTools []fantasy.AgentTool
364 for _, tool := range allTools {
365 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
366 filteredTools = append(filteredTools, tool)
367 }
368 }
369
370 for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) {
371 if agent.AllowedMCP == nil {
372 // No MCP restrictions
373 filteredTools = append(filteredTools, tool)
374 continue
375 }
376 if len(agent.AllowedMCP) == 0 {
377 // No MCPs allowed
378 slog.Debug("no MCPs allowed", "tool", tool.Name(), "agent", agent.Name)
379 break
380 }
381
382 for mcp, tools := range agent.AllowedMCP {
383 if mcp != tool.MCP() {
384 continue
385 }
386 if len(tools) == 0 || slices.Contains(tools, tool.MCPToolName()) {
387 filteredTools = append(filteredTools, tool)
388 }
389 }
390 slog.Debug("MCP not allowed", "tool", tool.Name(), "agent", agent.Name)
391 }
392 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
393 return strings.Compare(a.Info().Name, b.Info().Name)
394 })
395 return filteredTools, nil
396}
397
398// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
399func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
400 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
401 if !ok {
402 return Model{}, Model{}, errors.New("large model not selected")
403 }
404 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
405 if !ok {
406 return Model{}, Model{}, errors.New("small model not selected")
407 }
408
409 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
410 if !ok {
411 return Model{}, Model{}, errors.New("large model provider not configured")
412 }
413
414 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
415 if err != nil {
416 return Model{}, Model{}, err
417 }
418
419 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
420 if !ok {
421 return Model{}, Model{}, errors.New("large model provider not configured")
422 }
423
424 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
425 if err != nil {
426 return Model{}, Model{}, err
427 }
428
429 var largeCatwalkModel *catwalk.Model
430 var smallCatwalkModel *catwalk.Model
431
432 for _, m := range largeProviderCfg.Models {
433 if m.ID == largeModelCfg.Model {
434 largeCatwalkModel = &m
435 }
436 }
437 for _, m := range smallProviderCfg.Models {
438 if m.ID == smallModelCfg.Model {
439 smallCatwalkModel = &m
440 }
441 }
442
443 if largeCatwalkModel == nil {
444 return Model{}, Model{}, errors.New("large model not found in provider config")
445 }
446
447 if smallCatwalkModel == nil {
448 return Model{}, Model{}, errors.New("snall model not found in provider config")
449 }
450
451 largeModelID := largeModelCfg.Model
452 smallModelID := smallModelCfg.Model
453
454 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
455 largeModelID += ":exacto"
456 }
457
458 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
459 smallModelID += ":exacto"
460 }
461
462 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
463 if err != nil {
464 return Model{}, Model{}, err
465 }
466 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
467 if err != nil {
468 return Model{}, Model{}, err
469 }
470
471 return Model{
472 Model: largeModel,
473 CatwalkCfg: *largeCatwalkModel,
474 ModelCfg: largeModelCfg,
475 }, Model{
476 Model: smallModel,
477 CatwalkCfg: *smallCatwalkModel,
478 ModelCfg: smallModelCfg,
479 }, nil
480}
481
482func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
483 hasBearerAuth := false
484 for key := range headers {
485 if strings.ToLower(key) == "authorization" {
486 hasBearerAuth = true
487 break
488 }
489 }
490
491 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
492
493 var opts []anthropic.Option
494 if apiKey != "" && !hasBearerAuth {
495 if isBearerToken {
496 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
497 headers["Authorization"] = apiKey
498 apiKey = "" // clear apiKey to avoid using X-Api-Key header
499 }
500 }
501
502 if apiKey != "" {
503 // Use standard X-Api-Key header
504 opts = append(opts, anthropic.WithAPIKey(apiKey))
505 }
506
507 if len(headers) > 0 {
508 opts = append(opts, anthropic.WithHeaders(headers))
509 }
510
511 if baseURL != "" {
512 opts = append(opts, anthropic.WithBaseURL(baseURL))
513 }
514
515 if c.cfg.Options.Debug {
516 httpClient := log.NewHTTPClient()
517 opts = append(opts, anthropic.WithHTTPClient(httpClient))
518 }
519
520 return anthropic.New(opts...)
521}
522
523func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
524 opts := []openai.Option{
525 openai.WithAPIKey(apiKey),
526 openai.WithUseResponsesAPI(),
527 }
528 if c.cfg.Options.Debug {
529 httpClient := log.NewHTTPClient()
530 opts = append(opts, openai.WithHTTPClient(httpClient))
531 }
532 if len(headers) > 0 {
533 opts = append(opts, openai.WithHeaders(headers))
534 }
535 if baseURL != "" {
536 opts = append(opts, openai.WithBaseURL(baseURL))
537 }
538 return openai.New(opts...)
539}
540
541func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
542 opts := []openrouter.Option{
543 openrouter.WithAPIKey(apiKey),
544 }
545 if c.cfg.Options.Debug {
546 httpClient := log.NewHTTPClient()
547 opts = append(opts, openrouter.WithHTTPClient(httpClient))
548 }
549 if len(headers) > 0 {
550 opts = append(opts, openrouter.WithHeaders(headers))
551 }
552 return openrouter.New(opts...)
553}
554
555func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
556 opts := []openaicompat.Option{
557 openaicompat.WithBaseURL(baseURL),
558 openaicompat.WithAPIKey(apiKey),
559 }
560 if c.cfg.Options.Debug {
561 httpClient := log.NewHTTPClient()
562 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
563 }
564 if len(headers) > 0 {
565 opts = append(opts, openaicompat.WithHeaders(headers))
566 }
567
568 for extraKey, extraValue := range extraBody {
569 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
570 }
571
572 return openaicompat.New(opts...)
573}
574
575func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
576 opts := []azure.Option{
577 azure.WithBaseURL(baseURL),
578 azure.WithAPIKey(apiKey),
579 azure.WithUseResponsesAPI(),
580 }
581 if c.cfg.Options.Debug {
582 httpClient := log.NewHTTPClient()
583 opts = append(opts, azure.WithHTTPClient(httpClient))
584 }
585 if options == nil {
586 options = make(map[string]string)
587 }
588 if apiVersion, ok := options["apiVersion"]; ok {
589 opts = append(opts, azure.WithAPIVersion(apiVersion))
590 }
591 if len(headers) > 0 {
592 opts = append(opts, azure.WithHeaders(headers))
593 }
594
595 return azure.New(opts...)
596}
597
598func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
599 var opts []bedrock.Option
600 if c.cfg.Options.Debug {
601 httpClient := log.NewHTTPClient()
602 opts = append(opts, bedrock.WithHTTPClient(httpClient))
603 }
604 if len(headers) > 0 {
605 opts = append(opts, bedrock.WithHeaders(headers))
606 }
607 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
608 if bearerToken != "" {
609 opts = append(opts, bedrock.WithAPIKey(bearerToken))
610 }
611 return bedrock.New(opts...)
612}
613
614func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
615 opts := []google.Option{
616 google.WithBaseURL(baseURL),
617 google.WithGeminiAPIKey(apiKey),
618 }
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 return google.New(opts...)
627}
628
629func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
630 opts := []google.Option{}
631 if c.cfg.Options.Debug {
632 httpClient := log.NewHTTPClient()
633 opts = append(opts, google.WithHTTPClient(httpClient))
634 }
635 if len(headers) > 0 {
636 opts = append(opts, google.WithHeaders(headers))
637 }
638
639 project := options["project"]
640 location := options["location"]
641
642 opts = append(opts, google.WithVertex(project, location))
643
644 return google.New(opts...)
645}
646
647func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
648 if model.Think {
649 return true
650 }
651
652 if model.ProviderOptions == nil {
653 return false
654 }
655
656 opts, err := anthropic.ParseOptions(model.ProviderOptions)
657 if err != nil {
658 return false
659 }
660 if opts.Thinking != nil {
661 return true
662 }
663 return false
664}
665
666func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
667 headers := maps.Clone(providerCfg.ExtraHeaders)
668 if headers == nil {
669 headers = make(map[string]string)
670 }
671
672 // handle special headers for anthropic
673 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
674 if v, ok := headers["anthropic-beta"]; ok {
675 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
676 } else {
677 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
678 }
679 }
680
681 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
682 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
683
684 switch providerCfg.Type {
685 case openai.Name:
686 return c.buildOpenaiProvider(baseURL, apiKey, headers)
687 case anthropic.Name:
688 return c.buildAnthropicProvider(baseURL, apiKey, headers)
689 case openrouter.Name:
690 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
691 case azure.Name:
692 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
693 case bedrock.Name:
694 return c.buildBedrockProvider(headers)
695 case google.Name:
696 return c.buildGoogleProvider(baseURL, apiKey, headers)
697 case "google-vertex":
698 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
699 case openaicompat.Name:
700 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
701 default:
702 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
703 }
704}
705
706func isExactoSupported(modelID string) bool {
707 supportedModels := []string{
708 "moonshotai/kimi-k2-0905",
709 "deepseek/deepseek-v3.1-terminus",
710 "z-ai/glm-4.6",
711 "openai/gpt-oss-120b",
712 "qwen/qwen3-coder",
713 }
714 return slices.Contains(supportedModels, modelID)
715}
716
717func (c *coordinator) Cancel(sessionID string) {
718 c.currentAgent.Cancel(sessionID)
719}
720
721func (c *coordinator) CancelAll() {
722 c.currentAgent.CancelAll()
723}
724
725func (c *coordinator) ClearQueue(sessionID string) {
726 c.currentAgent.ClearQueue(sessionID)
727}
728
729func (c *coordinator) IsBusy() bool {
730 return c.currentAgent.IsBusy()
731}
732
733func (c *coordinator) IsSessionBusy(sessionID string) bool {
734 return c.currentAgent.IsSessionBusy(sessionID)
735}
736
737func (c *coordinator) Model() Model {
738 return c.currentAgent.Model()
739}
740
741func (c *coordinator) UpdateModels(ctx context.Context) error {
742 // build the models again so we make sure we get the latest config
743 large, small, err := c.buildAgentModels(ctx)
744 if err != nil {
745 return err
746 }
747 c.currentAgent.SetModels(large, small)
748
749 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
750 if !ok {
751 return errors.New("coder agent not configured")
752 }
753
754 tools, err := c.buildTools(ctx, agentCfg)
755 if err != nil {
756 return err
757 }
758 c.currentAgent.SetTools(tools)
759 return nil
760}
761
762func (c *coordinator) QueuedPrompts(sessionID string) int {
763 return c.currentAgent.QueuedPrompts(sessionID)
764}
765
766func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
767 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
768 if !ok {
769 return errors.New("model provider not configured")
770 }
771 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
772}