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