Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,452 @@
package synthesizer
import (
"fmt"
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// ConfigSynthesizer generates Auth entries from configuration API keys.
// It handles Gemini, Interactions, Claude, Codex, xAI, OpenAI-compat, and Vertex-compat providers.
type ConfigSynthesizer struct{}
// NewConfigSynthesizer creates a new ConfigSynthesizer instance.
func NewConfigSynthesizer() *ConfigSynthesizer {
return &ConfigSynthesizer{}
}
func addWeightToAttrs(weight *int, attrs map[string]string) {
if weight == nil {
return
}
normalized := *weight
if normalized <= 0 {
normalized = 0
}
attrs[coreauth.AttributeWeight] = strconv.Itoa(normalized)
}
// Synthesize generates Auth entries from config API keys.
func (s *ConfigSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, error) {
out := make([]*coreauth.Auth, 0, 32)
if ctx == nil || ctx.Config == nil {
return out, nil
}
if errValidate := ctx.Config.ValidateCredentialWeights(); errValidate != nil {
return nil, fmt.Errorf("synthesize config API key auths: %w", errValidate)
}
// Gemini API Keys
out = append(out, s.synthesizeGeminiKeys(ctx)...)
// Native Interactions API Keys
out = append(out, s.synthesizeInteractionsKeys(ctx)...)
// Claude API Keys
out = append(out, s.synthesizeClaudeKeys(ctx)...)
// Codex API Keys
out = append(out, s.synthesizeCodexKeys(ctx)...)
// xAI API Keys
out = append(out, s.synthesizeXAIKeys(ctx)...)
// OpenAI-compat
out = append(out, s.synthesizeOpenAICompat(ctx)...)
// Vertex-compat
out = append(out, s.synthesizeVertexCompat(ctx)...)
return out, nil
}
// synthesizeGeminiKeys creates Auth entries for Gemini API keys.
func (s *ConfigSynthesizer) synthesizeGeminiKeys(ctx *SynthesisContext) []*coreauth.Auth {
return s.synthesizeGeminiKeyEntries(ctx, ctx.Config.GeminiKey, "gemini:apikey", "gemini", "gemini-apikey", constant.Gemini)
}
// synthesizeInteractionsKeys creates Auth entries for native Interactions API keys.
func (s *ConfigSynthesizer) synthesizeInteractionsKeys(ctx *SynthesisContext) []*coreauth.Auth {
return s.synthesizeGeminiKeyEntries(ctx, ctx.Config.InteractionsKey, "gemini-interactions:apikey", "interactions", "interactions-apikey", constant.GeminiInteractions)
}
func (s *ConfigSynthesizer) synthesizeGeminiKeyEntries(ctx *SynthesisContext, entries []config.GeminiKey, idKind, sourceName, label, provider string) []*coreauth.Auth {
cfg := ctx.Config
now := ctx.Now
idGen := ctx.IDGenerator
out := make([]*coreauth.Auth, 0, len(entries))
for i := range entries {
entry := entries[i]
key := strings.TrimSpace(entry.APIKey)
base := strings.TrimSpace(entry.BaseURL)
if key == "" && base == "" {
continue
}
prefix := strings.TrimSpace(entry.Prefix)
proxyURL := strings.TrimSpace(entry.ProxyURL)
id, token := idGen.Next(idKind, key, base, proxyURL, prefix, config.FormatSortedHeaders(entry.Headers))
attrs := map[string]string{
"source": fmt.Sprintf("config:%s[%s]", sourceName, token),
"config_index": strconv.Itoa(i),
}
if key != "" {
attrs["api_key"] = key
}
metadata := map[string]any{}
if entry.DisableCooling != nil {
metadata["disable_cooling"] = *entry.DisableCooling
}
addRequestRetryToMetadata(entry.RequestRetry, metadata)
addRequestScopedErrorsToMetadata(entry.RequestScopedErrors, metadata)
if entry.Priority != 0 {
attrs["priority"] = strconv.Itoa(entry.Priority)
}
addWeightToAttrs(entry.Weight, attrs)
if base != "" {
attrs["base_url"] = base
}
if hash := diff.ComputeGeminiModelsHash(entry.Models); hash != "" {
attrs["models_hash"] = hash
}
addConfigHeadersToAttrs(entry.Headers, attrs)
a := &coreauth.Auth{
ID: id,
Provider: provider,
Label: label,
Prefix: prefix,
Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
ApplyAuthExcludedModelsMeta(a, cfg, entry.ExcludedModels, "apikey")
if len(a.Metadata) == 0 {
a.Metadata = nil
}
out = append(out, a)
}
return out
}
// synthesizeClaudeKeys creates Auth entries for Claude API keys.
func (s *ConfigSynthesizer) synthesizeClaudeKeys(ctx *SynthesisContext) []*coreauth.Auth {
cfg := ctx.Config
now := ctx.Now
idGen := ctx.IDGenerator
out := make([]*coreauth.Auth, 0, len(cfg.ClaudeKey))
for i := range cfg.ClaudeKey {
ck := cfg.ClaudeKey[i]
key := strings.TrimSpace(ck.APIKey)
base := strings.TrimSpace(ck.BaseURL)
if key == "" && base == "" {
continue
}
prefix := strings.TrimSpace(ck.Prefix)
proxyURL := strings.TrimSpace(ck.ProxyURL)
id, token := idGen.Next("claude:apikey", key, base, proxyURL, prefix, config.FormatSortedHeaders(ck.Headers))
attrs := map[string]string{
"source": fmt.Sprintf("config:claude[%s]", token),
"config_index": strconv.Itoa(i),
}
if key != "" {
attrs["api_key"] = key
}
metadata := map[string]any{}
if ck.DisableCooling != nil {
metadata["disable_cooling"] = *ck.DisableCooling
}
addRequestRetryToMetadata(ck.RequestRetry, metadata)
addRequestScopedErrorsToMetadata(ck.RequestScopedErrors, metadata)
if ck.Priority != 0 {
attrs["priority"] = strconv.Itoa(ck.Priority)
}
addWeightToAttrs(ck.Weight, attrs)
if base != "" {
attrs["base_url"] = base
}
if ck.RebuildMidSystemMessage {
attrs["rebuild_mid_system_message"] = "true"
}
if profile := strings.ToLower(strings.TrimSpace(ck.FingerprintProfile)); profile != "" {
attrs["fingerprint_profile"] = profile
}
if hash := diff.ComputeClaudeModelsHash(ck.Models); hash != "" {
attrs["models_hash"] = hash
}
addConfigHeadersToAttrs(ck.Headers, attrs)
a := &coreauth.Auth{
ID: id,
Provider: "claude",
Label: "claude-apikey",
Prefix: prefix,
Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
ApplyAuthExcludedModelsMeta(a, cfg, ck.ExcludedModels, "apikey")
if len(a.Metadata) == 0 {
a.Metadata = nil
}
out = append(out, a)
}
return out
}
// synthesizeCodexKeys creates Auth entries for Codex API keys.
func (s *ConfigSynthesizer) synthesizeCodexKeys(ctx *SynthesisContext) []*coreauth.Auth {
return s.synthesizeCodexStyleKeys(ctx, ctx.Config.CodexKey, "codex")
}
// synthesizeXAIKeys creates Auth entries for xAI API keys.
func (s *ConfigSynthesizer) synthesizeXAIKeys(ctx *SynthesisContext) []*coreauth.Auth {
return s.synthesizeCodexStyleKeys(ctx, ctx.Config.XAIKey, "xai")
}
func (s *ConfigSynthesizer) synthesizeCodexStyleKeys(ctx *SynthesisContext, entries []config.CodexKey, provider string) []*coreauth.Auth {
cfg := ctx.Config
now := ctx.Now
idGen := ctx.IDGenerator
out := make([]*coreauth.Auth, 0, len(entries))
for i := range entries {
entry := entries[i]
key := strings.TrimSpace(entry.APIKey)
baseURL := strings.TrimSpace(entry.BaseURL)
if key == "" && baseURL == "" {
continue
}
prefix := strings.TrimSpace(entry.Prefix)
proxyURL := strings.TrimSpace(entry.ProxyURL)
id, token := idGen.Next(provider+":apikey", key, baseURL, proxyURL, prefix, config.FormatSortedHeaders(entry.Headers))
attrs := map[string]string{
"source": fmt.Sprintf("config:%s[%s]", provider, token),
"config_index": strconv.Itoa(i),
}
if key != "" {
attrs["api_key"] = key
}
metadata := map[string]any{}
if entry.DisableCooling != nil {
metadata["disable_cooling"] = *entry.DisableCooling
}
addRequestRetryToMetadata(entry.RequestRetry, metadata)
addRequestScopedErrorsToMetadata(entry.RequestScopedErrors, metadata)
if entry.Priority != 0 {
attrs["priority"] = strconv.Itoa(entry.Priority)
}
addWeightToAttrs(entry.Weight, attrs)
if baseURL != "" {
attrs["base_url"] = baseURL
}
if entry.Websockets {
attrs["websockets"] = "true"
}
if provider == "codex" && entry.AlphaSearch {
attrs[coreauth.AttributeCodexAlphaSearch] = "true"
}
if hash := diff.ComputeCodexModelsHash(entry.Models); hash != "" {
attrs["models_hash"] = hash
}
addConfigHeadersToAttrs(entry.Headers, attrs)
a := &coreauth.Auth{
ID: id,
Provider: provider,
Label: provider + "-apikey",
Prefix: prefix,
Status: coreauth.StatusActive,
ProxyURL: strings.TrimSpace(entry.ProxyURL),
Attributes: attrs,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
ApplyAuthExcludedModelsMeta(a, cfg, entry.ExcludedModels, "apikey")
if len(a.Metadata) == 0 {
a.Metadata = nil
}
out = append(out, a)
}
return out
}
// synthesizeOpenAICompat creates Auth entries for OpenAI-compatible providers.
func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*coreauth.Auth {
cfg := ctx.Config
now := ctx.Now
idGen := ctx.IDGenerator
out := make([]*coreauth.Auth, 0)
for i := range cfg.OpenAICompatibility {
compat := &cfg.OpenAICompatibility[i]
if compat.Disabled {
continue
}
prefix := strings.TrimSpace(compat.Prefix)
providerName := strings.ToLower(strings.TrimSpace(compat.Name))
if providerName == "" {
providerName = "openai-compatibility"
}
internalProviderKey := util.OpenAICompatibleProviderKey(providerName)
base := strings.TrimSpace(compat.BaseURL)
disableCooling := compat.DisableCooling
// Handle new APIKeyEntries format (preferred)
createdEntries := 0
for j := range compat.APIKeyEntries {
entry := &compat.APIKeyEntries[j]
key := strings.TrimSpace(entry.APIKey)
proxyURL := strings.TrimSpace(entry.ProxyURL)
idKind := fmt.Sprintf("openai-compatibility:%s", providerName)
id, token := idGen.Next(idKind, key, base, proxyURL)
attrs := map[string]string{
"source": fmt.Sprintf("config:%s[%s]", providerName, token),
"base_url": base,
"compat_name": compat.Name,
"provider_key": internalProviderKey,
"config_index": strconv.Itoa(i),
}
metadata := map[string]any{}
if disableCooling != nil {
metadata["disable_cooling"] = *disableCooling
}
addRequestRetryToMetadata(compat.RequestRetry, metadata)
addRequestScopedErrorsToMetadata(compat.RequestScopedErrors, metadata)
if compat.Priority != 0 {
attrs["priority"] = strconv.Itoa(compat.Priority)
}
addWeightToAttrs(entry.Weight, attrs)
if key != "" {
attrs["api_key"] = key
}
if hash := diff.ComputeOpenAICompatModelsHash(compat.Models); hash != "" {
attrs["models_hash"] = hash
}
addConfigHeadersToAttrs(compat.Headers, attrs)
a := &coreauth.Auth{
ID: id,
Provider: internalProviderKey,
Label: compat.Name,
Prefix: prefix,
Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
if len(a.Metadata) == 0 {
a.Metadata = nil
}
out = append(out, a)
createdEntries++
}
// Fallback: create entry without API key if no APIKeyEntries
if createdEntries == 0 {
idKind := fmt.Sprintf("openai-compatibility:%s", providerName)
id, token := idGen.Next(idKind, base)
attrs := map[string]string{
"source": fmt.Sprintf("config:%s[%s]", providerName, token),
"base_url": base,
"compat_name": compat.Name,
"provider_key": internalProviderKey,
"config_index": strconv.Itoa(i),
}
metadata := map[string]any{}
if disableCooling != nil {
metadata["disable_cooling"] = *disableCooling
}
addRequestRetryToMetadata(compat.RequestRetry, metadata)
addRequestScopedErrorsToMetadata(compat.RequestScopedErrors, metadata)
if compat.Priority != 0 {
attrs["priority"] = strconv.Itoa(compat.Priority)
}
if hash := diff.ComputeOpenAICompatModelsHash(compat.Models); hash != "" {
attrs["models_hash"] = hash
}
addConfigHeadersToAttrs(compat.Headers, attrs)
a := &coreauth.Auth{
ID: id,
Provider: internalProviderKey,
Label: compat.Name,
Prefix: prefix,
Status: coreauth.StatusActive,
Attributes: attrs,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
if len(a.Metadata) == 0 {
a.Metadata = nil
}
out = append(out, a)
}
}
return out
}
// synthesizeVertexCompat creates Auth entries for Vertex-compatible providers.
func (s *ConfigSynthesizer) synthesizeVertexCompat(ctx *SynthesisContext) []*coreauth.Auth {
cfg := ctx.Config
now := ctx.Now
idGen := ctx.IDGenerator
out := make([]*coreauth.Auth, 0, len(cfg.VertexCompatAPIKey))
for i := range cfg.VertexCompatAPIKey {
compat := &cfg.VertexCompatAPIKey[i]
providerName := "vertex"
base := strings.TrimSpace(compat.BaseURL)
key := strings.TrimSpace(compat.APIKey)
prefix := strings.TrimSpace(compat.Prefix)
proxyURL := strings.TrimSpace(compat.ProxyURL)
idKind := "vertex:apikey"
id, token := idGen.Next(idKind, key, base, proxyURL)
attrs := map[string]string{
"source": fmt.Sprintf("config:vertex-apikey[%s]", token),
"base_url": base,
"provider_key": providerName,
"config_index": strconv.Itoa(i),
}
if compat.Priority != 0 {
attrs["priority"] = strconv.Itoa(compat.Priority)
}
addWeightToAttrs(compat.Weight, attrs)
if key != "" {
attrs["api_key"] = key
}
if hash := diff.ComputeVertexCompatModelsHash(compat.Models); hash != "" {
attrs["models_hash"] = hash
}
addConfigHeadersToAttrs(compat.Headers, attrs)
metadata := map[string]any{}
if compat.DisableCooling != nil {
metadata["disable_cooling"] = *compat.DisableCooling
}
addRequestRetryToMetadata(compat.RequestRetry, metadata)
a := &coreauth.Auth{
ID: id,
Provider: providerName,
Label: "vertex-apikey",
Prefix: prefix,
Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
ApplyAuthExcludedModelsMeta(a, cfg, compat.ExcludedModels, "apikey")
if len(a.Metadata) == 0 {
a.Metadata = nil
}
out = append(out, a)
}
return out
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
package synthesizer
import (
"context"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
// PluginAuthParser parses auth JSON owned by plugin providers.
type PluginAuthParser interface {
ParseAuth(context.Context, pluginapi.AuthParseRequest) (*coreauth.Auth, bool, error)
}
// PluginMultiAuthParser expands one auth JSON payload into multiple plugin auth records.
// Returning handled=true with an empty slice means the plugin intentionally suppresses built-in parsing.
type PluginMultiAuthParser interface {
ParseAuths(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error)
}
// SynthesisContext provides the context needed for auth synthesis.
type SynthesisContext struct {
// Config is the current configuration
Config *config.Config
// AuthDir is the directory containing auth files
AuthDir string
// Now is the current time for timestamps
Now time.Time
// IDGenerator generates stable IDs for auth entries
IDGenerator *StableIDGenerator
// PluginAuthParser parses plugin-owned auth files
PluginAuthParser PluginAuthParser
}

View file

@ -0,0 +1,95 @@
package synthesizer
import (
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func boolPointer(value bool) *bool {
return &value
}
func TestConfigSynthesizerPreservesExplicitFalseCoolingOverrides(t *testing.T) {
disableCooling := false
tests := []struct {
name string
cfg *config.Config
}{
{
name: "gemini",
cfg: &config.Config{GeminiKey: []config.GeminiKey{{
APIKey: "gemini-key",
DisableCooling: &disableCooling,
}}},
},
{
name: "interactions",
cfg: &config.Config{InteractionsKey: []config.GeminiKey{{
APIKey: "interactions-key",
DisableCooling: &disableCooling,
}}},
},
{
name: "claude",
cfg: &config.Config{ClaudeKey: []config.ClaudeKey{{
APIKey: "claude-key",
DisableCooling: &disableCooling,
}}},
},
{
name: "codex",
cfg: &config.Config{CodexKey: []config.CodexKey{{
APIKey: "codex-key",
BaseURL: "https://codex.example.com",
DisableCooling: &disableCooling,
}}},
},
{
name: "xai",
cfg: &config.Config{XAIKey: []config.XAIKey{{
APIKey: "xai-key",
BaseURL: "https://api.x.ai/v1",
DisableCooling: &disableCooling,
}}},
},
{
name: "openai compatibility",
cfg: &config.Config{OpenAICompatibility: []config.OpenAICompatibility{{
Name: "compat",
BaseURL: "https://compat.example.com",
DisableCooling: &disableCooling,
APIKeyEntries: []config.OpenAICompatibilityAPIKey{{APIKey: "compat-key"}},
}}},
},
{
name: "vertex",
cfg: &config.Config{VertexCompatAPIKey: []config.VertexCompatKey{{
APIKey: "vertex-key",
BaseURL: "https://vertex.example.com",
DisableCooling: &disableCooling,
}}},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
auths, errSynthesize := NewConfigSynthesizer().Synthesize(&SynthesisContext{
Config: tc.cfg,
Now: time.Unix(100, 0).UTC(),
IDGenerator: NewStableIDGenerator(),
})
if errSynthesize != nil {
t.Fatalf("Synthesize() error = %v", errSynthesize)
}
if len(auths) != 1 {
t.Fatalf("auth count = %d, want 1", len(auths))
}
disabled, present := auths[0].DisableCoolingOverride()
if !present || disabled {
t.Fatalf("DisableCoolingOverride() = %t, %t, want false, true", disabled, present)
}
})
}
}

View file

@ -0,0 +1,337 @@
package synthesizer
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
log "github.com/sirupsen/logrus"
)
// FileSynthesizer generates Auth entries from OAuth JSON files.
// It handles file-based authentication.
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
}
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
}
auths, errSynthesize := synthesizeFileAuths(ctx, full, data)
if errSynthesize != nil {
log.WithError(errSynthesize).Warnf("skipping auth file %s", name)
continue
}
if len(auths) == 0 {
continue
}
out = append(out, auths...)
}
return out, nil
}
// SynthesizeAuthFile generates Auth entries for one auth JSON file payload.
// It shares exactly the same mapping behavior as FileSynthesizer.Synthesize.
func SynthesizeAuthFile(ctx *SynthesisContext, fullPath string, data []byte) ([]*coreauth.Auth, error) {
return synthesizeFileAuths(ctx, fullPath, data)
}
func synthesizeFileAuths(ctx *SynthesisContext, fullPath string, data []byte) ([]*coreauth.Auth, error) {
if ctx == nil || len(data) == 0 {
return nil, nil
}
now := ctx.Now
cfg := ctx.Config
var metadata map[string]any
if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil {
return nil, nil
}
coreauth.NormalizeCredentialMetadata(metadata)
if errWeight := coreauth.ValidateAuthWeight(&coreauth.Auth{Metadata: metadata}); errWeight != nil {
return nil, fmt.Errorf("invalid weight in %s: %w", filepath.Base(fullPath), errWeight)
}
t, _ := metadata["type"].(string)
provider := strings.ToLower(strings.TrimSpace(t))
if provider == "gemini" {
provider = "gemini-cli"
}
if ctx.PluginAuthParser != nil {
auths, handled, errParse := parsePluginFileAuths(ctx.PluginAuthParser, pluginapi.AuthParseRequest{
Provider: provider,
Path: fullPath,
FileName: filepath.Base(fullPath),
RawJSON: data,
})
if errParse == nil && handled {
auths = compactPluginAuths(auths)
if len(auths) == 0 {
return nil, nil
}
perAccountExcluded := extractExcludedModelsFromMetadata(metadata)
perAccountModelAliases := extractOAuthModelAliasesFromMetadata(metadata)
disabled, _ := metadata["disabled"].(bool)
for index, auth := range auths {
if auth == nil {
continue
}
coreauth.NormalizeCredentialMetadata(auth.Metadata)
if len(auths) > 1 {
coreauth.MarkPluginVirtualAuth(auth, fullPath, index)
}
auth.CreatedAt = now
auth.UpdatedAt = now
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
auth.Attributes[coreauth.AttributePath] = fullPath
auth.Attributes[coreauth.AttributeSource] = fullPath
auth.Attributes[coreauth.AttributeSourceBackend] = coreauth.AuthSourceFile
if disabled {
auth.Disabled = true
auth.Status = coreauth.StatusDisabled
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
auth.Metadata["disabled"] = true
}
if errWeight := coreauth.ApplyAuthWeightMetadata(auth, metadata); errWeight != nil {
return nil, fmt.Errorf("invalid plugin auth weight in %s: %w", filepath.Base(fullPath), errWeight)
}
coreauth.SetOAuthModelAliasesAttribute(auth, perAccountModelAliases)
ApplyAuthExcludedModelsMeta(auth, cfg, perAccountExcluded, "oauth")
coreauth.ApplyCustomHeadersFromMetadata(auth)
applyFingerprintProfileAttribute(auth, metadata)
}
return auths, nil
}
}
if provider == "" || provider == "gemini-cli" {
return nil, nil
}
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 := fullPath
if strings.TrimSpace(ctx.AuthDir) != "" {
if rel, errRel := filepath.Rel(ctx.AuthDir, fullPath); errRel == nil && rel != "" {
id = rel
}
}
if runtime.GOOS == "windows" {
id = strings.ToLower(id)
}
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
}
// Read per-account excluded models from the OAuth JSON file.
perAccountExcluded := extractExcludedModelsFromMetadata(metadata)
perAccountModelAliases := extractOAuthModelAliasesFromMetadata(metadata)
a := &coreauth.Auth{
ID: id,
Provider: provider,
Label: label,
Prefix: prefix,
Status: status,
Disabled: disabled,
Attributes: map[string]string{
coreauth.AttributeSource: fullPath,
coreauth.AttributePath: fullPath,
coreauth.AttributeSourceBackend: coreauth.AuthSourceFile,
},
ProxyURL: proxyURL,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
// Read priority from auth file.
if rawPriority, ok := metadata["priority"]; ok {
switch v := rawPriority.(type) {
case float64:
a.Attributes["priority"] = strconv.Itoa(int(v))
case string:
priority := strings.TrimSpace(v)
if _, errAtoi := strconv.Atoi(priority); errAtoi == nil {
a.Attributes["priority"] = priority
}
}
}
if errWeight := coreauth.ApplyAuthWeightMetadata(a, metadata); errWeight != nil {
return nil, fmt.Errorf("invalid auth weight in %s: %w", filepath.Base(fullPath), errWeight)
}
// Read note from auth file.
if rawNote, ok := metadata["note"]; ok {
if note, isStr := rawNote.(string); isStr {
if trimmed := strings.TrimSpace(note); trimmed != "" {
a.Attributes["note"] = trimmed
}
}
}
coreauth.ApplyCustomHeadersFromMetadata(a)
coreauth.SetOAuthModelAliasesAttribute(a, perAccountModelAliases)
ApplyAuthExcludedModelsMeta(a, cfg, perAccountExcluded, "oauth")
applyFingerprintProfileAttribute(a, metadata)
// For codex auth files, extract plan_type from the JWT id_token.
if provider == "codex" {
if idTokenRaw, ok := metadata["id_token"].(string); ok && strings.TrimSpace(idTokenRaw) != "" {
if claims, errParse := codex.ParseJWTToken(idTokenRaw); errParse == nil && claims != nil {
if pt := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); pt != "" {
a.Attributes["plan_type"] = pt
}
}
}
}
return []*coreauth.Auth{a}, nil
}
func parsePluginFileAuths(parser PluginAuthParser, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
if parser == nil {
return nil, false, nil
}
if multiParser, ok := parser.(PluginMultiAuthParser); ok {
return multiParser.ParseAuths(context.Background(), req)
}
auth, handled, errParse := parser.ParseAuth(context.Background(), req)
if errParse != nil || !handled || auth == nil {
return nil, handled, errParse
}
return []*coreauth.Auth{auth}, true, nil
}
func compactPluginAuths(auths []*coreauth.Auth) []*coreauth.Auth {
if len(auths) == 0 {
return nil
}
out := auths[:0]
for _, auth := range auths {
if auth == nil {
continue
}
if errWeight := coreauth.ValidateAuthWeight(auth); errWeight != nil {
continue
}
out = append(out, auth)
}
return out
}
// extractOAuthModelAliasesFromMetadata reads per-account model aliases from OAuth JSON metadata.
// "model_aliases" is canonical; "model-aliases" remains a legacy alias.
func extractOAuthModelAliasesFromMetadata(metadata map[string]any) []config.OAuthModelAlias {
if metadata == nil {
return nil
}
raw, ok := metadata["model_aliases"]
if !ok {
raw, ok = metadata["model-aliases"]
}
if !ok || raw == nil {
return nil
}
data, errMarshal := json.Marshal(raw)
if errMarshal != nil {
return nil
}
var aliases []config.OAuthModelAlias
if errUnmarshal := json.Unmarshal(data, &aliases); errUnmarshal != nil {
return nil
}
cfg := config.Config{
OAuthModelAlias: map[string][]config.OAuthModelAlias{
"auth": aliases,
},
}
cfg.SanitizeOAuthModelAlias()
return cfg.OAuthModelAlias["auth"]
}
// extractExcludedModelsFromMetadata reads per-account excluded models from the OAuth JSON metadata.
// "excluded_models" is canonical; "excluded-models" remains a legacy alias.
func extractExcludedModelsFromMetadata(metadata map[string]any) []string {
if metadata == nil {
return nil
}
raw, ok := metadata["excluded_models"]
if !ok {
raw, ok = metadata["excluded-models"]
}
if !ok || raw == nil {
return nil
}
var stringSlice []string
switch v := raw.(type) {
case []string:
stringSlice = v
case []interface{}:
stringSlice = make([]string, 0, len(v))
for _, item := range v {
if s, ok := item.(string); ok {
stringSlice = append(stringSlice, s)
}
}
default:
return nil
}
result := make([]string, 0, len(stringSlice))
for _, s := range stringSlice {
if trimmed := strings.TrimSpace(s); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}

View file

@ -0,0 +1,832 @@
package synthesizer
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
func TestNewFileSynthesizer(t *testing.T) {
synth := NewFileSynthesizer()
if synth == nil {
t.Fatal("expected non-nil synthesizer")
}
}
func TestFileSynthesizer_Synthesize_NilContext(t *testing.T) {
synth := NewFileSynthesizer()
auths, err := synth.Synthesize(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 0 {
t.Fatalf("expected empty auths, got %d", len(auths))
}
}
func TestFileSynthesizer_Synthesize_EmptyAuthDir(t *testing.T) {
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: "",
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 0 {
t.Fatalf("expected empty auths, got %d", len(auths))
}
}
func TestFileSynthesizer_Synthesize_NonExistentDir(t *testing.T) {
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: "/non/existent/path",
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 0 {
t.Fatalf("expected empty auths, got %d", len(auths))
}
}
func TestFileSynthesizer_Synthesize_ValidAuthFile(t *testing.T) {
tempDir := t.TempDir()
// Create a valid auth file
authData := map[string]any{
"type": "claude",
"email": "test@example.com",
"proxy_url": "http://proxy.local",
"prefix": "test-prefix",
"headers": map[string]string{
" X-Test ": " value ",
"X-Empty": " ",
},
"disable_cooling": true,
"request_retry": 2,
}
data, _ := json.Marshal(authData)
err := os.WriteFile(filepath.Join(tempDir, "claude-auth.json"), data, 0644)
if err != nil {
t.Fatalf("failed to write auth file: %v", err)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
if auths[0].Provider != "claude" {
t.Errorf("expected provider claude, got %s", auths[0].Provider)
}
if auths[0].Label != "test@example.com" {
t.Errorf("expected label test@example.com, got %s", auths[0].Label)
}
if auths[0].Prefix != "test-prefix" {
t.Errorf("expected prefix test-prefix, got %s", auths[0].Prefix)
}
if auths[0].ProxyURL != "http://proxy.local" {
t.Errorf("expected proxy_url http://proxy.local, got %s", auths[0].ProxyURL)
}
if got := auths[0].Attributes["header:X-Test"]; got != "value" {
t.Errorf("expected header:X-Test value, got %q", got)
}
if _, ok := auths[0].Attributes["header:X-Empty"]; ok {
t.Errorf("expected header:X-Empty to be absent, got %q", auths[0].Attributes["header:X-Empty"])
}
if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v {
t.Errorf("expected disable_cooling true, got %v", auths[0].Metadata["disable_cooling"])
}
if v, ok := auths[0].Metadata["request_retry"].(float64); !ok || int(v) != 2 {
t.Errorf("expected request_retry 2, got %v", auths[0].Metadata["request_retry"])
}
if auths[0].Status != coreauth.StatusActive {
t.Errorf("expected status active, got %s", auths[0].Status)
}
}
func TestFileSynthesizer_Synthesize_LegacyKimiFingerprintProfile(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "kimi",
"access_token": "kimi-access-token",
"refresh_token": "kimi-refresh-token",
"fingerprint-profile": "claude-code-cli",
}
data, errMarshal := json.Marshal(authData)
if errMarshal != nil {
t.Fatalf("marshal kimi auth: %v", errMarshal)
}
if err := os.WriteFile(filepath.Join(tempDir, "kimi-auth.json"), data, 0644); err != nil {
t.Fatalf("failed to write kimi auth file: %v", err)
}
auths, err := NewFileSynthesizer().Synthesize(&SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
IDGenerator: NewStableIDGenerator(),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
if auths[0].Provider != "kimi" {
t.Fatalf("provider = %q, want kimi", auths[0].Provider)
}
if got := auths[0].Attributes["fingerprint_profile"]; got != "claude-code-cli" {
t.Fatalf("attributes fingerprint_profile = %q, want claude-code-cli", got)
}
if got, _ := auths[0].Metadata["fingerprint_profile"].(string); got != "claude-code-cli" {
t.Fatalf("metadata fingerprint_profile = %q, want claude-code-cli", got)
}
if _, exists := auths[0].Metadata["fingerprint-profile"]; exists {
t.Fatalf("legacy fingerprint-profile was not normalized: %#v", auths[0].Metadata)
}
}
func TestFileSynthesizer_Synthesize_IgnoresGeminiProviderFile(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "gemini",
"email": "gemini@example.com",
}
data, _ := json.Marshal(authData)
err := os.WriteFile(filepath.Join(tempDir, "gemini-auth.json"), data, 0644)
if err != nil {
t.Fatalf("failed to write auth file: %v", err)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 0 {
t.Fatalf("expected Gemini auth file to be ignored, got %d auths", len(auths))
}
}
func TestSynthesizeAuthFileExpandsPluginMultiAuths(t *testing.T) {
tempDir := t.TempDir()
fullPath := filepath.Join(tempDir, "geminicli.json")
raw := []byte(`{"type":"gemini-cli","excluded_models":["model-a"],"headers":{"X-Test":"value"}}`)
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
PluginAuthParser: multiAuthParserFunc(func(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
if req.Provider != "gemini-cli" || req.Path != fullPath || req.FileName != "geminicli.json" {
t.Fatalf("ParseAuths request = %#v, want file context", req)
}
return []*coreauth.Auth{
{
ID: "geminicli.json",
Provider: "gemini-cli",
Metadata: map[string]any{
"type": "gemini-cli",
"headers": map[string]any{
"X-Test": "value",
},
},
},
nil,
{
ID: "geminicli-project-a.json",
Provider: "gemini-cli",
Metadata: map[string]any{
"type": "gemini-cli",
"project_id": "project-a",
"headers": map[string]any{
"X-Test": "value",
},
},
},
}, true, nil
}),
}
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw)
if errSynthesize != nil {
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
}
if len(auths) != 2 {
t.Fatalf("SynthesizeAuthFile() len = %d, want two plugin auths", len(auths))
}
if firstIndex, secondIndex := auths[0].EnsureIndex(), auths[1].EnsureIndex(); firstIndex == "" || firstIndex == secondIndex {
t.Fatalf("auth indexes = %q/%q, want distinct non-empty indexes", firstIndex, secondIndex)
}
for _, auth := range auths {
if !coreauth.IsPluginVirtualAuth(auth) {
t.Fatalf("auth attributes = %#v, want plugin virtual marker", auth.Attributes)
}
if auth.Attributes[coreauth.AttributeVirtualSource] != fullPath {
t.Fatalf("virtual_source = %q, want %q", auth.Attributes[coreauth.AttributeVirtualSource], fullPath)
}
if auth.Attributes["path"] != fullPath || auth.Attributes["source"] != fullPath {
t.Fatalf("auth attributes = %#v, want source path", auth.Attributes)
}
if gotHeader := auth.Attributes["header:X-Test"]; gotHeader != "value" {
t.Fatalf("header:X-Test = %q, want value", gotHeader)
}
if gotKind := auth.Attributes["auth_kind"]; gotKind != "oauth" {
t.Fatalf("auth_kind = %q, want oauth", gotKind)
}
}
if gotProject := auths[1].Metadata["project_id"]; gotProject != "project-a" {
t.Fatalf("project_id = %#v, want project-a", gotProject)
}
}
func TestSynthesizeAuthFileSkipsInvalidPluginAuthWeight(t *testing.T) {
tempDir := t.TempDir()
fullPath := filepath.Join(tempDir, "plugin.json")
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
return []*coreauth.Auth{
{ID: "invalid", Provider: "plugin", Attributes: map[string]string{coreauth.AttributeWeight: "1.5"}},
{ID: "valid", Provider: "plugin", Attributes: map[string]string{coreauth.AttributeWeight: "0"}},
}, true, nil
}),
}
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, []byte(`{"type":"plugin"}`))
if errSynthesize != nil {
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
}
if len(auths) != 1 || auths[0].ID != "valid" {
t.Fatalf("SynthesizeAuthFile() auths = %#v, want only valid zero-weight auth", auths)
}
}
func TestSynthesizeAuthFileAppliesSourceDisabledToPluginMultiAuths(t *testing.T) {
tempDir := t.TempDir()
fullPath := filepath.Join(tempDir, "geminicli.json")
raw := []byte(`{"type":"gemini-cli","disabled":true}`)
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
return []*coreauth.Auth{
{ID: "geminicli.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli"}},
{ID: "geminicli-project-a.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli", "project_id": "project-a"}},
}, true, nil
}),
}
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw)
if errSynthesize != nil {
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
}
if len(auths) != 2 {
t.Fatalf("SynthesizeAuthFile() len = %d, want two plugin auths", len(auths))
}
for _, auth := range auths {
if !auth.Disabled || auth.Status != coreauth.StatusDisabled {
t.Fatalf("auth %s disabled/status = %v/%s, want disabled", auth.ID, auth.Disabled, auth.Status)
}
if got, _ := auth.Metadata["disabled"].(bool); !got {
t.Fatalf("auth %s metadata disabled = %#v, want true", auth.ID, auth.Metadata["disabled"])
}
}
}
func TestSynthesizeAuthFilePluginHandledEmptySuppressesBuiltin(t *testing.T) {
tempDir := t.TempDir()
fullPath := filepath.Join(tempDir, "codex.json")
raw := []byte(`{"type":"codex","access_token":"token"}`)
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC),
PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
return nil, true, nil
}),
}
auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw)
if errSynthesize != nil {
t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize)
}
if len(auths) != 0 {
t.Fatalf("SynthesizeAuthFile() len = %d, want plugin-handled empty result", len(auths))
}
}
type multiAuthParserFunc func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error)
func (f multiAuthParserFunc) ParseAuth(context.Context, pluginapi.AuthParseRequest) (*coreauth.Auth, bool, error) {
return nil, false, nil
}
func (f multiAuthParserFunc) ParseAuths(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
return f(ctx, req)
}
func TestFileSynthesizer_Synthesize_SkipsInvalidFiles(t *testing.T) {
tempDir := t.TempDir()
// Create various invalid files
_ = os.WriteFile(filepath.Join(tempDir, "not-json.txt"), []byte("text content"), 0644)
_ = os.WriteFile(filepath.Join(tempDir, "invalid.json"), []byte("not valid json"), 0644)
_ = os.WriteFile(filepath.Join(tempDir, "empty.json"), []byte(""), 0644)
_ = os.WriteFile(filepath.Join(tempDir, "no-type.json"), []byte(`{"email": "test@example.com"}`), 0644)
// Create one valid file
validData, _ := json.Marshal(map[string]any{"type": "claude", "email": "valid@example.com"})
_ = os.WriteFile(filepath.Join(tempDir, "valid.json"), validData, 0644)
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 1 {
t.Fatalf("only valid auth file should be processed, got %d", len(auths))
}
if auths[0].Label != "valid@example.com" {
t.Errorf("expected label valid@example.com, got %s", auths[0].Label)
}
}
func TestFileSynthesizer_Synthesize_SkipsDirectories(t *testing.T) {
tempDir := t.TempDir()
// Create a subdirectory with a json file inside
subDir := filepath.Join(tempDir, "subdir.json")
err := os.Mkdir(subDir, 0755)
if err != nil {
t.Fatalf("failed to create subdir: %v", err)
}
// Create a valid file in root
validData, _ := json.Marshal(map[string]any{"type": "claude"})
_ = os.WriteFile(filepath.Join(tempDir, "valid.json"), validData, 0644)
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
}
func TestFileSynthesizer_Synthesize_RelativeID(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{"type": "claude"}
data, _ := json.Marshal(authData)
err := os.WriteFile(filepath.Join(tempDir, "my-auth.json"), data, 0644)
if err != nil {
t.Fatalf("failed to write auth file: %v", err)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
// ID should be relative path
if auths[0].ID != "my-auth.json" {
t.Errorf("expected ID my-auth.json, got %s", auths[0].ID)
}
}
func TestFileSynthesizer_Synthesize_PrefixValidation(t *testing.T) {
tests := []struct {
name string
prefix string
wantPrefix string
}{
{"valid prefix", "myprefix", "myprefix"},
{"prefix with slashes trimmed", "/myprefix/", "myprefix"},
{"prefix with spaces trimmed", " myprefix ", "myprefix"},
{"prefix with internal slash rejected", "my/prefix", ""},
{"empty prefix", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "claude",
"prefix": tt.prefix,
}
data, _ := json.Marshal(authData)
_ = os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
if auths[0].Prefix != tt.wantPrefix {
t.Errorf("expected prefix %q, got %q", tt.wantPrefix, auths[0].Prefix)
}
})
}
}
func TestFileSynthesizer_Synthesize_PriorityParsing(t *testing.T) {
tests := []struct {
name string
priority any
want string
hasValue bool
}{
{
name: "string with spaces",
priority: " 10 ",
want: "10",
hasValue: true,
},
{
name: "number",
priority: 8,
want: "8",
hasValue: true,
},
{
name: "invalid string",
priority: "1x",
hasValue: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "claude",
"priority": tt.priority,
}
data, _ := json.Marshal(authData)
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
if errWriteFile != nil {
t.Fatalf("failed to write auth file: %v", errWriteFile)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, errSynthesize := synth.Synthesize(ctx)
if errSynthesize != nil {
t.Fatalf("unexpected error: %v", errSynthesize)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
value, ok := auths[0].Attributes["priority"]
if tt.hasValue {
if !ok {
t.Fatal("expected priority attribute to be set")
}
if value != tt.want {
t.Fatalf("expected priority %q, got %q", tt.want, value)
}
return
}
if ok {
t.Fatalf("expected priority attribute to be absent, got %q", value)
}
})
}
}
func TestFileSynthesizer_Synthesize_WeightParsing(t *testing.T) {
tests := []struct {
name string
weight any
want string
valid bool
}{
{name: "number", weight: 5, want: "5", valid: true},
{name: "numeric string", weight: " 3 ", want: "3", valid: true},
{name: "zero excludes", weight: 0, want: "0", valid: true},
{name: "negative excludes", weight: -5, want: "0", valid: true},
{name: "maximum", weight: 1000000, want: "1000000", valid: true},
{name: "fraction rejected", weight: 1.5},
{name: "above maximum rejected", weight: 1000001},
{name: "overflow rejected", weight: "9223372036854775808"},
{name: "invalid string", weight: "heavy"},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
tempDir := t.TempDir()
data, errMarshal := json.Marshal(map[string]any{"type": "claude", "weight": testCase.weight})
if errMarshal != nil {
t.Fatalf("json.Marshal() error = %v", errMarshal)
}
if errWrite := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644); errWrite != nil {
t.Fatalf("WriteFile() error = %v", errWrite)
}
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, errSynthesize := NewFileSynthesizer().Synthesize(ctx)
if errSynthesize != nil {
t.Fatalf("Synthesize() error = %v", errSynthesize)
}
if !testCase.valid {
if len(auths) != 0 {
t.Fatalf("auth count = %d, want invalid credential skipped", len(auths))
}
if _, errDirect := SynthesizeAuthFile(ctx, filepath.Join(tempDir, "auth.json"), data); errDirect == nil {
t.Fatal("SynthesizeAuthFile() error = nil, want weight validation error")
}
return
}
if len(auths) != 1 {
t.Fatalf("auth count = %d, want 1", len(auths))
}
if gotWeight := auths[0].Attributes[coreauth.AttributeWeight]; gotWeight != testCase.want {
t.Fatalf("weight = %q, want %q", gotWeight, testCase.want)
}
})
}
}
func TestFileSynthesizer_Synthesize_OAuthExcludedModelsMerged(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "claude",
"excluded_models": []string{"custom-model", "MODEL-B"},
}
data, _ := json.Marshal(authData)
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
if errWriteFile != nil {
t.Fatalf("failed to write auth file: %v", errWriteFile)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{
OAuthExcludedModels: map[string][]string{
"claude": {"shared", "model-b"},
},
},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, errSynthesize := synth.Synthesize(ctx)
if errSynthesize != nil {
t.Fatalf("unexpected error: %v", errSynthesize)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
got := auths[0].Attributes["excluded_models"]
want := "custom-model,model-b,shared"
if got != want {
t.Fatalf("expected excluded_models %q, got %q", want, got)
}
}
func TestFileSynthesizer_Synthesize_OAuthModelAliases(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "codex",
"email": "codex@example.com",
"model_aliases": []map[string]any{
{"name": " gpt-5.3-codex-spark ", "alias": " gpt-5.5 "},
{"name": "gpt-5.3-codex-spark", "alias": "gpt-5.4", "fork": true},
{"name": "gpt-5.3-codex-spark", "alias": "gpt-5.5"},
{"name": "", "alias": "ignored"},
},
}
data, _ := json.Marshal(authData)
errWriteFile := os.WriteFile(filepath.Join(tempDir, "codex-auth.json"), data, 0644)
if errWriteFile != nil {
t.Fatalf("failed to write auth file: %v", errWriteFile)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, errSynthesize := synth.Synthesize(ctx)
if errSynthesize != nil {
t.Fatalf("unexpected error: %v", errSynthesize)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
got := auths[0].Attributes["model_aliases"]
want := `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5"},{"name":"gpt-5.3-codex-spark","alias":"gpt-5.4","fork":true}]`
if got != want {
t.Fatalf("expected model_aliases %q, got %q", want, got)
}
}
func TestFileSynthesizer_Synthesize_IgnoresGeminiOAuthFile(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "gemini",
"email": "multi@example.com",
"project_id": "project-a, project-b, project-c",
"priority": " 10 ",
}
data, _ := json.Marshal(authData)
err := os.WriteFile(filepath.Join(tempDir, "gemini-multi.json"), data, 0644)
if err != nil {
t.Fatalf("failed to write auth file: %v", err)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, err := synth.Synthesize(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(auths) != 0 {
t.Fatalf("expected Gemini auth file to be ignored, got %d auths", len(auths))
}
}
func TestFileSynthesizer_Synthesize_NoteParsing(t *testing.T) {
tests := []struct {
name string
note any
want string
hasValue bool
}{
{
name: "valid string note",
note: "hello world",
want: "hello world",
hasValue: true,
},
{
name: "string note with whitespace",
note: " trimmed note ",
want: "trimmed note",
hasValue: true,
},
{
name: "empty string note",
note: "",
hasValue: false,
},
{
name: "whitespace only note",
note: " ",
hasValue: false,
},
{
name: "non-string note ignored",
note: 12345,
hasValue: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
authData := map[string]any{
"type": "claude",
"note": tt.note,
}
data, _ := json.Marshal(authData)
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
if errWriteFile != nil {
t.Fatalf("failed to write auth file: %v", errWriteFile)
}
synth := NewFileSynthesizer()
ctx := &SynthesisContext{
Config: &config.Config{},
AuthDir: tempDir,
Now: time.Now(),
IDGenerator: NewStableIDGenerator(),
}
auths, errSynthesize := synth.Synthesize(ctx)
if errSynthesize != nil {
t.Fatalf("unexpected error: %v", errSynthesize)
}
if len(auths) != 1 {
t.Fatalf("expected 1 auth, got %d", len(auths))
}
value, ok := auths[0].Attributes["note"]
if tt.hasValue {
if !ok {
t.Fatal("expected note attribute to be set")
}
if value != tt.want {
t.Fatalf("expected note %q, got %q", tt.want, value)
}
return
}
if ok {
t.Fatalf("expected note attribute to be absent, got %q", value)
}
})
}
}

View file

@ -0,0 +1,167 @@
package synthesizer
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// StableIDGenerator generates stable, deterministic IDs for auth entries.
// It uses SHA256 hashing with collision handling via counters.
// It is not safe for concurrent use.
type StableIDGenerator struct {
counters map[string]int
}
// NewStableIDGenerator creates a new StableIDGenerator instance.
func NewStableIDGenerator() *StableIDGenerator {
return &StableIDGenerator{counters: make(map[string]int)}
}
// Next generates a stable ID based on the kind and parts.
// Returns the full ID (kind:hash) and the short hash portion.
func (g *StableIDGenerator) Next(kind string, parts ...string) (string, string) {
if g == nil {
return kind + ":000000000000", "000000000000"
}
hasher := sha256.New()
hasher.Write([]byte(kind))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
hasher.Write([]byte{0})
hasher.Write([]byte(trimmed))
}
digest := hex.EncodeToString(hasher.Sum(nil))
if len(digest) < 12 {
digest = fmt.Sprintf("%012s", digest)
}
short := digest[:12]
key := kind + ":" + short
index := g.counters[key]
g.counters[key] = index + 1
if index > 0 {
short = fmt.Sprintf("%s-%d", short, index)
}
return fmt.Sprintf("%s:%s", kind, short), short
}
// ApplyAuthExcludedModelsMeta applies excluded models metadata to an auth entry.
// It computes a hash of excluded models and sets the auth_kind attribute.
// For OAuth entries, perKey (from the JSON file's excluded-models field) is merged
// with the global oauth-excluded-models config for the provider.
func ApplyAuthExcludedModelsMeta(auth *coreauth.Auth, cfg *config.Config, perKey []string, authKind string) {
if auth == nil || cfg == nil {
return
}
authKindKey := strings.ToLower(strings.TrimSpace(authKind))
seen := make(map[string]struct{})
add := func(list []string) {
for _, entry := range list {
if trimmed := strings.TrimSpace(entry); trimmed != "" {
key := strings.ToLower(trimmed)
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
}
}
}
if authKindKey == "apikey" {
add(perKey)
} else {
// For OAuth: merge per-account excluded models with global provider-level exclusions
add(perKey)
if cfg.OAuthExcludedModels != nil {
providerKey := strings.ToLower(strings.TrimSpace(auth.Provider))
add(cfg.OAuthExcludedModels[providerKey])
}
}
combined := make([]string, 0, len(seen))
for k := range seen {
combined = append(combined, k)
}
sort.Strings(combined)
hash := diff.ComputeExcludedModelsHash(combined)
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
if hash != "" {
auth.Attributes["excluded_models_hash"] = hash
}
// Store the combined excluded models list so that routing can read it at runtime
if len(combined) > 0 {
auth.Attributes["excluded_models"] = strings.Join(combined, ",")
}
if authKind != "" {
auth.Attributes["auth_kind"] = authKind
}
}
// addRequestRetryToMetadata copies a per-credential request-retry override into metadata.
// Nil or negative values are treated as unset and are not written.
func addRequestRetryToMetadata(requestRetry *int, metadata map[string]any) {
if requestRetry == nil || *requestRetry < 0 || metadata == nil {
return
}
metadata["request_retry"] = *requestRetry
}
// addRequestScopedErrorsToMetadata copies per-credential request-scoped error rules into metadata.
func addRequestScopedErrorsToMetadata(rules []config.RequestScopedErrorRule, metadata map[string]any) {
if len(rules) == 0 || metadata == nil {
return
}
metadata["request_scoped_errors"] = rules
}
func fingerprintProfileFromMetadata(metadata map[string]any) string {
if metadata == nil {
return ""
}
for _, key := range []string{"fingerprint_profile", "fingerprint-profile"} {
raw, _ := metadata[key].(string)
if profile := strings.ToLower(strings.TrimSpace(raw)); profile != "" {
return profile
}
}
return ""
}
// applyFingerprintProfileAttribute copies fingerprint-profile from an OAuth JSON
// file (Kimi, Claude, etc.) onto auth attributes so Claude Messages opt-in works
// the same way as claude-api-key config.
func applyFingerprintProfileAttribute(auth *coreauth.Auth, metadata map[string]any) {
if auth == nil {
return
}
profile := fingerprintProfileFromMetadata(metadata)
if profile == "" {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
auth.Attributes["fingerprint_profile"] = profile
}
// addConfigHeadersToAttrs adds header configuration to auth attributes.
// Headers are prefixed with "header:" in the attributes map.
func addConfigHeadersToAttrs(headers map[string]string, attrs map[string]string) {
if len(headers) == 0 || attrs == nil {
return
}
for hk, hv := range headers {
key := strings.TrimSpace(hk)
val := strings.TrimSpace(hv)
if key == "" || val == "" {
continue
}
attrs["header:"+key] = val
}
}

View file

@ -0,0 +1,321 @@
package synthesizer
import (
"reflect"
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestNewStableIDGenerator(t *testing.T) {
gen := NewStableIDGenerator()
if gen == nil {
t.Fatal("expected non-nil generator")
}
if gen.counters == nil {
t.Fatal("expected non-nil counters map")
}
}
func TestStableIDGenerator_Next(t *testing.T) {
tests := []struct {
name string
kind string
parts []string
wantPrefix string
}{
{
name: "basic gemini apikey",
kind: "gemini:apikey",
parts: []string{"test-key", ""},
wantPrefix: "gemini:apikey:",
},
{
name: "claude with base url",
kind: "claude:apikey",
parts: []string{"sk-ant-xxx", "https://api.anthropic.com"},
wantPrefix: "claude:apikey:",
},
{
name: "empty parts",
kind: "codex:apikey",
parts: []string{},
wantPrefix: "codex:apikey:",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := NewStableIDGenerator()
id, short := gen.Next(tt.kind, tt.parts...)
if !strings.Contains(id, tt.wantPrefix) {
t.Errorf("expected id to contain %q, got %q", tt.wantPrefix, id)
}
if short == "" {
t.Error("expected non-empty short id")
}
if len(short) != 12 {
t.Errorf("expected short id length 12, got %d", len(short))
}
})
}
}
func TestStableIDGenerator_Stability(t *testing.T) {
gen1 := NewStableIDGenerator()
gen2 := NewStableIDGenerator()
id1, _ := gen1.Next("gemini:apikey", "test-key", "https://api.example.com")
id2, _ := gen2.Next("gemini:apikey", "test-key", "https://api.example.com")
if id1 != id2 {
t.Errorf("same inputs should produce same ID: got %q and %q", id1, id2)
}
}
func TestStableIDGenerator_CollisionHandling(t *testing.T) {
gen := NewStableIDGenerator()
id1, short1 := gen.Next("gemini:apikey", "same-key")
id2, short2 := gen.Next("gemini:apikey", "same-key")
if id1 == id2 {
t.Error("collision should be handled with suffix")
}
if short1 == short2 {
t.Error("short ids should differ")
}
if !strings.Contains(short2, "-1") {
t.Errorf("second short id should contain -1 suffix, got %q", short2)
}
}
func TestStableIDGenerator_NilReceiver(t *testing.T) {
var gen *StableIDGenerator = nil
id, short := gen.Next("test:kind", "part")
if id != "test:kind:000000000000" {
t.Errorf("expected test:kind:000000000000, got %q", id)
}
if short != "000000000000" {
t.Errorf("expected 000000000000, got %q", short)
}
}
func TestApplyAuthExcludedModelsMeta(t *testing.T) {
tests := []struct {
name string
auth *coreauth.Auth
cfg *config.Config
perKey []string
authKind string
wantHash bool
wantKind string
}{
{
name: "apikey with excluded models",
auth: &coreauth.Auth{
Provider: "gemini",
Attributes: make(map[string]string),
},
cfg: &config.Config{},
perKey: []string{"model-a", "model-b"},
authKind: "apikey",
wantHash: true,
wantKind: "apikey",
},
{
name: "oauth with provider excluded models",
auth: &coreauth.Auth{
Provider: "claude",
Attributes: make(map[string]string),
},
cfg: &config.Config{
OAuthExcludedModels: map[string][]string{
"claude": {"claude-2.0"},
},
},
perKey: nil,
authKind: "oauth",
wantHash: true,
wantKind: "oauth",
},
{
name: "nil auth",
auth: nil,
cfg: &config.Config{},
},
{
name: "nil config",
auth: &coreauth.Auth{Provider: "test"},
cfg: nil,
authKind: "apikey",
},
{
name: "nil attributes initialized",
auth: &coreauth.Auth{
Provider: "gemini",
Attributes: nil,
},
cfg: &config.Config{},
perKey: []string{"model-x"},
authKind: "apikey",
wantHash: true,
wantKind: "apikey",
},
{
name: "apikey with duplicate excluded models",
auth: &coreauth.Auth{
Provider: "gemini",
Attributes: make(map[string]string),
},
cfg: &config.Config{},
perKey: []string{"model-a", "MODEL-A", "model-b", "model-a"},
authKind: "apikey",
wantHash: true,
wantKind: "apikey",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ApplyAuthExcludedModelsMeta(tt.auth, tt.cfg, tt.perKey, tt.authKind)
if tt.auth != nil && tt.cfg != nil {
if tt.wantHash {
if _, ok := tt.auth.Attributes["excluded_models_hash"]; !ok {
t.Error("expected excluded_models_hash in attributes")
}
}
if tt.wantKind != "" {
if got := tt.auth.Attributes["auth_kind"]; got != tt.wantKind {
t.Errorf("expected auth_kind=%s, got %s", tt.wantKind, got)
}
}
}
})
}
}
func TestApplyAuthExcludedModelsMeta_OAuthMergeWritesCombinedModels(t *testing.T) {
auth := &coreauth.Auth{
Provider: "claude",
Attributes: make(map[string]string),
}
cfg := &config.Config{
OAuthExcludedModels: map[string][]string{
"claude": {"global-a", "shared"},
},
}
ApplyAuthExcludedModelsMeta(auth, cfg, []string{"per", "SHARED"}, "oauth")
const wantCombined = "global-a,per,shared"
if gotCombined := auth.Attributes["excluded_models"]; gotCombined != wantCombined {
t.Fatalf("expected excluded_models=%q, got %q", wantCombined, gotCombined)
}
expectedHash := diff.ComputeExcludedModelsHash([]string{"global-a", "per", "shared"})
if gotHash := auth.Attributes["excluded_models_hash"]; gotHash != expectedHash {
t.Fatalf("expected excluded_models_hash=%q, got %q", expectedHash, gotHash)
}
}
func TestAddConfigHeadersToAttrs(t *testing.T) {
tests := []struct {
name string
headers map[string]string
attrs map[string]string
want map[string]string
}{
{
name: "basic headers",
headers: map[string]string{
"Authorization": "Bearer token",
"X-Custom": "value",
},
attrs: map[string]string{"existing": "key"},
want: map[string]string{
"existing": "key",
"header:Authorization": "Bearer token",
"header:X-Custom": "value",
},
},
{
name: "empty headers",
headers: map[string]string{},
attrs: map[string]string{"existing": "key"},
want: map[string]string{"existing": "key"},
},
{
name: "nil headers",
headers: nil,
attrs: map[string]string{"existing": "key"},
want: map[string]string{"existing": "key"},
},
{
name: "nil attrs",
headers: map[string]string{"key": "value"},
attrs: nil,
want: nil,
},
{
name: "skip empty keys and values",
headers: map[string]string{
"": "value",
"key": "",
" ": "value",
"valid": "valid-value",
},
attrs: make(map[string]string),
want: map[string]string{
"header:valid": "valid-value",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addConfigHeadersToAttrs(tt.headers, tt.attrs)
if !reflect.DeepEqual(tt.attrs, tt.want) {
t.Errorf("expected %v, got %v", tt.want, tt.attrs)
}
})
}
}
func TestAddRequestRetryToMetadata(t *testing.T) {
zero := 0
positive := 2
negative := -1
metadata := map[string]any{}
addRequestRetryToMetadata(&zero, metadata)
if got, ok := metadata["request_retry"].(int); !ok || got != 0 {
t.Fatalf("zero request-retry = %v, want 0", metadata["request_retry"])
}
metadata = map[string]any{}
addRequestRetryToMetadata(&positive, metadata)
if got, ok := metadata["request_retry"].(int); !ok || got != 2 {
t.Fatalf("positive request-retry = %v, want 2", metadata["request_retry"])
}
metadata = map[string]any{}
addRequestRetryToMetadata(&negative, metadata)
if _, exists := metadata["request_retry"]; exists {
t.Fatalf("negative request-retry should be omitted, got %v", metadata["request_retry"])
}
metadata = map[string]any{}
addRequestRetryToMetadata(nil, metadata)
if _, exists := metadata["request_retry"]; exists {
t.Fatalf("nil request-retry should be omitted, got %v", metadata["request_retry"])
}
addRequestRetryToMetadata(&positive, nil)
}

View file

@ -0,0 +1,16 @@
// Package synthesizer provides auth synthesis strategies for the watcher package.
// It implements the Strategy pattern to support multiple auth sources:
// - ConfigSynthesizer: generates Auth entries from config API keys
// - FileSynthesizer: generates Auth entries from OAuth JSON files
package synthesizer
import (
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// AuthSynthesizer defines the interface for generating Auth entries from various sources.
type AuthSynthesizer interface {
// Synthesize generates Auth entries from the given context.
// Returns a slice of Auth pointers and any error encountered.
Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, error)
}