on 94de6bc help sign in
crush/internal/agent/coordinator.go
775 lines 23 kB history raw
1package agent
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"
17 "charm.land/fantasy"
18 "github.com/charmbracelet/catwalk/pkg/catwalk"
19 "github.com/charmbracelet/crush/internal/agent/prompt"
20 "github.com/charmbracelet/crush/internal/agent/tools"
21 "github.com/charmbracelet/crush/internal/config"
22 "github.com/charmbracelet/crush/internal/csync"
23 "github.com/charmbracelet/crush/internal/history"
24 "github.com/charmbracelet/crush/internal/log"
25 "github.com/charmbracelet/crush/internal/lsp"
26 "github.com/charmbracelet/crush/internal/message"
27 "github.com/charmbracelet/crush/internal/permission"
28 "github.com/charmbracelet/crush/internal/session"
29 "golang.org/x/sync/errgroup"
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"
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
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]
65 currentAgent SessionAgent
66 agents map[string]SessionAgent
68 readyWg errgroup.Group
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 }
90 agentCfg, ok := cfg.Agents[config.AgentCoder]
91 if !ok {
92 return nil, errors.New("coder agent not configured")
93 }
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 }
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
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 }
116 model := c.currentAgent.Model()
117 maxTokens := model.CatwalkCfg.DefaultMaxTokens
118 if model.ModelCfg.MaxTokens != 0 {
119 maxTokens = model.ModelCfg.MaxTokens
120 }
122 if !model.CatwalkCfg.SupportsImages && attachments != nil {
123 attachments = nil
124 }
126 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
127 if !ok {
128 return nil, errors.New("model provider not configured")
129 }
131 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
133 if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
134 slog.Info("Detected expired OAuth token, attempting refresh", "provider", providerCfg.ID)
135 if refreshErr := c.cfg.RefreshOAuthToken(ctx, providerCfg.ID); refreshErr != nil {
136 slog.Error("Failed to refresh OAuth token", "provider", providerCfg.ID, "error", refreshErr)
137 return nil, refreshErr
138 }
140 // Rebuild models with refreshed token
141 if updateErr := c.UpdateModels(ctx); updateErr != nil {
142 slog.Error("Failed to update models after token refresh", "error", updateErr)
143 return nil, updateErr
144 }
145 }
146 result, err := c.currentAgent.Run(ctx, SessionAgentCall{
147 SessionID: sessionID,
148 Prompt: prompt,
149 Attachments: attachments,
150 MaxOutputTokens: maxTokens,
151 ProviderOptions: mergedOptions,
152 Temperature: temp,
153 TopP: topP,
154 TopK: topK,
155 FrequencyPenalty: freqPenalty,
156 PresencePenalty: presPenalty,
157 })
158 return result, err
161func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
162 options := fantasy.ProviderOptions{}
164 cfgOpts := []byte("{}")
165 providerCfgOpts := []byte("{}")
166 catwalkOpts := []byte("{}")
168 if model.ModelCfg.ProviderOptions != nil {
169 data, err := json.Marshal(model.ModelCfg.ProviderOptions)
170 if err == nil {
171 cfgOpts = data
172 }
173 }
175 if providerCfg.ProviderOptions != nil {
176 data, err := json.Marshal(providerCfg.ProviderOptions)
177 if err == nil {
178 providerCfgOpts = data
179 }
180 }
182 if model.CatwalkCfg.Options.ProviderOptions != nil {
183 data, err := json.Marshal(model.CatwalkCfg.Options.ProviderOptions)
184 if err == nil {
185 catwalkOpts = data
186 }
187 }
189 readers := []io.Reader{
190 bytes.NewReader(catwalkOpts),
191 bytes.NewReader(providerCfgOpts),
192 bytes.NewReader(cfgOpts),
193 }
195 got, err := jsons.Merge(readers)
196 if err != nil {
197 slog.Error("Could not merge call config", "err", err)
198 return options
199 }
201 mergedOptions := make(map[string]any)
203 err = json.Unmarshal([]byte(got), &mergedOptions)
204 if err != nil {
205 slog.Error("Could not create config for call", "err", err)
206 return options
207 }
209 switch providerCfg.Type {
210 case openai.Name, azure.Name:
211 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
212 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
213 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
214 }
215 if openai.IsResponsesModel(model.CatwalkCfg.ID) {
216 if openai.IsResponsesReasoningModel(model.CatwalkCfg.ID) {
217 mergedOptions["reasoning_summary"] = "auto"
218 mergedOptions["include"] = []openai.IncludeType{openai.IncludeReasoningEncryptedContent}
219 }
220 parsed, err := openai.ParseResponsesOptions(mergedOptions)
221 if err == nil {
222 options[openai.Name] = parsed
223 }
224 } else {
225 parsed, err := openai.ParseOptions(mergedOptions)
226 if err == nil {
227 options[openai.Name] = parsed
228 }
229 }
230 case anthropic.Name:
231 _, hasThink := mergedOptions["thinking"]
232 if !hasThink && model.ModelCfg.Think {
233 mergedOptions["thinking"] = map[string]any{
234 // TODO: kujtim see if we need to make this dynamic
235 "budget_tokens": 2000,
236 }
237 }
238 parsed, err := anthropic.ParseOptions(mergedOptions)
239 if err == nil {
240 options[anthropic.Name] = parsed
241 }
243 case openrouter.Name:
244 _, hasReasoning := mergedOptions["reasoning"]
245 if !hasReasoning && model.ModelCfg.ReasoningEffort != "" {
246 mergedOptions["reasoning"] = map[string]any{
247 "enabled": true,
248 "effort": model.ModelCfg.ReasoningEffort,
249 }
250 }
251 parsed, err := openrouter.ParseOptions(mergedOptions)
252 if err == nil {
253 options[openrouter.Name] = parsed
254 }
255 case google.Name:
256 _, hasReasoning := mergedOptions["thinking_config"]
257 if !hasReasoning {
258 mergedOptions["thinking_config"] = map[string]any{
259 "thinking_budget": 2000,
260 "include_thoughts": true,
261 }
262 }
263 parsed, err := google.ParseOptions(mergedOptions)
264 if err == nil {
265 options[google.Name] = parsed
266 }
267 case openaicompat.Name:
268 _, hasReasoningEffort := mergedOptions["reasoning_effort"]
269 if !hasReasoningEffort && model.ModelCfg.ReasoningEffort != "" {
270 mergedOptions["reasoning_effort"] = model.ModelCfg.ReasoningEffort
271 }
272 parsed, err := openaicompat.ParseOptions(mergedOptions)
273 if err == nil {
274 options[openaicompat.Name] = parsed
275 }
276 }
278 return options
281func mergeCallOptions(model Model, cfg config.ProviderConfig) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
282 modelOptions := getProviderOptions(model, cfg)
283 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
284 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
285 topK := cmp.Or(model.ModelCfg.TopK, model.CatwalkCfg.Options.TopK)
286 freqPenalty := cmp.Or(model.ModelCfg.FrequencyPenalty, model.CatwalkCfg.Options.FrequencyPenalty)
287 presPenalty := cmp.Or(model.ModelCfg.PresencePenalty, model.CatwalkCfg.Options.PresencePenalty)
288 return modelOptions, temp, topP, topK, freqPenalty, presPenalty
291func (c *coordinator) buildAgent(ctx context.Context, prompt *prompt.Prompt, agent config.Agent) (SessionAgent, error) {
292 large, small, err := c.buildAgentModels(ctx)
293 if err != nil {
294 return nil, err
295 }
297 systemPrompt, err := prompt.Build(ctx, large.Model.Provider(), large.Model.Model(), *c.cfg)
298 if err != nil {
299 return nil, err
300 }
302 largeProviderCfg, _ := c.cfg.Providers.Get(large.ModelCfg.Provider)
303 result := NewSessionAgent(SessionAgentOptions{
304 large,
305 small,
306 largeProviderCfg.SystemPromptPrefix,
307 systemPrompt,
308 c.cfg.Options.DisableAutoSummarize,
309 c.permissions.SkipRequests(),
310 c.sessions,
311 c.messages,
312 nil,
313 })
314 c.readyWg.Go(func() error {
315 tools, err := c.buildTools(ctx, agent)
316 if err != nil {
317 return err
318 }
319 result.SetTools(tools)
320 return nil
321 })
323 return result, nil
326func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fantasy.AgentTool, error) {
327 var allTools []fantasy.AgentTool
328 if slices.Contains(agent.AllowedTools, AgentToolName) {
329 agentTool, err := c.agentTool(ctx)
330 if err != nil {
331 return nil, err
332 }
333 allTools = append(allTools, agentTool)
334 }
336 if slices.Contains(agent.AllowedTools, tools.AgenticFetchToolName) {
337 agenticFetchTool, err := c.agenticFetchTool(ctx, nil)
338 if err != nil {
339 return nil, err
340 }
341 allTools = append(allTools, agenticFetchTool)
342 }
344 // Get the model name for the agent
345 modelName := ""
346 if modelCfg, ok := c.cfg.Models[agent.Model]; ok {
347 if model := c.cfg.GetModel(modelCfg.Provider, modelCfg.Model); model != nil {
348 modelName = model.Name
349 }
350 }
352 allTools = append(allTools,
353 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution, modelName),
354 tools.NewJobOutputTool(),
355 tools.NewJobKillTool(),
356 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
357 tools.NewEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
358 tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
359 tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil),
360 tools.NewGlobTool(c.cfg.WorkingDir()),
361 tools.NewGrepTool(c.cfg.WorkingDir()),
362 tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls),
363 tools.NewSourcegraphTool(nil),
364 tools.NewViewTool(c.lspClients, c.permissions, c.cfg.WorkingDir()),
365 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
366 )
368 if len(c.cfg.LSP) > 0 {
369 allTools = append(allTools, tools.NewDiagnosticsTool(c.lspClients), tools.NewReferencesTool(c.lspClients))
370 }
372 var filteredTools []fantasy.AgentTool
373 for _, tool := range allTools {
374 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
375 filteredTools = append(filteredTools, tool)
376 }
377 }
379 for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) {
380 if agent.AllowedMCP == nil {
381 // No MCP restrictions
382 filteredTools = append(filteredTools, tool)
383 continue
384 }
385 if len(agent.AllowedMCP) == 0 {
386 // No MCPs allowed
387 slog.Debug("no MCPs allowed", "tool", tool.Name(), "agent", agent.Name)
388 break
389 }
391 for mcp, tools := range agent.AllowedMCP {
392 if mcp != tool.MCP() {
393 continue
394 }
395 if len(tools) == 0 || slices.Contains(tools, tool.MCPToolName()) {
396 filteredTools = append(filteredTools, tool)
397 }
398 }
399 slog.Debug("MCP not allowed", "tool", tool.Name(), "agent", agent.Name)
400 }
401 slices.SortFunc(filteredTools, func(a, b fantasy.AgentTool) int {
402 return strings.Compare(a.Info().Name, b.Info().Name)
403 })
404 return filteredTools, nil
407// TODO: when we support multiple agents we need to change this so that we pass in the agent specific model config
408func (c *coordinator) buildAgentModels(ctx context.Context) (Model, Model, error) {
409 largeModelCfg, ok := c.cfg.Models[config.SelectedModelTypeLarge]
410 if !ok {
411 return Model{}, Model{}, errors.New("large model not selected")
412 }
413 smallModelCfg, ok := c.cfg.Models[config.SelectedModelTypeSmall]
414 if !ok {
415 return Model{}, Model{}, errors.New("small model not selected")
416 }
418 largeProviderCfg, ok := c.cfg.Providers.Get(largeModelCfg.Provider)
419 if !ok {
420 return Model{}, Model{}, errors.New("large model provider not configured")
421 }
423 largeProvider, err := c.buildProvider(largeProviderCfg, largeModelCfg)
424 if err != nil {
425 return Model{}, Model{}, err
426 }
428 smallProviderCfg, ok := c.cfg.Providers.Get(smallModelCfg.Provider)
429 if !ok {
430 return Model{}, Model{}, errors.New("large model provider not configured")
431 }
433 smallProvider, err := c.buildProvider(smallProviderCfg, largeModelCfg)
434 if err != nil {
435 return Model{}, Model{}, err
436 }
438 var largeCatwalkModel *catwalk.Model
439 var smallCatwalkModel *catwalk.Model
441 for _, m := range largeProviderCfg.Models {
442 if m.ID == largeModelCfg.Model {
443 largeCatwalkModel = &m
444 }
445 }
446 for _, m := range smallProviderCfg.Models {
447 if m.ID == smallModelCfg.Model {
448 smallCatwalkModel = &m
449 }
450 }
452 if largeCatwalkModel == nil {
453 return Model{}, Model{}, errors.New("large model not found in provider config")
454 }
456 if smallCatwalkModel == nil {
457 return Model{}, Model{}, errors.New("snall model not found in provider config")
458 }
460 largeModelID := largeModelCfg.Model
461 smallModelID := smallModelCfg.Model
463 if largeModelCfg.Provider == openrouter.Name && isExactoSupported(largeModelID) {
464 largeModelID += ":exacto"
465 }
467 if smallModelCfg.Provider == openrouter.Name && isExactoSupported(smallModelID) {
468 smallModelID += ":exacto"
469 }
471 largeModel, err := largeProvider.LanguageModel(ctx, largeModelID)
472 if err != nil {
473 return Model{}, Model{}, err
474 }
475 smallModel, err := smallProvider.LanguageModel(ctx, smallModelID)
476 if err != nil {
477 return Model{}, Model{}, err
478 }
480 return Model{
481 Model: largeModel,
482 CatwalkCfg: *largeCatwalkModel,
483 ModelCfg: largeModelCfg,
484 }, Model{
485 Model: smallModel,
486 CatwalkCfg: *smallCatwalkModel,
487 ModelCfg: smallModelCfg,
488 }, nil
491func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
492 var opts []anthropic.Option
494 if strings.HasPrefix(apiKey, "Bearer ") {
495 // NOTE: Prevent the SDK from picking up the API key from env.
496 os.Setenv("ANTHROPIC_API_KEY", "")
498 headers["Authorization"] = apiKey
499 } else if apiKey != "" {
500 // X-Api-Key header
501 opts = append(opts, anthropic.WithAPIKey(apiKey))
502 }
504 if len(headers) > 0 {
505 opts = append(opts, anthropic.WithHeaders(headers))
506 }
508 if baseURL != "" {
509 opts = append(opts, anthropic.WithBaseURL(baseURL))
510 }
512 if c.cfg.Options.Debug {
513 httpClient := log.NewHTTPClient()
514 opts = append(opts, anthropic.WithHTTPClient(httpClient))
515 }
517 return anthropic.New(opts...)
520func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
521 opts := []openai.Option{
522 openai.WithAPIKey(apiKey),
523 openai.WithUseResponsesAPI(),
524 }
525 if c.cfg.Options.Debug {
526 httpClient := log.NewHTTPClient()
527 opts = append(opts, openai.WithHTTPClient(httpClient))
528 }
529 if len(headers) > 0 {
530 opts = append(opts, openai.WithHeaders(headers))
531 }
532 if baseURL != "" {
533 opts = append(opts, openai.WithBaseURL(baseURL))
534 }
535 return openai.New(opts...)
538func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) (fantasy.Provider, error) {
539 opts := []openrouter.Option{
540 openrouter.WithAPIKey(apiKey),
541 }
542 if c.cfg.Options.Debug {
543 httpClient := log.NewHTTPClient()
544 opts = append(opts, openrouter.WithHTTPClient(httpClient))
545 }
546 if len(headers) > 0 {
547 opts = append(opts, openrouter.WithHeaders(headers))
548 }
549 return openrouter.New(opts...)
552func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string, extraBody map[string]any) (fantasy.Provider, error) {
553 opts := []openaicompat.Option{
554 openaicompat.WithBaseURL(baseURL),
555 openaicompat.WithAPIKey(apiKey),
556 }
557 if c.cfg.Options.Debug {
558 httpClient := log.NewHTTPClient()
559 opts = append(opts, openaicompat.WithHTTPClient(httpClient))
560 }
561 if len(headers) > 0 {
562 opts = append(opts, openaicompat.WithHeaders(headers))
563 }
565 for extraKey, extraValue := range extraBody {
566 opts = append(opts, openaicompat.WithSDKOptions(openaisdk.WithJSONSet(extraKey, extraValue)))
567 }
569 return openaicompat.New(opts...)
572func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) (fantasy.Provider, error) {
573 opts := []azure.Option{
574 azure.WithBaseURL(baseURL),
575 azure.WithAPIKey(apiKey),
576 azure.WithUseResponsesAPI(),
577 }
578 if c.cfg.Options.Debug {
579 httpClient := log.NewHTTPClient()
580 opts = append(opts, azure.WithHTTPClient(httpClient))
581 }
582 if options == nil {
583 options = make(map[string]string)
584 }
585 if apiVersion, ok := options["apiVersion"]; ok {
586 opts = append(opts, azure.WithAPIVersion(apiVersion))
587 }
588 if len(headers) > 0 {
589 opts = append(opts, azure.WithHeaders(headers))
590 }
592 return azure.New(opts...)
595func (c *coordinator) buildBedrockProvider(headers map[string]string) (fantasy.Provider, error) {
596 var opts []bedrock.Option
597 if c.cfg.Options.Debug {
598 httpClient := log.NewHTTPClient()
599 opts = append(opts, bedrock.WithHTTPClient(httpClient))
600 }
601 if len(headers) > 0 {
602 opts = append(opts, bedrock.WithHeaders(headers))
603 }
604 bearerToken := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
605 if bearerToken != "" {
606 opts = append(opts, bedrock.WithAPIKey(bearerToken))
607 }
608 return bedrock.New(opts...)
611func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
612 opts := []google.Option{
613 google.WithBaseURL(baseURL),
614 google.WithGeminiAPIKey(apiKey),
615 }
616 if c.cfg.Options.Debug {
617 httpClient := log.NewHTTPClient()
618 opts = append(opts, google.WithHTTPClient(httpClient))
619 }
620 if len(headers) > 0 {
621 opts = append(opts, google.WithHeaders(headers))
622 }
623 return google.New(opts...)
626func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) (fantasy.Provider, error) {
627 opts := []google.Option{}
628 if c.cfg.Options.Debug {
629 httpClient := log.NewHTTPClient()
630 opts = append(opts, google.WithHTTPClient(httpClient))
631 }
632 if len(headers) > 0 {
633 opts = append(opts, google.WithHeaders(headers))
634 }
636 project := options["project"]
637 location := options["location"]
639 opts = append(opts, google.WithVertex(project, location))
641 return google.New(opts...)
644func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
645 if model.Think {
646 return true
647 }
649 if model.ProviderOptions == nil {
650 return false
651 }
653 opts, err := anthropic.ParseOptions(model.ProviderOptions)
654 if err != nil {
655 return false
656 }
657 if opts.Thinking != nil {
658 return true
659 }
660 return false
663func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
664 headers := maps.Clone(providerCfg.ExtraHeaders)
665 if headers == nil {
666 headers = make(map[string]string)
667 }
669 // handle special headers for anthropic
670 if providerCfg.Type == anthropic.Name && c.isAnthropicThinking(model) {
671 if v, ok := headers["anthropic-beta"]; ok {
672 headers["anthropic-beta"] = v + ",interleaved-thinking-2025-05-14"
673 } else {
674 headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
675 }
676 }
678 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
679 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
681 switch providerCfg.Type {
682 case openai.Name:
683 return c.buildOpenaiProvider(baseURL, apiKey, headers)
684 case anthropic.Name:
685 return c.buildAnthropicProvider(baseURL, apiKey, headers)
686 case openrouter.Name:
687 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
688 case azure.Name:
689 return c.buildAzureProvider(baseURL, apiKey, headers, providerCfg.ExtraParams)
690 case bedrock.Name:
691 return c.buildBedrockProvider(headers)
692 case google.Name:
693 return c.buildGoogleProvider(baseURL, apiKey, headers)
694 case "google-vertex":
695 return c.buildGoogleVertexProvider(headers, providerCfg.ExtraParams)
696 case openaicompat.Name:
697 if providerCfg.ID == string(catwalk.InferenceProviderZAI) {
698 if providerCfg.ExtraBody == nil {
699 providerCfg.ExtraBody = map[string]any{}
700 }
701 providerCfg.ExtraBody["tool_stream"] = true
702 }
703 return c.buildOpenaiCompatProvider(baseURL, apiKey, headers, providerCfg.ExtraBody)
704 default:
705 return nil, fmt.Errorf("provider type not supported: %q", providerCfg.Type)
706 }
709func isExactoSupported(modelID string) bool {
710 supportedModels := []string{
711 "moonshotai/kimi-k2-0905",
712 "deepseek/deepseek-v3.1-terminus",
713 "z-ai/glm-4.6",
714 "openai/gpt-oss-120b",
715 "qwen/qwen3-coder",
716 }
717 return slices.Contains(supportedModels, modelID)
720func (c *coordinator) Cancel(sessionID string) {
721 c.currentAgent.Cancel(sessionID)
724func (c *coordinator) CancelAll() {
725 c.currentAgent.CancelAll()
728func (c *coordinator) ClearQueue(sessionID string) {
729 c.currentAgent.ClearQueue(sessionID)
732func (c *coordinator) IsBusy() bool {
733 return c.currentAgent.IsBusy()
736func (c *coordinator) IsSessionBusy(sessionID string) bool {
737 return c.currentAgent.IsSessionBusy(sessionID)
740func (c *coordinator) Model() Model {
741 return c.currentAgent.Model()
744func (c *coordinator) UpdateModels(ctx context.Context) error {
745 // build the models again so we make sure we get the latest config
746 large, small, err := c.buildAgentModels(ctx)
747 if err != nil {
748 return err
749 }
750 c.currentAgent.SetModels(large, small)
752 agentCfg, ok := c.cfg.Agents[config.AgentCoder]
753 if !ok {
754 return errors.New("coder agent not configured")
755 }
757 tools, err := c.buildTools(ctx, agentCfg)
758 if err != nil {
759 return err
760 }
761 c.currentAgent.SetTools(tools)
762 return nil
765func (c *coordinator) QueuedPrompts(sessionID string) int {
766 return c.currentAgent.QueuedPrompts(sessionID)
769func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
770 providerCfg, ok := c.cfg.Providers.Get(c.currentAgent.Model().ModelCfg.Provider)
771 if !ok {
772 return errors.New("model provider not configured")
773 }
774 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))