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