diff --git a/backend/internal/pkg/xai/models.go b/backend/internal/pkg/xai/models.go index 25584f0843..c60c400ac3 100644 --- a/backend/internal/pkg/xai/models.go +++ b/backend/internal/pkg/xai/models.go @@ -8,15 +8,24 @@ import ( // runtimeMappingOpts holds operator-configured defaults applied when Grok // accounts leave credentials.model_mapping empty. Updated from settings. var runtimeMappingOpts atomic.Value // ModelMappingOptions +var runtimeMappingVersion atomic.Uint64 func init() { runtimeMappingOpts.Store(ModelMappingOptions{}) + runtimeMappingVersion.Store(1) } // SetRuntimeModelMappingOptions updates process-wide defaults used by // DefaultModelMapping (e.g. after settings load). Safe for concurrent use. func SetRuntimeModelMappingOptions(opts ModelMappingOptions) { runtimeMappingOpts.Store(opts) + runtimeMappingVersion.Add(1) +} + +// RuntimeModelMappingVersion changes whenever runtime mapping options change. +// Account-level caches include it so settings updates take effect without a restart. +func RuntimeModelMappingVersion() uint64 { + return runtimeMappingVersion.Load() } // RuntimeModelMappingOptions returns the last options set via SetRuntimeModelMappingOptions. diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index ab54d3c805..eceaed0613 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -71,6 +71,7 @@ type Account struct { modelMappingCacheRawPtr uintptr modelMappingCacheRawLen int modelMappingCacheRawSig uint64 + modelMappingCacheRuntimeVersion uint64 // header_overrides 热路径缓存(非持久化字段,同 model_mapping 缓存先例) headerOverrideCache map[string]string @@ -553,6 +554,7 @@ func stringMappingFromRaw(raw any) map[string]string { } func (a *Account) GetModelMapping() map[string]string { + runtimeVersion := xai.RuntimeModelMappingVersion() credentialsPtr := mapPtr(a.Credentials) rawMapping, _ := a.Credentials["model_mapping"].(map[string]any) rawPtr := mapPtr(rawMapping) @@ -563,7 +565,8 @@ func (a *Account) GetModelMapping() map[string]string { if a.modelMappingCacheReady && a.modelMappingCacheCredentialsPtr == credentialsPtr && a.modelMappingCacheRawPtr == rawPtr && - a.modelMappingCacheRawLen == rawLen { + a.modelMappingCacheRawLen == rawLen && + a.modelMappingCacheRuntimeVersion == runtimeVersion { rawSig = modelMappingSignature(rawMapping) rawSigReady = true if a.modelMappingCacheRawSig == rawSig { @@ -582,6 +585,7 @@ func (a *Account) GetModelMapping() map[string]string { a.modelMappingCacheRawPtr = rawPtr a.modelMappingCacheRawLen = rawLen a.modelMappingCacheRawSig = rawSig + a.modelMappingCacheRuntimeVersion = runtimeVersion return mapping } diff --git a/backend/internal/service/account_wildcard_test.go b/backend/internal/service/account_wildcard_test.go index 6ce804bb16..15c5a66fa1 100644 --- a/backend/internal/service/account_wildcard_test.go +++ b/backend/internal/service/account_wildcard_test.go @@ -6,8 +6,31 @@ import ( "testing" "github.com/Wei-Shaw/sub2api/internal/domain" + "github.com/Wei-Shaw/sub2api/internal/pkg/xai" ) +func TestGrokAccountModelMappingCacheInvalidatesWithRuntimeSettings(t *testing.T) { + original := xai.RuntimeModelMappingOptions() + t.Cleanup(func() { xai.SetRuntimeModelMappingOptions(original) }) + account := &Account{Platform: PlatformGrok, Credentials: map[string]any{}} + + xai.SetRuntimeModelMappingOptions(xai.ModelMappingOptions{}) + requireMappedModel(t, account, "claude-sonnet-4-5", "claude-sonnet-4-5") + + xai.SetRuntimeModelMappingOptions(xai.ModelMappingOptions{ + DefaultText: "grok-build-0.1", + EnableCrossClientMap: true, + }) + requireMappedModel(t, account, "claude-sonnet-4-5", "grok-build-0.1") +} + +func requireMappedModel(t *testing.T, account *Account, requested, expected string) { + t.Helper() + if actual := account.GetMappedModel(requested); actual != expected { + t.Fatalf("GetMappedModel(%q) = %q, want %q", requested, actual, expected) + } +} + func TestMatchWildcard(t *testing.T) { tests := []struct { name string