ccproxy-api 0.1.2__py3-none-any.whl → 0.1.3__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/_version.py +2 -2
- ccproxy/adapters/openai/__init__.py +1 -2
- ccproxy/adapters/openai/adapter.py +218 -180
- ccproxy/adapters/openai/streaming.py +247 -65
- ccproxy/api/__init__.py +0 -3
- ccproxy/api/app.py +173 -40
- ccproxy/api/dependencies.py +62 -3
- ccproxy/api/middleware/errors.py +3 -7
- ccproxy/api/middleware/headers.py +0 -2
- ccproxy/api/middleware/logging.py +4 -3
- ccproxy/api/middleware/request_content_logging.py +297 -0
- ccproxy/api/middleware/request_id.py +5 -0
- ccproxy/api/middleware/server_header.py +0 -4
- ccproxy/api/routes/__init__.py +9 -1
- ccproxy/api/routes/claude.py +23 -32
- ccproxy/api/routes/health.py +58 -4
- ccproxy/api/routes/mcp.py +171 -0
- ccproxy/api/routes/metrics.py +4 -8
- ccproxy/api/routes/permissions.py +217 -0
- ccproxy/api/routes/proxy.py +0 -53
- ccproxy/api/services/__init__.py +6 -0
- ccproxy/api/services/permission_service.py +368 -0
- ccproxy/api/ui/__init__.py +6 -0
- ccproxy/api/ui/permission_handler_protocol.py +33 -0
- ccproxy/api/ui/terminal_permission_handler.py +593 -0
- ccproxy/auth/conditional.py +2 -2
- ccproxy/auth/dependencies.py +1 -1
- ccproxy/auth/oauth/models.py +0 -1
- ccproxy/auth/oauth/routes.py +1 -3
- ccproxy/auth/storage/json_file.py +0 -1
- ccproxy/auth/storage/keyring.py +0 -3
- ccproxy/claude_sdk/__init__.py +2 -0
- ccproxy/claude_sdk/client.py +91 -8
- ccproxy/claude_sdk/converter.py +405 -210
- ccproxy/claude_sdk/options.py +76 -29
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -2
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +30 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +5 -16
- ccproxy/cli/options/claude_options.py +19 -1
- ccproxy/cli/options/core_options.py +0 -3
- ccproxy/cli/options/security_options.py +0 -2
- ccproxy/cli/options/server_options.py +3 -2
- ccproxy/config/auth.py +0 -1
- ccproxy/config/claude.py +78 -2
- ccproxy/config/discovery.py +0 -1
- ccproxy/config/docker_settings.py +0 -1
- ccproxy/config/loader.py +1 -4
- ccproxy/config/scheduler.py +20 -0
- ccproxy/config/security.py +7 -2
- ccproxy/config/server.py +5 -0
- ccproxy/config/settings.py +13 -7
- ccproxy/config/validators.py +1 -1
- ccproxy/core/async_utils.py +1 -4
- ccproxy/core/errors.py +45 -1
- ccproxy/core/http_transformers.py +4 -3
- ccproxy/core/interfaces.py +2 -2
- ccproxy/core/logging.py +97 -95
- ccproxy/core/middleware.py +1 -1
- ccproxy/core/proxy.py +1 -1
- ccproxy/core/transformers.py +1 -1
- ccproxy/core/types.py +1 -1
- ccproxy/docker/models.py +1 -1
- ccproxy/docker/protocol.py +0 -3
- ccproxy/models/__init__.py +41 -0
- ccproxy/models/claude_sdk.py +420 -0
- ccproxy/models/messages.py +45 -18
- ccproxy/models/permissions.py +115 -0
- ccproxy/models/requests.py +1 -1
- ccproxy/models/responses.py +29 -2
- ccproxy/observability/access_logger.py +1 -2
- ccproxy/observability/context.py +17 -1
- ccproxy/observability/metrics.py +1 -3
- ccproxy/observability/pushgateway.py +0 -2
- ccproxy/observability/stats_printer.py +2 -4
- ccproxy/observability/storage/duckdb_simple.py +1 -1
- ccproxy/observability/storage/models.py +0 -1
- ccproxy/pricing/cache.py +0 -1
- ccproxy/pricing/loader.py +5 -21
- ccproxy/pricing/updater.py +0 -1
- ccproxy/scheduler/__init__.py +1 -0
- ccproxy/scheduler/core.py +6 -6
- ccproxy/scheduler/manager.py +35 -7
- ccproxy/scheduler/registry.py +1 -1
- ccproxy/scheduler/tasks.py +127 -2
- ccproxy/services/claude_sdk_service.py +220 -328
- ccproxy/services/credentials/manager.py +0 -1
- ccproxy/services/credentials/oauth_client.py +1 -2
- ccproxy/services/proxy_service.py +93 -222
- ccproxy/testing/config.py +1 -1
- ccproxy/testing/mock_responses.py +0 -1
- ccproxy/utils/model_mapping.py +197 -0
- ccproxy/utils/models_provider.py +150 -0
- ccproxy/utils/simple_request_logger.py +284 -0
- ccproxy/utils/version_checker.py +184 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.3.dist-info/RECORD +166 -0
- ccproxy/cli/commands/permission.py +0 -128
- ccproxy_api-0.1.2.dist-info/RECORD +0 -150
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
ccproxy/api/app.py
CHANGED
|
@@ -5,8 +5,7 @@ from contextlib import asynccontextmanager
|
|
|
5
5
|
from datetime import UTC, datetime
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from fastapi import
|
|
9
|
-
from fastapi.responses import JSONResponse
|
|
8
|
+
from fastapi import APIRouter, FastAPI
|
|
10
9
|
from fastapi.staticfiles import StaticFiles
|
|
11
10
|
from structlog import get_logger
|
|
12
11
|
|
|
@@ -14,33 +13,62 @@ from ccproxy import __version__
|
|
|
14
13
|
from ccproxy.api.middleware.cors import setup_cors_middleware
|
|
15
14
|
from ccproxy.api.middleware.errors import setup_error_handlers
|
|
16
15
|
from ccproxy.api.middleware.logging import AccessLogMiddleware
|
|
16
|
+
from ccproxy.api.middleware.request_content_logging import (
|
|
17
|
+
RequestContentLoggingMiddleware,
|
|
18
|
+
)
|
|
17
19
|
from ccproxy.api.middleware.request_id import RequestIDMiddleware
|
|
18
20
|
from ccproxy.api.middleware.server_header import ServerHeaderMiddleware
|
|
19
21
|
from ccproxy.api.routes.claude import router as claude_router
|
|
22
|
+
from ccproxy.api.routes.health import get_claude_cli_info
|
|
20
23
|
from ccproxy.api.routes.health import router as health_router
|
|
24
|
+
from ccproxy.api.routes.mcp import setup_mcp
|
|
21
25
|
from ccproxy.api.routes.metrics import (
|
|
22
26
|
dashboard_router,
|
|
23
27
|
logs_router,
|
|
24
28
|
prometheus_router,
|
|
25
29
|
)
|
|
30
|
+
from ccproxy.api.routes.permissions import router as permissions_router
|
|
26
31
|
from ccproxy.api.routes.proxy import router as proxy_router
|
|
32
|
+
from ccproxy.api.services.permission_service import get_permission_service
|
|
33
|
+
from ccproxy.auth.credentials_adapter import CredentialsAuthManager
|
|
27
34
|
from ccproxy.auth.exceptions import CredentialsNotFoundError
|
|
28
35
|
from ccproxy.auth.oauth.routes import router as oauth_router
|
|
29
36
|
from ccproxy.config.settings import Settings, get_settings
|
|
30
37
|
from ccproxy.core.logging import setup_logging
|
|
38
|
+
from ccproxy.observability import get_metrics
|
|
31
39
|
from ccproxy.observability.storage.duckdb_simple import SimpleDuckDBStorage
|
|
40
|
+
from ccproxy.scheduler.errors import SchedulerError
|
|
32
41
|
from ccproxy.scheduler.manager import start_scheduler, stop_scheduler
|
|
42
|
+
from ccproxy.services.claude_sdk_service import ClaudeSDKService
|
|
33
43
|
from ccproxy.services.credentials import CredentialsManager
|
|
44
|
+
from ccproxy.utils.models_provider import get_models_list
|
|
34
45
|
|
|
35
46
|
|
|
36
47
|
logger = get_logger(__name__)
|
|
37
48
|
|
|
38
49
|
|
|
50
|
+
# Create shared models router
|
|
51
|
+
models_router = APIRouter(tags=["models"])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@models_router.get("/v1/models", response_model=None)
|
|
55
|
+
async def list_models() -> dict[str, Any]:
|
|
56
|
+
"""List available models.
|
|
57
|
+
|
|
58
|
+
Returns a combined list of Anthropic models and recent OpenAI models.
|
|
59
|
+
This endpoint is shared between both SDK and proxy APIs.
|
|
60
|
+
"""
|
|
61
|
+
return get_models_list()
|
|
62
|
+
|
|
63
|
+
|
|
39
64
|
@asynccontextmanager
|
|
40
65
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
41
66
|
"""Application lifespan manager."""
|
|
42
67
|
settings = get_settings()
|
|
43
68
|
|
|
69
|
+
# Store settings in app state for reuse in dependencies
|
|
70
|
+
app.state.settings = settings
|
|
71
|
+
|
|
44
72
|
# Startup
|
|
45
73
|
logger.info(
|
|
46
74
|
"server_start",
|
|
@@ -77,14 +105,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
77
105
|
).total_seconds()
|
|
78
106
|
/ 3600
|
|
79
107
|
)
|
|
80
|
-
logger.
|
|
108
|
+
logger.debug(
|
|
81
109
|
"auth_token_valid",
|
|
82
110
|
expires_in_hours=hours_until_expiry,
|
|
83
111
|
subscription_type=oauth_token.subscription_type,
|
|
84
112
|
credentials_path=str(validation.path) if validation.path else None,
|
|
85
113
|
)
|
|
86
114
|
else:
|
|
87
|
-
logger.
|
|
115
|
+
logger.debug("auth_token_valid", credentials_path=str(validation.path))
|
|
88
116
|
elif validation.expired:
|
|
89
117
|
logger.warning(
|
|
90
118
|
"auth_token_expired",
|
|
@@ -108,32 +136,63 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
108
136
|
"auth_token_validation_error",
|
|
109
137
|
error=str(e),
|
|
110
138
|
message="Failed to validate authentication token. The server will continue without authentication.",
|
|
139
|
+
exc_info=True,
|
|
111
140
|
)
|
|
112
141
|
|
|
113
|
-
# Validate Claude binary at startup
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
142
|
+
# Validate Claude binary at startup using the new function
|
|
143
|
+
try:
|
|
144
|
+
claude_info = await get_claude_cli_info()
|
|
145
|
+
|
|
146
|
+
if claude_info.status == "available":
|
|
147
|
+
logger.info(
|
|
148
|
+
"claude_cli_available",
|
|
149
|
+
status=claude_info.status,
|
|
150
|
+
version=claude_info.version,
|
|
151
|
+
binary_path=claude_info.binary_path,
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
logger.warning(
|
|
155
|
+
"claude_cli_unavailable",
|
|
156
|
+
status=claude_info.status,
|
|
157
|
+
error=claude_info.error,
|
|
158
|
+
binary_path=claude_info.binary_path,
|
|
159
|
+
message=f"Claude CLI status: {claude_info.status}",
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(
|
|
163
|
+
"claude_cli_check_failed",
|
|
164
|
+
error=str(e),
|
|
165
|
+
message="Failed to check Claude CLI status during startup",
|
|
121
166
|
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
167
|
+
|
|
168
|
+
# Initialize ClaudeSDKService and store in app state
|
|
169
|
+
try:
|
|
170
|
+
# Create auth manager with settings
|
|
171
|
+
auth_manager = CredentialsAuthManager()
|
|
172
|
+
|
|
173
|
+
# Get global metrics instance
|
|
174
|
+
metrics = get_metrics()
|
|
175
|
+
|
|
176
|
+
# Create ClaudeSDKService instance
|
|
177
|
+
claude_service = ClaudeSDKService(
|
|
178
|
+
auth_manager=auth_manager,
|
|
179
|
+
metrics=metrics,
|
|
180
|
+
settings=settings,
|
|
129
181
|
)
|
|
130
182
|
|
|
183
|
+
# Store in app state for reuse in dependencies
|
|
184
|
+
app.state.claude_service = claude_service
|
|
185
|
+
logger.debug("claude_sdk_service_initialized")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error("claude_sdk_service_initialization_failed", error=str(e))
|
|
188
|
+
# Continue startup even if ClaudeSDKService fails (graceful degradation)
|
|
189
|
+
|
|
131
190
|
# Start scheduler system
|
|
132
191
|
try:
|
|
133
192
|
scheduler = await start_scheduler(settings)
|
|
134
193
|
app.state.scheduler = scheduler
|
|
135
194
|
logger.debug("scheduler_initialized")
|
|
136
|
-
except
|
|
195
|
+
except SchedulerError as e:
|
|
137
196
|
logger.error("scheduler_initialization_failed", error=str(e))
|
|
138
197
|
# Continue startup even if scheduler fails (graceful degradation)
|
|
139
198
|
|
|
@@ -158,19 +217,81 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
158
217
|
logger.error("log_storage_initialization_failed", error=str(e))
|
|
159
218
|
# Continue without log storage (graceful degradation)
|
|
160
219
|
|
|
220
|
+
# Initialize permission service
|
|
221
|
+
try:
|
|
222
|
+
permission_service = get_permission_service()
|
|
223
|
+
|
|
224
|
+
# Only connect terminal handler if not using external handler
|
|
225
|
+
if settings.server.use_terminal_permission_handler:
|
|
226
|
+
# terminal_handler = TerminalPermissionHandler()
|
|
227
|
+
|
|
228
|
+
# TODO: Terminal handler should subscribe to events from the service
|
|
229
|
+
# instead of trying to set a handler directly
|
|
230
|
+
# The service uses an event-based architecture, not direct handlers
|
|
231
|
+
|
|
232
|
+
# logger.info(
|
|
233
|
+
# "permission_handler_configured",
|
|
234
|
+
# handler_type="terminal",
|
|
235
|
+
# message="Connected terminal handler to permission service",
|
|
236
|
+
# )
|
|
237
|
+
# app.state.terminal_handler = terminal_handler
|
|
238
|
+
pass
|
|
239
|
+
else:
|
|
240
|
+
logger.debug(
|
|
241
|
+
"permission_handler_configured",
|
|
242
|
+
handler_type="external_sse",
|
|
243
|
+
message="Terminal permission handler disabled - use 'ccproxy permission-handler connect' to handle permissions",
|
|
244
|
+
)
|
|
245
|
+
logger.warning(
|
|
246
|
+
"permission_handler_required",
|
|
247
|
+
message="Start external handler with: ccproxy permission-handler connect",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Start the permission service
|
|
251
|
+
await permission_service.start()
|
|
252
|
+
|
|
253
|
+
# Store references in app state
|
|
254
|
+
app.state.permission_service = permission_service
|
|
255
|
+
|
|
256
|
+
logger.debug(
|
|
257
|
+
"permission_service_initialized",
|
|
258
|
+
timeout_seconds=permission_service._timeout_seconds,
|
|
259
|
+
terminal_handler_enabled=settings.server.use_terminal_permission_handler,
|
|
260
|
+
)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error("permission_service_initialization_failed", error=str(e))
|
|
263
|
+
# Continue without permission service (API will work but without prompts)
|
|
264
|
+
|
|
161
265
|
yield
|
|
162
266
|
|
|
163
267
|
# Shutdown
|
|
164
268
|
logger.debug("server_stop")
|
|
165
269
|
|
|
270
|
+
# Flush any remaining streaming log batches
|
|
271
|
+
try:
|
|
272
|
+
from ccproxy.utils.simple_request_logger import flush_all_streaming_batches
|
|
273
|
+
|
|
274
|
+
await flush_all_streaming_batches()
|
|
275
|
+
logger.debug("streaming_batches_flushed")
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error("streaming_batches_flush_failed", error=str(e))
|
|
278
|
+
|
|
166
279
|
# Stop scheduler system
|
|
167
280
|
try:
|
|
168
281
|
scheduler = getattr(app.state, "scheduler", None)
|
|
169
282
|
await stop_scheduler(scheduler)
|
|
170
|
-
logger.debug("
|
|
171
|
-
except
|
|
283
|
+
logger.debug("scheduler_stopped_lifespan")
|
|
284
|
+
except SchedulerError as e:
|
|
172
285
|
logger.error("scheduler_stop_failed", error=str(e))
|
|
173
286
|
|
|
287
|
+
# Stop permission service
|
|
288
|
+
if hasattr(app.state, "permission_service") and app.state.permission_service:
|
|
289
|
+
try:
|
|
290
|
+
await app.state.permission_service.stop()
|
|
291
|
+
logger.debug("permission_service_stopped")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error("permission_service_stop_failed", error=str(e))
|
|
294
|
+
|
|
174
295
|
# Close log storage if initialized
|
|
175
296
|
if hasattr(app.state, "log_storage") and app.state.log_storage:
|
|
176
297
|
try:
|
|
@@ -191,28 +312,28 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
191
312
|
"""
|
|
192
313
|
if settings is None:
|
|
193
314
|
settings = get_settings()
|
|
194
|
-
|
|
195
315
|
# Configure logging based on settings BEFORE any module uses logger
|
|
196
316
|
# This is needed for reload mode where the app is re-imported
|
|
197
|
-
import logging
|
|
198
317
|
|
|
199
318
|
import structlog
|
|
200
319
|
|
|
201
|
-
from ccproxy.config.settings import config_manager
|
|
202
|
-
|
|
203
320
|
# Only configure if not already configured or if no file handler exists
|
|
204
|
-
root_logger = logging.getLogger()
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
321
|
+
# okay we have the first debug line but after uvicorn start they are not show root_logger = logging.getLogger()
|
|
322
|
+
# for h in root_logger.handlers:
|
|
323
|
+
# print(h)
|
|
324
|
+
# has_file_handler = any(
|
|
325
|
+
# isinstance(h, logging.FileHandler) for h in root_logger.handlers
|
|
326
|
+
# )
|
|
327
|
+
|
|
328
|
+
if not structlog.is_configured():
|
|
329
|
+
# Only setup logging if structlog is not configured at all
|
|
330
|
+
# Always use console output, but respect file logging from settings
|
|
212
331
|
json_logs = False
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
332
|
+
setup_logging(
|
|
333
|
+
json_logs=json_logs,
|
|
334
|
+
log_level_name=settings.server.log_level,
|
|
335
|
+
log_file=settings.server.log_file,
|
|
336
|
+
)
|
|
216
337
|
|
|
217
338
|
app = FastAPI(
|
|
218
339
|
title="CCProxy API Server",
|
|
@@ -225,10 +346,13 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
225
346
|
setup_cors_middleware(app, settings)
|
|
226
347
|
setup_error_handlers(app)
|
|
227
348
|
|
|
228
|
-
# Add
|
|
349
|
+
# Add request content logging middleware first (will run third due to middleware order)
|
|
350
|
+
app.add_middleware(RequestContentLoggingMiddleware)
|
|
351
|
+
|
|
352
|
+
# Add custom access log middleware second (will run second due to middleware order)
|
|
229
353
|
app.add_middleware(AccessLogMiddleware)
|
|
230
354
|
|
|
231
|
-
# Add request ID middleware
|
|
355
|
+
# Add request ID middleware third (will run first to initialize context)
|
|
232
356
|
app.add_middleware(RequestIDMiddleware)
|
|
233
357
|
|
|
234
358
|
# Add server header middleware (for non-proxy routes)
|
|
@@ -243,7 +367,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
243
367
|
app.include_router(prometheus_router, tags=["metrics"])
|
|
244
368
|
|
|
245
369
|
if settings.observability.logs_endpoints_enabled:
|
|
246
|
-
app.include_router(logs_router, tags=["logs"])
|
|
370
|
+
app.include_router(logs_router, prefix="/logs", tags=["logs"])
|
|
247
371
|
|
|
248
372
|
if settings.observability.dashboard_enabled:
|
|
249
373
|
app.include_router(dashboard_router, tags=["dashboard"])
|
|
@@ -256,6 +380,15 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
256
380
|
# New /api/ routes for proxy endpoints (includes OpenAI-compatible /v1/chat/completions)
|
|
257
381
|
app.include_router(proxy_router, prefix="/api", tags=["proxy-api"])
|
|
258
382
|
|
|
383
|
+
# Shared models endpoints for both SDK and proxy APIs
|
|
384
|
+
app.include_router(models_router, prefix="/sdk", tags=["claude-sdk", "models"])
|
|
385
|
+
app.include_router(models_router, prefix="/api", tags=["proxy-api", "models"])
|
|
386
|
+
|
|
387
|
+
# Confirmation endpoints for SSE streaming and responses
|
|
388
|
+
app.include_router(permissions_router, prefix="/permissions", tags=["permissions"])
|
|
389
|
+
|
|
390
|
+
setup_mcp(app)
|
|
391
|
+
|
|
259
392
|
# Mount static files for dashboard SPA
|
|
260
393
|
from pathlib import Path
|
|
261
394
|
|
ccproxy/api/dependencies.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
from fastapi import Depends, Request
|
|
6
6
|
from structlog import get_logger
|
|
7
7
|
|
|
8
|
-
from ccproxy.auth.dependencies import AuthManagerDep
|
|
8
|
+
from ccproxy.auth.dependencies import AuthManagerDep
|
|
9
9
|
from ccproxy.config.settings import Settings, get_settings
|
|
10
10
|
from ccproxy.core.http import BaseProxyClient
|
|
11
11
|
from ccproxy.observability import PrometheusMetrics, get_metrics
|
|
@@ -17,8 +17,67 @@ from ccproxy.services.proxy_service import ProxyService
|
|
|
17
17
|
|
|
18
18
|
logger = get_logger(__name__)
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
def get_cached_settings(request: Request) -> Settings:
|
|
22
|
+
"""Get cached settings from app state.
|
|
23
|
+
|
|
24
|
+
This avoids recomputing settings on every request by using the
|
|
25
|
+
settings instance computed during application startup.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
request: FastAPI request object
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Settings instance from app state
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
RuntimeError: If settings are not available in app state
|
|
35
|
+
"""
|
|
36
|
+
settings = getattr(request.app.state, "settings", None)
|
|
37
|
+
if settings is None:
|
|
38
|
+
# Fallback to get_settings() for safety, but this should not happen
|
|
39
|
+
# in normal operation after lifespan startup
|
|
40
|
+
logger.warning(
|
|
41
|
+
"Settings not found in app state, falling back to get_settings()"
|
|
42
|
+
)
|
|
43
|
+
settings = get_settings()
|
|
44
|
+
return settings
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_cached_claude_service(request: Request) -> ClaudeSDKService:
|
|
48
|
+
"""Get cached ClaudeSDKService from app state.
|
|
49
|
+
|
|
50
|
+
This avoids recreating the ClaudeSDKService on every request by using the
|
|
51
|
+
service instance created during application startup.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
request: FastAPI request object
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
ClaudeSDKService instance from app state
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
RuntimeError: If ClaudeSDKService is not available in app state
|
|
61
|
+
"""
|
|
62
|
+
claude_service = getattr(request.app.state, "claude_service", None)
|
|
63
|
+
if claude_service is None:
|
|
64
|
+
# Fallback to get_claude_service() for safety, but this should not happen
|
|
65
|
+
# in normal operation after lifespan startup
|
|
66
|
+
logger.warning(
|
|
67
|
+
"ClaudeSDKService not found in app state, falling back to get_claude_service()"
|
|
68
|
+
)
|
|
69
|
+
# Get dependencies manually for fallback
|
|
70
|
+
settings = get_cached_settings(request)
|
|
71
|
+
# Create a simple auth manager for fallback
|
|
72
|
+
from ccproxy.auth.credentials_adapter import CredentialsAuthManager
|
|
73
|
+
|
|
74
|
+
auth_manager = CredentialsAuthManager()
|
|
75
|
+
claude_service = get_claude_service(settings, auth_manager)
|
|
76
|
+
return claude_service
|
|
77
|
+
|
|
78
|
+
|
|
20
79
|
# Type aliases for dependency injection
|
|
21
|
-
SettingsDep = Annotated[Settings, Depends(
|
|
80
|
+
SettingsDep = Annotated[Settings, Depends(get_cached_settings)]
|
|
22
81
|
|
|
23
82
|
|
|
24
83
|
def get_claude_service(
|
|
@@ -134,7 +193,7 @@ async def get_duckdb_storage(request: Request) -> SimpleDuckDBStorage | None:
|
|
|
134
193
|
|
|
135
194
|
|
|
136
195
|
# Type aliases for service dependencies
|
|
137
|
-
ClaudeServiceDep = Annotated[ClaudeSDKService, Depends(
|
|
196
|
+
ClaudeServiceDep = Annotated[ClaudeSDKService, Depends(get_cached_claude_service)]
|
|
138
197
|
ProxyServiceDep = Annotated[ProxyService, Depends(get_proxy_service)]
|
|
139
198
|
ObservabilityMetricsDep = Annotated[
|
|
140
199
|
PrometheusMetrics, Depends(get_observability_metrics)
|
ccproxy/api/middleware/errors.py
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"""Error handling middleware for CCProxy API Server."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import structlog
|
|
7
3
|
from fastapi import FastAPI, HTTPException, Request
|
|
8
4
|
from fastapi.responses import JSONResponse
|
|
9
5
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
@@ -582,10 +578,10 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
582
578
|
else:
|
|
583
579
|
# Log with basic stack trace (no local variables)
|
|
584
580
|
stack_trace = None
|
|
585
|
-
|
|
586
|
-
|
|
581
|
+
# For structlog, we can always include traceback since structlog handles filtering
|
|
582
|
+
import traceback
|
|
587
583
|
|
|
588
|
-
|
|
584
|
+
stack_trace = traceback.format_exc()
|
|
589
585
|
|
|
590
586
|
logger.error(
|
|
591
587
|
"HTTP exception",
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Header preservation middleware to maintain proxy response headers."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
|
|
5
3
|
from fastapi import Request, Response
|
|
6
4
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
7
5
|
from starlette.types import ASGIApp
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Access logging middleware for structured HTTP request/response logging."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
import structlog
|
|
7
7
|
from fastapi import Request, Response
|
|
8
8
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
9
|
from starlette.types import ASGIApp
|
|
10
10
|
|
|
11
|
+
from ccproxy.api.dependencies import get_cached_settings
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
logger = structlog.get_logger(__name__)
|
|
13
15
|
|
|
@@ -37,9 +39,8 @@ class AccessLogMiddleware(BaseHTTPMiddleware):
|
|
|
37
39
|
start_time = time.perf_counter()
|
|
38
40
|
|
|
39
41
|
# Store log storage in request state if collection is enabled
|
|
40
|
-
from ccproxy.config.settings import get_settings
|
|
41
42
|
|
|
42
|
-
settings =
|
|
43
|
+
settings = get_cached_settings(request)
|
|
43
44
|
|
|
44
45
|
if settings.observability.logs_collection_enabled and hasattr(
|
|
45
46
|
request.app.state, "log_storage"
|