mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
242 lines
6.5 KiB
Go
242 lines
6.5 KiB
Go
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
|
|
}
|
|
}
|
|
|
|
disabled, _ := metadata["disabled"].(bool)
|
|
status := coreauth.StatusActive
|
|
if disabled {
|
|
status = coreauth.StatusDisabled
|
|
}
|
|
|
|
a := &coreauth.Auth{
|
|
ID: id,
|
|
Provider: provider,
|
|
Label: label,
|
|
Prefix: prefix,
|
|
Status: status,
|
|
Disabled: disabled,
|
|
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"],
|
|
}
|
|
if v, ok := metadata["disable_cooling"]; ok {
|
|
metadataCopy["disable_cooling"] = v
|
|
} else if v, ok := metadata["disable-cooling"]; ok {
|
|
metadataCopy["disable_cooling"] = v
|
|
}
|
|
if v, ok := metadata["request_retry"]; ok {
|
|
metadataCopy["request_retry"] = v
|
|
} else if v, ok := metadata["request-retry"]; ok {
|
|
metadataCopy["request_retry"] = v
|
|
}
|
|
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))
|
|
}
|