Files
CC-Switch/src-tauri/src/commands/xai_oauth.rs
T
Jason a35209a6e7 feat(xai): add Grok OAuth device-flow backend and proxy routing
Add an xAI OAuth manager using the OAuth 2.0 Device Authorization Grant
with endpoints resolved from xAI's OIDC discovery document. All HTTP goes
through the app-managed proxy client.

- Managed provider kind xai_oauth: forced openai_responses wire format,
  pinned api.x.ai base URL, bearer injection gated to the xAI origin,
  tokens registered for log redaction, single-auth-key takeover policy.
- Token cache cannot bypass account state: cache hits re-validate account
  usability, refresh commits run under the mutation lock with a
  refresh-token CAS check, and pending logins are re-checked before an
  account is persisted.
- Refresh classification: 401/403 with any body and 400 with a non-JSON
  body mark the account for re-auth; 429/5xx stay transient.
- Shared auth_* commands dispatch to xAI with guard types mirroring the
  Copilot/Codex branches.
2026-07-21 16:39:33 +08:00

73 lines
2.1 KiB
Rust

//! xAI OAuth state and xAI-specific commands.
use crate::proxy::providers::xai_oauth_auth::XaiOAuthManager;
use crate::proxy::providers::XAI_API_BASE_URL;
use crate::services::model_fetch::FetchedModel;
use serde::Deserialize;
use std::sync::Arc;
use std::time::Duration;
use tauri::State;
use tokio::sync::RwLock;
pub struct XaiOAuthState(pub Arc<RwLock<XaiOAuthManager>>);
#[derive(Debug, Deserialize)]
struct ModelsResponse {
#[serde(default)]
data: Vec<ModelEntry>,
}
#[derive(Debug, Deserialize)]
struct ModelEntry {
id: String,
#[serde(default)]
owned_by: Option<String>,
}
#[tauri::command(rename_all = "camelCase")]
pub async fn get_xai_oauth_models(
account_id: Option<String>,
state: State<'_, XaiOAuthState>,
) -> Result<Vec<FetchedModel>, String> {
let manager = state.0.read().await;
let resolved = match account_id
.as_deref()
.map(str::trim)
.filter(|id| !id.is_empty())
{
Some(id) => Some(id.to_string()),
None => manager.default_account_id().await,
};
let account_id = resolved.ok_or_else(|| "No usable xAI account available".to_string())?;
let token = manager
.get_valid_token_for_account(&account_id)
.await
.map_err(|error| format!("xAI OAuth token unavailable: {error}"))?;
let response = crate::proxy::http_client::get()
.get(format!("{XAI_API_BASE_URL}/models"))
.bearer_auth(token)
.timeout(Duration::from_secs(15))
.send()
.await
.map_err(|error| format!("xAI models request failed: {error}"))?;
let status = response.status();
if !status.is_success() {
return Err(format!("xAI models request failed: HTTP {status}"));
}
let payload: ModelsResponse = response
.json()
.await
.map_err(|_| "xAI models response was not valid JSON".to_string())?;
let mut models: Vec<FetchedModel> = payload
.data
.into_iter()
.map(|model| FetchedModel {
id: model.id,
owned_by: model.owned_by,
})
.collect();
models.sort_by(|a, b| a.id.cmp(&b.id));
Ok(models)
}