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