feat(grok): add provider pool and transport support

This commit is contained in:
mayrain
2026-05-16 19:22:54 +08:00
parent e5f1f52759
commit cbfe1d378f
20 changed files with 2616 additions and 21 deletions

View File

@@ -8,6 +8,7 @@ use crate::conversion::{
request_conversion_enabled_for_transport, request_conversion_transport_unsupported_reason,
request_pair_allowed_for_transport,
};
use crate::grok::grok_browser_resolved_transport_profile_from_auth_config;
use crate::network::{
resolve_transport_profile, resolve_transport_profile_id, transport_proxy_is_locally_supported,
};
@@ -89,6 +90,31 @@ pub fn build_transport_diagnostics(
.and_then(|value| value.get("transport_profile"))
.cloned()
.unwrap_or(Value::Null);
let configured_legacy_grok_transport_profile = if transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("grok")
{
transport
.key
.decrypted_auth_config
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.and_then(|value| value.as_object().cloned())
.and_then(|auth_config| {
grok_browser_resolved_transport_profile_from_auth_config(
&auth_config,
"grok_auth_config",
)
.and_then(|profile| serde_json::to_value(profile).ok())
})
.unwrap_or(Value::Null)
} else {
Value::Null
};
let has_oauth_config = transport.key.decrypted_auth_config.is_some();
let oauth_resolution_supported =
!has_oauth_config || crate::supports_local_oauth_request_auth_resolution(transport);
@@ -126,6 +152,7 @@ pub fn build_transport_diagnostics(
"fingerprint": transport.key.fingerprint,
"configured_key_transport_profile": configured_key_transport_profile,
"configured_provider_transport_profile": configured_provider_transport_profile,
"configured_legacy_grok_transport_profile": configured_legacy_grok_transport_profile,
"resolved_transport_profile_id": resolved_transport_profile_id,
"resolved_transport_profile": resolved_transport_profile,
"request_pair": {
@@ -423,6 +450,43 @@ mod tests {
);
}
fn sample_grok_transport_with_legacy_user_agent() -> GatewayProviderTransportSnapshot {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
})
.to_string(),
);
transport
}
#[test]
fn transport_diagnostics_include_legacy_grok_transport_profile() {
let diagnostics = build_transport_diagnostics(
&sample_grok_transport_with_legacy_user_agent(),
"openai:chat",
"openai:chat",
);
assert_eq!(
diagnostics["configured_legacy_grok_transport_profile"]["profile_id"],
"chrome137"
);
assert_eq!(
diagnostics["resolved_transport_profile"]["profile_id"],
"chrome137"
);
assert_eq!(
diagnostics["resolved_transport_profile"]["backend"],
"browser_wreq"
);
}
#[test]
fn request_trace_proxy_value_sanitizes_url_and_marks_config_source() {
let transport = sample_transport();

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ pub mod conversion;
mod diagnostics;
mod gemini_files;
mod generic_oauth;
pub mod grok;
mod headers;
pub mod kiro;
mod network;
@@ -44,6 +45,17 @@ pub use gemini_files::{
pub use generic_oauth::{
supports_local_generic_oauth_request_auth_resolution, GenericOAuthRefreshAdapter,
};
pub use grok::{
build_grok_app_chat_body, build_grok_browser_headers, build_grok_upstream_url, grok_base_url,
grok_browser_profile_id_from_user_agent,
grok_browser_profile_metadata_from_resolved_transport_profile,
grok_browser_resolved_transport_profile,
grok_browser_resolved_transport_profile_from_auth_config,
grok_browser_transport_fingerprint_from_auth_config, is_grok_provider_transport,
resolve_grok_session_auth, GrokBrowserProfileMetadata, GrokHeaderInput, GROK_CHAT_PATH,
GROK_DEFAULT_BASE_URL, GROK_DEFAULT_BROWSER_PROFILE, GROK_DEFAULT_USER_AGENT,
GROK_INTERNAL_HEADER, GROK_RATE_LIMITS_PATH,
};
pub use headers::{should_skip_request_header, should_skip_upstream_passthrough_header};
pub use network::{
resolve_transport_execution_timeouts, resolve_transport_profile, resolve_transport_profile_id,

View File

@@ -6,6 +6,8 @@ use async_trait::async_trait;
use serde_json::{json, Map, Value};
use tracing::warn;
use crate::grok::grok_browser_resolved_transport_profile_from_auth_config;
use super::snapshot::GatewayProviderTransportSnapshot;
const TUNNEL_BASE_URL_EXTRA_KEY: &str = "tunnel_base_url";
@@ -152,9 +154,39 @@ pub fn resolve_transport_profile(
) -> Option<ResolvedTransportProfile> {
resolve_transport_profile_from_fingerprint(transport.key.fingerprint.as_ref()).or_else(|| {
resolve_transport_profile_from_provider_config(transport.provider.config.as_ref())
.or_else(|| resolve_grok_browser_transport_profile(transport))
})
}
fn resolve_grok_browser_transport_profile(
transport: &GatewayProviderTransportSnapshot,
) -> Option<ResolvedTransportProfile> {
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("grok")
{
return None;
}
let auth_config = transport
.key
.decrypted_auth_config
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| serde_json::from_str::<Value>(value).ok())?;
let object = auth_config.as_object()?;
let has_session = json_string_field(object, "sso_token")
.or_else(|| json_string_field(object, "access_token"))
.or_else(|| json_string_field(object, "token"))
.is_some();
if !has_session {
return None;
}
grok_browser_resolved_transport_profile_from_auth_config(object, "grok_auth_config")
}
fn resolve_transport_profile_from_provider_config(
config: Option<&Value>,
) -> Option<ResolvedTransportProfile> {
@@ -529,4 +561,218 @@ mod tests {
assert!(resolve_transport_profile(&transport).is_none());
assert!(!transport_profile_is_configured(&transport));
}
#[test]
fn resolves_grok_browser_transport_profile_from_session_auth_config() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"browser_profile": "chrome136",
"cf_clearance": "clearance"
})
.to_string(),
);
let profile = resolve_transport_profile(&transport).expect("profile");
assert_eq!(profile.profile_id, "chrome136");
assert_eq!(profile.backend, "browser_wreq");
assert_eq!(profile.http_mode, "auto");
assert_eq!(profile.pool_scope, "key");
assert_eq!(
profile
.extra
.as_ref()
.and_then(|value| value.get("browser_profile"))
.and_then(Value::as_str),
Some("chrome136")
);
}
#[test]
fn resolves_grok_browser_transport_profile_default_from_session_auth_config() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token"
})
.to_string(),
);
let profile = resolve_transport_profile(&transport).expect("profile");
assert_eq!(profile.profile_id, "chrome136");
assert_eq!(profile.backend, "browser_wreq");
assert_eq!(
profile
.extra
.as_ref()
.and_then(|value| value.get("source"))
.and_then(Value::as_str),
Some("grok_auth_config")
);
}
#[test]
fn resolves_grok_browser_transport_profile_normalizes_auth_config_alias() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"browser_profile": "Chrome-137"
})
.to_string(),
);
let profile = resolve_transport_profile(&transport).expect("profile");
assert_eq!(profile.profile_id, "chrome137");
assert_eq!(
profile
.extra
.as_ref()
.and_then(|value| value.get("browser_profile"))
.and_then(Value::as_str),
Some("chrome137")
);
}
#[test]
fn resolves_grok_browser_transport_profile_from_legacy_user_agent() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
})
.to_string(),
);
let profile = resolve_transport_profile(&transport).expect("profile");
assert_eq!(profile.profile_id, "chrome137");
assert_eq!(
profile
.extra
.as_ref()
.and_then(|value| value.get("browser_profile"))
.and_then(Value::as_str),
Some("chrome137")
);
}
#[test]
fn key_fingerprint_wins_over_grok_auth_config_fallback() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.provider.config = None;
transport.key.fingerprint = Some(json!({
"transport_profile": {
"profile_id": "chrome136",
"backend": "browser_wreq",
"extra": {"browser_profile": "chrome136", "source": "key"}
}
}));
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"browser_profile": "chrome137"
})
.to_string(),
);
let profile = resolve_transport_profile(&transport).expect("profile");
assert_eq!(profile.profile_id, "chrome136");
assert_eq!(
profile
.extra
.as_ref()
.and_then(|value| value.get("source"))
.and_then(Value::as_str),
Some("key")
);
}
#[test]
fn provider_fingerprint_wins_over_grok_auth_config_fallback() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = Some(json!({
"fingerprint": {
"transport_profile": {
"profile_id": "chrome136",
"backend": "browser_wreq",
"extra": {"browser_profile": "chrome136", "source": "provider"}
}
}
}));
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"browser_profile": "chrome137"
})
.to_string(),
);
let profile = resolve_transport_profile(&transport).expect("profile");
assert_eq!(profile.profile_id, "chrome136");
assert_eq!(
profile
.extra
.as_ref()
.and_then(|value| value.get("source"))
.and_then(Value::as_str),
Some("provider")
);
}
#[test]
fn rejects_unsupported_grok_auth_config_browser_profile() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"browser_profile": "safari999"
})
.to_string(),
);
assert!(resolve_transport_profile(&transport).is_none());
}
#[test]
fn rejects_unsupported_grok_auth_config_user_agent_profile() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.fingerprint = None;
transport.provider.config = None;
transport.key.decrypted_auth_config = Some(
json!({
"sso_token": "sso-token",
"user_agent": "Mozilla/5.0 Version/18.0 Safari/605.1.15"
})
.to_string(),
);
assert!(resolve_transport_profile(&transport).is_none());
}
}

View File

@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use serde_json::Value;
use crate::auth::{build_passthrough_headers_with_auth, resolve_local_openai_bearer_auth};
use crate::grok::{is_grok_provider_transport, resolve_grok_session_auth};
use crate::policy::local_standard_transport_unsupported_reason_with_network;
use crate::rules::apply_local_header_rules_with_request_headers;
use crate::snapshot::GatewayProviderTransportSnapshot;
@@ -23,21 +24,33 @@ pub fn openai_image_transport_unsupported_reason(
api_format: &str,
) -> Option<&'static str> {
let reason = local_standard_transport_unsupported_reason_with_network(transport, api_format);
if reason == Some("transport_provider_type_unsupported")
&& transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("chatgpt_web")
if is_dedicated_openai_image_provider(transport)
&& matches!(
reason,
Some("transport_provider_type_unsupported")
| Some("transport_oauth_resolution_unsupported")
)
{
return None;
}
reason
}
fn is_dedicated_openai_image_provider(transport: &GatewayProviderTransportSnapshot) -> bool {
transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("chatgpt_web")
|| is_grok_provider_transport(transport)
}
pub fn resolve_openai_image_auth(
transport: &GatewayProviderTransportSnapshot,
) -> Option<(String, String)> {
if is_grok_provider_transport(transport) {
return resolve_grok_session_auth(transport);
}
resolve_local_openai_bearer_auth(transport)
}
@@ -159,6 +172,45 @@ mod tests {
);
}
#[test]
fn grok_is_supported_by_dedicated_openai_image_transport_policy() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
assert_eq!(
openai_image_transport_unsupported_reason(&transport, "openai:image"),
None
);
}
#[test]
fn grok_oauth_session_is_supported_by_dedicated_openai_image_transport_policy() {
let mut transport = sample_transport();
transport.provider.provider_type = "grok".to_string();
transport.key.auth_type = "oauth".to_string();
transport.key.decrypted_api_key = String::new();
transport.key.decrypted_auth_config = Some(json!({"sso_token":"abc"}).to_string());
assert_eq!(
openai_image_transport_unsupported_reason(&transport, "openai:image"),
None
);
}
#[test]
fn chatgpt_web_oauth_is_supported_by_dedicated_openai_image_transport_policy() {
let mut transport = sample_transport();
transport.provider.provider_type = "chatgpt_web".to_string();
transport.key.auth_type = "oauth".to_string();
transport.key.decrypted_api_key = String::new();
transport.key.decrypted_auth_config = Some(json!({"access_token":"token"}).to_string());
assert_eq!(
openai_image_transport_unsupported_reason(&transport, "openai:image"),
None
);
}
#[test]
fn builds_json_eventstream_headers_and_applies_rules() {
let headers = build_openai_image_headers(ProviderOpenAiImageHeadersInput {

View File

@@ -238,6 +238,15 @@ const ANTIGRAVITY_RUNTIME_POLICY: ProviderRuntimePolicy = ProviderRuntimePolicy
supports_local_same_format_transport: false,
..STANDARD_RUNTIME_POLICY
};
const GROK_RUNTIME_POLICY: ProviderRuntimePolicy = ProviderRuntimePolicy {
fixed_provider: true,
api_format_inheritance: ProviderApiFormatInheritance::OAuth,
enable_format_conversion_by_default: true,
supports_model_fetch: false,
supports_local_openai_chat_transport: false,
supports_local_same_format_transport: false,
..STANDARD_RUNTIME_POLICY
};
const CLAUDE_CODE_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProviderTemplate {
provider_type: "claude_code",
@@ -352,6 +361,39 @@ const ANTIGRAVITY_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProvider
runtime_policy: ANTIGRAVITY_RUNTIME_POLICY,
};
const GROK_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProviderTemplate {
provider_type: "grok",
version: 1,
base_url: "https://grok.com",
endpoints: &[
FixedProviderEndpointTemplate {
item_key: "openai:chat",
api_format: "openai:chat",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
FixedProviderEndpointTemplate {
item_key: "openai:responses",
api_format: "openai:responses",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
FixedProviderEndpointTemplate {
item_key: "claude:messages",
api_format: "claude:messages",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
FixedProviderEndpointTemplate {
item_key: "openai:image",
api_format: "openai:image",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
],
runtime_policy: GROK_RUNTIME_POLICY,
};
pub fn provider_type_is_fixed(provider_type: &str) -> bool {
provider_runtime_policy(provider_type).fixed_provider
}
@@ -398,6 +440,7 @@ pub fn fixed_provider_template(provider_type: &str) -> Option<&'static FixedProv
"codex" => Some(&CODEX_FIXED_PROVIDER_TEMPLATE),
"chatgpt_web" => Some(&CHATGPT_WEB_FIXED_PROVIDER_TEMPLATE),
"kiro" => Some(&KIRO_FIXED_PROVIDER_TEMPLATE),
"grok" => Some(&GROK_FIXED_PROVIDER_TEMPLATE),
"gemini_cli" => Some(&GEMINI_CLI_FIXED_PROVIDER_TEMPLATE),
"vertex_ai" => Some(&VERTEX_AI_FIXED_PROVIDER_TEMPLATE),
"antigravity" => Some(&ANTIGRAVITY_FIXED_PROVIDER_TEMPLATE),
@@ -598,6 +641,29 @@ mod tests {
);
}
#[test]
fn grok_fixed_provider_template_exposes_chat_responses_messages_and_image() {
let template = fixed_provider_template("grok").expect("grok template should exist");
assert_eq!(template.base_url, "https://grok.com");
assert_eq!(template.version, 1);
assert_eq!(
template
.endpoints
.iter()
.map(|item| item.api_format)
.collect::<Vec<_>>(),
vec![
"openai:chat",
"openai:responses",
"claude:messages",
"openai:image"
]
);
assert!(!template.runtime_policy.supports_model_fetch);
assert!(!template.runtime_policy.supports_local_openai_chat_transport);
assert!(!template.runtime_policy.supports_local_same_format_transport);
}
#[test]
fn fixed_provider_key_inheritance_keeps_oauth_and_kiro_configured_bearer_keys_open() {
assert!(fixed_provider_key_inherits_api_formats(

View File

@@ -9,6 +9,7 @@ use crate::auth::{
};
use crate::claude_code::build_claude_code_passthrough_headers;
use crate::claude_code::local_claude_code_transport_unsupported_reason_with_network;
use crate::grok::{is_grok_provider_transport, resolve_grok_session_auth};
use crate::kiro::{
build_kiro_provider_headers, build_kiro_provider_request_body, is_kiro_provider_transport,
local_kiro_request_transport_unsupported_reason_with_network, KiroAuthConfig,
@@ -351,6 +352,10 @@ pub fn same_format_provider_transport_unsupported_reason(
family: SameFormatProviderFamily,
api_format: &str,
) -> Option<&'static str> {
if is_grok_provider_transport(transport) && matches!(family, SameFormatProviderFamily::Standard)
{
return None;
}
if behavior.is_kiro {
local_kiro_request_transport_unsupported_reason_with_network(transport)
} else if behavior.is_antigravity {
@@ -434,6 +439,10 @@ pub fn resolve_same_format_provider_direct_auth(
transport: &GatewayProviderTransportSnapshot,
family: SameFormatProviderFamily,
) -> Option<(String, String)> {
if is_grok_provider_transport(transport) && matches!(family, SameFormatProviderFamily::Standard)
{
return resolve_grok_session_auth(transport);
}
if behavior.is_vertex {
None
} else {