on main help sign in
add xai support (#135)
603a3e3Kujtim Hoxha
internal/config/config.go+14−2
@@ -242,6 +242,13 @@ func setProviderDefaults() {
242 if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
242 if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
243 viper.SetDefault("providers.openrouter.apiKey", apiKey)
243 viper.SetDefault("providers.openrouter.apiKey", apiKey)
244 }
244 }
245+ if apiKey := os.Getenv("XAI_API_KEY"); apiKey != "" {
246+ viper.SetDefault("providers.xai.apiKey", apiKey)
247+ }
248+ if apiKey := os.Getenv("AZURE_OPENAI_ENDPOINT"); apiKey != "" {
249+ // api-key may be empty when using Entra ID credentials – that's okay
250+ viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
251+ }
245
252
246 // Use this order to set the default models
253 // Use this order to set the default models
247 // 1. Anthropic
254 // 1. Anthropic
@@ -292,6 +299,13 @@ func setProviderDefaults() {
292 return
299 return
293 }
300 }
294
301
302+ if viper.Get("providers.xai.apiKey") != "" {
303+ viper.SetDefault("agents.coder.model", models.XAIGrok3Beta)
304+ viper.SetDefault("agents.task.model", models.XAIGrok3Beta)
305+ viper.SetDefault("agents.title.model", models.XAiGrok3MiniFastBeta)
306+ return
307+ }
308+
295 // AWS Bedrock configuration
309 // AWS Bedrock configuration
296 if hasAWSCredentials() {
310 if hasAWSCredentials() {
297 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
311 viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
@@ -301,8 +315,6 @@ func setProviderDefaults() {
301 }
315 }
302
316
303 if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
317 if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
304− // api-key may be empty when using Entra ID credentials – that's okay
305− viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
306 viper.SetDefault("agents.coder.model", models.AzureGPT41)
318 viper.SetDefault("agents.coder.model", models.AzureGPT41)
307 viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
319 viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
308 viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
320 viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
internal/llm/models/models.go+1
@@ -89,4 +89,5 @@ func init() {
89 maps.Copy(SupportedModels, GroqModels)
89 maps.Copy(SupportedModels, GroqModels)
90 maps.Copy(SupportedModels, AzureModels)
90 maps.Copy(SupportedModels, AzureModels)
91 maps.Copy(SupportedModels, OpenRouterModels)
91 maps.Copy(SupportedModels, OpenRouterModels)
92+ maps.Copy(SupportedModels, XAIModels)
92}
93}
internal/llm/models/xai.gonew file+61
@@ -0,0 +1,61 @@
1+package models
2+
3+const (
4+ ProviderXAI ModelProvider = "xai"
5+
6+ XAIGrok3Beta ModelID = "grok-3-beta"
7+ XAIGrok3MiniBeta ModelID = "grok-3-mini-beta"
8+ XAIGrok3FastBeta ModelID = "grok-3-fast-beta"
9+ XAiGrok3MiniFastBeta ModelID = "grok-3-mini-fast-beta"
10+)
11+
12+var XAIModels = map[ModelID]Model{
13+ XAIGrok3Beta: {
14+ ID: XAIGrok3Beta,
15+ Name: "Grok3 Beta",
16+ Provider: ProviderXAI,
17+ APIModel: "grok-3-beta",
18+ CostPer1MIn: 3.0,
19+ CostPer1MInCached: 0,
20+ CostPer1MOut: 15,
21+ CostPer1MOutCached: 0,
22+ ContextWindow: 131_072,
23+ DefaultMaxTokens: 20_000,
24+ },
25+ XAIGrok3MiniBeta: {
26+ ID: XAIGrok3MiniBeta,
27+ Name: "Grok3 Mini Beta",
28+ Provider: ProviderXAI,
29+ APIModel: "grok-3-mini-beta",
30+ CostPer1MIn: 0.3,
31+ CostPer1MInCached: 0,
32+ CostPer1MOut: 0.5,
33+ CostPer1MOutCached: 0,
34+ ContextWindow: 131_072,
35+ DefaultMaxTokens: 20_000,
36+ },
37+ XAIGrok3FastBeta: {
38+ ID: XAIGrok3FastBeta,
39+ Name: "Grok3 Fast Beta",
40+ Provider: ProviderXAI,
41+ APIModel: "grok-3-fast-beta",
42+ CostPer1MIn: 5,
43+ CostPer1MInCached: 0,
44+ CostPer1MOut: 25,
45+ CostPer1MOutCached: 0,
46+ ContextWindow: 131_072,
47+ DefaultMaxTokens: 20_000,
48+ },
49+ XAiGrok3MiniFastBeta: {
50+ ID: XAiGrok3MiniFastBeta,
51+ Name: "Grok3 Mini Fast Beta",
52+ Provider: ProviderXAI,
53+ APIModel: "grok-3-mini-fast-beta",
54+ CostPer1MIn: 0.6,
55+ CostPer1MInCached: 0,
56+ CostPer1MOut: 4.0,
57+ CostPer1MOutCached: 0,
58+ ContextWindow: 131_072,
59+ DefaultMaxTokens: 20_000,
60+ },
61+}
internal/llm/provider/openai.go+3−10
@@ -258,15 +258,6 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
258 chunk := openaiStream.Current()
258 chunk := openaiStream.Current()
259 acc.AddChunk(chunk)
259 acc.AddChunk(chunk)
260
260
261− if tool, ok := acc.JustFinishedToolCall(); ok {
262− toolCalls = append(toolCalls, message.ToolCall{
263− ID: tool.Id,
264− Name: tool.Name,
265− Input: tool.Arguments,
266− Type: "function",
267− })
268− }
269−
270 for _, choice := range chunk.Choices {
261 for _, choice := range chunk.Choices {
271 if choice.Delta.Content != "" {
262 if choice.Delta.Content != "" {
272 eventChan <- ProviderEvent{
263 eventChan <- ProviderEvent{
@@ -282,7 +273,9 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
282 if err == nil || errors.Is(err, io.EOF) {
273 if err == nil || errors.Is(err, io.EOF) {
283 // Stream completed successfully
274 // Stream completed successfully
284 finishReason := o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason))
275 finishReason := o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason))
285−
276+ if len(acc.ChatCompletion.Choices[0].Message.ToolCalls) > 0 {
277+ toolCalls = append(toolCalls, o.toolCalls(acc.ChatCompletion)...)
278+ }
286 if len(toolCalls) > 0 {
279 if len(toolCalls) > 0 {
287 finishReason = message.FinishReasonToolUse
280 finishReason = message.FinishReasonToolUse
288 }
281 }
internal/llm/provider/provider.go+9
@@ -132,6 +132,15 @@ func NewProvider(providerName models.ModelProvider, opts ...ProviderClientOption
132 options: clientOptions,
132 options: clientOptions,
133 client: newOpenAIClient(clientOptions),
133 client: newOpenAIClient(clientOptions),
134 }, nil
134 }, nil
135+ case models.ProviderXAI:
136+ clientOptions.openaiOptions = append(clientOptions.openaiOptions,
137+ WithOpenAIBaseURL("https://api.x.ai/v1"),
138+ )
139+ return &baseProvider[OpenAIClient]{
140+ options: clientOptions,
141+ client: newOpenAIClient(clientOptions),
142+ }, nil
143+
135 case models.ProviderMock:
144 case models.ProviderMock:
136 // TODO: implement mock client for test
145 // TODO: implement mock client for test
137 panic("not implemented")
146 panic("not implemented")
internal/tui/tui.go+2−2
@@ -56,8 +56,8 @@ var keys = keyMap{
56 ),
56 ),
57
57
58 Models: key.NewBinding(
58 Models: key.NewBinding(
59− key.WithKeys("ctrl+m"),
59+ key.WithKeys("ctrl+o"),
60− key.WithHelp("ctrl+m", "model selection"),
60+ key.WithHelp("ctrl+o", "model selection"),
61 ),
61 ),
62
62
63 SwitchTheme: key.NewBinding(
63 SwitchTheme: key.NewBinding(