Skip to content

Commit 4406c8b

Browse files
authored
Merge pull request #4266 from Hmbown/codex/v0868-xai-provider-4257
feat(provider): add xAI API-key route (#4257)
2 parents cf46d03 + 266ede2 commit 4406c8b

18 files changed

Lines changed: 326 additions & 9 deletions

File tree

config.example.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
# `--provider volcengine`, `--provider openrouter`, `--provider xiaomi-mimo`,
1717
# `--provider fireworks`, `--provider siliconflow`, `--provider siliconflow-CN`,
1818
# `/provider arcee`, `/provider moonshot`, `/provider qianfan`, `/provider sglang`, `/provider vllm`,
19-
# `/provider ollama`, `/provider huggingface`, `/provider stepfun`, `/provider openmodel`) toggle without having to re-enter keys. Top-level
19+
# `/provider ollama`, `/provider huggingface`, `/provider stepfun`, `/provider openmodel`, `/provider xai`) toggle without having to re-enter keys. Top-level
2020
# `api_key` / `base_url` are
2121
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
2222
# (backward compatibility).
23-
provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | together | qianfan | openai-codex | anthropic | openmodel | deepinfra | sakana | longcat
23+
provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | together | qianfan | openai-codex | anthropic | openmodel | deepinfra | sakana | longcat | xai
2424
api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty
2525
base_url = "https://api.deepseek.com/beta"
2626
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
@@ -563,6 +563,15 @@ max_subagents = 10 # optional (1-20)
563563
# base_url = "https://api.longcat.chat/openai/v1"
564564
# model = "LongCat-2.0"
565565

566+
# xAI / Grok Provider (https://console.x.ai/)
567+
# OpenAI-compatible Chat Completions API-key route.
568+
# Provider aliases: xai, x-ai, x_ai, grok
569+
# Env var aliases: XAI_API_KEY, XAI_BASE_URL, XAI_MODEL
570+
[providers.xai]
571+
# api_key = "YOUR_XAI_API_KEY"
572+
# base_url = "https://api.x.ai/v1"
573+
# model = "grok-4.5" # or grok-4.3, grok-build
574+
566575
# ─────────────────────────────────────────────────────────────────────────────────
567576
# Together AI Provider (https://www.together.ai/)
568577
# Env var aliases: TOGETHER_API_KEY, TOGETHER_BASE_URL, TOGETHER_MODEL

crates/agent/src/lib.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,49 @@ impl Default for ModelRegistry {
872872
supports_tools: true,
873873
supports_reasoning: true,
874874
},
875+
// xAI / Grok (https://api.x.ai/v1)
876+
ModelInfo {
877+
id: "grok-4.5".to_string(),
878+
provider: ProviderKind::Xai,
879+
aliases: vec!["grok".to_string(), "xai-grok-4.5".to_string()],
880+
supports_tools: true,
881+
supports_reasoning: true,
882+
},
883+
ModelInfo {
884+
id: "grok-4.3".to_string(),
885+
provider: ProviderKind::Xai,
886+
aliases: vec!["xai-grok-4.3".to_string()],
887+
supports_tools: true,
888+
supports_reasoning: true,
889+
},
890+
ModelInfo {
891+
id: "grok-build".to_string(),
892+
provider: ProviderKind::Xai,
893+
aliases: vec!["xai-grok-build".to_string()],
894+
supports_tools: true,
895+
supports_reasoning: true,
896+
},
897+
ModelInfo {
898+
id: "grok-composer-2.5-fast".to_string(),
899+
provider: ProviderKind::Xai,
900+
aliases: vec!["xai-grok-composer".to_string()],
901+
supports_tools: true,
902+
supports_reasoning: false,
903+
},
904+
ModelInfo {
905+
id: "grok-4.20-0309-reasoning".to_string(),
906+
provider: ProviderKind::Xai,
907+
aliases: vec!["xai-grok-reasoning".to_string()],
908+
supports_tools: true,
909+
supports_reasoning: true,
910+
},
911+
ModelInfo {
912+
id: "grok-4.20-0309-non-reasoning".to_string(),
913+
provider: ProviderKind::Xai,
914+
aliases: vec!["xai-grok-fast".to_string()],
915+
supports_tools: true,
916+
supports_reasoning: false,
917+
},
875918
];
876919
Self::new(models)
877920
}
@@ -1479,6 +1522,7 @@ mod tests {
14791522
(ProviderKind::Stepfun, "step-3.7-flash"),
14801523
(ProviderKind::Minimax, "MiniMax-M2.1"),
14811524
(ProviderKind::Openmodel, "deepseek-v4-flash"),
1525+
(ProviderKind::Xai, "grok-4.5"),
14821526
] {
14831527
assert!(
14841528
models
@@ -1489,6 +1533,38 @@ mod tests {
14891533
}
14901534
}
14911535

1536+
#[test]
1537+
fn xai_grok_models_resolve_when_provider_hinted() {
1538+
let registry = ModelRegistry::default();
1539+
1540+
let default = registry.resolve(None, Some(ProviderKind::Xai));
1541+
assert_eq!(default.resolved.provider, ProviderKind::Xai);
1542+
assert_eq!(default.resolved.id, "grok-4.5");
1543+
assert!(default.used_fallback);
1544+
1545+
let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
1546+
assert_eq!(alias.resolved.provider, ProviderKind::Xai);
1547+
assert_eq!(alias.resolved.id, "grok-4.5");
1548+
assert!(!alias.used_fallback);
1549+
1550+
let fast = registry.resolve(
1551+
Some("grok-4.20-0309-non-reasoning"),
1552+
Some(ProviderKind::Xai),
1553+
);
1554+
assert_eq!(fast.resolved.provider, ProviderKind::Xai);
1555+
assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
1556+
assert!(!fast.resolved.supports_reasoning);
1557+
}
1558+
1559+
#[test]
1560+
fn grok_ids_stay_in_grok_family() {
1561+
assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
1562+
assert_eq!(
1563+
model_family("grok-4.20-0309-non-reasoning"),
1564+
ModelFamily::Grok
1565+
);
1566+
}
1567+
14921568
#[test]
14931569
fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
14941570
let registry = ModelRegistry::default();

crates/cli/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ enum ProviderArg {
6666
Sakana,
6767
#[value(alias = "long-cat", alias = "meituan-longcat", alias = "meituan")]
6868
LongCat,
69+
#[value(alias = "x-ai", alias = "x_ai", alias = "grok")]
70+
Xai,
6971
}
7072

7173
impl From<ProviderArg> for ProviderKind {
@@ -99,6 +101,7 @@ impl From<ProviderArg> for ProviderKind {
99101
ProviderArg::Deepinfra => ProviderKind::Deepinfra,
100102
ProviderArg::Sakana => ProviderKind::Sakana,
101103
ProviderArg::LongCat => ProviderKind::LongCat,
104+
ProviderArg::Xai => ProviderKind::Xai,
102105
}
103106
}
104107
}

crates/config/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ pub struct ProvidersToml {
176176
alias = "meituan"
177177
)]
178178
pub longcat: ProviderConfigToml,
179+
#[serde(default, alias = "x-ai", alias = "x_ai", alias = "grok")]
180+
pub xai: ProviderConfigToml,
179181
/// Catch-all table for the dynamic OpenAI-compatible custom provider
180182
/// identity (#1519). Arbitrary `[providers.<name>]` tables are handled by
181183
/// the tui-side flatten map; this named slot keeps the canonical
@@ -275,6 +277,7 @@ impl ProvidersToml {
275277
ProviderKind::Deepinfra => &self.deepinfra,
276278
ProviderKind::Sakana => &self.sakana,
277279
ProviderKind::LongCat => &self.longcat,
280+
ProviderKind::Xai => &self.xai,
278281
ProviderKind::Custom => &self.custom,
279282
}
280283
}
@@ -311,6 +314,7 @@ impl ProvidersToml {
311314
ProviderKind::Deepinfra => &mut self.deepinfra,
312315
ProviderKind::Sakana => &mut self.sakana,
313316
ProviderKind::LongCat => &mut self.longcat,
317+
ProviderKind::Xai => &mut self.xai,
314318
ProviderKind::Custom => &mut self.custom,
315319
}
316320
}
@@ -2042,6 +2046,7 @@ impl ConfigToml {
20422046
ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL.to_string(),
20432047
ProviderKind::Sakana => DEFAULT_SAKANA_BASE_URL.to_string(),
20442048
ProviderKind::LongCat => DEFAULT_LONGCAT_BASE_URL.to_string(),
2049+
ProviderKind::Xai => DEFAULT_XAI_BASE_URL.to_string(),
20452050
// The custom provider has no built-in endpoint; fall back to its
20462051
// descriptor placeholder so the lookup is total. Real custom
20472052
// routes always supply a configured base_url before this point.
@@ -2294,6 +2299,7 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
22942299
| ProviderKind::Minimax
22952300
| ProviderKind::Qianfan
22962301
| ProviderKind::Ollama
2302+
| ProviderKind::Xai
22972303
) {
22982304
return model.to_string();
22992305
}
@@ -2619,6 +2625,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
26192625
ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_MODEL,
26202626
ProviderKind::Sakana => DEFAULT_SAKANA_MODEL,
26212627
ProviderKind::LongCat => DEFAULT_LONGCAT_MODEL,
2628+
ProviderKind::Xai => DEFAULT_XAI_MODEL,
26222629
// No built-in default model; the registry placeholder keeps this total.
26232630
ProviderKind::Custom => provider.provider().default_model(),
26242631
}
@@ -2656,6 +2663,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
26562663
ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL,
26572664
ProviderKind::Sakana => DEFAULT_SAKANA_BASE_URL,
26582665
ProviderKind::LongCat => DEFAULT_LONGCAT_BASE_URL,
2666+
ProviderKind::Xai => DEFAULT_XAI_BASE_URL,
26592667
// No built-in default base URL; the registry placeholder keeps this total.
26602668
ProviderKind::Custom => provider.provider().default_base_url(),
26612669
}
@@ -4201,6 +4209,8 @@ struct EnvRuntimeOverrides {
42014209
sakana_model: Option<String>,
42024210
longcat_base_url: Option<String>,
42034211
longcat_model: Option<String>,
4212+
xai_base_url: Option<String>,
4213+
xai_model: Option<String>,
42044214
}
42054215

42064216
impl EnvRuntimeOverrides {
@@ -4445,6 +4455,12 @@ impl EnvRuntimeOverrides {
44454455
longcat_model: std::env::var("LONGCAT_MODEL")
44464456
.ok()
44474457
.filter(|v| !v.trim().is_empty()),
4458+
xai_base_url: std::env::var("XAI_BASE_URL")
4459+
.ok()
4460+
.filter(|v| !v.trim().is_empty()),
4461+
xai_model: std::env::var("XAI_MODEL")
4462+
.ok()
4463+
.filter(|v| !v.trim().is_empty()),
44484464
}
44494465
}
44504466

@@ -4497,6 +4513,7 @@ impl EnvRuntimeOverrides {
44974513
ProviderKind::Deepinfra => self.deepinfra_base_url.clone(),
44984514
ProviderKind::Sakana => self.sakana_base_url.clone(),
44994515
ProviderKind::LongCat => self.longcat_base_url.clone(),
4516+
ProviderKind::Xai => self.xai_base_url.clone(),
45004517
// No dedicated CODEWHALE_CUSTOM_BASE_URL env override: a custom
45014518
// provider's base URL comes from its `[providers.<name>]` table.
45024519
ProviderKind::Custom => None,
@@ -4528,6 +4545,7 @@ impl EnvRuntimeOverrides {
45284545
ProviderKind::Deepinfra => self.deepinfra_model.clone(),
45294546
ProviderKind::Sakana => self.sakana_model.clone(),
45304547
ProviderKind::LongCat => self.longcat_model.clone(),
4548+
ProviderKind::Xai => self.xai_model.clone(),
45314549
_ => None,
45324550
}?;
45334551

crates/config/src/provider.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ use super::{
2222
DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL,
2323
DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL,
2424
DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL,
25-
DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL,
26-
DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL, ProviderKind,
25+
DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL,
26+
DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL,
27+
DEFAULT_ZAI_MODEL, ProviderKind,
2728
};
2829

2930
/// Wire protocol spoken by a provider.
@@ -616,6 +617,18 @@ provider!(
616617
aliases: ["long-cat", "meituan-longcat", "meituan"]
617618
);
618619

620+
provider!(
621+
Xai,
622+
Xai,
623+
"xai",
624+
"xAI",
625+
DEFAULT_XAI_BASE_URL,
626+
DEFAULT_XAI_MODEL,
627+
["XAI_API_KEY"],
628+
"xai",
629+
aliases: ["x-ai", "x_ai", "grok"]
630+
);
631+
619632
/// User-defined OpenAI-compatible endpoint (#1519).
620633
///
621634
/// A single dynamic provider identity for arbitrary `[providers.<name>]
@@ -699,9 +712,10 @@ static MINIMAX: Minimax = Minimax;
699712
static DEEPINFRA: Deepinfra = Deepinfra;
700713
static SAKANA: Sakana = Sakana;
701714
static LONGCAT: LongCat = LongCat;
715+
static XAI: Xai = Xai;
702716
static CUSTOM: Custom = Custom;
703717

704-
static PROVIDER_REGISTRY: [&dyn Provider; 31] = [
718+
static PROVIDER_REGISTRY: [&dyn Provider; 32] = [
705719
&DEEPSEEK,
706720
&DEEPSEEK_ANTHROPIC,
707721
&NVIDIA_NIM,
@@ -732,6 +746,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 31] = [
732746
&DEEPINFRA,
733747
&SAKANA,
734748
&LONGCAT,
749+
&XAI,
735750
&CUSTOM,
736751
];
737752

crates/config/src/provider_defaults.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,6 @@ pub(crate) const DEFAULT_SAKANA_BASE_URL: &str = "https://api.sakana.ai/v1";
133133
// Meituan LongCat defaults
134134
pub(crate) const DEFAULT_LONGCAT_MODEL: &str = "LongCat-2.0";
135135
pub(crate) const DEFAULT_LONGCAT_BASE_URL: &str = "https://api.longcat.chat/openai/v1";
136+
// xAI / Grok API-key route defaults
137+
pub(crate) const DEFAULT_XAI_MODEL: &str = "grok-4.5";
138+
pub(crate) const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";

crates/config/src/provider_kind.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ pub enum ProviderKind {
105105
Sakana,
106106
#[serde(alias = "long-cat", alias = "meituan-longcat", alias = "meituan")]
107107
LongCat,
108+
#[serde(alias = "x-ai", alias = "x_ai", alias = "grok")]
109+
Xai,
108110
/// User-defined OpenAI-compatible endpoint (#1519).
109111
///
110112
/// A single dynamic identity for arbitrary `[providers.<name>]
@@ -116,7 +118,7 @@ pub enum ProviderKind {
116118
}
117119

118120
impl ProviderKind {
119-
pub const ALL: [Self; 31] = [
121+
pub const ALL: [Self; 32] = [
120122
Self::Deepseek,
121123
Self::DeepseekAnthropic,
122124
Self::NvidiaNim,
@@ -147,6 +149,7 @@ impl ProviderKind {
147149
Self::Deepinfra,
148150
Self::Sakana,
149151
Self::LongCat,
152+
Self::Xai,
150153
Self::Custom,
151154
];
152155

0 commit comments

Comments
 (0)