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