From 45903b684bb1bb823d7835c2ae05bf9142ce9d44 Mon Sep 17 00:00:00 2001 From: Zyy Date: Mon, 27 Jul 2026 11:30:59 +0800 Subject: [PATCH] fix(admin): remove config revision fences from API writes Operator mutations no longer require callers to carry configRevision values through accounts, client keys, provider credentials, or settings. The store and admin use cases now apply current state directly, which removes stale-revision failures from normal create, update, and delete flows. xAI Responses catalog entries now let the upstream validate feature support instead of rejecting requests locally when optional catalog metadata is absent, matching the Build routing behavior used by grok2api. Co-authored-by: Codex --- .../gateway-admin/src/model/accounts.rs | 2 - .../gateway-admin/src/model/client_keys.rs | 5 - .../src/model/provider_credentials.rs | 15 -- .../gateway-admin/src/model/settings.rs | 1 - .../crates/gateway-admin/src/ports/store.rs | 6 +- .../gateway-admin/src/use_case/accounts.rs | 5 +- .../gateway-admin/src/use_case/client_keys.rs | 1 - .../crates/gateway-admin/src/use_case/mod.rs | 21 +- .../gateway-admin/src/use_case/openai.rs | 11 +- .../gateway-admin/src/use_case/settings.rs | 6 +- .../crates/gateway-admin/src/use_case/xai.rs | 9 +- .../gateway-admin/tests/use_case/accounts.rs | 24 +-- .../gateway-admin/tests/use_case/mod.rs | 2 - .../tests/use_case/observability.rs | 2 - .../gateway-admin/tests/use_case/openai.rs | 9 - .../gateway-admin/tests/use_case/settings.rs | 5 +- .../gateway-admin/tests/use_case/xai.rs | 8 +- .../crates/gateway-api/src/admin/accounts.rs | 69 +----- .../gateway-api/src/admin/client_keys.rs | 114 +++------- .../crates/gateway-api/src/admin/settings.rs | 17 +- .../gateway-api/tests/admin/accounts.rs | 34 +-- .../gateway-api/tests/admin/client_keys.rs | 53 +++-- backend/crates/gateway-api/tests/admin/mod.rs | 13 +- .../gateway-api/tests/admin/settings.rs | 20 +- backend/crates/gateway-store/src/lib.rs | 16 +- .../gateway-store/src/postgres/client_keys.rs | 6 +- .../crates/gateway-store/src/postgres/mod.rs | 41 +--- .../src/postgres/provider_accounts.rs | 196 +++++++----------- .../src/postgres/runtime_settings.rs | 52 ++--- .../tests/postgres/provider_accounts.rs | 171 ++++++--------- backend/crates/providers/openai/src/admin.rs | 8 - .../crates/providers/openai/tests/admin.rs | 8 +- .../openai/tests/credential/oauth.rs | 2 - backend/crates/providers/xai/src/admin.rs | 7 - backend/crates/providers/xai/src/provider.rs | 13 +- backend/crates/providers/xai/tests/admin.rs | 8 +- .../providers/xai/tests/provider/contract.rs | 40 +++- docs/api.md | 34 ++- docs/architecture.md | 5 +- frontend/src/api/modules/accounts.ts | 5 - frontend/src/api/modules/api-keys.ts | 3 - frontend/src/api/modules/settings.ts | 1 - .../composables/useAccountMutations.ts | 41 +--- .../composables/useAccountOnboarding.ts | 58 +----- .../accounts/composables/useAccountsQuery.ts | 3 - frontend/src/views/accounts/index.vue | 2 - .../composables/useApiKeyMutations.ts | 18 +- .../api-keys/composables/useApiKeysQuery.ts | 16 -- frontend/src/views/api-keys/index.vue | 3 +- .../settings/composables/useSettingsForm.ts | 3 - 50 files changed, 349 insertions(+), 863 deletions(-) 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,