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