ccproxy-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,79 @@
1
+ """Authentication exceptions."""
2
+
3
+
4
+ class AuthenticationError(Exception):
5
+ """Base authentication error."""
6
+
7
+ pass
8
+
9
+
10
+ class AuthenticationRequiredError(AuthenticationError):
11
+ """Authentication is required but not provided."""
12
+
13
+ pass
14
+
15
+
16
+ class InvalidTokenError(AuthenticationError):
17
+ """Invalid or expired token."""
18
+
19
+ pass
20
+
21
+
22
+ class InsufficientPermissionsError(AuthenticationError):
23
+ """Insufficient permissions for the requested operation."""
24
+
25
+ pass
26
+
27
+
28
+ class CredentialsError(AuthenticationError):
29
+ """Base credentials error."""
30
+
31
+ pass
32
+
33
+
34
+ class CredentialsNotFoundError(CredentialsError):
35
+ """Credentials not found error."""
36
+
37
+ pass
38
+
39
+
40
+ class CredentialsExpiredError(CredentialsError):
41
+ """Credentials expired error."""
42
+
43
+ pass
44
+
45
+
46
+ class CredentialsInvalidError(CredentialsError):
47
+ """Credentials are invalid or malformed."""
48
+
49
+ pass
50
+
51
+
52
+ class CredentialsStorageError(CredentialsError):
53
+ """Error occurred during credentials storage operations."""
54
+
55
+ pass
56
+
57
+
58
+ class OAuthError(AuthenticationError):
59
+ """Base OAuth error."""
60
+
61
+ pass
62
+
63
+
64
+ class OAuthLoginError(OAuthError):
65
+ """OAuth login failed."""
66
+
67
+ pass
68
+
69
+
70
+ class OAuthTokenRefreshError(OAuthError):
71
+ """OAuth token refresh failed."""
72
+
73
+ pass
74
+
75
+
76
+ class OAuthCallbackError(OAuthError):
77
+ """OAuth callback failed."""
78
+
79
+ pass
@@ -0,0 +1,102 @@
1
+ """Authentication manager interfaces for centralized auth handling."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Protocol
5
+
6
+ from ccproxy.auth.models import ClaudeCredentials, UserProfile
7
+
8
+
9
+ class AuthManager(Protocol):
10
+ """Protocol for authentication managers."""
11
+
12
+ async def get_access_token(self) -> str:
13
+ """Get valid access token.
14
+
15
+ Returns:
16
+ Access token string
17
+
18
+ Raises:
19
+ AuthenticationError: If authentication fails
20
+ """
21
+ ...
22
+
23
+ async def get_credentials(self) -> ClaudeCredentials:
24
+ """Get valid credentials.
25
+
26
+ Returns:
27
+ Valid credentials
28
+
29
+ Raises:
30
+ AuthenticationError: If authentication fails
31
+ """
32
+ ...
33
+
34
+ async def is_authenticated(self) -> bool:
35
+ """Check if current authentication is valid.
36
+
37
+ Returns:
38
+ True if authenticated, False otherwise
39
+ """
40
+ ...
41
+
42
+ async def get_user_profile(self) -> UserProfile | None:
43
+ """Get user profile information.
44
+
45
+ Returns:
46
+ UserProfile if available, None otherwise
47
+ """
48
+ ...
49
+
50
+
51
+ class BaseAuthManager(ABC):
52
+ """Base class for authentication managers."""
53
+
54
+ @abstractmethod
55
+ async def get_access_token(self) -> str:
56
+ """Get valid access token.
57
+
58
+ Returns:
59
+ Access token string
60
+
61
+ Raises:
62
+ AuthenticationError: If authentication fails
63
+ """
64
+ pass
65
+
66
+ @abstractmethod
67
+ async def get_credentials(self) -> ClaudeCredentials:
68
+ """Get valid credentials.
69
+
70
+ Returns:
71
+ Valid credentials
72
+
73
+ Raises:
74
+ AuthenticationError: If authentication fails
75
+ """
76
+ pass
77
+
78
+ @abstractmethod
79
+ async def is_authenticated(self) -> bool:
80
+ """Check if current authentication is valid.
81
+
82
+ Returns:
83
+ True if authenticated, False otherwise
84
+ """
85
+ pass
86
+
87
+ async def get_user_profile(self) -> UserProfile | None:
88
+ """Get user profile information.
89
+
90
+ Returns:
91
+ UserProfile if available, None otherwise
92
+ """
93
+ return None
94
+
95
+ async def __aenter__(self) -> "BaseAuthManager":
96
+ """Async context manager entry."""
97
+ return self
98
+
99
+ @abstractmethod
100
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
101
+ """Async context manager exit."""
102
+ pass
ccproxy/auth/models.py ADDED
@@ -0,0 +1,118 @@
1
+ """Data models for authentication."""
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class OAuthToken(BaseModel):
9
+ """OAuth token information from Claude credentials."""
10
+
11
+ access_token: str = Field(..., alias="accessToken")
12
+ refresh_token: str = Field(..., alias="refreshToken")
13
+ expires_at: int | None = Field(None, alias="expiresAt")
14
+ scopes: list[str] = Field(default_factory=list)
15
+ subscription_type: str | None = Field(None, alias="subscriptionType")
16
+ token_type: str = Field(default="Bearer", alias="tokenType")
17
+
18
+ def __repr__(self) -> str:
19
+ """Safe string representation that masks sensitive tokens."""
20
+ access_preview = (
21
+ f"{self.access_token[:8]}...{self.access_token[-8:]}"
22
+ if len(self.access_token) > 16
23
+ else "***"
24
+ )
25
+ refresh_preview = (
26
+ f"{self.refresh_token[:8]}...{self.refresh_token[-8:]}"
27
+ if len(self.refresh_token) > 16
28
+ else "***"
29
+ )
30
+
31
+ expires_at = (
32
+ datetime.fromtimestamp(self.expires_at / 1000, tz=UTC).isoformat()
33
+ if self.expires_at is not None
34
+ else "None"
35
+ )
36
+ return (
37
+ f"OAuthToken(access_token='{access_preview}', "
38
+ f"refresh_token='{refresh_preview}', "
39
+ f"expires_at={expires_at}, "
40
+ f"scopes={self.scopes}, "
41
+ f"subscription_type='{self.subscription_type}', "
42
+ f"token_type='{self.token_type}')"
43
+ )
44
+
45
+ @property
46
+ def is_expired(self) -> bool:
47
+ """Check if the token is expired."""
48
+ if self.expires_at is None:
49
+ # If no expiration info, assume not expired for backward compatibility
50
+ return False
51
+ now = datetime.now(UTC).timestamp() * 1000 # Convert to milliseconds
52
+ return now >= self.expires_at
53
+
54
+ @property
55
+ def expires_at_datetime(self) -> datetime:
56
+ """Get expiration as datetime object."""
57
+ if self.expires_at is None:
58
+ # Return a far future date if no expiration info
59
+ return datetime.fromtimestamp(2147483647, tz=UTC) # Year 2038
60
+ return datetime.fromtimestamp(self.expires_at / 1000, tz=UTC)
61
+
62
+
63
+ class OrganizationInfo(BaseModel):
64
+ """Organization information from OAuth API."""
65
+
66
+ uuid: str
67
+ name: str
68
+ organization_type: str | None = None
69
+ billing_type: str | None = None
70
+ rate_limit_tier: str | None = None
71
+
72
+
73
+ class AccountInfo(BaseModel):
74
+ """Account information from OAuth API."""
75
+
76
+ uuid: str
77
+ email: str
78
+ full_name: str | None = None
79
+ display_name: str | None = None
80
+ has_claude_max: bool | None = None
81
+ has_claude_pro: bool | None = None
82
+
83
+ @property
84
+ def email_address(self) -> str:
85
+ """Compatibility property for email_address."""
86
+ return self.email
87
+
88
+
89
+ class UserProfile(BaseModel):
90
+ """User profile information from Anthropic OAuth API."""
91
+
92
+ organization: OrganizationInfo | None = None
93
+ account: AccountInfo | None = None
94
+
95
+
96
+ class ClaudeCredentials(BaseModel):
97
+ """Claude credentials from the credentials file."""
98
+
99
+ claude_ai_oauth: OAuthToken = Field(..., alias="claudeAiOauth")
100
+
101
+ def __repr__(self) -> str:
102
+ """Safe string representation that masks sensitive tokens."""
103
+ return f"ClaudeCredentials(claude_ai_oauth={repr(self.claude_ai_oauth)})"
104
+
105
+
106
+ class ValidationResult(BaseModel):
107
+ """Result of credentials validation."""
108
+
109
+ valid: bool
110
+ expired: bool | None = None
111
+ credentials: ClaudeCredentials | None = None
112
+ path: str | None = None
113
+
114
+
115
+ # Backwards compatibility - provide common aliases
116
+ User = UserProfile
117
+ Credentials = ClaudeCredentials
118
+ Profile = UserProfile
@@ -0,0 +1,26 @@
1
+ """OAuth authentication module for Anthropic OAuth login."""
2
+
3
+ from ccproxy.auth.oauth.models import (
4
+ OAuthCallbackRequest,
5
+ OAuthState,
6
+ OAuthTokenRequest,
7
+ OAuthTokenResponse,
8
+ )
9
+ from ccproxy.auth.oauth.routes import (
10
+ get_oauth_flow_result,
11
+ register_oauth_flow,
12
+ router,
13
+ )
14
+
15
+
16
+ __all__ = [
17
+ # Router
18
+ "router",
19
+ "register_oauth_flow",
20
+ "get_oauth_flow_result",
21
+ # Models
22
+ "OAuthState",
23
+ "OAuthCallbackRequest",
24
+ "OAuthTokenRequest",
25
+ "OAuthTokenResponse",
26
+ ]
@@ -0,0 +1,49 @@
1
+ """OAuth-specific models for authentication."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class OAuthState(BaseModel):
10
+ """OAuth state information for pending flows."""
11
+
12
+ code_verifier: str = Field(..., description="PKCE code verifier")
13
+ custom_paths: list[str] | None = Field(None, description="Custom credential paths")
14
+ completed: bool = Field(default=False, description="Whether the flow is completed")
15
+ success: bool = Field(default=False, description="Whether the flow was successful")
16
+ error: str | None = Field(None, description="Error message if failed")
17
+ created_at: datetime = Field(
18
+ default_factory=datetime.utcnow, description="Creation timestamp"
19
+ )
20
+
21
+
22
+ class OAuthCallbackRequest(BaseModel):
23
+ """OAuth callback request parameters."""
24
+
25
+ code: str | None = Field(None, description="Authorization code")
26
+ state: str | None = Field(None, description="State parameter")
27
+ error: str | None = Field(None, description="OAuth error")
28
+ error_description: str | None = Field(None, description="OAuth error description")
29
+
30
+
31
+ class OAuthTokenRequest(BaseModel):
32
+ """OAuth token exchange request."""
33
+
34
+ grant_type: str = Field(default="authorization_code")
35
+ code: str = Field(..., description="Authorization code")
36
+ redirect_uri: str = Field(..., description="Redirect URI")
37
+ client_id: str = Field(..., description="Client ID")
38
+ code_verifier: str = Field(..., description="PKCE code verifier")
39
+
40
+
41
+ class OAuthTokenResponse(BaseModel):
42
+ """OAuth token exchange response."""
43
+
44
+ access_token: str = Field(..., description="Access token")
45
+ refresh_token: str | None = Field(None, description="Refresh token")
46
+ expires_in: int | None = Field(None, description="Token expiration in seconds")
47
+ scope: str | None = Field(None, description="Granted scopes")
48
+ subscription_type: str | None = Field(None, description="Subscription type")
49
+ token_type: str = Field(default="Bearer", description="Token type")