mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-11 10:09:47 +08:00
fix(meta): exclude cleared expired timestamp on persist, add runtime alias resolution, and fix test race
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 := ""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user