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