tiphys.auth.profiles

Auth profile management for LLM providers.

Supports multiple API keys with rotation and cooldown tracking.

 1"""
 2Auth profile management for LLM providers.
 3
 4Supports multiple API keys with rotation and cooldown tracking.
 5"""
 6
 7from dataclasses import dataclass, field
 8from datetime import datetime, timedelta
 9
10
11@dataclass
12class AuthProfile:
13    """A single authentication profile (API key)."""
14
15    id: str
16    provider: str
17    api_key: str
18    models: list[str] = field(default_factory=list)  # Models this key can access
19    priority: int = 0  # Lower = preferred
20
21    # State
22    cooldown_until: datetime | None = None
23    failure_count: int = 0
24    last_used: datetime | None = None
25
26    def is_available(self, now: datetime | None = None) -> bool:
27        """Check if the profile is currently available."""
28        if now is None:
29            now = datetime.utcnow()
30
31        return not (self.cooldown_until and self.cooldown_until > now)
32
33
34class AuthProfileManager:
35    """
36    Manages multiple auth profiles for rotation and failover.
37    """
38
39    def __init__(self) -> None:
40        """Initialize the auth profile manager."""
41        self._profiles: list[AuthProfile] = []
42
43    def add_profile(self, profile: AuthProfile) -> None:
44        """Add a new auth profile."""
45        self._profiles.append(profile)
46        # Sort by priority
47        self._profiles.sort(key=lambda p: p.priority)
48
49    def get_active(self, provider: str, model: str | None = None) -> AuthProfile | None:
50        """
51        Get an active auth profile for the provider and model.
52
53        Args:
54            provider: LLM provider (e.g., 'openai', 'anthropic').
55            model: Optional model identifier.
56
57        Returns:
58            The best available profile or None.
59        """
60        now = datetime.utcnow()
61
62        # Filter by provider and availability
63        candidates = [p for p in self._profiles if p.provider == provider and p.is_available(now)]
64
65        # Filter by model if provided
66        if model:
67            model_candidates = [p for p in candidates if not p.models or model in p.models]
68            if model_candidates:
69                candidates = model_candidates
70
71        if not candidates:
72            return None
73
74        # If multiple candidates with same priority, pick one randomly or use last_used
75        # For now, just pick the one that was used longest ago
76        candidates.sort(key=lambda p: (p.priority, p.last_used or datetime.min))
77
78        profile = candidates[0]
79        profile.last_used = now
80        return profile
81
82    def mark_rate_limited(self, profile_id: str, cooldown_seconds: int = 60) -> None:
83        """Mark a profile as rate limited."""
84        for p in self._profiles:
85            if p.id == profile_id:
86                p.cooldown_until = datetime.utcnow() + timedelta(seconds=cooldown_seconds)
87                p.failure_count += 1
88                break
89
90    def mark_failure(self, profile_id: str) -> None:
91        """Mark a profile as failed."""
92        for p in self._profiles:
93            if p.id == profile_id:
94                p.failure_count += 1
95                # Small cooldown on failure
96                p.cooldown_until = datetime.utcnow() + timedelta(seconds=10)
97                break
@dataclass
class AuthProfile:
12@dataclass
13class AuthProfile:
14    """A single authentication profile (API key)."""
15
16    id: str
17    provider: str
18    api_key: str
19    models: list[str] = field(default_factory=list)  # Models this key can access
20    priority: int = 0  # Lower = preferred
21
22    # State
23    cooldown_until: datetime | None = None
24    failure_count: int = 0
25    last_used: datetime | None = None
26
27    def is_available(self, now: datetime | None = None) -> bool:
28        """Check if the profile is currently available."""
29        if now is None:
30            now = datetime.utcnow()
31
32        return not (self.cooldown_until and self.cooldown_until > now)

A single authentication profile (API key).

AuthProfile( id: str, provider: str, api_key: str, models: list[str] = <factory>, priority: int = 0, cooldown_until: datetime.datetime | None = None, failure_count: int = 0, last_used: datetime.datetime | None = None)
id: str
provider: str
api_key: str
models: list[str]
priority: int = 0
cooldown_until: datetime.datetime | None = None
failure_count: int = 0
last_used: datetime.datetime | None = None
def is_available(self, now: datetime.datetime | None = None) -> bool:
27    def is_available(self, now: datetime | None = None) -> bool:
28        """Check if the profile is currently available."""
29        if now is None:
30            now = datetime.utcnow()
31
32        return not (self.cooldown_until and self.cooldown_until > now)

Check if the profile is currently available.

class AuthProfileManager:
35class AuthProfileManager:
36    """
37    Manages multiple auth profiles for rotation and failover.
38    """
39
40    def __init__(self) -> None:
41        """Initialize the auth profile manager."""
42        self._profiles: list[AuthProfile] = []
43
44    def add_profile(self, profile: AuthProfile) -> None:
45        """Add a new auth profile."""
46        self._profiles.append(profile)
47        # Sort by priority
48        self._profiles.sort(key=lambda p: p.priority)
49
50    def get_active(self, provider: str, model: str | None = None) -> AuthProfile | None:
51        """
52        Get an active auth profile for the provider and model.
53
54        Args:
55            provider: LLM provider (e.g., 'openai', 'anthropic').
56            model: Optional model identifier.
57
58        Returns:
59            The best available profile or None.
60        """
61        now = datetime.utcnow()
62
63        # Filter by provider and availability
64        candidates = [p for p in self._profiles if p.provider == provider and p.is_available(now)]
65
66        # Filter by model if provided
67        if model:
68            model_candidates = [p for p in candidates if not p.models or model in p.models]
69            if model_candidates:
70                candidates = model_candidates
71
72        if not candidates:
73            return None
74
75        # If multiple candidates with same priority, pick one randomly or use last_used
76        # For now, just pick the one that was used longest ago
77        candidates.sort(key=lambda p: (p.priority, p.last_used or datetime.min))
78
79        profile = candidates[0]
80        profile.last_used = now
81        return profile
82
83    def mark_rate_limited(self, profile_id: str, cooldown_seconds: int = 60) -> None:
84        """Mark a profile as rate limited."""
85        for p in self._profiles:
86            if p.id == profile_id:
87                p.cooldown_until = datetime.utcnow() + timedelta(seconds=cooldown_seconds)
88                p.failure_count += 1
89                break
90
91    def mark_failure(self, profile_id: str) -> None:
92        """Mark a profile as failed."""
93        for p in self._profiles:
94            if p.id == profile_id:
95                p.failure_count += 1
96                # Small cooldown on failure
97                p.cooldown_until = datetime.utcnow() + timedelta(seconds=10)
98                break

Manages multiple auth profiles for rotation and failover.

AuthProfileManager()
40    def __init__(self) -> None:
41        """Initialize the auth profile manager."""
42        self._profiles: list[AuthProfile] = []

Initialize the auth profile manager.

def add_profile(self, profile: AuthProfile) -> None:
44    def add_profile(self, profile: AuthProfile) -> None:
45        """Add a new auth profile."""
46        self._profiles.append(profile)
47        # Sort by priority
48        self._profiles.sort(key=lambda p: p.priority)

Add a new auth profile.

def get_active( self, provider: str, model: str | None = None) -> AuthProfile | None:
50    def get_active(self, provider: str, model: str | None = None) -> AuthProfile | None:
51        """
52        Get an active auth profile for the provider and model.
53
54        Args:
55            provider: LLM provider (e.g., 'openai', 'anthropic').
56            model: Optional model identifier.
57
58        Returns:
59            The best available profile or None.
60        """
61        now = datetime.utcnow()
62
63        # Filter by provider and availability
64        candidates = [p for p in self._profiles if p.provider == provider and p.is_available(now)]
65
66        # Filter by model if provided
67        if model:
68            model_candidates = [p for p in candidates if not p.models or model in p.models]
69            if model_candidates:
70                candidates = model_candidates
71
72        if not candidates:
73            return None
74
75        # If multiple candidates with same priority, pick one randomly or use last_used
76        # For now, just pick the one that was used longest ago
77        candidates.sort(key=lambda p: (p.priority, p.last_used or datetime.min))
78
79        profile = candidates[0]
80        profile.last_used = now
81        return profile

Get an active auth profile for the provider and model.

Args: provider: LLM provider (e.g., 'openai', 'anthropic'). model: Optional model identifier.

Returns: The best available profile or None.

def mark_rate_limited(self, profile_id: str, cooldown_seconds: int = 60) -> None:
83    def mark_rate_limited(self, profile_id: str, cooldown_seconds: int = 60) -> None:
84        """Mark a profile as rate limited."""
85        for p in self._profiles:
86            if p.id == profile_id:
87                p.cooldown_until = datetime.utcnow() + timedelta(seconds=cooldown_seconds)
88                p.failure_count += 1
89                break

Mark a profile as rate limited.

def mark_failure(self, profile_id: str) -> None:
91    def mark_failure(self, profile_id: str) -> None:
92        """Mark a profile as failed."""
93        for p in self._profiles:
94            if p.id == profile_id:
95                p.failure_count += 1
96                # Small cooldown on failure
97                p.cooldown_until = datetime.utcnow() + timedelta(seconds=10)
98                break

Mark a profile as failed.