mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
feat(xai): expose CLI release check status
Record successful and failed official npm checks, retain the last successful version on failure, and publish the checked timestamp through the dashboard wire profile. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -11,8 +11,9 @@ use gateway_admin::model::accounts::{
|
||||
AccountAvailability as AdminAccountAvailability, AccountRecord,
|
||||
};
|
||||
use gateway_admin::model::observability::{
|
||||
CalculatedBillingBreakdown, CurrencyCost, DashboardWireAttribute, DashboardWireProfile,
|
||||
DashboardWireTarget, DecimalAmount, ProviderBillingInput,
|
||||
CalculatedBillingBreakdown, CurrencyCost, DashboardDesktopRelease, DashboardWireAttribute,
|
||||
DashboardWireProfile, DashboardWireTarget, DecimalAmount, DesktopReleaseStatus,
|
||||
ProviderBillingInput,
|
||||
};
|
||||
use gateway_admin::model::provider_credentials::{
|
||||
AuthorizationMutationTarget, AuthorizationOwner, AuthorizationOwnerBinding,
|
||||
@@ -57,6 +58,7 @@ use crate::credential::{
|
||||
PreparedGrokCredentialRotation, PreparedGrokCredentialRotationGuard, RedirectUriAllowlist,
|
||||
RotateManagedGrokCredential, SecretValue, VerifiedGrokAccount, VerifiedTokenSet,
|
||||
};
|
||||
use crate::transport::profile::{GrokCliReleaseSnapshot, GrokCliReleaseStatus};
|
||||
use crate::transport::{GROK_CLI_BASE_URL, XAI_PROVIDER_NAME, grok_billing_breakdown};
|
||||
|
||||
const PENDING_SCHEMA_VERSION: u64 = 2;
|
||||
@@ -66,6 +68,7 @@ const MAX_PENDING_TEXT_BYTES: usize = 512;
|
||||
pub(crate) struct XaiAdminProvider {
|
||||
provider_kind: ProviderKind,
|
||||
wire_profile: XaiWireProfileState,
|
||||
cli_release: GrokCliReleaseStatus,
|
||||
accounts: Arc<dyn ProviderAccountStore>,
|
||||
repository: GrokCredentialRepository,
|
||||
oauth_config: GrokOAuthConfig,
|
||||
@@ -95,10 +98,12 @@ impl XaiAdminProvider {
|
||||
wire_profile: XaiWireProfileState,
|
||||
accounts: Arc<dyn ProviderAccountStore>,
|
||||
services: XaiAdminServices,
|
||||
cli_release: GrokCliReleaseStatus,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider_kind,
|
||||
wire_profile,
|
||||
cli_release,
|
||||
accounts,
|
||||
repository: services.repository,
|
||||
oauth_config: services.oauth_config,
|
||||
@@ -216,34 +221,42 @@ impl ProviderAdmin for XaiAdminProvider {
|
||||
}
|
||||
|
||||
fn dashboard_wire_profile(&self) -> Option<DashboardWireProfile> {
|
||||
let profile = self.wire_profile.snapshot();
|
||||
let release = dashboard_cli_release(&profile.client_version, self.cli_release.snapshot());
|
||||
Some(DashboardWireProfile {
|
||||
provider: self.provider_kind.as_str().to_owned(),
|
||||
product: "Grok Build".to_owned(),
|
||||
version: self.wire_profile.client_version(),
|
||||
version: profile.client_version.clone(),
|
||||
build: None,
|
||||
target: DashboardWireTarget {
|
||||
os_type: self.wire_profile.target_os(),
|
||||
os_type: profile.target_os.clone(),
|
||||
os_version: "—".to_owned(),
|
||||
arch: self.wire_profile.target_arch(),
|
||||
terminal: self.wire_profile.client_mode(),
|
||||
arch: profile.target_arch.clone(),
|
||||
terminal: profile.client_mode.clone(),
|
||||
},
|
||||
user_agent: self.wire_profile.user_agent(),
|
||||
user_agent: format!(
|
||||
"{}/{} ({}; {})",
|
||||
profile.client_identifier,
|
||||
profile.client_version,
|
||||
profile.target_os,
|
||||
profile.target_arch
|
||||
),
|
||||
attributes: vec![
|
||||
DashboardWireAttribute {
|
||||
label: "客户端标识".to_owned(),
|
||||
value: self.wire_profile.client_identifier(),
|
||||
value: profile.client_identifier,
|
||||
},
|
||||
DashboardWireAttribute {
|
||||
label: "运行模式".to_owned(),
|
||||
value: self.wire_profile.client_mode(),
|
||||
value: profile.client_mode,
|
||||
},
|
||||
DashboardWireAttribute {
|
||||
label: "Token 认证".to_owned(),
|
||||
value: "xai-grok-cli".to_owned(),
|
||||
},
|
||||
],
|
||||
verified_at: Some(self.wire_profile.verified_at()),
|
||||
release: None,
|
||||
verified_at: Some(profile.verified_at),
|
||||
release: Some(release),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -771,6 +784,36 @@ fn validate_account_record(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dashboard_cli_release(
|
||||
profile_version: &str,
|
||||
snapshot: GrokCliReleaseSnapshot,
|
||||
) -> DashboardDesktopRelease {
|
||||
let status = if snapshot.checked_at.is_none() {
|
||||
DesktopReleaseStatus::Unchecked
|
||||
} else if snapshot.last_error.is_some() {
|
||||
DesktopReleaseStatus::Failed
|
||||
} else if snapshot.latest_version.as_deref() == Some(profile_version) {
|
||||
DesktopReleaseStatus::Current
|
||||
} else if snapshot.latest_version.is_some() {
|
||||
DesktopReleaseStatus::UpdateAvailable
|
||||
} else {
|
||||
DesktopReleaseStatus::Failed
|
||||
};
|
||||
DashboardDesktopRelease {
|
||||
status,
|
||||
checked_at: snapshot.checked_at,
|
||||
latest_version: snapshot.latest_version,
|
||||
latest_build: None,
|
||||
published_at: None,
|
||||
minimum_system_version: None,
|
||||
hardware_requirements: None,
|
||||
download_url: None,
|
||||
download_size: None,
|
||||
signature_present: None,
|
||||
error: snapshot.last_error,
|
||||
}
|
||||
}
|
||||
|
||||
fn account_from_record(account: &AccountRecord) -> Result<ProviderAccount, ProviderAdminError> {
|
||||
let account_id = ProviderAccountId::new(account.id.clone())
|
||||
.map_err(|_| provider_error(ProviderAdminErrorKind::Invalid))?;
|
||||
|
||||
@@ -100,6 +100,7 @@ pub async fn initialize(
|
||||
OfficialGrokCliReleaseTransport::new().map_err(|_| XaiInitializeError::Transport)?,
|
||||
),
|
||||
));
|
||||
let cli_release_status = cli_release.status();
|
||||
let provider_kind =
|
||||
ProviderKind::new(XAI_PROVIDER_NAME).map_err(|_| XaiInitializeError::ProviderKind)?;
|
||||
let accounts: Arc<dyn ProviderAccountStore> = ports.accounts();
|
||||
@@ -194,6 +195,7 @@ pub async fn initialize(
|
||||
catalog: Arc::clone(&catalog),
|
||||
runtime_policy,
|
||||
},
|
||||
cli_release_status,
|
||||
));
|
||||
let worker_contributions = provider::worker_contributions(
|
||||
refresh,
|
||||
|
||||
@@ -145,18 +145,82 @@ impl GrokCliReleaseTransport for OfficialGrokCliReleaseTransport {
|
||||
pub struct GrokCliReleaseService {
|
||||
profile: XaiWireProfileState,
|
||||
transport: Arc<dyn GrokCliReleaseTransport>,
|
||||
status: GrokCliReleaseStatus,
|
||||
}
|
||||
|
||||
impl GrokCliReleaseService {
|
||||
#[must_use]
|
||||
pub fn new(profile: XaiWireProfileState, transport: Arc<dyn GrokCliReleaseTransport>) -> Self {
|
||||
Self { profile, transport }
|
||||
Self {
|
||||
profile,
|
||||
transport,
|
||||
status: GrokCliReleaseStatus::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn status(&self) -> GrokCliReleaseStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
pub async fn refresh(&self) -> Result<String, GrokCliReleaseError> {
|
||||
let version = self.transport.fetch().await?;
|
||||
self.profile.update_client_version(&version);
|
||||
Ok(version)
|
||||
let checked_at = Utc::now();
|
||||
let result = self.transport.fetch().await;
|
||||
match result {
|
||||
Ok(version) => {
|
||||
self.profile.update_client_version(&version);
|
||||
self.status.record_success(checked_at, version.clone());
|
||||
Ok(version)
|
||||
}
|
||||
Err(error) => {
|
||||
self.status.record_failure(checked_at, &error);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 最近一次官方 Grok CLI 发布检查结果。
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub(crate) struct GrokCliReleaseSnapshot {
|
||||
pub(crate) checked_at: Option<DateTime<Utc>>,
|
||||
pub(crate) latest_version: Option<String>,
|
||||
pub(crate) last_error: Option<String>,
|
||||
}
|
||||
|
||||
/// Provider 内共享的 Grok CLI 发布检查观察状态。
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct GrokCliReleaseStatus {
|
||||
snapshot: Arc<RwLock<GrokCliReleaseSnapshot>>,
|
||||
}
|
||||
|
||||
impl GrokCliReleaseStatus {
|
||||
#[must_use]
|
||||
pub(crate) fn snapshot(&self) -> GrokCliReleaseSnapshot {
|
||||
self.snapshot
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn record_success(&self, checked_at: DateTime<Utc>, latest_version: String) {
|
||||
*self
|
||||
.snapshot
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = GrokCliReleaseSnapshot {
|
||||
checked_at: Some(checked_at),
|
||||
latest_version: Some(latest_version),
|
||||
last_error: None,
|
||||
};
|
||||
}
|
||||
|
||||
fn record_failure(&self, checked_at: DateTime<Utc>, error: &GrokCliReleaseError) {
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
snapshot.checked_at = Some(checked_at);
|
||||
snapshot.last_error = Some(error.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,3 +255,91 @@ pub enum GrokCliReleaseError {
|
||||
#[error("Grok CLI release version is invalid")]
|
||||
InvalidVersion,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct ReleaseTransport {
|
||||
outcomes: Mutex<VecDeque<Result<String, GrokCliReleaseError>>>,
|
||||
}
|
||||
|
||||
impl ReleaseTransport {
|
||||
fn new(outcomes: impl IntoIterator<Item = Result<String, GrokCliReleaseError>>) -> Self {
|
||||
Self {
|
||||
outcomes: Mutex::new(outcomes.into_iter().collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GrokCliReleaseTransport for ReleaseTransport {
|
||||
fn fetch(&self) -> BoxFuture<'_, Result<String, GrokCliReleaseError>> {
|
||||
let outcome = self
|
||||
.outcomes
|
||||
.lock()
|
||||
.expect("release outcomes lock")
|
||||
.pop_front()
|
||||
.expect("release outcome");
|
||||
Box::pin(async move { outcome })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn release_refresh_should_update_profile_and_observation() {
|
||||
let profile = wire_profile();
|
||||
let service = GrokCliReleaseService::new(
|
||||
profile.clone(),
|
||||
Arc::new(ReleaseTransport::new([Ok("0.2.112".to_owned())])),
|
||||
);
|
||||
let status = service.status();
|
||||
|
||||
assert_eq!(service.refresh().await.expect("release"), "0.2.112");
|
||||
assert_eq!(profile.client_version(), "0.2.112");
|
||||
let snapshot = status.snapshot();
|
||||
assert!(snapshot.checked_at.is_some());
|
||||
assert_eq!(snapshot.latest_version.as_deref(), Some("0.2.112"));
|
||||
assert!(snapshot.last_error.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_release_refresh_should_keep_last_successful_version() {
|
||||
let profile = wire_profile();
|
||||
let service = GrokCliReleaseService::new(
|
||||
profile.clone(),
|
||||
Arc::new(ReleaseTransport::new([
|
||||
Ok("0.2.112".to_owned()),
|
||||
Err(GrokCliReleaseError::InvalidDocument),
|
||||
])),
|
||||
);
|
||||
let status = service.status();
|
||||
|
||||
service.refresh().await.expect("first release");
|
||||
assert!(matches!(
|
||||
service.refresh().await,
|
||||
Err(GrokCliReleaseError::InvalidDocument)
|
||||
));
|
||||
|
||||
assert_eq!(profile.client_version(), "0.2.112");
|
||||
let snapshot = status.snapshot();
|
||||
assert!(snapshot.checked_at.is_some());
|
||||
assert_eq!(snapshot.latest_version.as_deref(), Some("0.2.112"));
|
||||
assert_eq!(
|
||||
snapshot.last_error.as_deref(),
|
||||
Some("Grok CLI release document is invalid")
|
||||
);
|
||||
}
|
||||
|
||||
fn wire_profile() -> XaiWireProfileState {
|
||||
XaiWireProfileState::new(XaiWireProfileConfig {
|
||||
client_identifier: "grok-shell".to_owned(),
|
||||
client_version: "0.2.106".to_owned(),
|
||||
client_mode: "headless".to_owned(),
|
||||
target_os: "linux".to_owned(),
|
||||
target_arch: "x86_64".to_owned(),
|
||||
verified_at: Utc::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ use futures::{StreamExt as _, future::BoxFuture};
|
||||
use gateway_admin::model::accounts::{
|
||||
AccountAvailability as AdminAccountAvailability, AccountRecord,
|
||||
};
|
||||
use gateway_admin::model::observability::{CurrencyCost, ProviderBillingInput};
|
||||
use gateway_admin::model::observability::{
|
||||
CurrencyCost, DesktopReleaseStatus, ProviderBillingInput,
|
||||
};
|
||||
use gateway_admin::model::provider_credentials::{
|
||||
CompleteAuthorization, PrepareCredentialImport, PrepareCredentialRefresh,
|
||||
PrepareCredentialRotation, ProviderDocument, ProviderExportCredentialInput,
|
||||
@@ -281,7 +283,9 @@ async fn xai_admin_provider_validates_known_billing_breakdown() {
|
||||
assert_eq!(profile.product, "Grok Build");
|
||||
assert_eq!(profile.version, "0.2.106");
|
||||
assert_eq!(profile.user_agent, "grok-shell/0.2.106 (linux; x86_64)");
|
||||
assert!(profile.release.is_none());
|
||||
let release = profile.release.expect("release status");
|
||||
assert_eq!(release.status, DesktopReleaseStatus::Unchecked);
|
||||
assert!(release.checked_at.is_none());
|
||||
|
||||
let billing = admin
|
||||
.calculated_billing(&ProviderBillingInput {
|
||||
|
||||
Reference in New Issue
Block a user