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/notification"
27 "git.secluded.site/crush/internal/permission"
28 "git.secluded.site/crush/internal/session"
29 "github.com/charmbracelet/catwalk/pkg/catwalk"
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 if slices.Contains(agent.AllowedTools, tools.AgenticFetchToolName) {
356 agenticFetchTool, err := c.agenticFetchTool(ctx, nil)
357 if err != nil {
358 return nil, err
359 }
360 allTools = append(allTools, agenticFetchTool)
361 }
362
363 // Get the model name for the agent
364 modelName := ""
365 if modelCfg, ok := c.cfg.Models[agent.Model]; ok {
366 if model := c.cfg.GetModel(modelCfg.Provider, modelCfg.Model); model != nil {
367 modelName = model.Name
368 }
369 }
370
371 allTools = append(allTools,
372 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution, modelName),
373 tools.NewJobOutputTool(),
374 tools.NewJobKillTool(),
375 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
376 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
377 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
378 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
379 tools.NewGlobTool(c.cfg.WorkingDir()),
380 tools.NewGrepTool(c.cfg.WorkingDir()),
381 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
382 tools.NewSourcegraphTool(nil),
383 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
384 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
385 )
386
387 if len(c.cfg.LSP) > 0 {
388 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
389 }
390
391 var filteredTools []fantasy.AgentTool
392 for _, tool := range allTools {
393 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
394 filteredTools = append(filteredTools, tool)
395 }
396 }
397
398 for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) {
399 if agent.AllowedMCP == nil {
400 // No MCP restrictions
401 filteredTools = append(filteredTools, tool)
402 continue
403 }
404 if len(agent.AllowedMCP) == 0 {
405 // No MCPs allowed
406 slog.Warn("MCPs not allowed")
407 break
408 }
409
410 for mcp, tools := range agent.AllowedMCP {
411 if mcp != tool.MCP() {
412 continue
413 }
414 if len(tools) == 0 || slices.Contains(tools, tool.MCPToolName()) {
415 filteredTools = append(filteredTools, tool)
416 }
417 }
418 }
419 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
420 return strings.Compare(a.Info().Name, b.Info().Name)
421 })
422 return filteredTools, nil
423}
424
425// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
426func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
427 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
428 if !ok {
429 return Model{}, Model{}, errors.New("large model not selected")
430 }
431 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
432 if !ok {
433 return Model{}, Model{}, errors.New("small model not selected")
434 }
435
436 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
437 if !ok {
438 return Model{}, Model{}, errors.New("large model provider not configured")
439 }
440
441 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
442 if err != nil {
443 return Model{}, Model{}, err
444 }
445
446 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
447 if !ok {
448 return Model{}, Model{}, errors.New("large model provider not configured")
449 }
450
451 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
452 if err != nil {
453 return Model{}, Model{}, err
454 }
455
456 var largeCatwalkModel *catwalk.Model
457 var smallCatwalkModel *catwalk.Model
458
459 for _, m := range largeProviderCfg.Models {
460 if m.ID == largeModelCfg.Model {
461 largeCatwalkModel = &m
462 }
463 }
464 for _, m := range smallProviderCfg.Models {
465 if m.ID == smallModelCfg.Model {
466 smallCatwalkModel = &m
467 }
468 }
469
470 if largeCatwalkModel == nil {
471 return Model{}, Model{}, errors.New("large model not found in provider config")
472 }
473
474 if smallCatwalkModel == nil {
475 return Model{}, Model{}, errors.New("snall model not found in provider config")
476 }
477
478 largeModelID := largeModelCfg.Model
479 smallModelID := smallModelCfg.Model
480
481 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
482 largeModelID += ":exacto"
483 }
484
485 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
486 smallModelID += ":exacto"
487 }
488
489 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
490 if err != nil {
491 return Model{}, Model{}, err
492 }
493 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
494 if err != nil {
495 return Model{}, Model{}, err
496 }
497
498 return Model{
499 Model: largeModel,
500 CatwalkCfg: *largeCatwalkModel,
501 ModelCfg: largeModelCfg,
502 }, Model{
503 Model: smallModel,
504 CatwalkCfg: *smallCatwalkModel,
505 ModelCfg: smallModelCfg,
506 }, nil
507}
508
509func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
510 hasBearerAuth := false
511 for key := range headers {
512 if strings.ToLower(key) == "authorization" {
513 hasBearerAuth = true
514 break
515 }
516 }
517
518 isBearerToken := strings.HasPrefix(apiKey, "Bearer ")
519
520 var opts []anthropic.Option
521 if apiKey != "" && !hasBearerAuth {
522 if isBearerToken {
523 slog.Debug("API key starts with 'Bearer ', using as Authorization header")
524 headers["Authorization"] = apiKey
525 apiKey = "" // clear apiKey to avoid using X-Api-Key header
526 }
527 }
528
529 if apiKey != "" {
530 // Use standard X-Api-Key header
531 opts = append(opts, anthropic.WithAPIKey(apiKey))
532 }
533
534 if len(headers) > 0 {
535 opts = append(opts, anthropic.WithHeaders(headers))
536 }
537
538 if baseURL != "" {
539 opts = append(opts, anthropic.WithBaseURL(baseURL))
540 }
541
542 if c.cfg.Options.Debug {
543 httpClient := log.NewHTTPClient()
544 opts = append(opts, anthropic.WithHTTPClient(httpClient))
545 }
546
547 return anthropic.New(opts...)
548}
549
550func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
551 opts := []openai.Option{
552 openai.WithAPIKey(apiKey),
553 openai.WithUseResponsesAPI(),
554 }
555 if c.cfg.Options.Debug {
556 httpClient := log.NewHTTPClient()
557 opts = append(opts, openai.WithHTTPClient(httpClient))
558 }
559 if len(headers) > 0 {
560 opts = append(opts, openai.WithHeaders(headers))
561 }
562 if baseURL != "" {
563 opts = append(opts, openai.WithBaseURL(baseURL))
564 }
565 return openai.New(opts...)
566}
567
568func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
569 opts := []openrouter.Option{
570 openrouter.WithAPIKey(apiKey),
571 }
572 if c.cfg.Options.Debug {
573 httpClient := log.NewHTTPClient()
574 opts = append(opts, openrouter.WithHTTPClient(httpClient))
575 }
576 if len(headers) > 0 {
577 opts = append(opts, openrouter.WithHeaders(headers))
578 }
579 return openrouter.New(opts...)
580}
581
582func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
583 opts := []openaicompat.Option{
584 openaicompat.WithBaseURL(baseURL),
585 openaicompat.WithAPIKey(apiKey),
586 }
587 if c.cfg.Options.Debug {
588 httpClient := log.NewHTTPClient()
589 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
590 }
591 if len(headers) > 0 {
592 opts = append(opts, openaicompat.WithHeaders(headers))
593 }
594
595 for extraKey, extraValue := range extraBody {
596 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
597 }
598
599 return openaicompat.New(opts...)
600}
601
602func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
603 opts := []azure.Option{
604 azure.WithBaseURL(baseURL),
605 azure.WithAPIKey(apiKey),
606 azure.WithUseResponsesAPI(),
607 }
608 if c.cfg.Options.Debug {
609 httpClient := log.NewHTTPClient()
610 opts = append(opts, azure.WithHTTPClient(httpClient))
611 }
612 if options == nil {
613 options = make(map[string]string)
614 }
615 if apiVersion, ok := options["apiVersion"]; ok {
616 opts = append(opts, azure.WithAPIVersion(apiVersion))
617 }
618 if len(headers) > 0 {
619 opts = append(opts, azure.WithHeaders(headers))
620 }
621
622 return azure.New(opts...)
623}
624
625func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
626 var opts []bedrock.Option
627 if c.cfg.Options.Debug {
628 httpClient := log.NewHTTPClient()
629 opts = append(opts, bedrock.WithHTTPClient(httpClient))
630 }
631 if len(headers) > 0 {
632 opts = append(opts, bedrock.WithHeaders(headers))
633 }
634 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
635 if bearerToken != "" {
636 opts = append(opts, bedrock.WithAPIKey(bearerToken))
637 }
638 return bedrock.New(opts...)
639}
640
641func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
642 opts := []google.Option{
643 google.WithBaseURL(baseURL),
644 google.WithGeminiAPIKey(apiKey),
645 }
646 if c.cfg.Options.Debug {
647 httpClient := log.NewHTTPClient()
648 opts = append(opts, google.WithHTTPClient(httpClient))
649 }
650 if len(headers) > 0 {
651 opts = append(opts, google.WithHeaders(headers))
652 }
653 return google.New(opts...)
654}
655
656func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
657 opts := []google.Option{}
658 if c.cfg.Options.Debug {
659 httpClient := log.NewHTTPClient()
660 opts = append(opts, google.WithHTTPClient(httpClient))
661 }
662 if len(headers) > 0 {
663 opts = append(opts, google.WithHeaders(headers))
664 }
665
666 project := options["project"]
667 location := options["location"]
668
669 opts = append(opts, google.WithVertex(project, location))
670
671 return google.New(opts...)
672}
673
674func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
675 if model.Think {
676 return true
677 }
678
679 if model.ProviderOptions == nil {
680 return false
681 }
682
683 opts, err := anthropic.ParseOptions(model.ProviderOptions)
684 if err != nil {
685 return false
686 }
687 if opts.Thinking != nil {
688 return true
689 }
690 return false
691}
692
693func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
694 headers := maps.Clone(providerCfg.ExtraHeaders)
695 if headers == nil {
696 headers = make(map[string]string)
697 }
698
699 // handle special headers for anthropic
700 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
701 if v, ok := headers["anthropic-beta"]; ok {
702 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
703 } else {
704 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
705 }
706 }
707
708 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
709 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
710
711 switch providerCfg.Type {
712 case openai.Name:
713 return c.buildOpenaiProvider(baseURL, apiKey, headers)
714 case anthropic.Name:
715 return c.buildAnthropicProvider(baseURL, apiKey, headers)
716 case openrouter.Name:
717 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
718 case azure.Name:
719 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
720 case bedrock.Name:
721 return c.buildBedrockProvider(headers)
722 case google.Name:
723 return c.buildGoogleProvider(baseURL, apiKey, headers)
724 case "google-vertex":
725 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
726 case openaicompat.Name:
727 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
728 default:
729 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
730 }
731}
732
733func isExactoSupported(modelID string) bool {
734 supportedModels := []string{
735 "moonshotai/kimi-k2-0905",
736 "deepseek/deepseek-v3.1-terminus",
737 "z-ai/glm-4.6",
738 "openai/gpt-oss-120b",
739 "qwen/qwen3-coder",
740 }
741 return slices.Contains(supportedModels, modelID)
742}
743
744func (c *coordinator) Cancel(sessionID string) {
745 c.currentAgent.Cancel(sessionID)
746}
747
748func (c *coordinator) CancelAll() {
749 c.currentAgent.CancelAll()
750}
751
752func (c *coordinator) ClearQueue(sessionID string) {
753 c.currentAgent.ClearQueue(sessionID)
754}
755
756func (c *coordinator) IsBusy() bool {
757 return c.currentAgent.IsBusy()
758}
759
760func (c *coordinator) IsSessionBusy(sessionID string) bool {
761 return c.currentAgent.IsSessionBusy(sessionID)
762}
763
764func (c *coordinator) Model() Model {
765 return c.currentAgent.Model()
766}
767
768func (c *coordinator) UpdateModels(ctx context.Context) error {
769 // build the models again so we make sure we get the latest config
770 large, small, err := c.buildAgentModels(ctx)
771 if err != nil {
772 return err
773 }
774 c.currentAgent.SetModels(large, small)
775
776 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
777 if !ok {
778 return errors.New("coder agent not configured")
779 }
780
781 tools, err := c.buildTools(ctx, agentCfg)
782 if err != nil {
783 return err
784 }
785 c.currentAgent.SetTools(tools)
786 return nil
787}
788
789func (c *coordinator) QueuedPrompts(sessionID string) int {
790 return c.currentAgent.QueuedPrompts(sessionID)
791}
792
793func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
794 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
795 if !ok {
796 return errors.New("model provider not configured")
797 }
798 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
799}