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,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
+ }