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