Unified · Split
| @@ -111,6 +111,22 @@ func (ts *MetaTokenStorage) SetMetadata(meta map[string]any) { · 110 unmodified lines | |||
| 111 | ts.Metadata = meta | 111 | ts.Metadata = meta |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | // metaCredentialFields identifies credential and lifecycle fields managed by MetaTokenStorage. | ||
| 115 | // These fields must never be restored from disk or metadata when omitted from storage. | ||
| 116 | var metaCredentialFields = map[string]struct{}{ | ||
| 117 | "type": {}, | ||
| 118 | "auth_kind": {}, | ||
| 119 | "access_token": {}, | ||
| 120 | "token_type": {}, | ||
| 121 | "dca_token": {}, | ||
| 122 | "api_key": {}, | ||
| 123 | "expires_in": {}, | ||
| 124 | "expired": {}, | ||
| 125 | "dca_expired": {}, | ||
| 126 | "dca_expires_at": {}, | ||
| 127 | "last_refresh": {}, | ||
| 128 | } | ||
| 129 | |||
| 114 | // SaveTokenToFile writes Meta credentials to a JSON auth file. | 130 | // SaveTokenToFile writes Meta credentials to a JSON auth file. |
| 115 | func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error { | 131 | func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error { |
| 116 | ts.Type = "meta" | 132 | ts.Type = "meta" |
| @@ -161,6 +177,9 @@ func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error { · 44 unmodified lines | |||
| 161 | var existing map[string]any | 177 | var existing map[string]any |
| 162 | if errJSON := json.Unmarshal(raw, &existing); errJSON == nil { | 178 | if errJSON := json.Unmarshal(raw, &existing); errJSON == nil { |
| 163 | for k, v := range existing { | 179 | for k, v := range existing { |
| 180 | if _, isCred := metaCredentialFields[k]; isCred { | ||
| 181 | continue | ||
| 182 | } | ||
| 164 | if _, exists := data[k]; !exists { | 183 | if _, exists := data[k]; !exists { |
| 165 | data[k] = v | 184 | data[k] = v |
| 166 | } | 185 | } |
| @@ -168,6 +187,9 @@ func (ts *MetaTokenStorage) SaveTokenToFile(authFilePath string) error { · 1 unmodified line | |||
| 168 | } | 187 | } |
| 169 | } | 188 | } |
| 170 | for k, v := range ts.Metadata { | 189 | for k, v := range ts.Metadata { |
| 190 | if _, isCred := metaCredentialFields[k]; isCred { | ||
| 191 | continue | ||
| 192 | } | ||
| 171 | if _, exists := data[k]; !exists { | 193 | if _, exists := data[k]; !exists { |
| 172 | data[k] = v | 194 | data[k] = v |
| 173 | } | 195 | } |
| @@ -190,3 +190,130 @@ func TestReadLocalMuseCLIAuth(t *testing.T) { · 189 unmodified lines | |||
| 190 | t.Errorf("expected email engineer@meta.com, got %s", cred.Email) | 190 | t.Errorf("expected email engineer@meta.com, got %s", cred.Email) |
| 191 | } | 191 | } |
| 192 | } | 192 | } |
| 193 | |||
| 194 | func TestSaveTokenToFile_PreservesCustomMetadata(t *testing.T) { | ||
| 195 | tempDir := t.TempDir() | ||
| 196 | authFilePath := filepath.Join(tempDir, "meta-test.json") | ||
| 197 | |||
| 198 | storage := &MetaTokenStorage{ | ||
| 199 | Type: "meta", | ||
| 200 | AuthKind: "oauth", | ||
| 201 | AccessToken: "test-access-token", | ||
| 202 | APIKey: "test-api-key", | ||
| 203 | DCAToken: "dca:token", | ||
| 204 | Email: "user@example.com", | ||
| 205 | } | ||
| 206 | storage.SetMetadata(map[string]any{ | ||
| 207 | "priority": float64(10), | ||
| 208 | "disable_cooling": true, | ||
| 209 | "proxy_url": "http://proxy:8080", | ||
| 210 | }) | ||
| 211 | |||
| 212 | if errSave := storage.SaveTokenToFile(authFilePath); errSave != nil { | ||
| 213 | t.Fatalf("SaveTokenToFile() error = %v", errSave) | ||
| 214 | } | ||
| 215 | |||
| 216 | savedRaw, errRead := os.ReadFile(authFilePath) | ||
| 217 | if errRead != nil { | ||
| 218 | t.Fatalf("os.ReadFile error = %v", errRead) | ||
| 219 | } | ||
| 220 | |||
| 221 | var saved map[string]any | ||
| 222 | if errUnmarshal := json.Unmarshal(savedRaw, &saved); errUnmarshal != nil { | ||
| 223 | t.Fatalf("json.Unmarshal error = %v", errUnmarshal) | ||
| 224 | } | ||
| 225 | |||
| 226 | if saved["api_key"] != "test-api-key" { | ||
| 227 | t.Errorf("api_key = %v, want test-api-key", saved["api_key"]) | ||
| 228 | } | ||
| 229 | if saved["priority"] != float64(10) { | ||
| 230 | t.Errorf("priority = %v, want 10", saved["priority"]) | ||
| 231 | } | ||
| 232 | if saved["disable_cooling"] != true { | ||
| 233 | t.Errorf("disable_cooling = %v, want true", saved["disable_cooling"]) | ||
| 234 | } | ||
| 235 | if saved["proxy_url"] != "http://proxy:8080" { | ||
| 236 | t.Errorf("proxy_url = %v, want http://proxy:8080", saved["proxy_url"]) | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | func TestSaveTokenToFile_DoesNotRestoreClearedCredentialFields(t *testing.T) { | ||
| 241 | tempDir := t.TempDir() | ||
| 242 | authFilePath := filepath.Join(tempDir, "meta-test.json") | ||
| 243 | |||
| 244 | // Pre-populate an auth file that had expired DCA credentials and custom configuration | ||
| 245 | initial := map[string]any{ | ||
| 246 | "type": "meta", | ||
| 247 | "auth_kind": "oauth", | ||
| 248 | "access_token": "old-dca-token", | ||
| 249 | "dca_token": "old-dca-token", | ||
| 250 | "expired": "2026-01-01T00:00:00Z", | ||
| 251 | "dca_expired": "2026-01-01T00:00:00Z", | ||
| 252 | "dca_expires_at": float64(1767225600), | ||
| 253 | "priority": float64(42), | ||
| 254 | "models": []any{"muse-latest"}, | ||
| 255 | "disable-cooling": true, | ||
| 256 | } | ||
| 257 | raw, err := json.Marshal(initial) | ||
| 258 | if err != nil { | ||
| 259 | t.Fatalf("json.Marshal error = %v", err) | ||
| 260 | } | ||
| 261 | if err := os.WriteFile(authFilePath, raw, 0600); err != nil { | ||
| 262 | t.Fatalf("WriteFile error = %v", err) | ||
| 263 | } | ||
| 264 | |||
| 265 | // Token storage after minting an API key: Expired is explicitly empty, new APIKey set | ||
| 266 | storage := &MetaTokenStorage{ | ||
| 267 | Type: "meta", | ||
| 268 | AuthKind: "oauth", | ||
| 269 | AccessToken: "new-minted-key", | ||
| 270 | APIKey: "new-minted-key", | ||
| 271 | DCAToken: "new-dca-token", | ||
| 272 | Expired: "", // Deliberately cleared on refresh | ||
| 273 | Email: "user@example.com", | ||
| 274 | } | ||
| 275 | |||
| 276 | if errSave := storage.SaveTokenToFile(authFilePath); errSave != nil { | ||
| 277 | t.Fatalf("SaveTokenToFile error = %v", errSave) | ||
| 278 | } | ||
| 279 | |||
| 280 | savedRaw, errRead := os.ReadFile(authFilePath) | ||
| 281 | if errRead != nil { | ||
| 282 | t.Fatalf("os.ReadFile error = %v", errRead) | ||
| 283 | } | ||
| 284 | |||
| 285 | var saved map[string]any | ||
| 286 | if errUnmarshal := json.Unmarshal(savedRaw, &saved); errUnmarshal != nil { | ||
| 287 | t.Fatalf("json.Unmarshal error = %v", errUnmarshal) | ||
| 288 | } | ||
| 289 | |||
| 290 | // Verify cleared credential fields were NOT restored from disk | ||
| 291 | if val, exists := saved["expired"]; exists && val != "" { | ||
| 292 | t.Errorf("SaveTokenToFile restored expired timestamp: %v", val) | ||
| 293 | } | ||
| 294 | if val, exists := saved["dca_expired"]; exists && val != "" { | ||
| 295 | t.Errorf("SaveTokenToFile restored dca_expired timestamp: %v", val) | ||
| 296 | } | ||
| 297 | if _, exists := saved["dca_expires_at"]; exists { | ||
| 298 | t.Errorf("SaveTokenToFile restored dca_expires_at timestamp") | ||
| 299 | } | ||
| 300 | |||
| 301 | // Verify new credentials were written | ||
| 302 | if saved["api_key"] != "new-minted-key" { | ||
| 303 | t.Errorf("api_key = %v, want new-minted-key", saved["api_key"]) | ||
| 304 | } | ||
| 305 | if saved["access_token"] != "new-minted-key" { | ||
| 306 | t.Errorf("access_token = %v, want new-minted-key", saved["access_token"]) | ||
| 307 | } | ||
| 308 | |||
| 309 | // Verify non-credential configuration from disk was preserved | ||
| 310 | if saved["priority"] != float64(42) { | ||
| 311 | t.Errorf("priority = %v, want 42", saved["priority"]) | ||
| 312 | } | ||
| 313 | if saved["disable-cooling"] != true { | ||
| 314 | t.Errorf("disable-cooling = %v, want true", saved["disable-cooling"]) | ||
| 315 | } | ||
| 316 | if models, ok := saved["models"].([]any); !ok || len(models) != 1 || models[0] != "muse-latest" { | ||
| 317 | t.Errorf("models = %v, want [muse-latest]", saved["models"]) | ||
| 318 | } | ||
| 319 | } | ||
| @@ -306,7 +306,7 @@ func TestMetaExecutor_Refresh_SingleflightAndMultiAccount(t *testing.T) { · 305 unmodified lines | |||
| 306 | wg.Add(1) | 306 | wg.Add(1) |
| 307 | go func() { | 307 | go func() { |
| 308 | defer wg.Done() | 308 | defer wg.Done() |
| 309 | res, err := exec.Refresh(context.Background(), auth1) | 309 | res, err := exec.Refresh(context.Background(), auth1.Clone()) |
| 310 | if err != nil { | 310 | if err != nil { |
| 311 | t.Errorf("Refresh acct1 error: %v", err) | 311 | t.Errorf("Refresh acct1 error: %v", err) |
| 312 | } | 312 | } |
| @@ -326,7 +326,7 @@ func TestMetaExecutor_Refresh_SingleflightAndMultiAccount(t *testing.T) { · 13 unmodified lines | |||
| 326 | Provider: "meta", | 326 | Provider: "meta", |
| 327 | Metadata: map[string]any{"dca_token": "dca:acct2"}, | 327 | Metadata: map[string]any{"dca_token": "dca:acct2"}, |
| 328 | } | 328 | } |
| 329 | res2, err2 := exec.Refresh(context.Background(), auth2) | 329 | res2, err2 := exec.Refresh(context.Background(), auth2.Clone()) |
| 330 | if err2 != nil { | 330 | if err2 != nil { |
| 331 | t.Fatalf("Refresh acct2 error: %v", err2) | 331 | t.Fatalf("Refresh acct2 error: %v", err2) |
| 332 | } | 332 | } |
| @@ -291,3 +291,69 @@ func TestResolveAPIKeyModelAliasWithResult_ForceMappingUsesConfigAliasNotRequest · 290 unmodified lines | |||
| 291 | t.Fatalf("OriginalAlias = %q want claude-sonnet-4-5", result.OriginalAlias) | 291 | t.Fatalf("OriginalAlias = %q want claude-sonnet-4-5", result.OriginalAlias) |
| 292 | } | 292 | } |
| 293 | } | 293 | } |
| 294 | |||
| 295 | func TestLookupAPIKeyUpstreamModel_MetaKey(t *testing.T) { | ||
| 296 | cfg := &internalconfig.Config{ | ||
| 297 | MetaKey: []internalconfig.MetaKey{ | ||
| 298 | { | ||
| 299 | APIKey: "meta-key", | ||
| 300 | BaseURL: "https://api.meta.ai/v1", | ||
| 301 | Models: []internalconfig.CodexModel{ | ||
| 302 | {Name: "muse-spark-1.3", Alias: "muse-latest"}, | ||
| 303 | }, | ||
| 304 | }, | ||
| 305 | }, | ||
| 306 | } | ||
| 307 | |||
| 308 | mgr := NewManager(nil, nil, nil) | ||
| 309 | mgr.SetConfig(cfg) | ||
| 310 | |||
| 311 | ctx := context.Background() | ||
| 312 | auth := &Auth{ | ||
| 313 | ID: "meta-auth-1", | ||
| 314 | Provider: "meta", | ||
| 315 | Attributes: map[string]string{ | ||
| 316 | "api_key": "meta-key", | ||
| 317 | "base_url": "https://api.meta.ai/v1", | ||
| 318 | "auth_kind": "apikey", | ||
| 319 | }, | ||
| 320 | } | ||
| 321 | if _, err := mgr.Register(ctx, auth); err != nil { | ||
| 322 | t.Fatalf("register auth: %v", err) | ||
| 323 | } | ||
| 324 | |||
| 325 | // 1. Fast path: lookup per-auth mapping table compiled during register. | ||
| 326 | resolved := mgr.lookupAPIKeyUpstreamModel("meta-auth-1", "muse-latest") | ||
| 327 | if resolved != "muse-spark-1.3" { | ||
| 328 | t.Fatalf("lookupAPIKeyUpstreamModel() = %q, want muse-spark-1.3", resolved) | ||
| 329 | } | ||
| 330 | |||
| 331 | // 2. Slow path: directly call mgr.applyAPIKeyModelAliasWithRouting with an empty alias table to exercise config resolution fallback. | ||
| 332 | slowRouting := &apiKeyModelRoutingSnapshot{ | ||
| 333 | config: cfg, | ||
| 334 | aliases: make(apiKeyModelAliasTable), | ||
| 335 | } | ||
| 336 | slowResolved := mgr.applyAPIKeyModelAliasWithRouting(slowRouting, auth, "muse-latest") | ||
| 337 | if slowResolved != "muse-spark-1.3" { | ||
| 338 | t.Fatalf("applyAPIKeyModelAliasWithRouting(slow) = %q, want muse-spark-1.3", slowResolved) | ||
| 339 | } | ||
| 340 | |||
| 341 | // 3. Model alias result with force mapping / alias metadata | ||
| 342 | aliasResult := mgr.resolveAPIKeyModelAliasWithResult(auth, "muse-latest") | ||
| 343 | if aliasResult.UpstreamModel != "muse-spark-1.3" { | ||
| 344 | t.Fatalf("resolveAPIKeyModelAliasWithResult() upstream = %q, want muse-spark-1.3", aliasResult.UpstreamModel) | ||
| 345 | } | ||
| 346 | |||
| 347 | // 4. Configured alias entries helper | ||
| 348 | entries := configuredModelAliasEntries(cfg, auth) | ||
| 349 | found := false | ||
| 350 | for _, e := range entries { | ||
| 351 | if e.GetAlias() == "muse-latest" && e.GetName() == "muse-spark-1.3" { | ||
| 352 | found = true | ||
| 353 | break | ||
| 354 | } | ||
| 355 | } | ||
| 356 | if !found { | ||
| 357 | t.Fatalf("configuredModelAliasEntries did not contain muse-latest -> muse-spark-1.3: %+v", entries) | ||
| 358 | } | ||
| 359 | } | ||
| @@ -202,6 +202,10 @@ func compileAPIKeyModelCapabilitiesForAuth(cfg *internalconfig.Config, auth *Aut · 201 unmodified lines | |||
| 202 | if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { | 202 | if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { |
| 203 | compileConfiguredModelCapabilities(out, entry.Models, "gemini") | 203 | compileConfiguredModelCapabilities(out, entry.Models, "gemini") |
| 204 | } | 204 | } |
| 205 | case "meta": | ||
| 206 | if entry := resolveMetaAPIKeyConfig(cfg, auth); entry != nil { | ||
| 207 | compileConfiguredModelCapabilities(out, entry.Models, "meta") | ||
| 208 | } | ||
| 205 | default: | 209 | default: |
| 206 | providerKey, compatName := "", "" | 210 | providerKey, compatName := "", "" |
| 207 | if auth.Attributes != nil { | 211 | if auth.Attributes != nil { |
| @@ -378,7 +378,7 @@ func requestToFormat(provider string, executor ProviderExecutor, req cliproxyexe · 377 unmodified lines | |||
| 378 | return sdktranslator.FormatClaude | 378 | return sdktranslator.FormatClaude |
| 379 | case "gemini", "vertex", "aistudio": | 379 | case "gemini", "vertex", "aistudio": |
| 380 | return sdktranslator.FormatGemini | 380 | return sdktranslator.FormatGemini |
| 381 | case "kimi": | 381 | case "kimi", "meta": |
| 382 | return sdktranslator.FormatOpenAI | 382 | return sdktranslator.FormatOpenAI |
| 383 | case "antigravity": | 383 | case "antigravity": |
| 384 | return sdktranslator.FormatAntigravity | 384 | return sdktranslator.FormatAntigravity |
| @@ -465,6 +465,10 @@ func configuredModelAliasEntries(cfg *internalconfig.Config, auth *Auth) []model · 464 unmodified lines | |||
| 465 | if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { | 465 | if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { |
| 466 | models = asModelAliasEntries(entry.Models) | 466 | models = asModelAliasEntries(entry.Models) |
| 467 | } | 467 | } |
| 468 | case "meta": | ||
| 469 | if entry := resolveMetaAPIKeyConfig(cfg, auth); entry != nil { | ||
| 470 | models = asModelAliasEntries(entry.Models) | ||
| 471 | } | ||
| 468 | default: | 472 | default: |
| 469 | providerKey := "" | 473 | providerKey := "" |
| 470 | compatName := "" | 474 | compatName := "" |
| @@ -621,6 +625,10 @@ func (m *Manager) rebuildAPIKeyModelAliasLocked(cfg *internalconfig.Config) { · 150 unmodified lines | |||
| 621 | if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { | 625 | if entry := resolveVertexAPIKeyConfig(cfg, auth); entry != nil { |
| 622 | compileAPIKeyModelAliasForModels(byAlias, entry.Models) | 626 | compileAPIKeyModelAliasForModels(byAlias, entry.Models) |
| 623 | } | 627 | } |
| 628 | case "meta": | ||
| 629 | if entry := resolveMetaAPIKeyConfig(cfg, auth); entry != nil { | ||
| 630 | compileAPIKeyModelAliasForModels(byAlias, entry.Models) | ||
| 631 | } | ||
| 624 | default: | 632 | default: |
| 625 | // OpenAI-compat uses config selection from auth.Attributes. | 633 | // OpenAI-compat uses config selection from auth.Attributes. |
| 626 | providerKey := "" | 634 | providerKey := "" |
| @@ -741,6 +749,8 @@ func (m *Manager) applyAPIKeyModelAliasWithRouting(routing *apiKeyModelRoutingSn · 114 unmodified lines | |||
| 741 | upstreamModel = resolveUpstreamModelForXAIAPIKey(cfg, auth, requestedModel) | 749 | upstreamModel = resolveUpstreamModelForXAIAPIKey(cfg, auth, requestedModel) |
| 742 | case "vertex": | 750 | case "vertex": |
| 743 | upstreamModel = resolveUpstreamModelForVertexAPIKey(cfg, auth, requestedModel) | 751 | upstreamModel = resolveUpstreamModelForVertexAPIKey(cfg, auth, requestedModel) |
| 752 | case "meta": | ||
| 753 | upstreamModel = resolveUpstreamModelForMetaAPIKey(cfg, auth, requestedModel) | ||
| 744 | default: | 754 | default: |
| 745 | upstreamModel = resolveUpstreamModelForOpenAICompatAPIKey(cfg, auth, requestedModel) | 755 | upstreamModel = resolveUpstreamModelForOpenAICompatAPIKey(cfg, auth, requestedModel) |
| 746 | } | 756 | } |
| @@ -848,6 +858,13 @@ func resolveVertexAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internal · 101 unmodified lines | |||
| 848 | return resolveAPIKeyConfig(cfg.VertexCompatAPIKey, auth) | 858 | return resolveAPIKeyConfig(cfg.VertexCompatAPIKey, auth) |
| 849 | } | 859 | } |
| 850 | 860 | ||
| 861 | func resolveMetaAPIKeyConfig(cfg *internalconfig.Config, auth *Auth) *internalconfig.MetaKey { | ||
| 862 | if cfg == nil { | ||
| 863 | return nil | ||
| 864 | } | ||
| 865 | return resolveAPIKeyConfig(cfg.MetaKey, auth) | ||
| 866 | } | ||
| 867 | |||
| 851 | func resolveUpstreamModelForGeminiAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string { | 868 | func resolveUpstreamModelForGeminiAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string { |
| 852 | entry := resolveGeminiAPIKeyConfig(cfg, auth) | 869 | entry := resolveGeminiAPIKeyConfig(cfg, auth) |
| 853 | if entry == nil { | 870 | if entry == nil { |
| @@ -896,6 +913,14 @@ func resolveUpstreamModelForVertexAPIKey(cfg *internalconfig.Config, auth *Auth, · 42 unmodified lines | |||
| 896 | return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models)) | 913 | return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models)) |
| 897 | } | 914 | } |
| 898 | 915 | ||
| 916 | func resolveUpstreamModelForMetaAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string { | ||
| 917 | entry := resolveMetaAPIKeyConfig(cfg, auth) | ||
| 918 | if entry == nil { | ||
| 919 | return "" | ||
| 920 | } | ||
| 921 | return resolveModelAliasFromConfigModels(requestedModel, asModelAliasEntries(entry.Models)) | ||
| 922 | } | ||
| 923 | |||
| 899 | func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string { | 924 | func resolveUpstreamModelForOpenAICompatAPIKey(cfg *internalconfig.Config, auth *Auth, requestedModel string) string { |
| 900 | providerKey := "" | 925 | providerKey := "" |
| 901 | compatName := "" | 926 | compatName := "" |
| @@ -143,6 +143,10 @@ func extractRequestScopedErrorRules(auth *Auth, cfg *internalconfig.Config) []in · 142 unmodified lines | |||
| 143 | if index >= 0 && index < len(cfg.XAIKey) { | 143 | if index >= 0 && index < len(cfg.XAIKey) { |
| 144 | return cfg.XAIKey[index].RequestScopedErrors | 144 | return cfg.XAIKey[index].RequestScopedErrors |
| 145 | } | 145 | } |
| 146 | case "meta": | ||
| 147 | if index >= 0 && index < len(cfg.MetaKey) { | ||
| 148 | return cfg.MetaKey[index].RequestScopedErrors | ||
| 149 | } | ||
| 146 | case "gemini": | 150 | case "gemini": |
| 147 | if index >= 0 && index < len(cfg.GeminiKey) { | 151 | if index >= 0 && index < len(cfg.GeminiKey) { |
| 148 | return cfg.GeminiKey[index].RequestScopedErrors | 152 | return cfg.GeminiKey[index].RequestScopedErrors |