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,192 @@
|
|
|
1
|
+
"""OS keyring storage implementation for token storage."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import keyring
|
|
7
|
+
from structlog import get_logger
|
|
8
|
+
|
|
9
|
+
from ccproxy.auth.exceptions import (
|
|
10
|
+
CredentialsInvalidError,
|
|
11
|
+
CredentialsStorageError,
|
|
12
|
+
)
|
|
13
|
+
from ccproxy.auth.models import ClaudeCredentials
|
|
14
|
+
from ccproxy.auth.storage.base import TokenStorage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KeyringTokenStorage(TokenStorage):
|
|
21
|
+
"""OS keyring storage implementation for Claude credentials."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, service_name: str = "claude-code-proxy", username: str = "default"
|
|
25
|
+
):
|
|
26
|
+
"""Initialize keyring storage.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
service_name: Name of the service in the keyring
|
|
30
|
+
username: Username to associate with the stored credentials
|
|
31
|
+
"""
|
|
32
|
+
self.service_name = service_name
|
|
33
|
+
self.username = username
|
|
34
|
+
|
|
35
|
+
async def load(self) -> ClaudeCredentials | None:
|
|
36
|
+
"""Load credentials from the OS keyring.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Parsed credentials if found and valid, None otherwise
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
CredentialsStorageError: If the stored data is invalid
|
|
43
|
+
CredentialsStorageError: If there's an error reading from keyring
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
import keyring
|
|
47
|
+
except ImportError as e:
|
|
48
|
+
raise CredentialsStorageError(
|
|
49
|
+
"keyring package is required for keyring storage. "
|
|
50
|
+
"Install it with: pip install keyring"
|
|
51
|
+
) from e
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
logger.debug(
|
|
55
|
+
"credentials_load_start",
|
|
56
|
+
source="keyring",
|
|
57
|
+
service_name=self.service_name,
|
|
58
|
+
)
|
|
59
|
+
password = keyring.get_password(self.service_name, self.username)
|
|
60
|
+
|
|
61
|
+
if password is None:
|
|
62
|
+
logger.debug(
|
|
63
|
+
"credentials_not_found",
|
|
64
|
+
source="keyring",
|
|
65
|
+
service_name=self.service_name,
|
|
66
|
+
)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
# Parse the stored JSON
|
|
70
|
+
data = json.loads(password)
|
|
71
|
+
credentials = ClaudeCredentials.model_validate(data)
|
|
72
|
+
|
|
73
|
+
self._log_credential_details(credentials)
|
|
74
|
+
return credentials
|
|
75
|
+
|
|
76
|
+
except json.JSONDecodeError as e:
|
|
77
|
+
raise CredentialsStorageError(
|
|
78
|
+
f"Failed to parse credentials from keyring: {e}"
|
|
79
|
+
) from e
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise CredentialsStorageError(
|
|
82
|
+
f"Error loading credentials from keyring: {e}"
|
|
83
|
+
) from e
|
|
84
|
+
|
|
85
|
+
def _log_credential_details(self, credentials: ClaudeCredentials) -> None:
|
|
86
|
+
"""Log credential details safely."""
|
|
87
|
+
oauth_token = credentials.claude_ai_oauth
|
|
88
|
+
logger.debug(
|
|
89
|
+
"credentials_load_completed",
|
|
90
|
+
source="keyring",
|
|
91
|
+
subscription_type=oauth_token.subscription_type,
|
|
92
|
+
expires_at=str(oauth_token.expires_at_datetime),
|
|
93
|
+
is_expired=oauth_token.is_expired,
|
|
94
|
+
scopes=oauth_token.scopes,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def save(self, credentials: ClaudeCredentials) -> bool:
|
|
98
|
+
"""Save credentials to the OS keyring.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
credentials: Credentials to save
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if saved successfully, False otherwise
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
CredentialsStorageError: If there's an error writing to keyring
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
import keyring
|
|
111
|
+
except ImportError as e:
|
|
112
|
+
raise CredentialsStorageError(
|
|
113
|
+
"keyring package is required for keyring storage. "
|
|
114
|
+
"Install it with: pip install keyring"
|
|
115
|
+
) from e
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Convert to JSON string
|
|
119
|
+
data = credentials.model_dump(by_alias=True)
|
|
120
|
+
json_data = json.dumps(data)
|
|
121
|
+
|
|
122
|
+
# Store in keyring
|
|
123
|
+
keyring.set_password(self.service_name, self.username, json_data)
|
|
124
|
+
|
|
125
|
+
logger.debug(
|
|
126
|
+
"credentials_save_completed",
|
|
127
|
+
source="keyring",
|
|
128
|
+
service_name=self.service_name,
|
|
129
|
+
)
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
raise CredentialsStorageError(
|
|
134
|
+
f"Error saving credentials to keyring: {e}"
|
|
135
|
+
) from e
|
|
136
|
+
|
|
137
|
+
async def exists(self) -> bool:
|
|
138
|
+
"""Check if credentials exist in the keyring.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if credentials exist, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
import keyring
|
|
145
|
+
except ImportError:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
password = keyring.get_password(self.service_name, self.username)
|
|
150
|
+
return password is not None
|
|
151
|
+
except Exception:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
async def delete(self) -> bool:
|
|
155
|
+
"""Delete credentials from the keyring.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if deleted successfully, False otherwise
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
CredentialsStorageError: If there's an error deleting from keyring
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
import keyring
|
|
165
|
+
except ImportError as e:
|
|
166
|
+
raise CredentialsStorageError(
|
|
167
|
+
"keyring package is required for keyring storage. "
|
|
168
|
+
"Install it with: pip install keyring"
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
if await self.exists():
|
|
173
|
+
keyring.delete_password(self.service_name, self.username)
|
|
174
|
+
logger.debug(
|
|
175
|
+
"credentials_delete_completed",
|
|
176
|
+
source="keyring",
|
|
177
|
+
service_name=self.service_name,
|
|
178
|
+
)
|
|
179
|
+
return True
|
|
180
|
+
return False
|
|
181
|
+
except Exception as e:
|
|
182
|
+
raise CredentialsStorageError(
|
|
183
|
+
f"Error deleting credentials from keyring: {e}"
|
|
184
|
+
) from e
|
|
185
|
+
|
|
186
|
+
def get_location(self) -> str:
|
|
187
|
+
"""Get the storage location description.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Description of the keyring storage location
|
|
191
|
+
"""
|
|
192
|
+
return f"OS keyring (service: {self.service_name}, user: {self.username})"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Claude SDK integration module."""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
ClaudeSDKClient,
|
|
5
|
+
ClaudeSDKConnectionError,
|
|
6
|
+
ClaudeSDKError,
|
|
7
|
+
ClaudeSDKProcessError,
|
|
8
|
+
)
|
|
9
|
+
from .converter import MessageConverter
|
|
10
|
+
from .options import OptionsHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ClaudeSDKClient",
|
|
15
|
+
"ClaudeSDKError",
|
|
16
|
+
"ClaudeSDKConnectionError",
|
|
17
|
+
"ClaudeSDKProcessError",
|
|
18
|
+
"MessageConverter",
|
|
19
|
+
"OptionsHandler",
|
|
20
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Claude SDK client wrapper for handling core Claude Code SDK interactions."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from ccproxy.core.async_utils import patched_typing
|
|
9
|
+
from ccproxy.core.errors import ClaudeProxyError, ServiceUnavailableError
|
|
10
|
+
from ccproxy.observability import timed_operation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
with patched_typing():
|
|
14
|
+
from claude_code_sdk import (
|
|
15
|
+
AssistantMessage,
|
|
16
|
+
ClaudeCodeOptions,
|
|
17
|
+
CLIConnectionError,
|
|
18
|
+
CLIJSONDecodeError,
|
|
19
|
+
CLINotFoundError,
|
|
20
|
+
ProcessError,
|
|
21
|
+
ResultMessage,
|
|
22
|
+
SystemMessage,
|
|
23
|
+
UserMessage,
|
|
24
|
+
query,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = structlog.get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClaudeSDKError(Exception):
|
|
31
|
+
"""Base exception for Claude SDK errors."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ClaudeSDKConnectionError(ClaudeSDKError):
|
|
35
|
+
"""Raised when unable to connect to Claude Code."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClaudeSDKProcessError(ClaudeSDKError):
|
|
39
|
+
"""Raised when Claude Code process fails."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ClaudeSDKClient:
|
|
43
|
+
"""
|
|
44
|
+
Minimal Claude SDK client wrapper that handles core SDK interactions.
|
|
45
|
+
|
|
46
|
+
This class provides a clean interface to the Claude Code SDK while handling
|
|
47
|
+
error translation and basic query execution.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
"""Initialize the Claude SDK client."""
|
|
52
|
+
self._last_api_call_time_ms: float = 0.0
|
|
53
|
+
|
|
54
|
+
async def query_completion(
|
|
55
|
+
self, prompt: str, options: ClaudeCodeOptions, request_id: str | None = None
|
|
56
|
+
) -> AsyncIterator[UserMessage | AssistantMessage | SystemMessage | ResultMessage]:
|
|
57
|
+
"""
|
|
58
|
+
Execute a query using the Claude Code SDK.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
prompt: The prompt string to send to Claude
|
|
62
|
+
options: Claude Code options configuration
|
|
63
|
+
request_id: Optional request ID for correlation
|
|
64
|
+
|
|
65
|
+
Yields:
|
|
66
|
+
Messages from the Claude Code SDK
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ClaudeSDKError: If the query fails
|
|
70
|
+
"""
|
|
71
|
+
async with timed_operation("claude_sdk_query", request_id) as op:
|
|
72
|
+
try:
|
|
73
|
+
logger.debug("claude_sdk_query_start", prompt_length=len(prompt))
|
|
74
|
+
|
|
75
|
+
message_count = 0
|
|
76
|
+
async for message in query(prompt=prompt, options=options):
|
|
77
|
+
message_count += 1
|
|
78
|
+
yield message
|
|
79
|
+
|
|
80
|
+
# Store final metrics
|
|
81
|
+
op["message_count"] = message_count
|
|
82
|
+
self._last_api_call_time_ms = op.get("duration_ms", 0.0)
|
|
83
|
+
|
|
84
|
+
logger.debug(
|
|
85
|
+
"claude_sdk_query_completed",
|
|
86
|
+
message_count=message_count,
|
|
87
|
+
duration_ms=op.get("duration_ms"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
except (CLINotFoundError, CLIConnectionError) as e:
|
|
91
|
+
logger.error(
|
|
92
|
+
"claude_sdk_connection_failed",
|
|
93
|
+
error=str(e),
|
|
94
|
+
error_type=type(e).__name__,
|
|
95
|
+
)
|
|
96
|
+
raise ServiceUnavailableError(
|
|
97
|
+
f"Claude CLI not available: {str(e)}"
|
|
98
|
+
) from e
|
|
99
|
+
except (ProcessError, CLIJSONDecodeError) as e:
|
|
100
|
+
logger.error(
|
|
101
|
+
"claude_sdk_process_failed",
|
|
102
|
+
error=str(e),
|
|
103
|
+
error_type=type(e).__name__,
|
|
104
|
+
)
|
|
105
|
+
raise ClaudeProxyError(
|
|
106
|
+
message=f"Claude process error: {str(e)}",
|
|
107
|
+
error_type="service_unavailable_error",
|
|
108
|
+
status_code=503,
|
|
109
|
+
) from e
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(
|
|
112
|
+
"claude_sdk_unexpected_error_occurred",
|
|
113
|
+
error=str(e),
|
|
114
|
+
error_type=type(e).__name__,
|
|
115
|
+
)
|
|
116
|
+
raise ClaudeProxyError(
|
|
117
|
+
message=f"Unexpected error: {str(e)}",
|
|
118
|
+
error_type="internal_server_error",
|
|
119
|
+
status_code=500,
|
|
120
|
+
) from e
|
|
121
|
+
|
|
122
|
+
def get_last_api_call_time_ms(self) -> float:
|
|
123
|
+
"""
|
|
124
|
+
Get the duration of the last Claude API call in milliseconds.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Duration in milliseconds, or 0.0 if no call has been made yet
|
|
128
|
+
"""
|
|
129
|
+
return self._last_api_call_time_ms
|
|
130
|
+
|
|
131
|
+
async def validate_health(self) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Validate that the Claude SDK is healthy.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if healthy, False otherwise
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
logger.debug("health_check_start", component="claude_sdk")
|
|
140
|
+
|
|
141
|
+
# Simple health check - the SDK is available if we can import it
|
|
142
|
+
# More sophisticated checks could be added here
|
|
143
|
+
is_healthy = True
|
|
144
|
+
|
|
145
|
+
logger.debug(
|
|
146
|
+
"health_check_completed", component="claude_sdk", healthy=is_healthy
|
|
147
|
+
)
|
|
148
|
+
return is_healthy
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(
|
|
151
|
+
"health_check_failed",
|
|
152
|
+
component="claude_sdk",
|
|
153
|
+
error=str(e),
|
|
154
|
+
error_type=type(e).__name__,
|
|
155
|
+
)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
async def close(self) -> None:
|
|
159
|
+
"""Close the client and cleanup resources."""
|
|
160
|
+
# Claude Code SDK doesn't require explicit cleanup
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
async def __aenter__(self) -> "ClaudeSDKClient":
|
|
164
|
+
"""Async context manager entry."""
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
168
|
+
"""Async context manager exit."""
|
|
169
|
+
await self.close()
|