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