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.
- ccproxy/__init__.py +4 -0
- ccproxy/__main__.py +7 -0
- ccproxy/_version.py +21 -0
- ccproxy/adapters/__init__.py +11 -0
- ccproxy/adapters/base.py +80 -0
- ccproxy/adapters/openai/__init__.py +43 -0
- ccproxy/adapters/openai/adapter.py +915 -0
- ccproxy/adapters/openai/models.py +412 -0
- ccproxy/adapters/openai/streaming.py +449 -0
- ccproxy/api/__init__.py +28 -0
- ccproxy/api/app.py +225 -0
- ccproxy/api/dependencies.py +140 -0
- ccproxy/api/middleware/__init__.py +11 -0
- ccproxy/api/middleware/auth.py +0 -0
- ccproxy/api/middleware/cors.py +55 -0
- ccproxy/api/middleware/errors.py +703 -0
- ccproxy/api/middleware/headers.py +51 -0
- ccproxy/api/middleware/logging.py +175 -0
- ccproxy/api/middleware/request_id.py +69 -0
- ccproxy/api/middleware/server_header.py +62 -0
- ccproxy/api/responses.py +84 -0
- ccproxy/api/routes/__init__.py +16 -0
- ccproxy/api/routes/claude.py +181 -0
- ccproxy/api/routes/health.py +489 -0
- ccproxy/api/routes/metrics.py +1033 -0
- ccproxy/api/routes/proxy.py +238 -0
- ccproxy/auth/__init__.py +75 -0
- ccproxy/auth/bearer.py +68 -0
- ccproxy/auth/credentials_adapter.py +93 -0
- ccproxy/auth/dependencies.py +229 -0
- ccproxy/auth/exceptions.py +79 -0
- ccproxy/auth/manager.py +102 -0
- ccproxy/auth/models.py +118 -0
- ccproxy/auth/oauth/__init__.py +26 -0
- ccproxy/auth/oauth/models.py +49 -0
- ccproxy/auth/oauth/routes.py +396 -0
- ccproxy/auth/oauth/storage.py +0 -0
- ccproxy/auth/storage/__init__.py +12 -0
- ccproxy/auth/storage/base.py +57 -0
- ccproxy/auth/storage/json_file.py +159 -0
- ccproxy/auth/storage/keyring.py +192 -0
- ccproxy/claude_sdk/__init__.py +20 -0
- ccproxy/claude_sdk/client.py +169 -0
- ccproxy/claude_sdk/converter.py +331 -0
- ccproxy/claude_sdk/options.py +120 -0
- ccproxy/cli/__init__.py +14 -0
- ccproxy/cli/commands/__init__.py +8 -0
- ccproxy/cli/commands/auth.py +553 -0
- ccproxy/cli/commands/config/__init__.py +14 -0
- ccproxy/cli/commands/config/commands.py +766 -0
- ccproxy/cli/commands/config/schema_commands.py +119 -0
- ccproxy/cli/commands/serve.py +630 -0
- ccproxy/cli/docker/__init__.py +34 -0
- ccproxy/cli/docker/adapter_factory.py +157 -0
- ccproxy/cli/docker/params.py +278 -0
- ccproxy/cli/helpers.py +144 -0
- ccproxy/cli/main.py +193 -0
- ccproxy/cli/options/__init__.py +14 -0
- ccproxy/cli/options/claude_options.py +216 -0
- ccproxy/cli/options/core_options.py +40 -0
- ccproxy/cli/options/security_options.py +48 -0
- ccproxy/cli/options/server_options.py +117 -0
- ccproxy/config/__init__.py +40 -0
- ccproxy/config/auth.py +154 -0
- ccproxy/config/claude.py +124 -0
- ccproxy/config/cors.py +79 -0
- ccproxy/config/discovery.py +87 -0
- ccproxy/config/docker_settings.py +265 -0
- ccproxy/config/loader.py +108 -0
- ccproxy/config/observability.py +158 -0
- ccproxy/config/pricing.py +88 -0
- ccproxy/config/reverse_proxy.py +31 -0
- ccproxy/config/scheduler.py +89 -0
- ccproxy/config/security.py +14 -0
- ccproxy/config/server.py +81 -0
- ccproxy/config/settings.py +534 -0
- ccproxy/config/validators.py +231 -0
- ccproxy/core/__init__.py +274 -0
- ccproxy/core/async_utils.py +675 -0
- ccproxy/core/constants.py +97 -0
- ccproxy/core/errors.py +256 -0
- ccproxy/core/http.py +328 -0
- ccproxy/core/http_transformers.py +428 -0
- ccproxy/core/interfaces.py +247 -0
- ccproxy/core/logging.py +189 -0
- ccproxy/core/middleware.py +114 -0
- ccproxy/core/proxy.py +143 -0
- ccproxy/core/system.py +38 -0
- ccproxy/core/transformers.py +259 -0
- ccproxy/core/types.py +129 -0
- ccproxy/core/validators.py +288 -0
- ccproxy/docker/__init__.py +67 -0
- ccproxy/docker/adapter.py +588 -0
- ccproxy/docker/docker_path.py +207 -0
- ccproxy/docker/middleware.py +103 -0
- ccproxy/docker/models.py +228 -0
- ccproxy/docker/protocol.py +192 -0
- ccproxy/docker/stream_process.py +264 -0
- ccproxy/docker/validators.py +173 -0
- ccproxy/models/__init__.py +123 -0
- ccproxy/models/errors.py +42 -0
- ccproxy/models/messages.py +243 -0
- ccproxy/models/requests.py +85 -0
- ccproxy/models/responses.py +227 -0
- ccproxy/models/types.py +102 -0
- ccproxy/observability/__init__.py +51 -0
- ccproxy/observability/access_logger.py +400 -0
- ccproxy/observability/context.py +447 -0
- ccproxy/observability/metrics.py +539 -0
- ccproxy/observability/pushgateway.py +366 -0
- ccproxy/observability/sse_events.py +303 -0
- ccproxy/observability/stats_printer.py +755 -0
- ccproxy/observability/storage/__init__.py +1 -0
- ccproxy/observability/storage/duckdb_simple.py +665 -0
- ccproxy/observability/storage/models.py +55 -0
- ccproxy/pricing/__init__.py +19 -0
- ccproxy/pricing/cache.py +212 -0
- ccproxy/pricing/loader.py +267 -0
- ccproxy/pricing/models.py +106 -0
- ccproxy/pricing/updater.py +309 -0
- ccproxy/scheduler/__init__.py +39 -0
- ccproxy/scheduler/core.py +335 -0
- ccproxy/scheduler/exceptions.py +34 -0
- ccproxy/scheduler/manager.py +186 -0
- ccproxy/scheduler/registry.py +150 -0
- ccproxy/scheduler/tasks.py +484 -0
- ccproxy/services/__init__.py +10 -0
- ccproxy/services/claude_sdk_service.py +614 -0
- ccproxy/services/credentials/__init__.py +55 -0
- ccproxy/services/credentials/config.py +105 -0
- ccproxy/services/credentials/manager.py +562 -0
- ccproxy/services/credentials/oauth_client.py +482 -0
- ccproxy/services/proxy_service.py +1536 -0
- ccproxy/static/.keep +0 -0
- ccproxy/testing/__init__.py +34 -0
- ccproxy/testing/config.py +148 -0
- ccproxy/testing/content_generation.py +197 -0
- ccproxy/testing/mock_responses.py +262 -0
- ccproxy/testing/response_handlers.py +161 -0
- ccproxy/testing/scenarios.py +241 -0
- ccproxy/utils/__init__.py +6 -0
- ccproxy/utils/cost_calculator.py +210 -0
- ccproxy/utils/streaming_metrics.py +199 -0
- ccproxy_api-0.1.0.dist-info/METADATA +253 -0
- ccproxy_api-0.1.0.dist-info/RECORD +148 -0
- ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
- ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
- ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""Health check endpoints for CCProxy API Server.
|
|
2
|
+
|
|
3
|
+
Implements modern health check patterns following 2024 best practices:
|
|
4
|
+
- /health/live: Liveness probe for Kubernetes (minimal, fast)
|
|
5
|
+
- /health/ready: Readiness probe for Kubernetes (critical dependencies)
|
|
6
|
+
- /health: Detailed diagnostics (comprehensive status)
|
|
7
|
+
|
|
8
|
+
Follows IETF Health Check Response Format draft standard.
|
|
9
|
+
TODO: health endpoint Content-Type header to only return application/health+json per IETF spec
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import functools
|
|
14
|
+
import shutil
|
|
15
|
+
import time
|
|
16
|
+
from datetime import UTC, datetime, timezone
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from fastapi import APIRouter, Response, status
|
|
20
|
+
from structlog import get_logger
|
|
21
|
+
|
|
22
|
+
from ccproxy import __version__
|
|
23
|
+
from ccproxy.auth.exceptions import CredentialsExpiredError, CredentialsNotFoundError
|
|
24
|
+
from ccproxy.core.async_utils import patched_typing
|
|
25
|
+
from ccproxy.services.credentials import CredentialsManager
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
router = APIRouter()
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
# Cache for Claude CLI check results
|
|
32
|
+
_claude_cli_cache: tuple[float, tuple[str, dict[str, Any]]] | None = None
|
|
33
|
+
_cache_ttl_seconds = 300 # Cache for 5 minutes
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _check_oauth2_credentials() -> tuple[str, dict[str, Any]]:
|
|
37
|
+
"""Check OAuth2 credentials health status.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
41
|
+
Details include token metadata without exposing sensitive data
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
manager = CredentialsManager()
|
|
45
|
+
validation = await manager.validate()
|
|
46
|
+
|
|
47
|
+
if validation.valid and not validation.expired:
|
|
48
|
+
# Get token metadata without exposing sensitive information
|
|
49
|
+
credentials = validation.credentials
|
|
50
|
+
oauth_token = credentials.claude_ai_oauth if credentials else None
|
|
51
|
+
|
|
52
|
+
details = {
|
|
53
|
+
"auth_status": "valid",
|
|
54
|
+
"credentials_path": str(validation.path) if validation.path else None,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if oauth_token:
|
|
58
|
+
details.update(
|
|
59
|
+
{
|
|
60
|
+
"expiration": oauth_token.expires_at_datetime.isoformat()
|
|
61
|
+
if oauth_token.expires_at_datetime
|
|
62
|
+
else None,
|
|
63
|
+
"subscription_type": oauth_token.subscription_type,
|
|
64
|
+
"expires_in_hours": str(
|
|
65
|
+
int(
|
|
66
|
+
(
|
|
67
|
+
oauth_token.expires_at_datetime - datetime.now(UTC)
|
|
68
|
+
).total_seconds()
|
|
69
|
+
/ 3600
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if oauth_token.expires_at_datetime
|
|
73
|
+
else None,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return "pass", details
|
|
78
|
+
else:
|
|
79
|
+
# Handle expired credentials
|
|
80
|
+
credentials = validation.credentials
|
|
81
|
+
oauth_token = credentials.claude_ai_oauth if credentials else None
|
|
82
|
+
|
|
83
|
+
details = {
|
|
84
|
+
"auth_status": "expired" if validation.expired else "invalid",
|
|
85
|
+
"credentials_path": str(validation.path) if validation.path else None,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if oauth_token and oauth_token.expires_at_datetime:
|
|
89
|
+
details.update(
|
|
90
|
+
{
|
|
91
|
+
"expiration": oauth_token.expires_at_datetime.isoformat(),
|
|
92
|
+
"subscription_type": oauth_token.subscription_type,
|
|
93
|
+
"expired_hours_ago": str(
|
|
94
|
+
int(
|
|
95
|
+
(
|
|
96
|
+
datetime.now(UTC) - oauth_token.expires_at_datetime
|
|
97
|
+
).total_seconds()
|
|
98
|
+
/ 3600
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
if validation.expired
|
|
102
|
+
else None,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return "warn", details
|
|
107
|
+
|
|
108
|
+
except CredentialsNotFoundError:
|
|
109
|
+
return "warn", {
|
|
110
|
+
"auth_status": "not_configured",
|
|
111
|
+
"error": "Claude credentials file not found",
|
|
112
|
+
"credentials_path": None,
|
|
113
|
+
}
|
|
114
|
+
except CredentialsExpiredError:
|
|
115
|
+
return "warn", {
|
|
116
|
+
"auth_status": "expired",
|
|
117
|
+
"error": "Claude credentials have expired",
|
|
118
|
+
}
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return "fail", {
|
|
121
|
+
"auth_status": "error",
|
|
122
|
+
"error": f"Unexpected error: {str(e)}",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@functools.lru_cache(maxsize=1)
|
|
127
|
+
def _get_claude_cli_path() -> str | None:
|
|
128
|
+
"""Get Claude CLI path with caching. Returns None if not found."""
|
|
129
|
+
return shutil.which("claude")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def _check_claude_code() -> tuple[str, dict[str, Any]]:
|
|
133
|
+
"""Check Claude Code CLI installation and version by running 'claude --version'.
|
|
134
|
+
|
|
135
|
+
Results are cached for 5 minutes to avoid repeated subprocess calls.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
139
|
+
Details include CLI version and binary path
|
|
140
|
+
"""
|
|
141
|
+
global _claude_cli_cache
|
|
142
|
+
|
|
143
|
+
# Check if we have a valid cached result
|
|
144
|
+
current_time = time.time()
|
|
145
|
+
if _claude_cli_cache is not None:
|
|
146
|
+
cache_time, cached_result = _claude_cli_cache
|
|
147
|
+
if current_time - cache_time < _cache_ttl_seconds:
|
|
148
|
+
logger.debug("claude_cli_check_cache_hit")
|
|
149
|
+
return cached_result
|
|
150
|
+
|
|
151
|
+
logger.debug("claude_cli_check_cache_miss")
|
|
152
|
+
|
|
153
|
+
# First check if claude binary exists in PATH (cached)
|
|
154
|
+
claude_path = _get_claude_cli_path()
|
|
155
|
+
|
|
156
|
+
if not claude_path:
|
|
157
|
+
result = (
|
|
158
|
+
"warn",
|
|
159
|
+
{
|
|
160
|
+
"installation_status": "not_found",
|
|
161
|
+
"cli_status": "not_installed",
|
|
162
|
+
"error": "Claude CLI binary not found in PATH",
|
|
163
|
+
"version": None,
|
|
164
|
+
"binary_path": None,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
# Cache the result
|
|
168
|
+
_claude_cli_cache = (current_time, result)
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Run 'claude --version' to get actual version
|
|
173
|
+
process = await asyncio.create_subprocess_exec(
|
|
174
|
+
"claude",
|
|
175
|
+
"--version",
|
|
176
|
+
stdout=asyncio.subprocess.PIPE,
|
|
177
|
+
stderr=asyncio.subprocess.PIPE,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
stdout, stderr = await process.communicate()
|
|
181
|
+
|
|
182
|
+
if process.returncode == 0:
|
|
183
|
+
version_output = stdout.decode().strip()
|
|
184
|
+
# Extract version from output (e.g., "1.0.48 (Claude Code)" -> "1.0.48")
|
|
185
|
+
if version_output:
|
|
186
|
+
import re
|
|
187
|
+
|
|
188
|
+
# Try to find a version pattern (e.g., "1.0.48", "v2.1.0")
|
|
189
|
+
version_match = re.search(
|
|
190
|
+
r"\b(?:v)?(\d+\.\d+(?:\.\d+)?)\b", version_output
|
|
191
|
+
)
|
|
192
|
+
if version_match:
|
|
193
|
+
version = version_match.group(1)
|
|
194
|
+
else:
|
|
195
|
+
# Fallback: take the first part if no version pattern found
|
|
196
|
+
parts = version_output.split()
|
|
197
|
+
version = parts[0] if parts else "unknown"
|
|
198
|
+
else:
|
|
199
|
+
version = "unknown"
|
|
200
|
+
|
|
201
|
+
result = (
|
|
202
|
+
"pass",
|
|
203
|
+
{
|
|
204
|
+
"installation_status": "found",
|
|
205
|
+
"cli_status": "available",
|
|
206
|
+
"version": version,
|
|
207
|
+
"binary_path": claude_path,
|
|
208
|
+
"version_output": version_output,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
# Cache the result
|
|
212
|
+
_claude_cli_cache = (current_time, result)
|
|
213
|
+
return result
|
|
214
|
+
else:
|
|
215
|
+
# Binary exists but --version failed
|
|
216
|
+
error_output = stderr.decode().strip() if stderr else "Unknown error"
|
|
217
|
+
result = (
|
|
218
|
+
"warn",
|
|
219
|
+
{
|
|
220
|
+
"installation_status": "found_with_issues",
|
|
221
|
+
"cli_status": "binary_found_but_errors",
|
|
222
|
+
"error": f"'claude --version' failed: {error_output}",
|
|
223
|
+
"version": None,
|
|
224
|
+
"binary_path": claude_path,
|
|
225
|
+
"return_code": str(process.returncode),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
# Cache the result
|
|
229
|
+
_claude_cli_cache = (current_time, result)
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
except TimeoutError:
|
|
233
|
+
result = (
|
|
234
|
+
"warn",
|
|
235
|
+
{
|
|
236
|
+
"installation_status": "found_with_issues",
|
|
237
|
+
"cli_status": "timeout",
|
|
238
|
+
"error": "Timeout running 'claude --version'",
|
|
239
|
+
"version": None,
|
|
240
|
+
"binary_path": claude_path,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
# Cache the result
|
|
244
|
+
_claude_cli_cache = (current_time, result)
|
|
245
|
+
return result
|
|
246
|
+
except Exception as e:
|
|
247
|
+
result = (
|
|
248
|
+
"fail",
|
|
249
|
+
{
|
|
250
|
+
"installation_status": "error",
|
|
251
|
+
"cli_status": "error",
|
|
252
|
+
"error": f"Unexpected error running 'claude --version': {str(e)}",
|
|
253
|
+
"version": None,
|
|
254
|
+
"binary_path": claude_path,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
# Cache the result
|
|
258
|
+
_claude_cli_cache = (current_time, result)
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def _check_claude_sdk() -> tuple[str, dict[str, Any]]:
|
|
263
|
+
"""Check Claude SDK installation and version.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
267
|
+
Details include SDK version and availability
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
# Try to import Claude Code SDK
|
|
271
|
+
with patched_typing():
|
|
272
|
+
from claude_code_sdk import __version__ as sdk_version
|
|
273
|
+
|
|
274
|
+
return "pass", {
|
|
275
|
+
"installation_status": "found",
|
|
276
|
+
"sdk_status": "available",
|
|
277
|
+
"version": sdk_version,
|
|
278
|
+
"import_successful": True,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
except ImportError as e:
|
|
282
|
+
return "warn", {
|
|
283
|
+
"installation_status": "not_found",
|
|
284
|
+
"sdk_status": "not_installed",
|
|
285
|
+
"error": f"Claude SDK not available: {str(e)}",
|
|
286
|
+
"version": None,
|
|
287
|
+
"import_successful": False,
|
|
288
|
+
}
|
|
289
|
+
except Exception as e:
|
|
290
|
+
return "fail", {
|
|
291
|
+
"installation_status": "error",
|
|
292
|
+
"sdk_status": "error",
|
|
293
|
+
"error": f"Unexpected error checking SDK: {str(e)}",
|
|
294
|
+
"version": None,
|
|
295
|
+
"import_successful": False,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@router.get("/health/live")
|
|
300
|
+
async def liveness_probe(response: Response) -> dict[str, Any]:
|
|
301
|
+
"""Liveness probe for Kubernetes.
|
|
302
|
+
|
|
303
|
+
Minimal health check that only verifies the application process is running.
|
|
304
|
+
Used by Kubernetes to determine if the pod should be restarted.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Simple health status following IETF health check format
|
|
308
|
+
"""
|
|
309
|
+
# Add cache control headers as per best practices
|
|
310
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
311
|
+
response.headers["Content-Type"] = "application/health+json"
|
|
312
|
+
|
|
313
|
+
logger.debug("Liveness probe request")
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"status": "pass",
|
|
317
|
+
"version": __version__,
|
|
318
|
+
"output": "Application process is running",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@router.get("/health/ready")
|
|
323
|
+
async def readiness_probe(response: Response) -> dict[str, Any]:
|
|
324
|
+
"""Readiness probe for Kubernetes.
|
|
325
|
+
|
|
326
|
+
Checks critical dependencies to determine if the service is ready to accept traffic.
|
|
327
|
+
Used by Kubernetes to determine if the pod should receive traffic.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Readiness status with critical dependency checks
|
|
331
|
+
"""
|
|
332
|
+
# Add cache control headers
|
|
333
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
334
|
+
response.headers["Content-Type"] = "application/health+json"
|
|
335
|
+
|
|
336
|
+
logger.debug("Readiness probe request")
|
|
337
|
+
|
|
338
|
+
# Check OAuth credentials, CLI, and SDK separately
|
|
339
|
+
oauth_status, oauth_details = await _check_oauth2_credentials()
|
|
340
|
+
cli_status, cli_details = await _check_claude_code()
|
|
341
|
+
sdk_status, sdk_details = await _check_claude_sdk()
|
|
342
|
+
|
|
343
|
+
# Service is ready if no check returns "fail"
|
|
344
|
+
# "warn" statuses (missing credentials/CLI/SDK) don't prevent readiness
|
|
345
|
+
if oauth_status == "fail" or cli_status == "fail" or sdk_status == "fail":
|
|
346
|
+
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
347
|
+
failed_components = []
|
|
348
|
+
|
|
349
|
+
if oauth_status == "fail":
|
|
350
|
+
failed_components.append("oauth2_credentials")
|
|
351
|
+
if cli_status == "fail":
|
|
352
|
+
failed_components.append("claude_cli")
|
|
353
|
+
if sdk_status == "fail":
|
|
354
|
+
failed_components.append("claude_sdk")
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"status": "fail",
|
|
358
|
+
"version": __version__,
|
|
359
|
+
"output": f"Critical dependency error: {', '.join(failed_components)}",
|
|
360
|
+
"checks": {
|
|
361
|
+
"oauth2_credentials": [
|
|
362
|
+
{
|
|
363
|
+
"status": oauth_status,
|
|
364
|
+
"output": oauth_details.get("error", "OAuth credentials error"),
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
"claude_cli": [
|
|
368
|
+
{
|
|
369
|
+
"status": cli_status,
|
|
370
|
+
"output": cli_details.get("error", "Claude CLI error"),
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
"claude_sdk": [
|
|
374
|
+
{
|
|
375
|
+
"status": sdk_status,
|
|
376
|
+
"output": sdk_details.get("error", "Claude SDK error"),
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
"status": "pass",
|
|
384
|
+
"version": __version__,
|
|
385
|
+
"output": "Service is ready to accept traffic",
|
|
386
|
+
"checks": {
|
|
387
|
+
"oauth2_credentials": [
|
|
388
|
+
{
|
|
389
|
+
"status": oauth_status,
|
|
390
|
+
"output": f"OAuth credentials: {oauth_details.get('auth_status', 'unknown')}",
|
|
391
|
+
}
|
|
392
|
+
],
|
|
393
|
+
"claude_cli": [
|
|
394
|
+
{
|
|
395
|
+
"status": cli_status,
|
|
396
|
+
"output": f"Claude CLI: {cli_details.get('cli_status', 'unknown')}",
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
"claude_sdk": [
|
|
400
|
+
{
|
|
401
|
+
"status": sdk_status,
|
|
402
|
+
"output": f"Claude SDK: {sdk_details.get('sdk_status', 'unknown')}",
|
|
403
|
+
}
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@router.get("/health")
|
|
410
|
+
async def detailed_health_check(response: Response) -> dict[str, Any]:
|
|
411
|
+
"""Comprehensive health check for diagnostics and monitoring.
|
|
412
|
+
|
|
413
|
+
Provides detailed status of all services and dependencies.
|
|
414
|
+
Used by monitoring dashboards, debugging, and operations teams.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Detailed health status following IETF health check format
|
|
418
|
+
"""
|
|
419
|
+
# Add cache control headers
|
|
420
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
421
|
+
response.headers["Content-Type"] = "application/health+json"
|
|
422
|
+
|
|
423
|
+
logger.debug("Detailed health check request")
|
|
424
|
+
|
|
425
|
+
# Perform all health checks
|
|
426
|
+
oauth_status, oauth_details = await _check_oauth2_credentials()
|
|
427
|
+
cli_status, cli_details = await _check_claude_code()
|
|
428
|
+
sdk_status, sdk_details = await _check_claude_sdk()
|
|
429
|
+
|
|
430
|
+
# Determine overall status - prioritize failures, then warnings
|
|
431
|
+
overall_status = "pass"
|
|
432
|
+
if oauth_status == "fail" or cli_status == "fail" or sdk_status == "fail":
|
|
433
|
+
overall_status = "fail"
|
|
434
|
+
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
435
|
+
elif oauth_status == "warn" or cli_status == "warn" or sdk_status == "warn":
|
|
436
|
+
overall_status = "warn"
|
|
437
|
+
response.status_code = status.HTTP_200_OK
|
|
438
|
+
|
|
439
|
+
current_time = datetime.now(UTC).isoformat()
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
"status": overall_status,
|
|
443
|
+
"version": __version__,
|
|
444
|
+
"serviceId": "claude-code-proxy",
|
|
445
|
+
"description": "CCProxy API Server",
|
|
446
|
+
"time": current_time,
|
|
447
|
+
"checks": {
|
|
448
|
+
"oauth2_credentials": [
|
|
449
|
+
{
|
|
450
|
+
"componentId": "oauth2-credentials",
|
|
451
|
+
"componentType": "authentication",
|
|
452
|
+
"status": oauth_status,
|
|
453
|
+
"time": current_time,
|
|
454
|
+
"output": f"OAuth2 credentials: {oauth_details.get('auth_status', 'unknown')}",
|
|
455
|
+
**oauth_details,
|
|
456
|
+
}
|
|
457
|
+
],
|
|
458
|
+
"claude_cli": [
|
|
459
|
+
{
|
|
460
|
+
"componentId": "claude-cli",
|
|
461
|
+
"componentType": "external_dependency",
|
|
462
|
+
"status": cli_status,
|
|
463
|
+
"time": current_time,
|
|
464
|
+
"output": f"Claude CLI: {cli_details.get('cli_status', 'unknown')}",
|
|
465
|
+
**cli_details,
|
|
466
|
+
}
|
|
467
|
+
],
|
|
468
|
+
"claude_sdk": [
|
|
469
|
+
{
|
|
470
|
+
"componentId": "claude-sdk",
|
|
471
|
+
"componentType": "python_package",
|
|
472
|
+
"status": sdk_status,
|
|
473
|
+
"time": current_time,
|
|
474
|
+
"output": f"Claude SDK: {sdk_details.get('sdk_status', 'unknown')}",
|
|
475
|
+
**sdk_details,
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
"proxy_service": [
|
|
479
|
+
{
|
|
480
|
+
"componentId": "proxy-service",
|
|
481
|
+
"componentType": "service",
|
|
482
|
+
"status": "pass",
|
|
483
|
+
"time": current_time,
|
|
484
|
+
"output": "Proxy service operational",
|
|
485
|
+
"version": __version__,
|
|
486
|
+
}
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
}
|