Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config/entities/providers-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"deepseek",
"gemini",
"groq",
"xai",
"mistral",
"openai",
"openrouter"
Expand Down Expand Up @@ -53,6 +54,7 @@
"deepseek",
"gemini",
"groq",
"xai",
"mistral",
"openai",
"openrouter"
Expand Down
8 changes: 8 additions & 0 deletions src/config/entities/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub enum ProviderConfig {
Gemini(configs::GeminiProviderConfig),
#[serde(rename = "groq")]
Groq(configs::GroqProviderConfig),
#[serde(rename = "xai")]
Xai(configs::XaiProviderConfig),
#[serde(rename = "mistral")]
Mistral(configs::MistralProviderConfig),
#[serde(rename = "openai")]
Expand All @@ -51,6 +53,7 @@ impl ProviderConfig {
Self::DeepSeek(_) => identifiers::DEEPSEEK,
Self::Gemini(_) => identifiers::GEMINI,
Self::Groq(_) => identifiers::GROQ,
Self::Xai(_) => identifiers::XAI,
Self::Mistral(_) => identifiers::MISTRAL,
Self::OpenAI(_) => identifiers::OPENAI,
Self::OpenRouter(_) => identifiers::OPENROUTER,
Expand Down Expand Up @@ -156,6 +159,11 @@ mod tests {
"type": "groq",
"config": { "api_key": "test_key" }
}), true, None)]
#[case::xai_ok(json!({
"name": "xai-primary",
"type": "xai",
"config": { "api_key": "test_key" }
}), true, None)]
#[case::mistral_ok(json!({
"name": "mistral-primary",
"type": "mistral",
Expand Down
14 changes: 11 additions & 3 deletions src/gateway/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod macros;
pub mod mistral;
pub mod openai;
pub mod openrouter;
pub mod xai;

pub use anthropic::AnthropicDef;
pub use azure::AzureDef;
Expand All @@ -18,9 +19,12 @@ pub use groq::Groq;
pub use mistral::Mistral;
pub use openai::OpenAIDef;
pub use openrouter::OpenRouter;
pub use xai::Xai;

pub mod identifiers {
use super::{anthropic, azure, bedrock, deepseek, gemini, groq, mistral, openai, openrouter};
use super::{
anthropic, azure, bedrock, deepseek, gemini, groq, mistral, openai, openrouter, xai,
};

pub const ANTHROPIC: &str = anthropic::IDENTIFIER;
pub const AZURE: &str = azure::IDENTIFIER;
Expand All @@ -31,14 +35,15 @@ pub mod identifiers {
pub const MISTRAL: &str = mistral::IDENTIFIER;
pub const OPENAI: &str = openai::IDENTIFIER;
pub const OPENROUTER: &str = openrouter::IDENTIFIER;
pub const XAI: &str = xai::IDENTIFIER;
}

pub mod configs {
pub use super::{
anthropic::AnthropicProviderConfig, azure::AzureProviderConfig,
bedrock::BedrockProviderConfig, deepseek::DeepSeekProviderConfig,
gemini::GeminiProviderConfig, groq::GroqProviderConfig, mistral::MistralProviderConfig,
openai::OpenAIProviderConfig, openrouter::OpenRouterProviderConfig,
openai::OpenAIProviderConfig, openrouter::OpenRouterProviderConfig, xai::XaiProviderConfig,
};
}

Expand All @@ -54,13 +59,15 @@ pub fn default_provider_registry() -> Result<ProviderRegistry> {
.register(Groq)?
.register(Mistral)?
.register(OpenAIDef)?
.register(OpenRouter)?;
.register(OpenRouter)?
.register(Xai)?;
Ok(builder.build())
}

#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

use super::default_provider_registry;

#[test]
Expand All @@ -76,6 +83,7 @@ mod tests {
assert_eq!(registry.get("mistral").unwrap().name(), "mistral");
assert_eq!(registry.get("deepseek").unwrap().name(), "deepseek");
assert_eq!(registry.get("openrouter").unwrap().name(), "openrouter");
assert_eq!(registry.get("xai").unwrap().name(), "xai");
assert!(registry.get("missing").is_none());
}
}
69 changes: 69 additions & 0 deletions src/gateway/providers/xai.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use serde::{Deserialize, Serialize};

use crate::gateway::providers::macros::provider;

/// Provider identifier string used to look up xAI in the gateway registry.
pub const IDENTIFIER: &str = "xai";

/// Configuration for an xAI provider deployment.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct XaiProviderConfig {
pub api_key: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub api_base: Option<String>,
}

provider!(Xai {
display_name: "xai",
base_url: "https://api.x.ai/v1",
auth: bearer,
quirks: {
unsupported_params: &["logit_bias"],
inject_stream_usage: true,
}
});

#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;

use super::Xai;
use crate::gateway::{
traits::{ChatTransform, ProviderMeta},
types::openai::ChatCompletionRequest,
};

#[test]
fn provider_macro_expands_correctly() {
let provider = Xai;

assert_eq!(provider.name(), "xai");
assert_eq!(provider.default_base_url(), "https://api.x.ai/v1");

assert_eq!(
provider.build_url(provider.default_base_url(), "ignored"),
"https://api.x.ai/v1/chat/completions"
);
}

#[test]
fn transform_request_applies_xai_quirks() {
let provider = Xai;
let request: ChatCompletionRequest = serde_json::from_value(json!({
"model": "grok-4.3",
"messages": [{"role": "user", "content": "hello"}],
"stream": true,
"max_tokens": 128,
"logit_bias": {"42": 100}
}))
.unwrap();

let transformed = provider.transform_request(&request).unwrap();

assert_eq!(transformed.get("logit_bias"), None);
assert_eq!(transformed["max_tokens"], 128);
assert_eq!(transformed["stream_options"]["include_usage"], true);
}
}
22 changes: 21 additions & 1 deletion src/proxy/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ fn provider_auth_and_base_url(config: &ProviderConfig) -> Result<(ProviderAuth,
ProviderAuth::ApiKey(config.api_key.clone()),
parse_base_url(config.api_base.as_deref())?,
),
ProviderConfig::Xai(config) => (
ProviderAuth::ApiKey(config.api_key.clone()),
parse_base_url(config.api_base.as_deref())?,
),
ProviderConfig::Mistral(config) => (
ProviderAuth::ApiKey(config.api_key.clone()),
parse_base_url(config.api_base.as_deref())?,
Expand Down Expand Up @@ -154,7 +158,7 @@ mod tests {
config::entities::providers::ProviderConfig,
gateway::providers::configs::{
AzureProviderConfig, BedrockProviderConfig, GroqProviderConfig, MistralProviderConfig,
OpenRouterProviderConfig,
OpenRouterProviderConfig, XaiProviderConfig,
},
};

Expand Down Expand Up @@ -245,6 +249,22 @@ mod tests {
);
}

#[test]
fn provider_auth_and_base_url_returns_xai_api_key_and_optional_base_url() {
let config = ProviderConfig::Xai(XaiProviderConfig {
api_key: "xai-key".into(),
api_base: Some("https://api.x.ai/v1".into()),
});

let (auth, base_url_override) = provider_auth_and_base_url(&config).unwrap();

assert_eq!(auth.api_key_for("xai").unwrap(), "xai-key");
assert_eq!(
base_url_override.as_ref().map(Url::as_str),
Some("https://api.x.ai/v1")
);
}

#[test]
fn provider_auth_and_base_url_returns_bedrock_static_credentials() {
let config = ProviderConfig::Bedrock(BedrockProviderConfig {
Expand Down
2 changes: 2 additions & 0 deletions ui/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"openai": "OpenAI",
"openrouter": "OpenRouter",
"groq": "Groq",
"xai": "xAI",
"mistral": "Mistral",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
Expand Down Expand Up @@ -221,6 +222,7 @@
"openai": "OpenAI",
"openrouter": "OpenRouter",
"groq": "Groq",
"xai": "xAI",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
"gemini": "Gemini",
Expand Down
2 changes: 2 additions & 0 deletions ui/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"openai": "OpenAI",
"openrouter": "OpenRouter",
"groq": "Groq",
"xai": "xAI",
"mistral": "Mistral",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
Expand Down Expand Up @@ -221,6 +222,7 @@
"openai": "OpenAI",
"openrouter": "OpenRouter",
"groq": "Groq",
"xai": "xAI",
"azure": "Azure OpenAI",
"anthropic": "Anthropic",
"gemini": "Gemini",
Expand Down
6 changes: 6 additions & 0 deletions ui/src/lib/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const PROVIDER_TYPE_VARIANTS = [
'openai',
'openrouter',
'groq',
'xai',
'mistral',
'azure',
'anthropic',
Expand Down Expand Up @@ -106,6 +107,11 @@ export type Provider =
type: 'groq';
config: ApiBaseProviderConfig;
}
| {
name: string;
type: 'xai';
config: ApiBaseProviderConfig;
}
| {
name: string;
type: 'mistral';
Expand Down