coordinator.go 21 KB

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