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