coordinator.go

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