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