diff --git a/backend/crates/gateway-admin/src/model/accounts.rs b/backend/crates/gateway-admin/src/model/accounts.rs index 6abea80ee..dceb96725 100644 --- a/backend/crates/gateway-admin/src/model/accounts.rs +++ b/backend/crates/gateway-admin/src/model/accounts.rs @@ -189,7 +189,6 @@ pub struct AccountSummary { /// 账号启停写入命令。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetAccountEnabled { - pub expected_config_revision: Revision, pub account_id: String, pub enabled: bool, } @@ -197,7 +196,6 @@ pub struct SetAccountEnabled { /// 账号批量删除命令。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteAccounts { - pub expected_config_revision: Revision, pub account_ids: Vec, } diff --git a/backend/crates/gateway-admin/src/model/client_keys.rs b/backend/crates/gateway-admin/src/model/client_keys.rs index f431147f8..d29919c48 100644 --- a/backend/crates/gateway-admin/src/model/client_keys.rs +++ b/backend/crates/gateway-admin/src/model/client_keys.rs @@ -141,7 +141,6 @@ impl fmt::Debug for ClientKeySecret { /// API 提交的 Client Key 创建命令。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateClientKey { - pub expected_config_revision: Revision, pub name: String, pub label: Option, pub provider_kind: ProviderKind, @@ -151,7 +150,6 @@ pub struct CreateClientKey { /// 管理用例生成 ID 与明文后的持久化命令。 #[derive(Clone, PartialEq, Eq)] pub struct NewClientKey { - pub expected_config_revision: Revision, pub id: ClientApiKeyId, pub name: String, pub label: Option, @@ -175,7 +173,6 @@ impl fmt::Debug for NewClientKey { /// 修改 Client Key 的公开策略字段。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateClientKey { - pub expected_config_revision: Revision, pub id: ClientApiKeyId, pub name: String, pub label: Option, @@ -186,7 +183,6 @@ pub struct UpdateClientKey { /// 修改 Client Key 启用状态。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetClientKeyEnabled { - pub expected_config_revision: Revision, pub id: ClientApiKeyId, pub enabled: bool, } @@ -194,7 +190,6 @@ pub struct SetClientKeyEnabled { /// 删除 Client Key。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteClientKey { - pub expected_config_revision: Revision, pub id: ClientApiKeyId, } diff --git a/backend/crates/gateway-admin/src/model/provider_credentials.rs b/backend/crates/gateway-admin/src/model/provider_credentials.rs index d20b472cd..5ea715510 100644 --- a/backend/crates/gateway-admin/src/model/provider_credentials.rs +++ b/backend/crates/gateway-admin/src/model/provider_credentials.rs @@ -101,7 +101,6 @@ pub struct CredentialDetails { /// Provider 正式文档批量导入命令。 pub struct ImportCredentials { pub context: MutationContext, - pub expected_config_revision: Revision, pub document: ProviderDocument, } @@ -110,7 +109,6 @@ impl fmt::Debug for ImportCredentials { formatter .debug_struct("ImportCredentials") .field("context", &self.context) - .field("expected_config_revision", &self.expected_config_revision) .field("document", &self.document) .finish() } @@ -169,7 +167,6 @@ pub struct PreparedCredentialImport { /// Admin 交给 Store 的导入事务命令。 #[derive(Debug, Clone, PartialEq)] pub struct CredentialImportCommit { - pub expected_config_revision: Revision, pub prepared: PreparedCredentialImport, } @@ -254,7 +251,6 @@ pub enum AuthorizationMutationTarget { /// 必须完整进入 Provider opaque pending payload、并在 complete 后原样恢复的事务信封。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PendingAuthorizationMutation { - expected_config_revision: Revision, provider_kind: ProviderKind, target: AuthorizationMutationTarget, owner_binding: AuthorizationOwnerBinding, @@ -263,24 +259,17 @@ pub struct PendingAuthorizationMutation { impl PendingAuthorizationMutation { #[must_use] pub const fn new( - expected_config_revision: Revision, provider_kind: ProviderKind, target: AuthorizationMutationTarget, owner_binding: AuthorizationOwnerBinding, ) -> Self { Self { - expected_config_revision, provider_kind, target, owner_binding, } } - #[must_use] - pub const fn expected_config_revision(&self) -> Revision { - self.expected_config_revision - } - #[must_use] pub const fn provider_kind(&self) -> &ProviderKind { &self.provider_kind @@ -301,7 +290,6 @@ impl PendingAuthorizationMutation { #[derive(Debug, Clone, PartialEq, Eq)] pub struct StartAuthorization { pub context: MutationContext, - pub expected_config_revision: Revision, pub name: String, pub reauthorization: Option, } @@ -406,7 +394,6 @@ impl fmt::Debug for CompleteAuthorization { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialMutation { pub context: MutationContext, - pub expected_config_revision: Revision, pub account_id: ProviderAccountId, } @@ -422,7 +409,6 @@ pub struct CredentialMutationResult { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialDeletion { pub context: MutationContext, - pub expected_config_revision: Revision, pub account_ids: Vec, } @@ -520,7 +506,6 @@ impl fmt::Debug for PreparedCredentialRotation { /// Admin 交给 Store 的轮换或 refresh 事务命令。 #[derive(Debug, Clone, PartialEq)] pub struct CredentialRotationCommit { - pub expected_config_revision: Revision, pub prepared: PreparedCredentialRotationFacts, } diff --git a/backend/crates/gateway-admin/src/model/settings.rs b/backend/crates/gateway-admin/src/model/settings.rs index f926bbc2c..da5687f6c 100644 --- a/backend/crates/gateway-admin/src/model/settings.rs +++ b/backend/crates/gateway-admin/src/model/settings.rs @@ -39,7 +39,6 @@ pub struct RuntimeSettings { /// 原子替换运行设置的命令。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReplaceRuntimeSettings { - pub expected_config_revision: Revision, pub model_mappings: ModelMappings, pub refresh_margin_seconds: u64, pub refresh_concurrency: u32, diff --git a/backend/crates/gateway-admin/src/ports/store.rs b/backend/crates/gateway-admin/src/ports/store.rs index 050e24317..70bd42871 100644 --- a/backend/crates/gateway-admin/src/ports/store.rs +++ b/backend/crates/gateway-admin/src/ports/store.rs @@ -179,7 +179,7 @@ pub trait AuthStore: Send + Sync { async fn append_audit_event(&self, event: AdminAuditEvent) -> AdminStoreResult<()>; } -/// Client API Key 管理及其 revision CAS 写入。 +/// Client API Key 管理写入。 #[async_trait] pub trait ClientKeyStore: Send + Sync { async fn list_client_keys(&self, query: ClientKeyListQuery) -> AdminStoreResult; @@ -265,7 +265,7 @@ pub trait ObservabilityStore: Send + Sync { async fn list_ops_errors(&self, query: OpsErrorQuery) -> AdminStoreResult; } -/// Runtime settings 与管理员 API Key 的 revision CAS 写入。 +/// Runtime settings 与管理员 API Key 写入。 #[async_trait] pub trait SettingsStore: Send + Sync { async fn load_runtime_settings(&self) -> AdminStoreResult; @@ -280,14 +280,12 @@ pub trait SettingsStore: Send + Sync { async fn replace_admin_api_key( &self, - expected_config_revision: Revision, key: AdminApiKey, context: &MutationContext, ) -> AdminStoreResult; async fn delete_admin_api_key( &self, - expected_config_revision: Revision, context: &MutationContext, ) -> AdminStoreResult; } diff --git a/backend/crates/gateway-admin/src/use_case/accounts.rs b/backend/crates/gateway-admin/src/use_case/accounts.rs index 2477bbc53..3ab74b3a9 100644 --- a/backend/crates/gateway-admin/src/use_case/accounts.rs +++ b/backend/crates/gateway-admin/src/use_case/accounts.rs @@ -15,7 +15,7 @@ use gateway_core::{ use crate::{ model::{ - AdminError, MutationContext, Revision, + AdminError, MutationContext, accounts::{ AccountConnectionTestEvent, AccountConnectionTestEventStream, AccountListQuery, AccountRecord, AccountStatus, AccountUsageWindowQuery, @@ -54,7 +54,6 @@ pub trait AccountsService: Send + Sync { async fn refresh( &self, context: &MutationContext, - expected_config_revision: Revision, account_id: ProviderAccountId, ) -> Result; @@ -354,7 +353,6 @@ impl AccountsService for DefaultAccountsService { async fn refresh( &self, context: &MutationContext, - expected_config_revision: Revision, account_id: ProviderAccountId, ) -> Result { let (account, provider) = self.provider_for_account(&account_id).await?; @@ -367,7 +365,6 @@ impl AccountsService for DefaultAccountsService { validate_prepared_rotation(&account, &prepared, "provider credential refresh")?; let result = commit_credential_refresh( self.accounts.as_ref(), - expected_config_revision, prepared, context, "provider credential refresh", diff --git a/backend/crates/gateway-admin/src/use_case/client_keys.rs b/backend/crates/gateway-admin/src/use_case/client_keys.rs index 3243843f2..2bcc2e2c2 100644 --- a/backend/crates/gateway-admin/src/use_case/client_keys.rs +++ b/backend/crates/gateway-admin/src/use_case/client_keys.rs @@ -94,7 +94,6 @@ impl ClientKeyService for DefaultClientKeyService { .store .create_client_key( NewClientKey { - expected_config_revision: command.expected_config_revision, id, name: command.name, label: command.label, diff --git a/backend/crates/gateway-admin/src/use_case/mod.rs b/backend/crates/gateway-admin/src/use_case/mod.rs index a16b6efcd..9261a786d 100644 --- a/backend/crates/gateway-admin/src/use_case/mod.rs +++ b/backend/crates/gateway-admin/src/use_case/mod.rs @@ -108,7 +108,6 @@ async fn pending_authorization( }, }; Ok(PendingAuthorizationMutation::new( - command.expected_config_revision, provider_kind.clone(), target, crate::model::provider_credentials::AuthorizationOwnerBinding::from_context( @@ -215,20 +214,13 @@ async fn commit_authorization( async fn commit_credential_rotation( accounts: &dyn AccountStore, - expected_config_revision: crate::model::Revision, prepared: PreparedCredentialRotation, context: &MutationContext, resource: &'static str, ) -> Result { let (facts, guard) = prepared.into_parts(); match accounts - .commit_credential_rotation( - CredentialRotationCommit { - expected_config_revision, - prepared: facts, - }, - context, - ) + .commit_credential_rotation(CredentialRotationCommit { prepared: facts }, context) .await { Ok(result) => { @@ -244,20 +236,13 @@ async fn commit_credential_rotation( async fn commit_credential_refresh( accounts: &dyn AccountStore, - expected_config_revision: crate::model::Revision, prepared: PreparedCredentialRotation, context: &MutationContext, resource: &'static str, ) -> Result { let (facts, guard) = prepared.into_parts(); match accounts - .commit_credential_refresh( - CredentialRotationCommit { - expected_config_revision, - prepared: facts, - }, - context, - ) + .commit_credential_refresh(CredentialRotationCommit { prepared: facts }, context) .await { Ok(result) => { @@ -289,7 +274,6 @@ async fn set_credential_enabled( let revision = accounts .set_account_enabled( SetAccountEnabled { - expected_config_revision: command.expected_config_revision, account_id: account_id.as_str().to_owned(), enabled, }, @@ -320,7 +304,6 @@ async fn delete_credentials( let revision = accounts .delete_accounts( DeleteAccounts { - expected_config_revision: command.expected_config_revision, account_ids: account_ids .iter() .map(|account_id| account_id.as_str().to_owned()) diff --git a/backend/crates/gateway-admin/src/use_case/openai.rs b/backend/crates/gateway-admin/src/use_case/openai.rs index 94e565f43..34fc06a8f 100644 --- a/backend/crates/gateway-admin/src/use_case/openai.rs +++ b/backend/crates/gateway-admin/src/use_case/openai.rs @@ -112,7 +112,6 @@ impl OpenAiService for DefaultOpenAiService { command: ImportCredentials, ) -> Result { let context = command.context; - let expected_config_revision = command.expected_config_revision; let prepared = self .provider .prepare_import(PrepareCredentialImport { @@ -127,13 +126,7 @@ impl OpenAiService for DefaultOpenAiService { )?; let result = self .accounts - .commit_credential_import( - CredentialImportCommit { - expected_config_revision, - prepared, - }, - &context, - ) + .commit_credential_import(CredentialImportCommit { prepared }, &context) .await .map_err(|error| map_store_error(error, "OpenAI credential import"))?; publish_committed(self.snapshot.as_ref(), result.config_revision).await?; @@ -200,7 +193,6 @@ impl OpenAiService for DefaultOpenAiService { command: RotateCredential, ) -> Result { let context = command.mutation.context; - let expected_config_revision = command.mutation.expected_config_revision; let account_id = command.mutation.account_id; let details = required_credential( self.accounts.as_ref(), @@ -227,7 +219,6 @@ impl OpenAiService for DefaultOpenAiService { validate_prepared_rotation(&account, &prepared, "OpenAI credential rotation")?; let result = commit_credential_rotation( self.accounts.as_ref(), - expected_config_revision, prepared, &context, "OpenAI credential rotation", diff --git a/backend/crates/gateway-admin/src/use_case/settings.rs b/backend/crates/gateway-admin/src/use_case/settings.rs index e41961f1d..73c138051 100644 --- a/backend/crates/gateway-admin/src/use_case/settings.rs +++ b/backend/crates/gateway-admin/src/use_case/settings.rs @@ -86,13 +86,12 @@ impl SettingsService for DefaultSettingsService { &self, context: &MutationContext, ) -> Result { - let settings = self.load().await?; let mut bytes = [0_u8; 32]; OsRng.fill_bytes(&mut bytes); let key = AdminApiKey::new(format!("admin-{}", hex::encode(bytes))); let mutation = self .store - .replace_admin_api_key(settings.config_revision, key.clone(), context) + .replace_admin_api_key(key.clone(), context) .await .map_err(|error| map_store_error(error, "administrator API key"))?; publish_committed(self.snapshot.as_ref(), mutation.config_revision).await?; @@ -103,10 +102,9 @@ impl SettingsService for DefaultSettingsService { &self, context: &MutationContext, ) -> Result { - let settings = self.load().await?; let mutation = self .store - .delete_admin_api_key(settings.config_revision, context) + .delete_admin_api_key(context) .await .map_err(|error| map_store_error(error, "administrator API key"))?; publish_committed(self.snapshot.as_ref(), mutation.config_revision).await?; diff --git a/backend/crates/gateway-admin/src/use_case/xai.rs b/backend/crates/gateway-admin/src/use_case/xai.rs index 6e6843625..1010b9ee6 100644 --- a/backend/crates/gateway-admin/src/use_case/xai.rs +++ b/backend/crates/gateway-admin/src/use_case/xai.rs @@ -89,7 +89,6 @@ impl XaiService for DefaultXaiService { command: ImportCredentials, ) -> Result { let context = command.context; - let expected_config_revision = command.expected_config_revision; let prepared = self .provider .prepare_import(PrepareCredentialImport { @@ -104,13 +103,7 @@ impl XaiService for DefaultXaiService { )?; let result = self .accounts - .commit_credential_import( - CredentialImportCommit { - expected_config_revision, - prepared, - }, - &context, - ) + .commit_credential_import(CredentialImportCommit { prepared }, &context) .await .map_err(|error| map_store_error(error, "xAI credential import"))?; publish_committed(self.snapshot.as_ref(), result.config_revision).await?; diff --git a/backend/crates/gateway-admin/tests/use_case/accounts.rs b/backend/crates/gateway-admin/tests/use_case/accounts.rs index 6fe28595e..17c1f54e9 100644 --- a/backend/crates/gateway-admin/tests/use_case/accounts.rs +++ b/backend/crates/gateway-admin/tests/use_case/accounts.rs @@ -342,7 +342,6 @@ pub(super) struct FakeAccountStore { account: AccountRecord, fail_commit: Mutex, audit_requests: Mutex>, - authorization_revision: Mutex>, quota_window_usage: Mutex>, } @@ -353,7 +352,6 @@ impl FakeAccountStore { account: account_record(kind), fail_commit: Mutex::new(false), audit_requests: Mutex::new(Vec::new()), - authorization_revision: Mutex::new(None), quota_window_usage: Mutex::new(Vec::new()), }) } @@ -366,13 +364,6 @@ impl FakeAccountStore { self.audit_requests.lock().expect("audit requests").clone() } - pub(super) fn authorization_revision(&self) -> Option { - *self - .authorization_revision - .lock() - .expect("authorization revision") - } - pub(super) fn set_quota_window_usage(&self, usage: Vec) { *self.quota_window_usage.lock().expect("quota window usage") = usage; } @@ -519,10 +510,6 @@ impl AccountStore for FakeAccountStore { ) -> AdminStoreResult { self.record("store.commit_authorization"); self.record_context(context); - *self - .authorization_revision - .lock() - .expect("authorization revision") = Some(command.pending.expected_config_revision()); self.require_commit()?; let (account_id, credential_revision) = match command.credential { AuthorizationCredentialCommit::Create(credential) => (credential.account_id, None), @@ -627,7 +614,6 @@ impl SettingsStore for StaticSettingsStore { async fn replace_admin_api_key( &self, - _: Revision, _: AdminApiKey, _: &MutationContext, ) -> AdminStoreResult { @@ -636,7 +622,6 @@ impl SettingsStore for StaticSettingsStore { async fn delete_admin_api_key( &self, - _: Revision, _: &MutationContext, ) -> AdminStoreResult { Err(store_unavailable()) @@ -769,7 +754,6 @@ async fn accounts_refresh_should_keep_guard_through_store_commit() { .accounts() .refresh( &context("refresh-request"), - revision(1), ProviderAccountId::new("acct_test").expect("account ID"), ) .await @@ -882,7 +866,6 @@ async fn accounts_refresh_provider_failure_should_not_call_store_commit() { .accounts() .refresh( &context("refresh-provider-error"), - revision(1), ProviderAccountId::new("acct_test").expect("account ID"), ) .await @@ -895,7 +878,7 @@ async fn accounts_refresh_provider_failure_should_not_call_store_commit() { } #[tokio::test] -async fn accounts_refresh_store_failure_should_drop_guard_after_cas_attempt() { +async fn accounts_refresh_store_failure_should_drop_guard_after_commit_attempt() { let events = events(); let provider = FakeProviderAdmin::new("openai", events.clone()); let store = FakeAccountStore::new("openai", events.clone()); @@ -906,11 +889,10 @@ async fn accounts_refresh_store_failure_should_drop_guard_after_cas_attempt() { .accounts() .refresh( &context("refresh-store-error"), - revision(1), ProviderAccountId::new("acct_test").expect("account ID"), ) .await - .expect_err("Store CAS must fail"); + .expect_err("Store commit must fail"); assert_eq!( recorded(&events), @@ -1039,7 +1021,7 @@ fn prepared_create_with_id( fn rotation_result(command: CredentialRotationCommit) -> CredentialMutationResult { CredentialMutationResult { - config_revision: revision(command.expected_config_revision.get() + 1), + config_revision: revision(2), account_id: command.prepared.account_id, credential_revision: Some(revision( command.prepared.expected_credential_revision.get() + 1, diff --git a/backend/crates/gateway-admin/tests/use_case/mod.rs b/backend/crates/gateway-admin/tests/use_case/mod.rs index c48e3b3e4..89f3872c0 100644 --- a/backend/crates/gateway-admin/tests/use_case/mod.rs +++ b/backend/crates/gateway-admin/tests/use_case/mod.rs @@ -449,7 +449,6 @@ impl SettingsStore for UnavailableStore { async fn replace_admin_api_key( &self, - _: Revision, _: AdminApiKey, _: &MutationContext, ) -> AdminStoreResult { @@ -458,7 +457,6 @@ impl SettingsStore for UnavailableStore { async fn delete_admin_api_key( &self, - _: Revision, _: &MutationContext, ) -> AdminStoreResult { Err(unavailable("admin API key")) diff --git a/backend/crates/gateway-admin/tests/use_case/observability.rs b/backend/crates/gateway-admin/tests/use_case/observability.rs index 2f8230560..7333bf3d0 100644 --- a/backend/crates/gateway-admin/tests/use_case/observability.rs +++ b/backend/crates/gateway-admin/tests/use_case/observability.rs @@ -518,7 +518,6 @@ impl SettingsStore for FixtureSettingsStore { async fn replace_admin_api_key( &self, - _: Revision, _: AdminApiKey, _: &MutationContext, ) -> AdminStoreResult { @@ -527,7 +526,6 @@ impl SettingsStore for FixtureSettingsStore { async fn delete_admin_api_key( &self, - _: Revision, _: &MutationContext, ) -> AdminStoreResult { Err(super::unavailable("admin API key")) diff --git a/backend/crates/gateway-admin/tests/use_case/openai.rs b/backend/crates/gateway-admin/tests/use_case/openai.rs index 61531c43a..b2a6186fe 100644 --- a/backend/crates/gateway-admin/tests/use_case/openai.rs +++ b/backend/crates/gateway-admin/tests/use_case/openai.rs @@ -114,7 +114,6 @@ async fn openai_import_should_prepare_before_atomic_store_commit() { .openai() .import_document(ImportCredentials { context: context("import-openai"), - expected_config_revision: revision(1), document: document(), }) .await @@ -151,7 +150,6 @@ async fn openai_import_should_refresh_quota_for_every_imported_account() { .openai() .import_document(ImportCredentials { context: context("import-openai-batch"), - expected_config_revision: revision(1), document: document(), }) .await @@ -193,7 +191,6 @@ async fn openai_import_should_remain_successful_when_quota_refresh_fails() { .openai() .import_document(ImportCredentials { context: context("import-openai-quota-failure"), - expected_config_revision: revision(1), document: document(), }) .await @@ -218,7 +215,6 @@ async fn openai_import_provider_error_should_not_touch_store_transaction() { .openai() .import_document(ImportCredentials { context: context("import-openai-error"), - expected_config_revision: revision(1), document: document(), }) .await @@ -237,7 +233,6 @@ async fn openai_reauthorization_should_restore_pending_envelope_and_hold_guard_t .openai() .start_authorization(StartAuthorization { context: context("oauth-start-openai"), - expected_config_revision: revision(7), name: "reauthorize".to_owned(), reauthorization: Some(ReauthorizationTarget { account_id: ProviderAccountId::new("acct_test").expect("account ID"), @@ -248,7 +243,6 @@ async fn openai_reauthorization_should_restore_pending_envelope_and_hold_guard_t .expect("start reauthorization"); let pending = provider.pending().expect("pending envelope"); - assert_eq!(pending.expected_config_revision(), revision(7)); assert_eq!(pending.provider_kind().as_str(), "openai"); assert_eq!( pending.owner_binding().started_request_id(), @@ -284,7 +278,6 @@ async fn openai_reauthorization_should_restore_pending_envelope_and_hold_guard_t "guard.finish", ] ); - assert_eq!(store.authorization_revision(), Some(revision(7))); assert_eq!(store.audit_requests(), ["oauth-complete-openai"]); } @@ -299,7 +292,6 @@ async fn service(provider: Arc, store: Arc) fn mutation(account_id: &str) -> CredentialMutation { CredentialMutation { context: context("request-openai"), - expected_config_revision: revision(1), account_id: ProviderAccountId::new(account_id).expect("account ID"), } } @@ -307,7 +299,6 @@ fn mutation(account_id: &str) -> CredentialMutation { fn deletion(account_id: &str) -> CredentialDeletion { CredentialDeletion { context: context("request-openai"), - expected_config_revision: revision(1), account_ids: vec![ProviderAccountId::new(account_id).expect("account ID")], } } diff --git a/backend/crates/gateway-admin/tests/use_case/settings.rs b/backend/crates/gateway-admin/tests/use_case/settings.rs index 5be2532c0..2a0f5aa66 100644 --- a/backend/crates/gateway-admin/tests/use_case/settings.rs +++ b/backend/crates/gateway-admin/tests/use_case/settings.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use gateway_admin::{ model::{ - AdminErrorKind, MutationContext, Revision, + AdminErrorKind, MutationContext, settings::{ AdminApiKey, AdminApiKeyMutation, ReplaceRuntimeSettings, RotationStrategy, RuntimeSettings, @@ -33,7 +33,6 @@ impl SettingsStore for UnusedSettingsStore { async fn replace_admin_api_key( &self, - _: Revision, _: AdminApiKey, _: &MutationContext, ) -> AdminStoreResult { @@ -42,7 +41,6 @@ impl SettingsStore for UnusedSettingsStore { async fn delete_admin_api_key( &self, - _: Revision, _: &MutationContext, ) -> AdminStoreResult { Err(unused()) @@ -63,7 +61,6 @@ async fn settings_should_reject_zero_refresh_margin_before_store_call() { request_id: "request-settings".to_owned(), }, ReplaceRuntimeSettings { - expected_config_revision: Revision::new(1).expect("revision"), model_mappings: Default::default(), refresh_margin_seconds: 0, refresh_concurrency: 1, diff --git a/backend/crates/gateway-admin/tests/use_case/xai.rs b/backend/crates/gateway-admin/tests/use_case/xai.rs index 3ad15651c..b05fdb68c 100644 --- a/backend/crates/gateway-admin/tests/use_case/xai.rs +++ b/backend/crates/gateway-admin/tests/use_case/xai.rs @@ -8,9 +8,7 @@ use gateway_admin::{ ports::provider::ProviderAdminErrorKind, }; -use super::accounts::{ - FakeAccountStore, FakeProviderAdmin, context, document, events, recorded, revision, -}; +use super::accounts::{FakeAccountStore, FakeProviderAdmin, context, document, events, recorded}; #[tokio::test] async fn xai_disable_should_commit_then_notify_the_stateless_provider() { @@ -47,7 +45,6 @@ async fn xai_import_should_prepare_before_atomic_store_commit() { .xai() .import_document(ImportCredentials { context: context("import-xai"), - expected_config_revision: revision(3), document: document(), }) .await @@ -84,7 +81,6 @@ async fn xai_import_should_refresh_quota_for_every_imported_account() { .xai() .import_document(ImportCredentials { context: context("import-xai-batch"), - expected_config_revision: revision(1), document: document(), }) .await @@ -119,7 +115,6 @@ async fn xai_import_should_remain_successful_when_quota_refresh_fails() { .xai() .import_document(ImportCredentials { context: context("import-xai-quota-failure"), - expected_config_revision: revision(1), document: document(), }) .await @@ -140,7 +135,6 @@ async fn service(provider: Arc, store: Arc) fn mutation(account_id: &str) -> CredentialMutation { CredentialMutation { context: context("request-xai"), - expected_config_revision: revision(1), account_id: ProviderAccountId::new(account_id).expect("account ID"), } } diff --git a/backend/crates/gateway-api/src/admin/accounts.rs b/backend/crates/gateway-api/src/admin/accounts.rs index 5c4eb0cd2..e93300880 100644 --- a/backend/crates/gateway-api/src/admin/accounts.rs +++ b/backend/crates/gateway-api/src/admin/accounts.rs @@ -161,7 +161,6 @@ fn parse_sort_direction(value: &str) -> Option { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountPageData { - pub config_revision: u64, pub items: Vec, pub page: PageMeta, pub summary: AccountSummaryView, @@ -391,31 +390,21 @@ impl AccountActionRequest { } } -/// 手工 OAuth 刷新会变更持久 credential,因此必须携带配置 CAS revision。 +/// 手工 OAuth 刷新会变更持久 credential。 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountRefreshRequest { pub account_id: String, - pub expected_config_revision: u64, } impl AccountRefreshRequest { pub fn validate(&self) -> Result<(), WireValidationError> { - require_account_id(&self.account_id, "accountId")?; - if self.expected_config_revision == 0 { - return Err(WireValidationError::new("expectedConfigRevision")); - } - Ok(()) + require_account_id(&self.account_id, "accountId") } - fn into_command(self) -> Result<(Revision, ProviderAccountId), WireValidationError> { + fn into_command(self) -> Result { self.validate()?; - Ok(( - Revision::new(self.expected_config_revision) - .map_err(|_| WireValidationError::new("expectedConfigRevision"))?, - ProviderAccountId::new(self.account_id) - .map_err(|_| WireValidationError::new("accountId"))?, - )) + ProviderAccountId::new(self.account_id).map_err(|_| WireValidationError::new("accountId")) } } @@ -460,7 +449,6 @@ pub struct AccountModelsData { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountRefreshData { - pub config_revision: u64, pub account: AccountView, } @@ -579,14 +567,12 @@ impl AccountProvider { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountImportRequest { pub provider: String, - pub expected_config_revision: u64, pub data: Value, } impl AccountImportRequest { pub fn validate(&self) -> Result<(), WireValidationError> { AccountProvider::parse(&self.provider)?; - require_positive(self.expected_config_revision, "expectedConfigRevision")?; if !self.data.is_object() || serde_json::to_vec(&self.data) .map_or(true, |encoded| encoded.len() > MAX_IMPORT_DATA_BYTES) @@ -606,10 +592,6 @@ impl AccountImportRequest { provider, ImportCredentials { context, - expected_config_revision: revision( - self.expected_config_revision, - "expectedConfigRevision", - )?, document: provider_document(self.data, "data")?, }, )) @@ -620,7 +602,6 @@ impl AccountImportRequest { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct StartAccountAuthorizationRequest { pub provider: String, - pub expected_config_revision: u64, pub name: String, pub account_id: Option, pub expected_credential_revision: Option, @@ -629,7 +610,6 @@ pub struct StartAccountAuthorizationRequest { impl StartAccountAuthorizationRequest { pub fn validate(&self) -> Result<(), WireValidationError> { AccountProvider::parse(&self.provider)?; - require_positive(self.expected_config_revision, "expectedConfigRevision")?; require_text(&self.name, MAX_NAME_BYTES, "name")?; match ( self.account_id.as_deref(), @@ -663,10 +643,6 @@ impl StartAccountAuthorizationRequest { provider, StartAuthorization { context, - expected_config_revision: revision( - self.expected_config_revision, - "expectedConfigRevision", - )?, name: self.name, reauthorization, }, @@ -721,14 +697,12 @@ impl CompleteAccountAuthorizationRequest { pub struct AccountMutationRequest { pub provider: String, pub account_id: String, - pub expected_config_revision: u64, } impl AccountMutationRequest { pub fn validate(&self) -> Result<(), WireValidationError> { AccountProvider::parse(&self.provider)?; - require_account_id(&self.account_id, "accountId")?; - require_positive(self.expected_config_revision, "expectedConfigRevision") + require_account_id(&self.account_id, "accountId") } fn into_command( @@ -741,10 +715,6 @@ impl AccountMutationRequest { provider, CredentialMutation { context, - expected_config_revision: revision( - self.expected_config_revision, - "expectedConfigRevision", - )?, account_id: ProviderAccountId::new(self.account_id) .map_err(|_| WireValidationError::new("accountId"))?, }, @@ -757,7 +727,6 @@ impl AccountMutationRequest { pub struct AccountDeletionRequest { pub provider: String, pub account_ids: Vec, - pub expected_config_revision: u64, } impl AccountDeletionRequest { @@ -773,7 +742,7 @@ impl AccountDeletionRequest { return Err(WireValidationError::new("accountIds")); } } - require_positive(self.expected_config_revision, "expectedConfigRevision") + Ok(()) } fn into_command( @@ -786,10 +755,6 @@ impl AccountDeletionRequest { provider, CredentialDeletion { context, - expected_config_revision: revision( - self.expected_config_revision, - "expectedConfigRevision", - )?, account_ids: self .account_ids .into_iter() @@ -806,7 +771,6 @@ impl AccountDeletionRequest { pub struct RotateAccountRequest { pub provider: String, pub account_id: String, - pub expected_config_revision: u64, pub expected_credential_revision: u64, pub access_token: String, pub refresh_token: Option, @@ -818,7 +782,6 @@ impl RotateAccountRequest { return Err(WireValidationError::new("provider")); } require_account_id(&self.account_id, "accountId")?; - require_positive(self.expected_config_revision, "expectedConfigRevision")?; require_positive( self.expected_credential_revision, "expectedCredentialRevision", @@ -840,10 +803,6 @@ impl RotateAccountRequest { Ok(RotateCredential { mutation: CredentialMutation { context, - expected_config_revision: revision( - self.expected_config_revision, - "expectedConfigRevision", - )?, account_id: ProviderAccountId::new(self.account_id) .map_err(|_| WireValidationError::new("accountId"))?, }, @@ -859,7 +818,6 @@ impl RotateAccountRequest { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountImportData { - pub config_revision: u64, pub imported_count: usize, pub account_ids: Vec, } @@ -872,7 +830,6 @@ impl From for AccountImportData { .map(|account_id| account_id.to_string()) .collect::>(); Self { - config_revision: result.config_revision.get(), imported_count: account_ids.len(), account_ids, } @@ -900,7 +857,6 @@ impl From for AccountAuthorizationData { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountMutationData { - pub config_revision: u64, pub account_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub credential_revision: Option, @@ -909,7 +865,6 @@ pub struct AccountMutationData { impl From for AccountMutationData { fn from(result: CredentialMutationResult) -> Self { Self { - config_revision: result.config_revision.get(), account_id: result.account_id.to_string(), credential_revision: result.credential_revision.map(Revision::get), } @@ -919,7 +874,6 @@ impl From for AccountMutationData { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountDeletionData { - pub config_revision: u64, pub deleted_count: usize, pub account_ids: Vec, } @@ -932,7 +886,6 @@ impl From for AccountDeletionData { .map(|account_id| account_id.to_string()) .collect::>(); Self { - config_revision: result.config_revision.get(), deleted_count: account_ids.len(), account_ids, } @@ -1264,15 +1217,11 @@ async fn refresh_account( where S: AdminSessionState + Send + Sync, { - let (expected_config_revision, account_id) = request.into_command().map_err(map_wire_error)?; + let account_id = request.into_command().map_err(map_wire_error)?; let result = state .admin_services() .accounts() - .refresh( - &auth.context().mutation_context(), - expected_config_revision, - account_id, - ) + .refresh(&auth.context().mutation_context(), account_id) .await .map_err(map_service_error)?; let data = account_refresh_data(result, Utc::now()); @@ -1394,7 +1343,6 @@ fn account_page_data( u32::try_from(result.total.div_ceil(u64::from(page_size))).unwrap_or(u32::MAX) }; AccountPageData { - config_revision: result.config_revision.get(), items: result .items .into_iter() @@ -1412,7 +1360,6 @@ fn account_page_data( fn account_refresh_data(result: AccountRefreshResult, now: DateTime) -> AccountRefreshData { AccountRefreshData { - config_revision: result.config_revision.get(), account: account_view(result.account, now), } } diff --git a/backend/crates/gateway-api/src/admin/client_keys.rs b/backend/crates/gateway-api/src/admin/client_keys.rs index 713968349..6244c18b4 100644 --- a/backend/crates/gateway-api/src/admin/client_keys.rs +++ b/backend/crates/gateway-api/src/admin/client_keys.rs @@ -4,14 +4,11 @@ use std::fmt; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use chrono::{DateTime, Utc}; -use gateway_admin::model::{ - Revision, - client_keys::{ - ClientKeyCursor, ClientKeyCursorValue as DomainCursorValue, ClientKeyListQuery, - ClientKeyMutation, ClientKeyPage, ClientKeyPageSize, ClientKeyRecord, ClientKeySecret, - ClientKeySort as DomainSort, ClientKeySortField as DomainSortField, CreateClientKey, - CreatedClientKey, DeleteClientKey, SetClientKeyEnabled, SortDirection, UpdateClientKey, - }, +use gateway_admin::model::client_keys::{ + ClientKeyCursor, ClientKeyCursorValue as DomainCursorValue, ClientKeyListQuery, + ClientKeyMutation, ClientKeyPage, ClientKeyPageSize, ClientKeyRecord, ClientKeySecret, + ClientKeySort as DomainSort, ClientKeySortField as DomainSortField, CreateClientKey, + CreatedClientKey, DeleteClientKey, SetClientKeyEnabled, SortDirection, UpdateClientKey, }; use gateway_core::{ policy::{ClientApiKeyId, RateLimits}, @@ -138,7 +135,6 @@ impl ClientKeySort { #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct CreateClientKeyRequest { - expected_config_revision: u64, name: String, label: Option, provider_kind: String, @@ -149,14 +145,12 @@ pub struct CreateClientKeyRequest { impl CreateClientKeyRequest { /// 校验 wire 边界并直接构造管理用例命令。 pub fn into_command(self) -> Result { - validate_revision(self.expected_config_revision)?; validate_required_text(&self.name, "name")?; validate_optional_text(self.label.as_deref(), "label")?; validate_provider_kind(&self.provider_kind)?; validate_limit(self.max_concurrency, "maxConcurrency")?; validate_limit(self.requests_per_minute, "requestsPerMinute")?; Ok(CreateClientKey { - expected_config_revision: revision(self.expected_config_revision)?, name: self.name, label: self.label, provider_kind: ProviderKind::new(self.provider_kind) @@ -174,7 +168,6 @@ impl CreateClientKeyRequest { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateClientKeyRequest { id: String, - expected_config_revision: u64, name: String, label: Option, provider_kind: String, @@ -186,14 +179,12 @@ impl UpdateClientKeyRequest { /// 校验 wire 边界并直接构造管理用例命令。 pub fn into_command(self) -> Result { validate_required_text(&self.id, "id")?; - validate_revision(self.expected_config_revision)?; validate_required_text(&self.name, "name")?; validate_optional_text(self.label.as_deref(), "label")?; validate_provider_kind(&self.provider_kind)?; validate_limit(self.max_concurrency, "maxConcurrency")?; validate_limit(self.requests_per_minute, "requestsPerMinute")?; Ok(UpdateClientKey { - expected_config_revision: revision(self.expected_config_revision)?, id: client_key_id(self.id, "clientKeyMutationNotFound")?, name: self.name, label: self.label, @@ -207,12 +198,11 @@ impl UpdateClientKeyRequest { } } -/// 只携带 ID 与配置 revision 的 Client Key mutation 请求。 +/// 只携带 ID 的 Client Key mutation 请求。 #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ClientKeyRevisionRequest { +pub struct ClientKeyMutationRequest { id: String, - expected_config_revision: u64, } /// 读取一次完整 Key 的 ID query。 @@ -233,20 +223,15 @@ impl ClientKeyIdQuery { } } -impl ClientKeyRevisionRequest { - /// 校验请求并取出 ID 与 revision。 - pub fn into_parts(self) -> Result<(String, u64), WireValidationError> { +impl ClientKeyMutationRequest { + /// 校验请求并取出 ID。 + pub fn into_id(self) -> Result { validate_required_text(&self.id, "id")?; - validate_revision(self.expected_config_revision)?; - Ok((self.id, self.expected_config_revision)) + Ok(self.id) } - fn into_command_parts(self) -> Result<(ClientApiKeyId, Revision), WireValidationError> { - let (id, expected_config_revision) = self.into_parts()?; - Ok(( - client_key_id(id, "clientKeyMutationNotFound")?, - revision(expected_config_revision)?, - )) + fn into_domain_id(self) -> Result { + client_key_id(self.into_id()?, "clientKeyMutationNotFound") } } @@ -289,7 +274,6 @@ impl From for ClientKeyView { #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientKeyListData { - config_revision: u64, items: Vec, next_cursor: Option, total: u64, @@ -298,14 +282,8 @@ pub struct ClientKeyListData { impl ClientKeyListData { /// 构造 Client Key 列表响应。 #[must_use] - pub fn new( - config_revision: u64, - items: Vec, - next_cursor: Option, - total: u64, - ) -> Self { + pub fn new(items: Vec, next_cursor: Option, total: u64) -> Self { Self { - config_revision, items, next_cursor, total, @@ -325,7 +303,6 @@ impl TryFrom for ClientKeyListData { .map(encode_client_key_cursor) .transpose()?; Ok(Self::new( - page.config_revision.get(), page.items.into_iter().map(Into::into).collect(), next_cursor, page.total, @@ -337,7 +314,6 @@ impl TryFrom for ClientKeyListData { #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CreatedClientKeyData { - config_revision: u64, id: String, prefix: String, plaintext_key: String, @@ -346,9 +322,8 @@ pub struct CreatedClientKeyData { impl CreatedClientKeyData { /// 构造一次性创建响应。 #[must_use] - pub fn new(config_revision: u64, id: String, prefix: String, plaintext_key: String) -> Self { + pub fn new(id: String, prefix: String, plaintext_key: String) -> Self { Self { - config_revision, id, prefix, plaintext_key, @@ -359,7 +334,6 @@ impl CreatedClientKeyData { impl From for CreatedClientKeyData { fn from(created: CreatedClientKey) -> Self { Self::new( - created.config_revision.get(), created.secret.record.id.to_string(), created.secret.record.prefix.clone(), created.secret.expose_for_response().to_owned(), @@ -371,7 +345,6 @@ impl fmt::Debug for CreatedClientKeyData { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("CreatedClientKeyData") - .field("config_revision", &self.config_revision) .field("id", &self.id) .field("prefix", &self.prefix) .field("plaintext_key", &"[REDACTED]") @@ -417,24 +390,20 @@ impl fmt::Debug for RevealedClientKeyData { #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MutatedClientKeyData { - config_revision: u64, id: String, } impl MutatedClientKeyData { /// 构造 mutation 响应。 #[must_use] - pub fn new(config_revision: u64, id: String) -> Self { - Self { - config_revision, - id, - } + pub fn new(id: String) -> Self { + Self { id } } } impl From for MutatedClientKeyData { fn from(mutation: ClientKeyMutation) -> Self { - Self::new(mutation.config_revision.get(), mutation.id.to_string()) + Self::new(mutation.id.to_string()) } } @@ -582,18 +551,6 @@ fn wire_cursor(cursor: ClientKeyCursor) -> Result Result<(), WireValidationError> { - if value == 0 { - return Err(WireValidationError::new("expectedConfigRevision")); - } - Ok(()) -} - -fn revision(value: u64) -> Result { - validate_revision(value)?; - Revision::new(value).map_err(|_| WireValidationError::new("expectedConfigRevision")) -} - fn client_key_id( value: String, field: &'static str, @@ -768,23 +725,19 @@ where async fn disable_client_key( auth: AdminAuth, State(state): State, - Json(payload): Json, + Json(payload): Json, ) -> Result where S: AdminSessionState + Send + Sync, { - let (id, expected_config_revision) = payload.into_command_parts().map_err(map_wire_error)?; + let id = payload.into_domain_id().map_err(map_wire_error)?; mutation_response( state .admin_services() .client_keys() .set_enabled( &auth.context().mutation_context(), - SetClientKeyEnabled { - expected_config_revision, - id, - enabled: false, - }, + SetClientKeyEnabled { id, enabled: false }, ) .await, ) @@ -793,23 +746,19 @@ where async fn enable_client_key( auth: AdminAuth, State(state): State, - Json(payload): Json, + Json(payload): Json, ) -> Result where S: AdminSessionState + Send + Sync, { - let (id, expected_config_revision) = payload.into_command_parts().map_err(map_wire_error)?; + let id = payload.into_domain_id().map_err(map_wire_error)?; mutation_response( state .admin_services() .client_keys() .set_enabled( &auth.context().mutation_context(), - SetClientKeyEnabled { - expected_config_revision, - id, - enabled: true, - }, + SetClientKeyEnabled { id, enabled: true }, ) .await, ) @@ -818,23 +767,17 @@ where async fn delete_client_key( auth: AdminAuth, State(state): State, - Json(payload): Json, + Json(payload): Json, ) -> Result where S: AdminSessionState + Send + Sync, { - let (id, expected_config_revision) = payload.into_command_parts().map_err(map_wire_error)?; + let id = payload.into_domain_id().map_err(map_wire_error)?; mutation_response( state .admin_services() .client_keys() - .delete( - &auth.context().mutation_context(), - DeleteClientKey { - expected_config_revision, - id, - }, - ) + .delete(&auth.context().mutation_context(), DeleteClientKey { id }) .await, ) } @@ -854,9 +797,6 @@ fn map_wire_error(error: WireValidationError) -> AdminError { "cursor" => AdminError::bad_request("Invalid client key cursor"), "clientKeyRevealNotFound" => AdminError::not_found("Client API key was not found"), "clientKeyMutationNotFound" => AdminError::not_found("client API key was not found"), - "expectedConfigRevision" => { - AdminError::bad_request("expectedConfigRevision must be positive") - } _ => AdminError::bad_request("Invalid client API key request"), } } diff --git a/backend/crates/gateway-api/src/admin/settings.rs b/backend/crates/gateway-api/src/admin/settings.rs index 3cd394a3d..9859fa9d0 100644 --- a/backend/crates/gateway-api/src/admin/settings.rs +++ b/backend/crates/gateway-api/src/admin/settings.rs @@ -10,12 +10,8 @@ use axum::{ routing::{get, post}, }; use chrono::{DateTime, Utc}; -use gateway_admin::model::{ - Revision, - settings::{ - ModelMappings as DomainModelMappings, ReplaceRuntimeSettings, RotationStrategy, - RuntimeSettings, - }, +use gateway_admin::model::settings::{ + ModelMappings as DomainModelMappings, ReplaceRuntimeSettings, RotationStrategy, RuntimeSettings, }; use gateway_core::routing::{PublicModelId, UpstreamModelId}; use serde::{Deserialize, Serialize}; @@ -32,7 +28,6 @@ pub type ModelMappings = BTreeMap; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeSettingsView { - pub config_revision: u64, pub model_mappings: ModelMappings, pub refresh_margin_seconds: u64, pub refresh_concurrency: u64, @@ -49,7 +44,6 @@ pub struct RuntimeSettingsView { #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateRuntimeSettingsRequest { - pub expected_config_revision: u64, pub model_mappings: ModelMappings, pub refresh_margin_seconds: u64, pub refresh_concurrency: u64, @@ -64,9 +58,6 @@ pub struct UpdateRuntimeSettingsRequest { impl UpdateRuntimeSettingsRequest { /// 校验公共运行参数。 pub fn validate(&self) -> Result<(), WireValidationError> { - if self.expected_config_revision == 0 { - return Err(WireValidationError::new("expectedConfigRevision")); - } validate_model_mappings(&self.model_mappings)?; for (value, field) in [ (self.refresh_margin_seconds, "refreshMarginSeconds"), @@ -93,8 +84,6 @@ impl UpdateRuntimeSettingsRequest { fn into_command(self) -> Result { self.validate()?; Ok(ReplaceRuntimeSettings { - expected_config_revision: Revision::new(self.expected_config_revision) - .map_err(|_| WireValidationError::new("settingsRevisionOverflow"))?, model_mappings: domain_model_mappings(self.model_mappings)?, refresh_margin_seconds: self.refresh_margin_seconds, refresh_concurrency: u32::try_from(self.refresh_concurrency) @@ -117,7 +106,6 @@ impl UpdateRuntimeSettingsRequest { impl From for RuntimeSettingsView { fn from(settings: RuntimeSettings) -> Self { Self { - config_revision: settings.config_revision.get(), model_mappings: wire_model_mappings(settings.model_mappings), refresh_margin_seconds: settings.refresh_margin_seconds, refresh_concurrency: u64::from(settings.refresh_concurrency), @@ -358,7 +346,6 @@ fn valid_model_name(value: &str, max_len: usize) -> bool { fn map_wire_error(error: WireValidationError) -> AdminError { let message = match error.field() { - "settingsRevisionOverflow" => "expectedConfigRevision must be positive".to_owned(), "settingsRefreshConcurrencyOverflow" => "Invalid refreshConcurrency".to_owned(), "settingsMaxConcurrencyOverflow" => "Invalid maxConcurrentPerAccount".to_owned(), "settingsUsageRetentionOverflow" => "Invalid usageRetentionDays".to_owned(), diff --git a/backend/crates/gateway-api/tests/admin/accounts.rs b/backend/crates/gateway-api/tests/admin/accounts.rs index e2d4e1a59..bb0c75f30 100644 --- a/backend/crates/gateway-api/tests/admin/accounts.rs +++ b/backend/crates/gateway-api/tests/admin/accounts.rs @@ -147,7 +147,7 @@ mod actions { } #[test] - fn account_actions_should_require_frozen_account_ids_and_revision() { + fn account_actions_should_require_frozen_account_ids_and_reject_unknown_revision() { let id: AccountIdQuery = serde_json::from_value(json!({ "accountId": "acct_1" })).expect("decode ID query"); assert!(id.validate().is_ok()); @@ -155,13 +155,16 @@ mod actions { serde_json::from_value(json!({ "accountId": "legacy-id" })).expect("decode action"); assert_eq!(action.validate().unwrap_err().field(), "accountId"); let refresh: AccountRefreshRequest = serde_json::from_value(json!({ - "accountId": "acct_1", - "expectedConfigRevision": 0 + "accountId": "acct_1" })) .expect("decode refresh"); - assert_eq!( - refresh.validate().unwrap_err().field(), - "expectedConfigRevision" + assert!(refresh.validate().is_ok()); + assert!( + serde_json::from_value::(json!({ + "accountId": "acct_1", + "expectedConfigRevision": 0 + })) + .is_err() ); } @@ -169,7 +172,6 @@ mod actions { fn account_import_should_use_provider_and_opaque_data_fields() { let valid: AccountImportRequest = serde_json::from_value(json!({ "provider": "openai", - "expectedConfigRevision": 7, "data": { "providerOwnedUnknownField": {"nested": [1, 2, 3]}, "accounts": [{"credentials": {"access_token": "provider-validates-this"}}] @@ -180,7 +182,6 @@ mod actions { let invalid: AccountImportRequest = serde_json::from_value(json!({ "provider": "xai", - "expectedConfigRevision": 7, "data": [] })) .expect("decode invalid account import"); @@ -188,27 +189,32 @@ mod actions { assert!( serde_json::from_value::(json!({ "provider": "openai", - "expectedConfigRevision": 7, "document": {} })) .is_err() ); + assert!( + serde_json::from_value::(json!({ + "provider": "openai", + "expectedConfigRevision": 7, + "data": {} + })) + .is_err() + ); } #[test] fn account_deletion_should_validate_one_provider_batch_and_emit_account_ids() { let request: AccountDeletionRequest = serde_json::from_value(json!({ "provider": "xai", - "accountIds": ["acct_1", "acct_2"], - "expectedConfigRevision": 8 + "accountIds": ["acct_1", "acct_2"] })) .expect("decode account deletion"); assert!(request.validate().is_ok()); let duplicate: AccountDeletionRequest = serde_json::from_value(json!({ "provider": "xai", - "accountIds": ["acct_1", "acct_1"], - "expectedConfigRevision": 8 + "accountIds": ["acct_1", "acct_1"] })) .expect("decode duplicate account deletion"); assert_eq!(duplicate.validate().unwrap_err().field(), "accountIds"); @@ -223,7 +229,6 @@ mod actions { assert_eq!( serde_json::to_value(response).expect("serialize account deletion"), json!({ - "configRevision": 9, "deletedCount": 2, "accountIds": ["acct_1", "acct_2"] }) @@ -239,7 +244,6 @@ mod actions { assert_eq!( serde_json::to_value(response).expect("serialize account import"), json!({ - "configRevision": 8, "importedCount": 1, "accountIds": ["acct_imported"] }) diff --git a/backend/crates/gateway-api/tests/admin/client_keys.rs b/backend/crates/gateway-api/tests/admin/client_keys.rs index 155ff81f4..44d0baba3 100644 --- a/backend/crates/gateway-api/tests/admin/client_keys.rs +++ b/backend/crates/gateway-api/tests/admin/client_keys.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, TimeZone as _, Utc}; use gateway_api::admin::client_keys::{ - self, ClientKeyCursorData, ClientKeyCursorValue, ClientKeyListData, ClientKeyRevisionRequest, + self, ClientKeyCursorData, ClientKeyCursorValue, ClientKeyListData, ClientKeyMutationRequest, ClientKeySort, ClientKeySortDirection, ClientKeySortField, ClientKeyView, CreateClientKeyRequest, CreatedClientKeyData, ListClientKeysQuery, MutatedClientKeyData, RevealedClientKeyData, UpdateClientKeyRequest, decode_client_key_cursor, @@ -101,9 +101,8 @@ fn client_key_queries_should_reject_unknown_zero_and_oversized_fields() { } #[test] -fn client_key_mutations_should_validate_revision_text_limits_and_unknown_fields() { +fn client_key_mutations_should_validate_text_limits_and_unknown_fields() { let valid = serde_json::from_value::(json!({ - "expectedConfigRevision": 7, "name": "terminal key", "label": "production", "providerKind": "openai", @@ -113,22 +112,11 @@ fn client_key_mutations_should_validate_revision_text_limits_and_unknown_fields( .expect("deserialize create") .into_command() .expect("validate create"); - assert_eq!(valid.expected_config_revision.get(), 7); + assert_eq!(valid.name, "terminal key"); for (payload, field) in [ ( json!({ - "expectedConfigRevision": 0, - "name": "key", - "providerKind": "openai", - "maxConcurrency": 0, - "requestsPerMinute": 0 - }), - "expectedConfigRevision", - ), - ( - json!({ - "expectedConfigRevision": 1, "name": " ", "providerKind": "openai", "maxConcurrency": 0, @@ -138,7 +126,6 @@ fn client_key_mutations_should_validate_revision_text_limits_and_unknown_fields( ), ( json!({ - "expectedConfigRevision": 1, "name": "key", "providerKind": "openai", "maxConcurrency": u64::MAX, @@ -170,14 +157,23 @@ fn client_key_mutations_should_validate_revision_text_limits_and_unknown_fields( })) .is_err() ); - let revision = serde_json::from_value::(json!({ - "id": "key_1", - "expectedConfigRevision": 3 + assert!( + serde_json::from_value::(json!({ + "expectedConfigRevision": 7, + "name": "terminal key", + "providerKind": "openai", + "maxConcurrency": 2, + "requestsPerMinute": 60 + })) + .is_err() + ); + let mutation_id = serde_json::from_value::(json!({ + "id": "key_1" })) - .expect("deserialize revision mutation") - .into_parts() - .expect("validate revision mutation"); - assert_eq!(revision, ("key_1".to_owned(), 3)); + .expect("deserialize mutation") + .into_id() + .expect("validate mutation"); + assert_eq!(mutation_id, "key_1"); } #[test] @@ -225,9 +221,9 @@ fn client_key_responses_should_keep_shape_and_redact_creation_debug() { updated_at: created_at, last_used_at: Some(created_at), }); - let list = serde_json::to_value(ClientKeyListData::new(9, vec![view], None, 1)) - .expect("serialize list"); - assert_eq!(list["configRevision"], 9); + let list = + serde_json::to_value(ClientKeyListData::new(vec![view], None, 1)).expect("serialize list"); + assert!(list.get("configRevision").is_none()); assert_eq!(list["total"], 1); assert_eq!(list["items"][0]["id"], "key_visible"); assert_eq!(list["items"][0]["providerKind"], "openai"); @@ -248,7 +244,6 @@ fn client_key_responses_should_keep_shape_and_redact_creation_debug() { let plaintext = format!("sk_{}", "a".repeat(43)); let created = CreatedClientKeyData::new( - 10, "key_created".to_owned(), "sk_aaaaaaaaa".to_owned(), plaintext.clone(), @@ -263,9 +258,9 @@ fn client_key_responses_should_keep_shape_and_redact_creation_debug() { json!({ "id": "key_created", "plaintextKey": plaintext }) ); assert_eq!( - serde_json::to_value(MutatedClientKeyData::new(11, "key_created".to_owned())) + serde_json::to_value(MutatedClientKeyData::new("key_created".to_owned())) .expect("serialize mutation"), - json!({ "configRevision": 11, "id": "key_created" }) + json!({ "id": "key_created" }) ); } diff --git a/backend/crates/gateway-api/tests/admin/mod.rs b/backend/crates/gateway-api/tests/admin/mod.rs index c288ae2f3..14d573ae5 100644 --- a/backend/crates/gateway-api/tests/admin/mod.rs +++ b/backend/crates/gateway-api/tests/admin/mod.rs @@ -279,8 +279,9 @@ impl SettingsStore for MemorySettingsStore { command: ReplaceRuntimeSettings, _: &MutationContext, ) -> AdminStoreResult { + let mut settings = self.settings.lock().expect("settings"); let updated = RuntimeSettings { - config_revision: next_revision(command.expected_config_revision), + config_revision: next_revision(settings.config_revision), model_mappings: command.model_mappings, refresh_margin_seconds: command.refresh_margin_seconds, refresh_concurrency: command.refresh_concurrency, @@ -292,31 +293,31 @@ impl SettingsStore for MemorySettingsStore { audit_retention_days: command.audit_retention_days, updated_at: Utc::now(), }; - *self.settings.lock().expect("settings") = updated.clone(); + *settings = updated.clone(); Ok(updated) } async fn replace_admin_api_key( &self, - expected_config_revision: Revision, key: AdminApiKey, _: &MutationContext, ) -> AdminStoreResult { *self.api_key.lock().expect("API key") = Some(key); + let revision = self.settings.lock().expect("settings").config_revision; Ok(AdminApiKeyMutation { - config_revision: next_revision(expected_config_revision), + config_revision: next_revision(revision), exists: true, }) } async fn delete_admin_api_key( &self, - expected_config_revision: Revision, _: &MutationContext, ) -> AdminStoreResult { *self.api_key.lock().expect("API key") = None; + let revision = self.settings.lock().expect("settings").config_revision; Ok(AdminApiKeyMutation { - config_revision: next_revision(expected_config_revision), + config_revision: next_revision(revision), exists: false, }) } diff --git a/backend/crates/gateway-api/tests/admin/settings.rs b/backend/crates/gateway-api/tests/admin/settings.rs index c790312f5..d9ce94ca4 100644 --- a/backend/crates/gateway-api/tests/admin/settings.rs +++ b/backend/crates/gateway-api/tests/admin/settings.rs @@ -37,7 +37,6 @@ async fn response_json(response: axum::response::Response) -> Value { fn update_body() -> Value { json!({ - "expectedConfigRevision": 7, "modelMappings": { "gpt-5.4": "gpt-5.5", "grok-latest": "grok-4.5" @@ -64,22 +63,11 @@ fn settings_request_should_reject_unknown_rotation_strategy() { } #[test] -fn settings_request_should_require_positive_revision() { +fn settings_request_should_reject_unknown_revision_field() { let mut body = update_body(); - body["expectedConfigRevision"] = json!(0); - let request: UpdateRuntimeSettingsRequest = - serde_json::from_value(body).expect("decode settings"); - assert_eq!( - request.validate().unwrap_err().field(), - "expectedConfigRevision" - ); + body["expectedConfigRevision"] = json!(7); - let mut missing = update_body(); - missing - .as_object_mut() - .expect("settings object") - .remove("expectedConfigRevision"); - assert!(serde_json::from_value::(missing).is_err()); + assert!(serde_json::from_value::(body).is_err()); } #[test] @@ -124,7 +112,7 @@ async fn settings_post_should_replace_global_model_mappings() { .expect("settings update response"); let data = response_json(response).await["data"].clone(); - assert_eq!(data["configRevision"], 8); + assert!(data.get("configRevision").is_none()); assert_eq!(data["modelMappings"]["gpt-5.4"], "gpt-5.5"); assert_eq!(data["modelMappings"]["grok-latest"], "grok-4.5"); } diff --git a/backend/crates/gateway-store/src/lib.rs b/backend/crates/gateway-store/src/lib.rs index 0afb4a601..cc2351981 100644 --- a/backend/crates/gateway-store/src/lib.rs +++ b/backend/crates/gateway-store/src/lib.rs @@ -582,7 +582,6 @@ impl SettingsStore for AdminSettingsStoreAdapter { }; let snapshot = postgres::ControlPlaneRepository::replace_control_plane( &self.control_plane, - store_revision(command.expected_config_revision)?, replacement, ) .await @@ -592,32 +591,24 @@ impl SettingsStore for AdminSettingsStoreAdapter { async fn replace_admin_api_key( &self, - expected_config_revision: AdminRevision, key: AdminApiKey, context: &MutationContext, ) -> AdminStoreResult { - self.replace_admin_api_key_value( - expected_config_revision, - Some(key.expose_for_auth().to_owned()), - context, - ) - .await + self.replace_admin_api_key_value(Some(key.expose_for_auth().to_owned()), context) + .await } async fn delete_admin_api_key( &self, - expected_config_revision: AdminRevision, context: &MutationContext, ) -> AdminStoreResult { - self.replace_admin_api_key_value(expected_config_revision, None, context) - .await + self.replace_admin_api_key_value(None, context).await } } impl AdminSettingsStoreAdapter { async fn replace_admin_api_key_value( &self, - expected_config_revision: AdminRevision, admin_api_key: Option, context: &MutationContext, ) -> AdminStoreResult { @@ -639,7 +630,6 @@ impl AdminSettingsStoreAdapter { }; let snapshot = postgres::ControlPlaneRepository::replace_control_plane( &self.control_plane, - store_revision(expected_config_revision)?, postgres::ControlPlaneReplacement { settings: update, audit: mutation_audit( diff --git a/backend/crates/gateway-store/src/postgres/client_keys.rs b/backend/crates/gateway-store/src/postgres/client_keys.rs index 479d06fbc..8581cb13d 100644 --- a/backend/crates/gateway-store/src/postgres/client_keys.rs +++ b/backend/crates/gateway-store/src/postgres/client_keys.rs @@ -36,7 +36,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder, Transaction}; use crate::{ StoreError, StoreResult, admin_revision, admin_store_error, mutation_audit, - postgres_unavailable, require_nonempty, store_revision, + postgres_unavailable, require_nonempty, }; use super::{ControlPlaneRepository, PgControlPlaneRepository}; @@ -633,7 +633,6 @@ impl ClientKeyStore for PgAdminClientKeyStore { let revision = self .control_plane .create_client_api_key( - store_revision(command.expected_config_revision)?, NewClientApiKey { id: id.as_str().to_owned(), name: command.name, @@ -676,7 +675,6 @@ impl ClientKeyStore for PgAdminClientKeyStore { let revision = self .control_plane .update_client_api_key( - store_revision(command.expected_config_revision)?, UpdateClientApiKeyDetails { id: id.as_str().to_owned(), name: command.name, @@ -716,7 +714,6 @@ impl ClientKeyStore for PgAdminClientKeyStore { let revision = self .control_plane .set_client_api_key_enabled( - store_revision(command.expected_config_revision)?, id.as_str(), command.enabled, mutation_audit( @@ -739,7 +736,6 @@ impl ClientKeyStore for PgAdminClientKeyStore { ) -> AdminStoreResult { self.control_plane .delete_client_api_key( - store_revision(command.expected_config_revision)?, command.id.as_str(), mutation_audit( context, diff --git a/backend/crates/gateway-store/src/postgres/mod.rs b/backend/crates/gateway-store/src/postgres/mod.rs index 905d47fb7..a4dbe78c1 100644 --- a/backend/crates/gateway-store/src/postgres/mod.rs +++ b/backend/crates/gateway-store/src/postgres/mod.rs @@ -74,27 +74,23 @@ pub trait ControlPlaneRepository: Send + Sync { async fn replace_control_plane( &self, - expected_revision: Revision, replacement: ControlPlaneReplacement, ) -> StoreResult; async fn create_client_api_key( &self, - expected_revision: Revision, key: NewClientApiKey, audit: AdminAuditEvent, ) -> StoreResult; async fn update_client_api_key( &self, - expected_revision: Revision, key: UpdateClientApiKeyDetails, audit: AdminAuditEvent, ) -> StoreResult; async fn set_client_api_key_enabled( &self, - expected_revision: Revision, id: &str, enabled: bool, audit: AdminAuditEvent, @@ -102,7 +98,6 @@ pub trait ControlPlaneRepository: Send + Sync { async fn delete_client_api_key( &self, - expected_revision: Revision, id: &str, audit: AdminAuditEvent, ) -> StoreResult; @@ -142,7 +137,6 @@ impl ControlPlaneRepository for PgControlPlaneRepository { async fn replace_control_plane( &self, - expected_revision: Revision, replacement: ControlPlaneReplacement, ) -> StoreResult { replacement.settings.validate()?; @@ -152,12 +146,9 @@ impl ControlPlaneRepository for PgControlPlaneRepository { .await .map_err(|_| postgres_unavailable("begin control plane replacement"))?; let result = async { - let revision = update_runtime_settings_in_transaction( - &mut transaction, - expected_revision, - &replacement.settings, - ) - .await?; + let revision = + update_runtime_settings_in_transaction(&mut transaction, &replacement.settings) + .await?; append_admin_audit_event_in_transaction(&mut transaction, replacement.audit, revision) .await?; load_control_plane_in_transaction(&mut transaction).await @@ -183,41 +174,29 @@ impl ControlPlaneRepository for PgControlPlaneRepository { async fn create_client_api_key( &self, - expected_revision: Revision, key: NewClientApiKey, audit: AdminAuditEvent, ) -> StoreResult { - self.apply_targeted_mutation( - expected_revision, - ControlPlaneMutation::CreateClientApiKey(key), - audit, - ) - .await + self.apply_targeted_mutation(ControlPlaneMutation::CreateClientApiKey(key), audit) + .await } async fn update_client_api_key( &self, - expected_revision: Revision, key: UpdateClientApiKeyDetails, audit: AdminAuditEvent, ) -> StoreResult { - self.apply_targeted_mutation( - expected_revision, - ControlPlaneMutation::UpdateClientApiKey(key), - audit, - ) - .await + self.apply_targeted_mutation(ControlPlaneMutation::UpdateClientApiKey(key), audit) + .await } async fn set_client_api_key_enabled( &self, - expected_revision: Revision, id: &str, enabled: bool, audit: AdminAuditEvent, ) -> StoreResult { self.apply_targeted_mutation( - expected_revision, ControlPlaneMutation::SetClientApiKeyEnabled { id: id.to_owned(), enabled, @@ -229,12 +208,10 @@ impl ControlPlaneRepository for PgControlPlaneRepository { async fn delete_client_api_key( &self, - expected_revision: Revision, id: &str, audit: AdminAuditEvent, ) -> StoreResult { self.apply_targeted_mutation( - expected_revision, ControlPlaneMutation::DeleteClientApiKey(id.to_owned()), audit, ) @@ -252,7 +229,6 @@ enum ControlPlaneMutation { impl PgControlPlaneRepository { async fn apply_targeted_mutation( &self, - expected_revision: Revision, mutation: ControlPlaneMutation, audit: AdminAuditEvent, ) -> StoreResult { @@ -262,8 +238,7 @@ impl PgControlPlaneRepository { .await .map_err(|_| postgres_unavailable("begin targeted control plane mutation"))?; let result = async { - let revision = - bump_config_revision_in_transaction(&mut transaction, expected_revision).await?; + let revision = bump_config_revision_in_transaction(&mut transaction).await?; match mutation { ControlPlaneMutation::CreateClientApiKey(key) => { insert_client_api_key_in_transaction(&mut transaction, &key).await?; diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts.rs b/backend/crates/gateway-store/src/postgres/provider_accounts.rs index a7c67367d..ce035e353 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts.rs @@ -485,25 +485,21 @@ pub trait ProviderAccountAdminRepository: Send + Sync { async fn import_provider_accounts( &self, - expected_config_revision: Revision, command: ImportProviderAccounts, ) -> StoreResult; async fn rotate_provider_account( &self, - expected_config_revision: Revision, command: RotateProviderAccount, ) -> StoreResult; async fn set_provider_account_enabled_admin( &self, - expected_config_revision: Revision, command: SetProviderAccountEnabled, ) -> StoreResult; async fn delete_provider_accounts_admin( &self, - expected_config_revision: Revision, command: DeleteProviderAccounts, ) -> StoreResult; } @@ -767,7 +763,6 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { async fn import_provider_accounts( &self, - expected_config_revision: Revision, command: ImportProviderAccounts, ) -> StoreResult { command.validate()?; @@ -777,9 +772,7 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { .await .map_err(|_| postgres_unavailable("begin provider account admin import"))?; let result = async { - let revision = - bump_config_revision_in_transaction(&mut transaction, expected_config_revision) - .await?; + let revision = bump_config_revision_in_transaction(&mut transaction).await?; let mut account_ids = Vec::with_capacity(command.accounts.len()); for account in &command.accounts { account_ids @@ -798,7 +791,6 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { async fn rotate_provider_account( &self, - expected_config_revision: Revision, command: RotateProviderAccount, ) -> StoreResult { command.scope.validate()?; @@ -814,9 +806,7 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { .await .map_err(|_| postgres_unavailable("begin provider account admin rotation"))?; let result = async { - let config_revision = - bump_config_revision_in_transaction(&mut transaction, expected_config_revision) - .await?; + let config_revision = bump_config_revision_in_transaction(&mut transaction).await?; let credential_revision = rotate_provider_account_in_transaction( &mut transaction, &command.scope, @@ -841,7 +831,6 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { async fn set_provider_account_enabled_admin( &self, - expected_config_revision: Revision, command: SetProviderAccountEnabled, ) -> StoreResult { command.scope.validate()?; @@ -852,9 +841,7 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { .await .map_err(|_| postgres_unavailable("begin provider account admin state change"))?; let result = async { - let revision = - bump_config_revision_in_transaction(&mut transaction, expected_config_revision) - .await?; + let revision = bump_config_revision_in_transaction(&mut transaction).await?; set_provider_account_enabled_in_transaction( &mut transaction, &command.scope, @@ -872,7 +859,6 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { async fn delete_provider_accounts_admin( &self, - expected_config_revision: Revision, command: DeleteProviderAccounts, ) -> StoreResult { command.scope.validate()?; @@ -883,9 +869,7 @@ impl ProviderAccountAdminRepository for PgProviderAccountRepository { .await .map_err(|_| postgres_unavailable("begin provider account admin deletion"))?; let result = async { - let revision = - bump_config_revision_in_transaction(&mut transaction, expected_config_revision) - .await?; + let revision = bump_config_revision_in_transaction(&mut transaction).await?; delete_provider_accounts_in_transaction( &mut transaction, &command.scope, @@ -1032,7 +1016,6 @@ impl PgAdminAccountStore { async fn commit_prepared_import( &self, - expected_config_revision: AdminRevision, prepared: PreparedCredentialImport, context: &MutationContext, action: &str, @@ -1046,22 +1029,19 @@ impl PgAdminAccountStore { .map_err(|error| admin_store_error(ENTITY, error))?; let imported = self .accounts - .import_provider_accounts( - store_revision(expected_config_revision)?, - ImportProviderAccounts { - scope: ProviderAccountAdminScope { - provider_kind: provider_kind.clone(), - }, - accounts, - audit: mutation_audit( - context, - action, - "provider_account", - &provider_kind, - vec!["credentials".to_owned()], - ), + .import_provider_accounts(ImportProviderAccounts { + scope: ProviderAccountAdminScope { + provider_kind: provider_kind.clone(), }, - ) + accounts, + audit: mutation_audit( + context, + action, + "provider_account", + &provider_kind, + vec!["credentials".to_owned()], + ), + }) .await .map_err(|error| admin_store_error(ENTITY, error))?; Ok(CredentialImportResult { @@ -1083,7 +1063,6 @@ impl PgAdminAccountStore { async fn commit_prepared_rotation( &self, - expected_config_revision: AdminRevision, prepared: PreparedCredentialRotationFacts, context: &MutationContext, action: &str, @@ -1094,36 +1073,31 @@ impl PgAdminAccountStore { }; let rotation = self .accounts - .rotate_provider_account( - store_revision(expected_config_revision)?, - RotateProviderAccount { - scope, - profile: UpdateProviderAccount { - id: account_id.as_str().to_owned(), - name: prepared.name, - email: prepared.email, - plan_type: prepared.plan_type, - }, - credential: ProviderCredentialUpdate { - account_id: account_id.as_str().to_owned(), - expected_revision: store_revision(prepared.expected_credential_revision)?, - provider_credentials_json: provider_document_json( - prepared.provider_material, - ) - .map_err(|error| admin_store_error(ENTITY, error))?, - has_refresh_token: prepared.has_refresh_token, - access_token_expires_at: prepared.access_token_expires_at, - next_refresh_at: prepared.next_refresh_at, - }, - audit: mutation_audit( - context, - action, - "provider_account", - account_id.as_str(), - vec!["credentials".to_owned()], - ), + .rotate_provider_account(RotateProviderAccount { + scope, + profile: UpdateProviderAccount { + id: account_id.as_str().to_owned(), + name: prepared.name, + email: prepared.email, + plan_type: prepared.plan_type, }, - ) + credential: ProviderCredentialUpdate { + account_id: account_id.as_str().to_owned(), + expected_revision: store_revision(prepared.expected_credential_revision)?, + provider_credentials_json: provider_document_json(prepared.provider_material) + .map_err(|error| admin_store_error(ENTITY, error))?, + has_refresh_token: prepared.has_refresh_token, + access_token_expires_at: prepared.access_token_expires_at, + next_refresh_at: prepared.next_refresh_at, + }, + audit: mutation_audit( + context, + action, + "provider_account", + account_id.as_str(), + vec!["credentials".to_owned()], + ), + }) .await .map_err(|error| admin_store_error(ENTITY, error))?; Ok(CredentialMutationResult { @@ -1383,13 +1357,8 @@ impl AccountStore for PgAdminAccountStore { command: CredentialImportCommit, context: &MutationContext, ) -> AdminStoreResult { - self.commit_prepared_import( - command.expected_config_revision, - command.prepared, - context, - "import_document", - ) - .await + self.commit_prepared_import(command.prepared, context, "import_document") + .await } async fn commit_authorization( @@ -1397,13 +1366,11 @@ impl AccountStore for PgAdminAccountStore { command: AuthorizationCommit, context: &MutationContext, ) -> AdminStoreResult { - let expected_config_revision = command.pending.expected_config_revision(); match command.credential { AuthorizationCredentialCommit::Create(credential) => { let account_id = credential.account_id.clone(); let result = self .commit_prepared_import( - expected_config_revision, PreparedCredentialImport { provider_kind: credential.provider_kind.clone(), credentials: vec![credential], @@ -1431,13 +1398,8 @@ impl AccountStore for PgAdminAccountStore { }) } AuthorizationCredentialCommit::Reauthorize(prepared) => { - self.commit_prepared_rotation( - expected_config_revision, - prepared, - context, - "reauthorize", - ) - .await + self.commit_prepared_rotation(prepared, context, "reauthorize") + .await } } } @@ -1447,13 +1409,8 @@ impl AccountStore for PgAdminAccountStore { command: CredentialRotationCommit, context: &MutationContext, ) -> AdminStoreResult { - self.commit_prepared_rotation( - command.expected_config_revision, - command.prepared, - context, - "rotate_credential", - ) - .await + self.commit_prepared_rotation(command.prepared, context, "rotate_credential") + .await } async fn commit_credential_refresh( @@ -1461,13 +1418,8 @@ impl AccountStore for PgAdminAccountStore { command: CredentialRotationCommit, context: &MutationContext, ) -> AdminStoreResult { - self.commit_prepared_rotation( - command.expected_config_revision, - command.prepared, - context, - "refresh_credential", - ) - .await + self.commit_prepared_rotation(command.prepared, context, "refresh_credential") + .await } async fn set_account_enabled( @@ -1478,21 +1430,18 @@ impl AccountStore for PgAdminAccountStore { let scope = self.required_scope(&command.account_id).await?; let action = if command.enabled { "enable" } else { "disable" }; self.accounts - .set_provider_account_enabled_admin( - store_revision(command.expected_config_revision)?, - SetProviderAccountEnabled { - scope, - account_id: command.account_id.clone(), - enabled: command.enabled, - audit: mutation_audit( - context, - action, - "provider_account", - &command.account_id, - vec!["enabled".to_owned()], - ), - }, - ) + .set_provider_account_enabled_admin(SetProviderAccountEnabled { + scope, + account_id: command.account_id.clone(), + enabled: command.enabled, + audit: mutation_audit( + context, + action, + "provider_account", + &command.account_id, + vec!["enabled".to_owned()], + ), + }) .await .map_err(|error| admin_store_error(ENTITY, error)) .and_then(admin_revision) @@ -1517,20 +1466,17 @@ impl AccountStore for PgAdminAccountStore { "provider_accounts".to_owned() }; self.accounts - .delete_provider_accounts_admin( - store_revision(command.expected_config_revision)?, - DeleteProviderAccounts { - scope, - account_ids: command.account_ids, - audit: mutation_audit( - context, - "delete", - "provider_account", - &audit_target, - Vec::new(), - ), - }, - ) + .delete_provider_accounts_admin(DeleteProviderAccounts { + scope, + account_ids: command.account_ids, + audit: mutation_audit( + context, + "delete", + "provider_account", + &audit_target, + Vec::new(), + ), + }) .await .map_err(|error| admin_store_error(ENTITY, error)) .and_then(admin_revision) diff --git a/backend/crates/gateway-store/src/postgres/runtime_settings.rs b/backend/crates/gateway-store/src/postgres/runtime_settings.rs index d5152ae15..de7c98d0d 100644 --- a/backend/crates/gateway-store/src/postgres/runtime_settings.rs +++ b/backend/crates/gateway-store/src/postgres/runtime_settings.rs @@ -12,7 +12,7 @@ use gateway_core::provider_ports::{ ProviderRefreshPolicy, ProviderRuntimePolicyPort, ProviderStoreError, ProviderStoreErrorKind, }; -use crate::{ConflictKind, Revision, StoreError, StoreResult, postgres_unavailable}; +use crate::{Revision, StoreError, StoreResult, postgres_unavailable}; #[derive(Clone, PartialEq, Eq)] pub struct RuntimeSettings { @@ -111,11 +111,8 @@ impl RuntimeSettingsUpdate { pub trait RuntimeSettingsRepository: Send + Sync { async fn load_runtime_settings(&self) -> StoreResult; - async fn update_runtime_settings( - &self, - expected_revision: Revision, - update: RuntimeSettingsUpdate, - ) -> StoreResult; + async fn update_runtime_settings(&self, update: RuntimeSettingsUpdate) + -> StoreResult; } #[derive(Clone)] @@ -169,7 +166,6 @@ impl RuntimeSettingsRepository for PgRuntimeSettingsRepository { async fn update_runtime_settings( &self, - expected_revision: Revision, update: RuntimeSettingsUpdate, ) -> StoreResult { let mut transaction = self @@ -177,9 +173,7 @@ impl RuntimeSettingsRepository for PgRuntimeSettingsRepository { .begin() .await .map_err(|_| postgres_unavailable("begin runtime settings update"))?; - let revision = - update_runtime_settings_in_transaction(&mut transaction, expected_revision, &update) - .await?; + let revision = update_runtime_settings_in_transaction(&mut transaction, &update).await?; transaction .commit() .await @@ -228,28 +222,26 @@ pub(crate) async fn load_runtime_settings_in_transaction( pub(crate) async fn update_runtime_settings_in_transaction( transaction: &mut Transaction<'_, Postgres>, - expected_revision: Revision, update: &RuntimeSettingsUpdate, ) -> StoreResult { update.validate()?; let next = sqlx::query_scalar::<_, i64>( "update runtime_settings set config_revision = config_revision + 1, - admin_api_key = $2, - refresh_margin_seconds = $3, - refresh_concurrency = $4, - max_concurrent_per_account = $5, - request_interval_ms = $6, - rotation_strategy = $7, - model_mappings_json = $8, - usage_retention_days = $9, - ops_event_retention_days = $10, - audit_retention_days = $11, - updated_at = now() - where id = 1 and config_revision = $1 - returning config_revision", + admin_api_key = $1, + refresh_margin_seconds = $2, + refresh_concurrency = $3, + max_concurrent_per_account = $4, + request_interval_ms = $5, + rotation_strategy = $6, + model_mappings_json = $7, + usage_retention_days = $8, + ops_event_retention_days = $9, + audit_retention_days = $10, + updated_at = now() + where id = 1 + returning config_revision", ) - .bind(i64::try_from(expected_revision.get()).map_err(|_| invalid_numeric())?) .bind(update.admin_api_key.as_deref()) .bind(i64::try_from(update.refresh_margin_seconds).map_err(|_| invalid_numeric())?) .bind(i64::from(update.refresh_concurrency)) @@ -263,32 +255,28 @@ pub(crate) async fn update_runtime_settings_in_transaction( .fetch_optional(&mut **transaction) .await .map_err(|_| postgres_unavailable("update runtime settings in transaction"))? - .ok_or_else(|| StoreError::Conflict { + .ok_or_else(|| StoreError::NotFound { entity: "runtime settings", id: "1".to_owned(), - kind: ConflictKind::StaleRevision, })?; Revision::new(u64::try_from(next).map_err(|_| invalid_numeric())?) } pub(crate) async fn bump_config_revision_in_transaction( transaction: &mut Transaction<'_, Postgres>, - expected_revision: Revision, ) -> StoreResult { let next = sqlx::query_scalar::<_, i64>( "update runtime_settings set config_revision = config_revision + 1, updated_at = now() - where id = 1 and config_revision = $1 + where id = 1 returning config_revision", ) - .bind(i64::try_from(expected_revision.get()).map_err(|_| invalid_numeric())?) .fetch_optional(&mut **transaction) .await .map_err(|_| postgres_unavailable("bump config revision in transaction"))? - .ok_or_else(|| StoreError::Conflict { + .ok_or_else(|| StoreError::NotFound { entity: "runtime settings", id: "1".to_owned(), - kind: ConflictKind::StaleRevision, })?; Revision::new(u64::try_from(next).map_err(|_| invalid_numeric())?) } diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts.rs index 74fb30bbb..2541c2abf 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts.rs @@ -3,7 +3,7 @@ use std::time::{Duration, SystemTime}; use chrono::{TimeDelta, Utc}; use gateway_admin::{ model::{ - MutationActor, MutationContext, PageSize, Revision as AdminRevision, + MutationActor, MutationContext, PageSize, accounts::{ AccountListQuery, AccountSort, AccountSortField, AccountStatus, AccountUsageWindowQuery, DeleteAccounts, SetAccountEnabled as AdminSetAccountEnabled, @@ -14,7 +14,7 @@ use gateway_admin::{ CredentialAvailabilityFilter, CredentialListQuery, CredentialListWindow, }, }, - ports::store::{AccountStore, AdminStoreErrorKind}, + ports::store::AccountStore, }; use gateway_core::engine::credential::{ CredentialCasOutcome, CredentialCasUpdate, CredentialRevision, OpaqueProviderData, @@ -525,7 +525,6 @@ async fn terminal_admin_mutations_keep_revision_account_and_audit_atomic() { let revision = store .set_account_enabled( AdminSetAccountEnabled { - expected_config_revision: AdminRevision::new(1).expect("initial revision"), account_id: "acct_terminal_mutation".to_owned(), enabled: false, }, @@ -542,32 +541,9 @@ async fn terminal_admin_mutations_keep_revision_account_and_audit_atomic() { .expect("read disabled state"); assert!(!enabled); - let error = store - .delete_accounts( - DeleteAccounts { - expected_config_revision: AdminRevision::new(1).expect("stale revision"), - account_ids: vec!["acct_terminal_mutation".to_owned()], - }, - &context, - ) - .await - .expect_err("stale delete must roll back"); - assert_eq!(error.kind(), AdminStoreErrorKind::StaleRevision); - assert_eq!(current_revision(&database.pool).await, 2); - assert_eq!( - account_count(&database.pool, "acct_terminal_mutation").await, - 1 - ); - let audit_count: i64 = sqlx::query_scalar("select count(*) from admin_audit_events") - .fetch_one(&database.pool) - .await - .expect("count audit rows after rollback"); - assert_eq!(audit_count, 1); - let revision = store .delete_accounts( DeleteAccounts { - expected_config_revision: revision, account_ids: vec!["acct_terminal_mutation".to_owned()], }, &context, @@ -616,7 +592,6 @@ async fn terminal_admin_delete_removes_enabled_accounts_in_one_transaction() { let revision = PgAdminAccountStore::new(database.pool.clone()) .delete_accounts( DeleteAccounts { - expected_config_revision: AdminRevision::new(1).expect("initial revision"), account_ids: vec![ "acct_enabled_delete_a".to_owned(), "acct_enabled_delete_b".to_owned(), @@ -649,18 +624,14 @@ async fn admin_import_updates_the_same_verified_identity_without_rebinding_it() provider_kind: "openai".to_owned(), }; let imported = repository - .import_provider_accounts( - Revision::new(1).expect("initial revision"), - ImportProviderAccounts { - scope: scope.clone(), - accounts: vec![account("acct_admin_upsert", "user-admin-upsert")], - audit: audit("audit_admin_upsert_create", "import", "acct_admin_upsert"), - }, - ) + .import_provider_accounts(ImportProviderAccounts { + scope: scope.clone(), + accounts: vec![account("acct_admin_upsert", "user-admin-upsert")], + audit: audit("audit_admin_upsert_create", "import", "acct_admin_upsert"), + }) .await .expect("create imported account"); assert_eq!(imported.account_ids, ["acct_admin_upsert"]); - let revision = imported.config_revision; sqlx::query( "update provider_accounts set provider_quota_json = '{}'::jsonb, quota_observed_at = now(), updated_at = now() @@ -676,14 +647,11 @@ async fn admin_import_updates_the_same_verified_identity_without_rebinding_it() updated.availability = ProviderAccountAvailability::Banned; updated.availability_reason = Some("upstream_account_deactivated".to_owned()); let imported = repository - .import_provider_accounts( - revision, - ImportProviderAccounts { - scope: scope.clone(), - accounts: vec![updated], - audit: audit("audit_admin_upsert_update", "import", "acct_admin_upsert"), - }, - ) + .import_provider_accounts(ImportProviderAccounts { + scope: scope.clone(), + accounts: vec![updated], + audit: audit("audit_admin_upsert_update", "import", "acct_admin_upsert"), + }) .await .expect("update the same imported identity"); assert_eq!(imported.account_ids, ["acct_admin_upsert"]); @@ -715,14 +683,11 @@ async fn admin_import_updates_the_same_verified_identity_without_rebinding_it() assert_eq!(row.5.as_deref(), Some("upstream_account_deactivated")); repository - .import_provider_accounts( - revision, - ImportProviderAccounts { - scope, - accounts: vec![account("acct_admin_upsert", "another-user")], - audit: audit("audit_admin_upsert_rebind", "import", "acct_admin_upsert"), - }, - ) + .import_provider_accounts(ImportProviderAccounts { + scope, + accounts: vec![account("acct_admin_upsert", "another-user")], + audit: audit("audit_admin_upsert_rebind", "import", "acct_admin_upsert"), + }) .await .expect_err("an existing account ID must not be rebound"); assert_eq!(current_revision(&database.pool).await, 3); @@ -859,18 +824,14 @@ async fn provider_account_admin_mutations_are_scoped_audited_and_atomic() { cooldown_account.availability = ProviderAccountAvailability::Cooldown; cooldown_account.cooldown_until = Some(Utc::now() + TimeDelta::minutes(10)); let imported = repository - .import_provider_accounts( - Revision::new(1).expect("initial revision"), - ImportProviderAccounts { - scope: scope.clone(), - accounts: vec![account("acct_admin_a", "user-admin-a"), cooldown_account], - audit: audit("audit_account_batch", "import_batch", "provider_accounts"), - }, - ) + .import_provider_accounts(ImportProviderAccounts { + scope: scope.clone(), + accounts: vec![account("acct_admin_a", "user-admin-a"), cooldown_account], + audit: audit("audit_account_batch", "import_batch", "provider_accounts"), + }) .await .expect("import provider accounts"); - let revision = imported.config_revision; - assert_eq!(revision.get(), 2); + assert_eq!(imported.config_revision.get(), 2); let ready: (bool, String, Option>) = sqlx::query_as( "select enabled, availability, cooldown_until from provider_accounts where id = $1", ) @@ -891,21 +852,18 @@ async fn provider_account_admin_mutations_are_scoped_audited_and_atomic() { assert!(cooldown.2.is_some()); repository - .import_provider_accounts( - revision, - ImportProviderAccounts { - scope: scope.clone(), - accounts: vec![ - account("acct_admin_transient", "user-admin-transient"), - account("acct_admin_a", "user-admin-duplicate"), - ], - audit: audit( - "audit_account_failed_batch", - "import_batch", - "provider_accounts", - ), - }, - ) + .import_provider_accounts(ImportProviderAccounts { + scope: scope.clone(), + accounts: vec![ + account("acct_admin_transient", "user-admin-transient"), + account("acct_admin_a", "user-admin-duplicate"), + ], + audit: audit( + "audit_account_failed_batch", + "import_batch", + "provider_accounts", + ), + }) .await .expect_err("duplicate batch row must roll back the entire import"); assert_eq!(current_revision(&database.pool).await, 2); @@ -918,15 +876,12 @@ async fn provider_account_admin_mutations_are_scoped_audited_and_atomic() { provider_kind: "xai".to_owned(), }; let wrong_scope_error = repository - .rotate_provider_account( - revision, - RotateProviderAccount { - scope: wrong_scope, - profile: profile("acct_admin_a", "wrong scope"), - credential: credential_update("acct_admin_a", 1, "wrong-scope-secret"), - audit: audit("audit_wrong_scope", "rotate", "acct_admin_a"), - }, - ) + .rotate_provider_account(RotateProviderAccount { + scope: wrong_scope, + profile: profile("acct_admin_a", "wrong scope"), + credential: credential_update("acct_admin_a", 1, "wrong-scope-secret"), + audit: audit("audit_wrong_scope", "rotate", "acct_admin_a"), + }) .await .expect_err("Provider endpoint must not rotate another Provider account"); assert!(matches!( @@ -939,32 +894,27 @@ async fn provider_account_admin_mutations_are_scoped_audited_and_atomic() { assert_eq!(current_revision(&database.pool).await, 2); let rotation = repository - .rotate_provider_account( - revision, - RotateProviderAccount { - scope: scope.clone(), - profile: profile("acct_admin_a", "rotated account"), - credential: credential_update("acct_admin_a", 1, "rotated-secret"), - audit: audit("audit_account_rotate", "rotate", "acct_admin_a"), - }, - ) + .rotate_provider_account(RotateProviderAccount { + scope: scope.clone(), + profile: profile("acct_admin_a", "rotated account"), + credential: credential_update("acct_admin_a", 1, "rotated-secret"), + audit: audit("audit_account_rotate", "rotate", "acct_admin_a"), + }) .await .expect("rotate provider account"); assert_eq!(rotation.config_revision.get(), 3); assert_eq!(rotation.credential_revision.get(), 2); let revision = repository - .set_provider_account_enabled_admin( - rotation.config_revision, - SetProviderAccountEnabled { - scope: scope.clone(), - account_id: "acct_admin_a".to_owned(), - enabled: false, - audit: audit("audit_account_disable", "disable", "acct_admin_a"), - }, - ) + .set_provider_account_enabled_admin(SetProviderAccountEnabled { + scope: scope.clone(), + account_id: "acct_admin_a".to_owned(), + enabled: false, + audit: audit("audit_account_disable", "disable", "acct_admin_a"), + }) .await .expect("disable provider account"); + assert_eq!(revision.get(), 4); let exports = repository .export_provider_accounts( scope.clone(), @@ -977,14 +927,11 @@ async fn provider_account_admin_mutations_are_scoped_audited_and_atomic() { assert!(!format!("{exports:?}").contains("rotated-secret")); let revision = repository - .delete_provider_accounts_admin( - revision, - DeleteProviderAccounts { - scope, - account_ids: vec!["acct_admin_a".to_owned(), "acct_admin_b".to_owned()], - audit: audit("audit_account_delete", "delete", "provider_accounts"), - }, - ) + .delete_provider_accounts_admin(DeleteProviderAccounts { + scope, + account_ids: vec!["acct_admin_a".to_owned(), "acct_admin_b".to_owned()], + audit: audit("audit_account_delete", "delete", "provider_accounts"), + }) .await .expect("delete selected accounts regardless of enabled state"); assert_eq!(revision.get(), 5); diff --git a/backend/crates/providers/openai/src/admin.rs b/backend/crates/providers/openai/src/admin.rs index 8568b27d8..6277bbf6a 100644 --- a/backend/crates/providers/openai/src/admin.rs +++ b/backend/crates/providers/openai/src/admin.rs @@ -942,7 +942,6 @@ struct PendingDocument { #[serde(deny_unknown_fields)] struct PendingMutationDocument { schema_version: u64, - expected_config_revision: u64, provider_kind: String, target: PendingTargetDocument, owner: PendingOwnerDocument, @@ -1025,10 +1024,6 @@ fn encode_mutation(mutation: &PendingAuthorizationMutation) -> Map MutationContext { fn create_mutation(request_id: &str) -> PendingAuthorizationMutation { let context = owner_context(request_id); PendingAuthorizationMutation::new( - Revision::new(1).expect("revision"), ProviderKind::new("openai").expect("provider"), AuthorizationMutationTarget::Create { name: "OAuth Account".to_owned(), @@ -184,7 +183,6 @@ fn create_mutation(request_id: &str) -> PendingAuthorizationMutation { fn reauthorization_mutation(request_id: &str, account_id: &str) -> PendingAuthorizationMutation { let context = owner_context(request_id); PendingAuthorizationMutation::new( - Revision::new(1).expect("revision"), ProviderKind::new("openai").expect("provider"), AuthorizationMutationTarget::Reauthorize { account_id: ProviderAccountId::new(account_id).expect("account id"), diff --git a/backend/crates/providers/xai/src/admin.rs b/backend/crates/providers/xai/src/admin.rs index 327728642..689d9c4ad 100644 --- a/backend/crates/providers/xai/src/admin.rs +++ b/backend/crates/providers/xai/src/admin.rs @@ -1071,7 +1071,6 @@ struct PendingDocument { #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct PendingMutationDocument { - expected_config_revision: u64, provider_kind: String, target: PendingTargetDocument, owner: PendingOwnerDocument, @@ -1126,10 +1125,6 @@ fn encode_pending(pending: &StoredXaiAuthorization) -> Map { fn encode_mutation(mutation: &PendingAuthorizationMutation) -> Map { let mut document = Map::new(); - document.insert( - "expected_config_revision".to_owned(), - Value::Number(Number::from(mutation.expected_config_revision().get())), - ); document.insert( "provider_kind".to_owned(), Value::String(mutation.provider_kind().as_str().to_owned()), @@ -1256,8 +1251,6 @@ fn decode_mutation( request_id: document.started_request_id, }; Ok(PendingAuthorizationMutation::new( - Revision::new(document.expected_config_revision) - .map_err(|_| provider_error(ProviderAdminErrorKind::Invalid))?, provider_kind, target, AuthorizationOwnerBinding::from_context(&context), diff --git a/backend/crates/providers/xai/src/provider.rs b/backend/crates/providers/xai/src/provider.rs index f494ddac9..0f9208a62 100644 --- a/backend/crates/providers/xai/src/provider.rs +++ b/backend/crates/providers/xai/src/provider.rs @@ -399,6 +399,7 @@ fn compile_grok_model_capabilities(model: GrokCatalogModel) -> ProviderModelCapa .max_output_tokens() .map(std::num::NonZeroU64::get), ) + .with_upstream_feature_validation() .with_feature( Feature::Reasoning, support(model.capabilities().reasoning_effort()), @@ -441,10 +442,7 @@ fn default_grok_model_presentation() -> ModelPresentation { fn grok_model_presentation(model: &GrokCatalogModel) -> ModelPresentation { let slug = model.request_model().as_str(); - let known_grok_4_5 = matches!( - slug, - DEFAULT_GROK_MODEL | "grok-4.5-latest" | "grok-4.5-build-free" | "grok-build-latest" - ); + let known_grok_4_5 = is_known_grok_4_5_model(slug); let reasoning_evidence = model.capabilities().reasoning_effort(); let catalog_reasoning_efforts = model .capabilities() @@ -513,6 +511,13 @@ fn grok_model_presentation(model: &GrokCatalogModel) -> ModelPresentation { .with_hidden(model.metadata().hidden().unwrap_or(false)) } +fn is_known_grok_4_5_model(slug: &str) -> bool { + matches!( + slug, + DEFAULT_GROK_MODEL | "grok-4.5-latest" | "grok-4.5-build-free" | "grok-build-latest" + ) +} + #[derive(Clone, Serialize, Deserialize)] struct XaiSessionState { account_id: String, diff --git a/backend/crates/providers/xai/tests/admin.rs b/backend/crates/providers/xai/tests/admin.rs index bcf6721ca..518d2a473 100644 --- a/backend/crates/providers/xai/tests/admin.rs +++ b/backend/crates/providers/xai/tests/admin.rs @@ -335,12 +335,7 @@ async fn xai_admin_provider_restores_full_pending_envelope_and_binds_owner() { .get("mutation") .and_then(Value::as_object) .expect("pending mutation"); - assert_eq!( - mutation - .get("expected_config_revision") - .and_then(Value::as_u64), - Some(7) - ); + assert_eq!(mutation.get("expected_config_revision"), None); assert_eq!( mutation.get("started_request_id").and_then(Value::as_str), Some("request-start") @@ -555,7 +550,6 @@ fn pending_payload(flow_id: &str, owner_ref: &str) -> OpaqueProviderData { "expires_at": (Utc::now() + chrono::Duration::minutes(30)).to_rfc3339(), "server_state": server_state, "mutation": { - "expected_config_revision": 7, "provider_kind": "xai", "target": { "kind": "create", diff --git a/backend/crates/providers/xai/tests/provider/contract.rs b/backend/crates/providers/xai/tests/provider/contract.rs index 9fbbca406..18df6126b 100644 --- a/backend/crates/providers/xai/tests/provider/contract.rs +++ b/backend/crates/providers/xai/tests/provider/contract.rs @@ -60,11 +60,11 @@ fn observed_transport_metrics() -> GrokInferenceTransportMetrics { )) } -const CATALOG_WITHOUT_TOOL_METADATA: &[u8] = br#"{ +const CATALOG_WITHOUT_FEATURE_METADATA: &[u8] = br#"{ "object": "list", "data": [{ - "id": "grok-4.5-catalog-entry", - "model": "grok-4.5", + "id": "grok-catalog-entry", + "model": "grok-catalog-entry", "contextWindow": 1000000, "maxCompletionTokens": 131072, "apiBackend": "responses", @@ -368,13 +368,13 @@ impl GrokModelCatalogTransport for StaticCatalogTransport { } } -struct CatalogWithoutToolMetadataTransport; +struct CatalogWithoutFeatureMetadataTransport; -impl GrokModelCatalogTransport for CatalogWithoutToolMetadataTransport { +impl GrokModelCatalogTransport for CatalogWithoutFeatureMetadataTransport { fn execute(&self, _: GrokModelCatalogRequest) -> GrokModelCatalogTransportFuture<'_> { Box::pin(async { Ok(GrokModelCatalogTransportResponse::new( - CATALOG_WITHOUT_TOOL_METADATA, + CATALOG_WITHOUT_FEATURE_METADATA, None, )) }) @@ -1918,7 +1918,7 @@ async fn missing_catalog_tool_metadata_keeps_build_tools_routable() { StubSelector::success(), StubInferenceTransport::success(), StubRecovery::new(GrokCredentialRecoveryOutcome::Unavailable), - Arc::new(CatalogWithoutToolMetadataTransport), + Arc::new(CatalogWithoutFeatureMetadataTransport), ) .await; let capabilities = provider @@ -1935,3 +1935,29 @@ async fn missing_catalog_tool_metadata_keeps_build_tools_routable() { .is_some() ); } + +#[tokio::test] +async fn missing_catalog_feature_metadata_keeps_build_responses_routable() { + let provider = provider_with_catalog_transport( + StubSelector::success(), + StubInferenceTransport::success(), + StubRecovery::new(GrokCredentialRecoveryOutcome::Unavailable), + Arc::new(CatalogWithoutFeatureMetadataTransport), + ) + .await; + let capabilities = provider + .query_model_capabilities() + .await + .expect("capabilities"); + + assert!( + capabilities[0] + .capabilities() + .match_requirements( + &gateway_core::operation::CapabilityRequirements::new(OperationKind::Generate) + .require(Feature::Vision) + .require(Feature::JsonSchema) + ) + .is_some() + ); +} diff --git a/docs/api.md b/docs/api.md index d3da2a8b7..7ee2efd2d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -41,13 +41,13 @@ Client Key 固定绑定一个 Provider:`openai` 或 `xai`。同一次请求不 错误响应仍使用 `code`、`message`、`data`,其中 `data` 为 `null`。稳定业务码包括参数错误 `40001`、会话缺失 `40101`、登录凭据错误 `40102`、管理 API Key 错误 `40103`、资源不存在 -`40401`、revision 冲突 `40901`、内部错误 `50001`、上游失败 `50201` 和依赖不可用 `50301`。 +`40401`、业务冲突 `40901`、内部错误 `50001`、上游失败 `50201` 和依赖不可用 `50301`。 -### 配置 revision +### 管理写入一致性 -会改变路由快照或安全配置的 mutation 使用 `expectedConfigRevision` 做乐观并发控制。客户端应从 -账号、Client Key 或设置查询结果读取最新 `configRevision`;收到 `40901` 后重新读取,不应盲目重试 -旧 revision。 +Admin API 不暴露全局配置版本,mutation 请求也不要求客户端提供全局配置版本。会改变路由快照或安全配置的写入由后端在事务内推进内部配置 revision,并用于快照发布与审计。 + +单个 credential 的轮换或重新授权仍使用 `expectedCredentialRevision`,它只保护该 credential 本身,不是全局配置锁。 ## 2. 健康检查 @@ -95,12 +95,12 @@ OpenAI 路径保留客户端 Responses wire 语义,上游事件字节在 HTTP | `GET` | `/api/admin/accounts` | `page`、`pageSize`、`provider`、`search`、`status`、排序字段 | 分页查询账号与汇总 | | `GET` | `/api/admin/accounts/detail` | `accountId` | 查询账号详情、额度和本地用量 | | `GET` | `/api/admin/accounts/export` | `accountIds`、`confirm=export_sensitive_accounts` | 显式导出最多 200 个账号的敏感 Provider 文档 | -| `POST` | `/api/admin/accounts/import` | `{ provider, expectedConfigRevision, data }` | 导入或按上游身份更新账号 | -| `POST` | `/api/admin/accounts/refresh` | `{ accountId, expectedConfigRevision }` | 手工刷新账号 credential | +| `POST` | `/api/admin/accounts/import` | `{ provider, data }` | 导入或按上游身份更新账号 | +| `POST` | `/api/admin/accounts/refresh` | `{ accountId }` | 手工刷新账号 credential | | `POST` | `/api/admin/accounts/rotate` | OpenAI rotation 字段 | 手工替换 OpenAI OAuth token | -| `POST` | `/api/admin/accounts/enable` | `{ provider, accountId, expectedConfigRevision }` | 启用账号 | -| `POST` | `/api/admin/accounts/disable` | `{ provider, accountId, expectedConfigRevision }` | 禁用账号 | -| `POST` | `/api/admin/accounts/delete` | `{ provider, accountIds, expectedConfigRevision }` | 批量删除 1–200 个账号 | +| `POST` | `/api/admin/accounts/enable` | `{ provider, accountId }` | 启用账号 | +| `POST` | `/api/admin/accounts/disable` | `{ provider, accountId }` | 禁用账号 | +| `POST` | `/api/admin/accounts/delete` | `{ provider, accountIds }` | 批量删除 1–200 个账号 | | `GET` | `/api/admin/accounts/quota` | `accountId` | 读取当前额度,不强制访问上游 | | `POST` | `/api/admin/accounts/quota/refresh` | `{ accountId }` | 立即刷新该账号额度 | | `GET` | `/api/admin/accounts/models` | `accountId` | 优先读取该 Provider + 套餐的模型 cache,缺失时有限实时拉取 | @@ -150,7 +150,6 @@ OpenAI rotation 请求字段为: { "provider": "openai", "accountId": "acct_...", - "expectedConfigRevision": 1, "expectedCredentialRevision": 1, "accessToken": "...", "refreshToken": "..." @@ -162,7 +161,6 @@ OAuth start 使用: ```json { "provider": "openai", - "expectedConfigRevision": 1, "name": "account name", "accountId": null, "expectedCredentialRevision": null @@ -179,18 +177,17 @@ OAuth start 使用: | `POST` | `/api/admin/client-keys/create` | 创建字段 | 创建绑定 Provider 的 Client Key | | `GET` | `/api/admin/client-keys/reveal` | `id` | 显式读取完整明文 Key | | `POST` | `/api/admin/client-keys/update` | 更新字段 | 原子更新名称、Provider 和限额 | -| `POST` | `/api/admin/client-keys/enable` | `{ id, expectedConfigRevision }` | 启用 | -| `POST` | `/api/admin/client-keys/disable` | `{ id, expectedConfigRevision }` | 禁用 | -| `POST` | `/api/admin/client-keys/delete` | `{ id, expectedConfigRevision }` | 删除 | +| `POST` | `/api/admin/client-keys/enable` | `{ id }` | 启用 | +| `POST` | `/api/admin/client-keys/disable` | `{ id }` | 禁用 | +| `POST` | `/api/admin/client-keys/delete` | `{ id }` | 删除 | -创建字段为 `expectedConfigRevision`、`name`、可选 `label`、`providerKind`、`maxConcurrency`、 -`requestsPerMinute`。更新请求再增加 `id`。创建和 reveal 响应会返回完整明文 Key,调用方必须立即安全保存。 +创建字段为 `name`、可选 `label`、`providerKind`、`maxConcurrency`、`requestsPerMinute`。更新请求再增加 `id`。创建和 reveal 响应会返回完整明文 Key,调用方必须立即安全保存。 ## 7. 运行设置 | 方法 | 路由 | 说明 | | --- | --- | --- | -| `GET` | `/api/admin/settings` | 读取运行设置与 `configRevision` | +| `GET` | `/api/admin/settings` | 读取运行设置 | | `POST` | `/api/admin/settings/update` | 原子替换全部运行设置 | | `GET` | `/api/admin/settings/admin-api-key` | 只返回管理 API Key 是否存在 | | `POST` | `/api/admin/settings/admin-api-key/delete` | 删除管理 API Key | @@ -199,7 +196,6 @@ OAuth start 使用: 设置更新字段包括: ```text -expectedConfigRevision modelMappings refreshMarginSeconds refreshConcurrency diff --git a/docs/architecture.md b/docs/architecture.md index b84188ee8..261e041d4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -114,11 +114,10 @@ credential 更新使用 `credential_revision` CAS。认证永久失败、封禁 `refresh_token_expires_at` 不是公共 SQL 列或 Core 权威状态。xAI 可在 `provider_credentials_json` 内保存它作为 Provider 私有提示;真正失效以 refresh endpoint 返回的永久错误为准。 -## 7. 控制面与 revision +## 7. 控制面与内部 revision -管理写入使用 `expected_config_revision` 做全局乐观并发控制。会改变调度快照或安全配置的写入,必须在同一 PostgreSQL 事务中: +Admin API 不要求客户端提交配置 revision,也不向客户端暴露配置 revision。会改变调度快照或安全配置的写入,必须在同一 PostgreSQL 事务中: -- 校验 expected revision; - 执行业务 mutation; - 推进 `runtime_settings.config_revision`; - 写入脱敏 `admin_audit_events`。 diff --git a/frontend/src/api/modules/accounts.ts b/frontend/src/api/modules/accounts.ts index bcaa5bbd8..e02218935 100644 --- a/frontend/src/api/modules/accounts.ts +++ b/frontend/src/api/modules/accounts.ts @@ -134,14 +134,12 @@ export interface AccountSummary { } export interface AccountListResponse { - configRevision: number items: Account[] page: AccountPageMeta summary: AccountSummary } export interface AccountRefreshResponse { - configRevision: number account: Account result?: string error?: string @@ -156,19 +154,16 @@ export interface AccountModelsResponse { } export interface AccountImportResponse { - configRevision: number importedCount: number accountIds: string[] } export interface AccountMutationResponse { - configRevision: number accountId: string credentialRevision?: number } export interface AccountDeletionResponse { - configRevision: number deletedCount: number accountIds: string[] } diff --git a/frontend/src/api/modules/api-keys.ts b/frontend/src/api/modules/api-keys.ts index 0181204c0..e32b3380c 100644 --- a/frontend/src/api/modules/api-keys.ts +++ b/frontend/src/api/modules/api-keys.ts @@ -15,14 +15,12 @@ export interface ApiKey { } export interface ApiKeyListResponse { - configRevision: number items: ApiKey[] nextCursor: string | null total: number } export interface ApiKeyCreateResponse { - configRevision: number id: string prefix: string plaintextKey: string @@ -34,7 +32,6 @@ export interface ApiKeyRevealResponse { } export interface ApiKeyMutationResponse { - configRevision: number id: string } diff --git a/frontend/src/api/modules/settings.ts b/frontend/src/api/modules/settings.ts index 1b3e673de..77fb9e08e 100644 --- a/frontend/src/api/modules/settings.ts +++ b/frontend/src/api/modules/settings.ts @@ -3,7 +3,6 @@ import request from '../request' export type RotationStrategy = 'smart' | 'quota_reset_priority' | 'round_robin' | 'sticky' export interface RuntimeSettings { - configRevision: number modelMappings: Record refreshMarginSeconds: number refreshConcurrency: number diff --git a/frontend/src/views/accounts/composables/useAccountMutations.ts b/frontend/src/views/accounts/composables/useAccountMutations.ts index a9304e522..114f603b6 100644 --- a/frontend/src/views/accounts/composables/useAccountMutations.ts +++ b/frontend/src/views/accounts/composables/useAccountMutations.ts @@ -13,7 +13,6 @@ import { refreshAccount, refreshAccountQuota, } from '@/api' -import { ApiError } from '@/api/request' import { toast } from '@/components/base/BaseToast' import { useAsyncAction } from '@/composables/useAsyncAction' import { useDownload } from '@/composables/useDownload' @@ -39,14 +38,12 @@ export function useAccountMutations(options: { sort: Ref selectedIds: Ref> totalAccounts: Ref - configRevision: Ref reload: () => Promise }) { const loadAccounts = options.reload const { downloadJson } = useDownload() const onboarding = useAccountOnboarding({ reload: loadAccounts, - configRevision: options.configRevision, }) const selectedAccountsById = new Map() const showDeleteModal = ref(false) @@ -169,10 +166,8 @@ export function useAccountMutations(options: { const result = await withMinimumDuration(() => refreshAccount({ accountId, - expectedConfigRevision: options.configRevision.value, }), ) - onboarding.commitConfigRevision(result.configRevision) await loadAccounts() if (result.result === 'skipped') { toast.warning(result.error || 'Token 正在刷新中') @@ -308,22 +303,12 @@ export function useAccountMutations(options: { const payload = { provider: account.provider, accountId: account.id, - expectedConfigRevision: await onboarding.requireConfigRevision(), - } - try { - const result = action === 'enable' - ? await enableAccount(payload) - : await disableAccount(payload) - if (!result) - throw new Error(`不支持的 Provider:${account.provider}`) - onboarding.commitConfigRevision(result.configRevision) - } - catch (error) { - if (error instanceof ApiError && error.status === 409) { - await loadAccounts() - } - throw error } + const result = action === 'enable' + ? await enableAccount(payload) + : await disableAccount(payload) + if (!result) + throw new Error(`不支持的 Provider:${account.provider}`) } async function deleteAccountBatch(accounts: AccountRow[]) { @@ -333,20 +318,10 @@ export function useAccountMutations(options: { const payload = { provider: account.provider, accountIds: accounts.map(account => account.id), - expectedConfigRevision: await onboarding.requireConfigRevision(), - } - try { - const result = await deleteAccounts(payload) - if (!result) - throw new Error(`不支持的 Provider:${account.provider}`) - onboarding.commitConfigRevision(result.configRevision) - } - catch (error) { - if (error instanceof ApiError && error.status === 409) { - await loadAccounts() - } - throw error } + const result = await deleteAccounts(payload) + if (!result) + throw new Error(`不支持的 Provider:${account.provider}`) } function accountDeletionGroups(accounts: AccountRow[]) { diff --git a/frontend/src/views/accounts/composables/useAccountOnboarding.ts b/frontend/src/views/accounts/composables/useAccountOnboarding.ts index 91b2baab8..18f98cbdc 100644 --- a/frontend/src/views/accounts/composables/useAccountOnboarding.ts +++ b/frontend/src/views/accounts/composables/useAccountOnboarding.ts @@ -6,7 +6,6 @@ import { importAccounts, startAccountOAuth, } from '@/api' -import { ApiError } from '@/api/request' import { toast } from '@/components/base/BaseToast' import { useAsyncAction } from '@/composables/useAsyncAction' import { providerDisplayName } from '@/utils/providers' @@ -21,11 +20,9 @@ interface MixedImportDocument { export function useAccountOnboarding(options: { reload: () => Promise - configRevision: { value: number } }) { const createModalOpen = shallowRef(false) const reauthorizingAccount = shallowRef(null) - const configRevision = options.configRevision const creatingAccountAction = useAsyncAction() const authorizingOAuthAction = useAsyncAction() const creatingAccount = creatingAccountAction.loading @@ -70,7 +67,7 @@ export function useAccountOnboarding(options: { async () => { const input = await newAccountInput() const account = reauthorizingAccount.value - const result = await withConflictRefresh(() => startAccountOAuth({ + const result = await startAccountOAuth({ ...input, ...(account ? { @@ -78,7 +75,7 @@ export function useAccountOnboarding(options: { expectedCredentialRevision: account.credentialRevision, } : {}), - })) + }) createForm.value = { ...createForm.value, @@ -104,12 +101,11 @@ export function useAccountOnboarding(options: { const callbackUrl = createForm.value.oauthCallback.trim() if (!callbackUrl) throw new Error('请粘贴 OAuth 回调地址') - const result = await withConflictRefresh(() => completeAccountOAuth({ + await completeAccountOAuth({ provider: createForm.value.provider, flowId: createForm.value.oauthFlowId, callbackUrl, - })) - commitConfigRevision(result.configRevision) + }) await finishCreate( reauthorizingAccount.value ? '账号重新授权成功' @@ -144,24 +140,10 @@ export function useAccountOnboarding(options: { void handleAuthorizeOAuth() } - async function requireConfigRevision() { - if (configRevision.value <= 0) - await options.reload() - if (configRevision.value <= 0) - throw new Error('当前配置 revision 不可用') - return configRevision.value - } - - function commitConfigRevision(revision: number) { - if (revision > 0) - configRevision.value = revision - } - async function newAccountInput() { const account = reauthorizingAccount.value return { provider: createForm.value.provider, - expectedConfigRevision: await requireConfigRevision(), name: account?.name || account?.email || `${createForm.value.provider} OAuth`, } } @@ -170,14 +152,12 @@ export function useAccountOnboarding(options: { const data = parseImportJson(createForm.value.importText) if (Array.isArray(data) || typeof data !== 'object' || data === null) throw new Error('导入文件必须是 JSON object') - const expectedConfigRevision = await requireConfigRevision() - const result = await withConflictRefresh(() => importAccounts({ + const result = await importAccounts({ provider: createForm.value.provider, - expectedConfigRevision, data, - })) - commitConfigRevision(result.configRevision) - return createForm.value.provider === 'xai' ? 'xAI 账号已导入' : 'OpenAI 账号已导入' + }) + const provider = createForm.value.provider === 'xai' ? 'xAI' : 'OpenAI' + return `${provider} 账号已导入 ${result.importedCount} 个` } async function importMixedAccountDocument() { @@ -187,13 +167,10 @@ export function useAccountOnboarding(options: { for (const entry of documents) { try { - const expectedConfigRevision = await requireConfigRevision() - const result = await withConflictRefresh(() => importAccounts({ + const result = await importAccounts({ provider: entry.provider, - expectedConfigRevision, data: entry.document, - })) - commitConfigRevision(result.configRevision) + }) importedCount += result.importedCount } catch (error) { @@ -220,18 +197,6 @@ export function useAccountOnboarding(options: { toast.success(message) } - async function withConflictRefresh(task: () => Promise) { - try { - return await task() - } - catch (error) { - if (error instanceof ApiError && error.status === 409) { - await options.reload() - } - throw error - } - } - watch( () => createForm.value.provider, () => { @@ -250,14 +215,11 @@ export function useAccountOnboarding(options: { reauthorizingAccount, creatingAccount, authorizingOAuth, - configRevision, createForm, handleCreate, handleAuthorizeOAuth, openCreateAccount, openReauthorizeAccount, - requireConfigRevision, - commitConfigRevision, } } diff --git a/frontend/src/views/accounts/composables/useAccountsQuery.ts b/frontend/src/views/accounts/composables/useAccountsQuery.ts index 7629a6821..62fdac456 100644 --- a/frontend/src/views/accounts/composables/useAccountsQuery.ts +++ b/frontend/src/views/accounts/composables/useAccountsQuery.ts @@ -11,7 +11,6 @@ export function useAccountsQuery() { const searchQuery = shallowRef('') const providerQuery = shallowRef('') const statusQuery = shallowRef('') - const configRevision = shallowRef(0) const sort = shallowRef() const accountSummary = shallowRef({ total: 0, @@ -34,7 +33,6 @@ export function useAccountsQuery() { }), onSuccess: (result) => { accountSummary.value = result.summary - configRevision.value = result.configRevision || configRevision.value }, onError: (error) => { toast.error(errorMessage(error, '账号加载失败')) @@ -94,7 +92,6 @@ export function useAccountsQuery() { statusQuery, sort, accountSummary, - configRevision, accountPagination, handlePageChange, handlePageSizeChange, diff --git a/frontend/src/views/accounts/index.vue b/frontend/src/views/accounts/index.vue index 2c46c5bdb..feb87995c 100644 --- a/frontend/src/views/accounts/index.vue +++ b/frontend/src/views/accounts/index.vue @@ -36,7 +36,6 @@ const { statusQuery, sort, accountSummary, - configRevision, accountPagination, handlePageChange, handlePageSizeChange, @@ -78,7 +77,6 @@ const { sort, selectedIds, totalAccounts, - configRevision, reload: loadAccounts, }) diff --git a/frontend/src/views/api-keys/composables/useApiKeyMutations.ts b/frontend/src/views/api-keys/composables/useApiKeyMutations.ts index 98b402f13..e532388c6 100644 --- a/frontend/src/views/api-keys/composables/useApiKeyMutations.ts +++ b/frontend/src/views/api-keys/composables/useApiKeyMutations.ts @@ -12,7 +12,6 @@ type ApiKeyRow = Awaited>['items'][number] export function useApiKeyMutations(options: { selectedIds: Ref> - configRevision: Ref reload: () => Promise }) { const copyText = useCopyText() @@ -50,13 +49,6 @@ export function useApiKeyMutations(options: { } }) - function currentRevision() { - if (options.configRevision.value <= 0) { - throw new Error('配置版本尚未加载,请刷新后重试') - } - return options.configRevision.value - } - async function handleCreate() { if (creatingKey.value) return @@ -70,7 +62,6 @@ export function useApiKeyMutations(options: { const name = createForm.value.name.trim() const providerKind = createForm.value.providerKind const result = await createApiKey({ - expectedConfigRevision: currentRevision(), name, label: createForm.value.label.trim() || undefined, providerKind, @@ -78,7 +69,6 @@ export function useApiKeyMutations(options: { requestsPerMinute: 0, }) - options.configRevision.value = result.configRevision createdKey.value = result.plaintextKey createdKeyName.value = name createdKeyProviderKind.value = providerKind @@ -145,22 +135,18 @@ export function useApiKeyMutations(options: { } async function deleteKey(keyId: string) { - const deleted = await deleteApiKey({ + await deleteApiKey({ id: keyId, - expectedConfigRevision: currentRevision(), }) - options.configRevision.value = deleted.configRevision } async function handleToggleStatus(key: ApiKeyRow) { await updatingStatusKeys.run(key.id, async () => { try { const mutation = key.enabled ? disableApiKey : enableApiKey - const result = await mutation({ + await mutation({ id: key.id, - expectedConfigRevision: currentRevision(), }) - options.configRevision.value = result.configRevision await loadApiKeys() toast.success(key.enabled ? '已禁用' : '已启用') } diff --git a/frontend/src/views/api-keys/composables/useApiKeysQuery.ts b/frontend/src/views/api-keys/composables/useApiKeysQuery.ts index 5ac825bd6..35d334223 100644 --- a/frontend/src/views/api-keys/composables/useApiKeysQuery.ts +++ b/frontend/src/views/api-keys/composables/useApiKeysQuery.ts @@ -20,7 +20,6 @@ export function useApiKeysQuery() { }> >([]) const loading = shallowRef(false) - const configRevision = shallowRef(0) const cursors = new Map([[1, undefined]]) let requestSequence = 0 @@ -59,15 +58,10 @@ export function useApiKeysQuery() { createdAtDisplay: formatDateTime(item.createdAt), lastUsedAtDisplay: item.lastUsedAt ? formatDateTime(item.lastUsedAt) : '—', })) - configRevision.value = result.configRevision total.value = result.total page.value = targetPage } - function paginationRevisionChanged(result: Awaited>) { - return configRevision.value > 0 && result.configRevision !== configRevision.value - } - async function execute(targetPage = page.value) { const requestId = ++requestSequence const requestedPage = Math.max(1, targetPage) @@ -83,10 +77,6 @@ export function useApiKeysQuery() { result = await fetchPage(cursors.get(currentPage), requestedPageSize, requestedSearch) if (requestId !== requestSequence) return false - if (paginationRevisionChanged(result)) { - resetCursorPagination() - return execute(1) - } if (!result.nextCursor) { applyPage(result, currentPage) @@ -100,11 +90,6 @@ export function useApiKeysQuery() { result = await fetchPage(cursors.get(currentPage), requestedPageSize, requestedSearch) if (requestId !== requestSequence) return false - if (paginationRevisionChanged(result)) { - resetCursorPagination() - if (currentPage !== 1) - return execute(1) - } if (result.nextCursor) cursors.set(currentPage + 1, result.nextCursor) @@ -162,7 +147,6 @@ export function useApiKeysQuery() { loading, apiKeys, loadApiKeys: reloadFromStart, - configRevision, searchQuery, sort, apiKeyPagination, diff --git a/frontend/src/views/api-keys/index.vue b/frontend/src/views/api-keys/index.vue index ddff84711..85a955a4a 100644 --- a/frontend/src/views/api-keys/index.vue +++ b/frontend/src/views/api-keys/index.vue @@ -26,7 +26,6 @@ const { loading, apiKeys, loadApiKeys, - configRevision, searchQuery, sort, apiKeyPagination, @@ -58,7 +57,7 @@ const { copyToClipboard, revealPlaintextKey, copyApiKey, -} = useApiKeyMutations({ selectedIds, configRevision, reload: loadApiKeys }) +} = useApiKeyMutations({ selectedIds, reload: loadApiKeys }) const { allSelected, indeterminate, selectedRowKeys, toggleSelection, toggleAll } = usePageSelection( apiKeys, diff --git a/frontend/src/views/settings/composables/useSettingsForm.ts b/frontend/src/views/settings/composables/useSettingsForm.ts index 0cadb1006..abb9ab6e3 100644 --- a/frontend/src/views/settings/composables/useSettingsForm.ts +++ b/frontend/src/views/settings/composables/useSettingsForm.ts @@ -13,7 +13,6 @@ export function useSettingsForm() { const error = shallowRef('') const mappings = ref>([]) const form = reactive({ - configRevision: 0, refreshMarginSeconds: null as number | null, refreshConcurrency: null as number | null, maxConcurrentPerAccount: null as number | null, @@ -44,7 +43,6 @@ export function useSettingsForm() { const requestIntervalMsValue = numericModel('requestIntervalMs') function applySettings(data: Awaited>) { - form.configRevision = data.configRevision form.refreshMarginSeconds = data.refreshMarginSeconds form.refreshConcurrency = data.refreshConcurrency form.maxConcurrentPerAccount = data.maxConcurrentPerAccount @@ -117,7 +115,6 @@ export function useSettingsForm() { try { saving.value = true const result = await updateSettings({ - expectedConfigRevision: form.configRevision, modelMappings: mappingPayload(), refreshMarginSeconds, refreshConcurrency,