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