package synthesizer import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" ) // FileSynthesizer generates Auth entries from OAuth JSON files. // It handles file-based authentication and Gemini virtual auth generation. type FileSynthesizer struct{} // NewFileSynthesizer creates a new FileSynthesizer instance. func NewFileSynthesizer() *FileSynthesizer { return &FileSynthesizer{} } // Synthesize generates Auth entries from auth files in the auth directory. func (s *FileSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, error) { out := make([]*coreauth.Auth, 0, 16) if ctx == nil || ctx.AuthDir == "" { return out, nil } entries, err := os.ReadDir(ctx.AuthDir) if err != nil { // Not an error if directory doesn't exist return out, nil } now := ctx.Now cfg := ctx.Config for _, e := range entries { if e.IsDir() { continue } name := e.Name() if !strings.HasSuffix(strings.ToLower(name), ".json") { continue } full := filepath.Join(ctx.AuthDir, name) data, errRead := os.ReadFile(full) if errRead != nil || len(data) == 0 { continue } var metadata map[string]any if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil { continue } t, _ := metadata["type"].(string) if t == "" { continue } provider := strings.ToLower(t) if provider == "gemini" { provider = "gemini-cli" } label := provider if email, _ := metadata["email"].(string); email != "" { label = email } // Use relative path under authDir as ID to stay consistent with the file-based token store id := full if rel, errRel := filepath.Rel(ctx.AuthDir, full); errRel == nil && rel != "" { id = rel } proxyURL := "" if p, ok := metadata["proxy_url"].(string); ok { proxyURL = p } prefix := "" if rawPrefix, ok := metadata["prefix"].(string); ok { trimmed := strings.TrimSpace(rawPrefix) trimmed = strings.Trim(trimmed, "/") if trimmed != "" && !strings.Contains(trimmed, "/") { prefix = trimmed } } a := &coreauth.Auth{ ID: id, Provider: provider, Label: label, Prefix: prefix, Status: coreauth.StatusActive, Attributes: map[string]string{ "source": full, "path": full, }, ProxyURL: proxyURL, Metadata: metadata, CreatedAt: now, UpdatedAt: now, } ApplyAuthExcludedModelsMeta(a, cfg, nil, "oauth") if provider == "gemini-cli" { if virtuals := SynthesizeGeminiVirtualAuths(a, metadata, now); len(virtuals) > 0 { for _, v := range virtuals { ApplyAuthExcludedModelsMeta(v, cfg, nil, "oauth") } out = append(out, a) out = append(out, virtuals...) continue } } out = append(out, a) } return out, nil } // SynthesizeGeminiVirtualAuths creates virtual Auth entries for multi-project Gemini credentials. // It disables the primary auth and creates one virtual auth per project. func SynthesizeGeminiVirtualAuths(primary *coreauth.Auth, metadata map[string]any, now time.Time) []*coreauth.Auth { if primary == nil || metadata == nil { return nil } projects := splitGeminiProjectIDs(metadata) if len(projects) <= 1 { return nil } email, _ := metadata["email"].(string) shared := geminicli.NewSharedCredential(primary.ID, email, metadata, projects) primary.Disabled = true primary.Status = coreauth.StatusDisabled primary.Runtime = shared if primary.Attributes == nil { primary.Attributes = make(map[string]string) } primary.Attributes["gemini_virtual_primary"] = "true" primary.Attributes["virtual_children"] = strings.Join(projects, ",") source := primary.Attributes["source"] authPath := primary.Attributes["path"] originalProvider := primary.Provider if originalProvider == "" { originalProvider = "gemini-cli" } label := primary.Label if label == "" { label = originalProvider } virtuals := make([]*coreauth.Auth, 0, len(projects)) for _, projectID := range projects { attrs := map[string]string{ "runtime_only": "true", "gemini_virtual_parent": primary.ID, "gemini_virtual_project": projectID, } if source != "" { attrs["source"] = source } if authPath != "" { attrs["path"] = authPath } metadataCopy := map[string]any{ "email": email, "project_id": projectID, "virtual": true, "virtual_parent_id": primary.ID, "type": metadata["type"], } proxy := strings.TrimSpace(primary.ProxyURL) if proxy != "" { metadataCopy["proxy_url"] = proxy } virtual := &coreauth.Auth{ ID: buildGeminiVirtualID(primary.ID, projectID), Provider: originalProvider, Label: fmt.Sprintf("%s [%s]", label, projectID), Status: coreauth.StatusActive, Attributes: attrs, Metadata: metadataCopy, ProxyURL: primary.ProxyURL, Prefix: primary.Prefix, CreatedAt: primary.CreatedAt, UpdatedAt: primary.UpdatedAt, Runtime: geminicli.NewVirtualCredential(projectID, shared), } virtuals = append(virtuals, virtual) } return virtuals } // splitGeminiProjectIDs extracts and deduplicates project IDs from metadata. func splitGeminiProjectIDs(metadata map[string]any) []string { raw, _ := metadata["project_id"].(string) trimmed := strings.TrimSpace(raw) if trimmed == "" { return nil } parts := strings.Split(trimmed, ",") result := make([]string, 0, len(parts)) seen := make(map[string]struct{}, len(parts)) for _, part := range parts { id := strings.TrimSpace(part) if id == "" { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} result = append(result, id) } return result } // buildGeminiVirtualID constructs a virtual auth ID from base ID and project ID. func buildGeminiVirtualID(baseID, projectID string) string { project := strings.TrimSpace(projectID) if project == "" { project = "project" } replacer := strings.NewReplacer("/", "_", "\\", "_", " ", "_") return fmt.Sprintf("%s::%s", baseID, replacer.Replace(project)) }