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