fix(meta): exclude cleared expired timestamp on persist, add runtime alias resolution, and fix test race

This commit is contained in:
Kenny
2026-09-05 02:31:47 +00:00
parent cee799f61a
commit 47cc31aed8
8 changed files with 251 additions and 3 deletions

View File

@@ -111,6 +111,22 @@ func (ts *MetaTokenStorage) SetMetadata(meta map[string]any) {
ts.Metadata = meta
}
// metaCredentialFields identifies credential and lifecycle fields managed by MetaTokenStorage.
// These fields must never be restored from disk or metadata when omitted from storage.
var metaCredentialFields = map[string]struct{}{
"type": {},
"auth_kind": {},
"access_token": {},
"token_type": {},
"dca_token": {},
"api_key": {},
"expires_in": {},
"expired": {},
"dca_expired": {},
"dca_expires_at": {},
"last_refresh": {},
}
// SaveTokenToFile writes Meta credentials to a JSON auth file.
func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error {
ts.Type = "meta"
@@ -161,6 +177,9 @@ func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error {
var existing map[string]any
if errJSON := json.Unmarshal(raw, &existing); errJSON == nil {
for k, v := range existing {
if _, isCred := metaCredentialFields[k]; isCred {
continue
}
if _, exists := data[k]; !exists {
data[k] = v
}
@@ -168,6 +187,9 @@ func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error {
}
}
for k, v := range ts.Metadata {
if _, isCred := metaCredentialFields[k]; isCred {
continue
}
if _, exists := data[k]; !exists {
data[k] = v
}

View File

@@ -190,3 +190,130 @@ func TestReadLocalMuseCLIAuth(t *testing.T) {
t.Errorf("expected email engineer@meta.com, got %s", cred.Email)
}
}
func TestSaveTokenToFile_PreservesCustomMetadata(t *testing.T) {
tempDir := t.TempDir()
authFilePath := filepath.Join(tempDir, "meta-test.json")
storage := &MetaTokenStorage{
Type: "meta",
AuthKind: "oauth",
AccessToken: "test-access-token",
APIKey: "test-api-key",
DCAToken: "dca:token",
Email: "user@example.com",
}
storage.SetMetadata(map[string]any{
"priority": float64(10),
"disable_cooling": true,
"proxy_url": "http://proxy:8080",
})
if errSave := storage.SaveTokenToFile(authFilePath); errSave != nil {
t.Fatalf("SaveTokenToFile() error = %v", errSave)
}
savedRaw, errRead := os.ReadFile(authFilePath)
if errRead != nil {
t.Fatalf("os.ReadFile error = %v", errRead)
}
var saved map[string]any
if errUnmarshal := json.Unmarshal(savedRaw, &saved); errUnmarshal != nil {
t.Fatalf("json.Unmarshal error = %v", errUnmarshal)
}
if saved["api_key"] != "test-api-key" {
t.Errorf("api_key = %v, want test-api-key", saved["api_key"])
}
if saved["priority"] != float64(10) {
t.Errorf("priority = %v, want 10", saved["priority"])
}
if saved["disable_cooling"] != true {
t.Errorf("disable_cooling = %v, want true", saved["disable_cooling"])
}
if saved["proxy_url"] != "http://proxy:8080" {
t.Errorf("proxy_url = %v, want http://proxy:8080", saved["proxy_url"])
}
}
func TestSaveTokenToFile_DoesNotRestoreClearedCredentialFields(t *testing.T) {
tempDir := t.TempDir()
authFilePath := filepath.Join(tempDir, "meta-test.json")
// Pre-populate an auth file that had expired DCA credentials and custom configuration
initial := map[string]any{
"type": "meta",
"auth_kind": "oauth",
"access_token": "old-dca-token",
"dca_token": "old-dca-token",
"expired": "2026-01-01T00:00:00Z",
"dca_expired": "2026-01-01T00:00:00Z",
"dca_expires_at": float64(1767225600),
"priority": float64(42),
"models": []any{"muse-latest"},
"disable-cooling": true,
}
raw, err := json.Marshal(initial)
if err != nil {
t.Fatalf("json.Marshal error = %v", err)
}
if err := os.WriteFile(authFilePath, raw, 0600); err != nil {
t.Fatalf("WriteFile error = %v", err)
}
// Token storage after minting an API key: Expired is explicitly empty, new APIKey set
storage := &MetaTokenStorage{
Type: "meta",
AuthKind: "oauth",
AccessToken: "new-minted-key",
APIKey: "new-minted-key",
DCAToken: "new-dca-token",
Expired: "", // Deliberately cleared on refresh
Email: "user@example.com",
}
if errSave := storage.SaveTokenToFile(authFilePath); errSave != nil {
t.Fatalf("SaveTokenToFile error = %v", errSave)
}
savedRaw, errRead := os.ReadFile(authFilePath)
if errRead != nil {
t.Fatalf("os.ReadFile error = %v", errRead)
}
var saved map[string]any
if errUnmarshal := json.Unmarshal(savedRaw, &saved); errUnmarshal != nil {
t.Fatalf("json.Unmarshal error = %v", errUnmarshal)
}
// Verify cleared credential fields were NOT restored from disk
if val, exists := saved["expired"]; exists && val != "" {
t.Errorf("SaveTokenToFile restored expired timestamp: %v", val)
}
if val, exists := saved["dca_expired"]; exists && val != "" {
t.Errorf("SaveTokenToFile restored dca_expired timestamp: %v", val)
}
if _, exists := saved["dca_expires_at"]; exists {
t.Errorf("SaveTokenToFile restored dca_expires_at timestamp")
}
// Verify new credentials were written
if saved["api_key"] != "new-minted-key" {
t.Errorf("api_key = %v, want new-minted-key", saved["api_key"])
}
if saved["access_token"] != "new-minted-key" {
t.Errorf("access_token = %v, want new-minted-key", saved["access_token"])
}
// Verify non-credential configuration from disk was preserved
if saved["priority"] != float64(42) {
t.Errorf("priority = %v, want 42", saved["priority"])
}
if saved["disable-cooling"] != true {
t.Errorf("disable-cooling = %v, want true", saved["disable-cooling"])
}
if models, ok := saved["models"].([]any); !ok || len(models) != 1 || models[0] != "muse-latest" {
t.Errorf("models = %v, want [muse-latest]", saved["models"])
}
}

View File

@@ -306,7 +306,7 @@ func TestMetaExecutor_Refresh_SingleflightAndMultiAccount(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
res, err := exec.Refresh(context.Background(), auth1)
res, err := exec.Refresh(context.Background(), auth1.Clone())
if err != nil {
t.Errorf("Refresh acct1 error: %v", err)
}
@@ -326,7 +326,7 @@ func TestMetaExecutor_Refresh_SingleflightAndMultiAccount(t *testing.T) {
Provider: "meta",
Metadata: map[string]any{"dca_token": "dca:acct2"},
}
res2, err2 := exec.Refresh(context.Background(), auth2)
res2, err2 := exec.Refresh(context.Background(), auth2.Clone())
if err2 != nil {
t.Fatalf("Refresh acct2 error: %v", err2)
}

View File

@@ -291,3 +291,69 @@ func TestResolveAPIKeyModelAliasWithResult_ForceMappingUsesConfigAliasNotRequest
t.Fatalf("OriginalAlias = %q want claude-sonnet-4-5", result.OriginalAlias)
}
}
func TestLookupAPIKeyUpstreamModel_MetaKey(t *testing.T) {
cfg := &internalconfig.Config{
MetaKey: []internalconfig.MetaKey{
{
APIKey: "meta-key",
BaseURL: "https://api.meta.ai/v1",
Models: []internalconfig.CodexModel{
{Name: "muse-spark-1.3", Alias: "muse-latest"},
},
},
},
}
mgr := NewManager(nil, nil, nil)
mgr.SetConfig(cfg)
ctx := context.Background()
auth := &Auth{
ID: "meta-auth-1",
Provider: "meta",
Attributes: map[string]string{
"api_key": "meta-key",
"base_url": "https://api.meta.ai/v1",
"auth_kind": "apikey",
},
}
if _, err := mgr.Register(ctx, auth); err != nil {
t.Fatalf("register auth: %v", err)
}
// 1. Fast path: lookup per-auth mapping table compiled during register.
resolved := mgr.lookupAPIKeyUpstreamModel("meta-auth-1", "muse-latest")
if resolved != "muse-spark-1.3" {
t.Fatalf("lookupAPIKeyUpstreamModel() = %q, want muse-spark-1.3", resolved)
}
// 2. Slow path: directly call mgr.applyAPIKeyModelAliasWithRouting with an empty alias table to exercise config resolution fallback.
slowRouting := &apiKeyModelRoutingSnapshot{
config: cfg,
aliases: make(apiKeyModelAliasTable),
}
slowResolved := mgr.applyAPIKeyModelAliasWithRouting(slowRouting, auth, "muse-latest")
if slowResolved != "muse-spark-1.3" {
t.Fatalf("applyAPIKeyModelAliasWithRouting(slow) = %q, want muse-spark-1.3", slowResolved)
}
// 3. Model alias result with force mapping / alias metadata
aliasResult := mgr.resolveAPIKeyModelAliasWithResult(auth, "muse-latest")
if aliasResult.UpstreamModel != "muse-spark-1.3" {
t.Fatalf("resolveAPIKeyModelAliasWithResult() upstream = %q, want muse-spark-1.3", aliasResult.UpstreamModel)
}
// 4. Configured alias entries helper
entries := configuredModelAliasEntries(cfg, auth)
found := false
for _, e := range entries {
if e.GetAlias() == "muse-latest" && e.GetName() == "muse-spark-1.3" {
found = true
break
}
}
if !found {
t.Fatalf("configuredModelAliasEntries did not contain muse-latest -> muse-spark-1.3: %+v", entries)
}
}

View File

@@ -202,6 +202,10 @@ func compileAPIKeyModelCapabilitiesForAuth(cfg *internalconfig.Config, auth *Aut
if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil {
compileConfiguredModelCapabilities(out, entry.Models, "gemini")
}
case "meta":
if entry := resolveMetaAPIKeyConfig(cfg, auth); entry != nil {
compileConfiguredModelCapabilities(out, entry.Models, "meta")
}
default:
providerKey, compatName := "", ""
if auth.Attributes != nil {

View File

@@ -378,7 +378,7 @@ func requestToFormat(provider string, executor ProviderExecutor, req cliproxyexe
return sdktranslator.FormatClaude
case "gemini", "vertex", "aistudio":
return sdktranslator.FormatGemini
case "kimi":
case "kimi", "meta":
return sdktranslator.FormatOpenAI
case "antigravity":
return sdktranslator.FormatAntigravity

View File

@@ -465,6 +465,10 @@ func configuredModelAliasEntries(cfg *internalconfig.Config, auth *Auth) []model
if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil {
models = asModelAliasEntries(entry.Models)
}
case "meta":
if entry := resolveMetaAPIKeyConfig(cfg, auth); entry != nil {
models = asModelAliasEntries(entry.Models)
}
default:
providerKey := ""
compatName := ""
@@ -621,6 +625,10 @@ func (m *Manager) rebuildAPIKeyModelAliasLocked(cfg *internalconfig.Config) {
if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil {
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
}
case "meta":
if entry := resolveMetaAPIKeyConfig(cfg, auth); entry != nil {
compileAPIKeyModelAliasForModels(byAlias, entry.Models)
}
default:
// OpenAI-compat uses config selection from auth.Attributes.
providerKey := ""
@@ -741,6 +749,8 @@ func (m *Manager) applyAPIKeyModelAliasWithRouting(routing *apiKeyModelRoutingSn
upstreamModel = resolveUpstreamModelForXAIAPIKey(cfg, auth, requestedModel)
case "vertex":
upstreamModel = resolveUpstreamModelForVertexAPIKey(cfg, auth, requestedModel)
case "meta":
upstreamModel = resolveUpstreamModelForMetaAPIKey(cfg, auth, requestedModel)
default:
upstreamModel = resolveUpstreamModelForOpenAICompatAPIKey(cfg, auth, requestedModel)
}
@@ -848,6 +858,13 @@ func resolveVertexAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internal
return resolveAPIKeyConfig(cfg.VertexCompatAPIKey, auth)
}
func resolveMetaAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.MetaKey {
if cfg == nil {
return nil
}
return resolveAPIKeyConfig(cfg.MetaKey, auth)
}
func resolveUpstreamModelForGeminiAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
entry := resolveGeminiAPIKeyConfig(cfg, auth)
if entry == nil {
@@ -896,6 +913,14 @@ func resolveUpstreamModelForVertexAPIKey(cfg *internalconfig.Config, auth *Auth,
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
}
func resolveUpstreamModelForMetaAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
entry := resolveMetaAPIKeyConfig(cfg, auth)
if entry == nil {
return ""
}
return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models))
}
func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string {
providerKey := ""
compatName := ""

View File

@@ -143,6 +143,10 @@ func extractRequestScopedErrorRules(auth *Auth, cfg *internalconfig.Config) []in
if index >= 0 && index < len(cfg.XAIKey) {
return cfg.XAIKey[index].RequestScopedErrors
}
case "meta":
if index >= 0 && index < len(cfg.MetaKey) {
return cfg.MetaKey[index].RequestScopedErrors
}
case "gemini":
if index >= 0 && index < len(cfg.GeminiKey) {
return cfg.GeminiKey[index].RequestScopedErrors