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.
Files changed (108) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/openai/__init__.py +1 -2
  3. ccproxy/adapters/openai/adapter.py +218 -180
  4. ccproxy/adapters/openai/streaming.py +247 -65
  5. ccproxy/api/__init__.py +0 -3
  6. ccproxy/api/app.py +173 -40
  7. ccproxy/api/dependencies.py +62 -3
  8. ccproxy/api/middleware/errors.py +3 -7
  9. ccproxy/api/middleware/headers.py +0 -2
  10. ccproxy/api/middleware/logging.py +4 -3
  11. ccproxy/api/middleware/request_content_logging.py +297 -0
  12. ccproxy/api/middleware/request_id.py +5 -0
  13. ccproxy/api/middleware/server_header.py +0 -4
  14. ccproxy/api/routes/__init__.py +9 -1
  15. ccproxy/api/routes/claude.py +23 -32
  16. ccproxy/api/routes/health.py +58 -4
  17. ccproxy/api/routes/mcp.py +171 -0
  18. ccproxy/api/routes/metrics.py +4 -8
  19. ccproxy/api/routes/permissions.py +217 -0
  20. ccproxy/api/routes/proxy.py +0 -53
  21. ccproxy/api/services/__init__.py +6 -0
  22. ccproxy/api/services/permission_service.py +368 -0
  23. ccproxy/api/ui/__init__.py +6 -0
  24. ccproxy/api/ui/permission_handler_protocol.py +33 -0
  25. ccproxy/api/ui/terminal_permission_handler.py +593 -0
  26. ccproxy/auth/conditional.py +2 -2
  27. ccproxy/auth/dependencies.py +1 -1
  28. ccproxy/auth/oauth/models.py +0 -1
  29. ccproxy/auth/oauth/routes.py +1 -3
  30. ccproxy/auth/storage/json_file.py +0 -1
  31. ccproxy/auth/storage/keyring.py +0 -3
  32. ccproxy/claude_sdk/__init__.py +2 -0
  33. ccproxy/claude_sdk/client.py +91 -8
  34. ccproxy/claude_sdk/converter.py +405 -210
  35. ccproxy/claude_sdk/options.py +76 -29
  36. ccproxy/claude_sdk/parser.py +200 -0
  37. ccproxy/claude_sdk/streaming.py +286 -0
  38. ccproxy/cli/commands/__init__.py +5 -2
  39. ccproxy/cli/commands/auth.py +2 -4
  40. ccproxy/cli/commands/permission_handler.py +553 -0
  41. ccproxy/cli/commands/serve.py +30 -12
  42. ccproxy/cli/docker/params.py +0 -4
  43. ccproxy/cli/helpers.py +0 -2
  44. ccproxy/cli/main.py +5 -16
  45. ccproxy/cli/options/claude_options.py +19 -1
  46. ccproxy/cli/options/core_options.py +0 -3
  47. ccproxy/cli/options/security_options.py +0 -2
  48. ccproxy/cli/options/server_options.py +3 -2
  49. ccproxy/config/auth.py +0 -1
  50. ccproxy/config/claude.py +78 -2
  51. ccproxy/config/discovery.py +0 -1
  52. ccproxy/config/docker_settings.py +0 -1
  53. ccproxy/config/loader.py +1 -4
  54. ccproxy/config/scheduler.py +20 -0
  55. ccproxy/config/security.py +7 -2
  56. ccproxy/config/server.py +5 -0
  57. ccproxy/config/settings.py +13 -7
  58. ccproxy/config/validators.py +1 -1
  59. ccproxy/core/async_utils.py +1 -4
  60. ccproxy/core/errors.py +45 -1
  61. ccproxy/core/http_transformers.py +4 -3
  62. ccproxy/core/interfaces.py +2 -2
  63. ccproxy/core/logging.py +97 -95
  64. ccproxy/core/middleware.py +1 -1
  65. ccproxy/core/proxy.py +1 -1
  66. ccproxy/core/transformers.py +1 -1
  67. ccproxy/core/types.py +1 -1
  68. ccproxy/docker/models.py +1 -1
  69. ccproxy/docker/protocol.py +0 -3
  70. ccproxy/models/__init__.py +41 -0
  71. ccproxy/models/claude_sdk.py +420 -0
  72. ccproxy/models/messages.py +45 -18
  73. ccproxy/models/permissions.py +115 -0
  74. ccproxy/models/requests.py +1 -1
  75. ccproxy/models/responses.py +29 -2
  76. ccproxy/observability/access_logger.py +1 -2
  77. ccproxy/observability/context.py +17 -1
  78. ccproxy/observability/metrics.py +1 -3
  79. ccproxy/observability/pushgateway.py +0 -2
  80. ccproxy/observability/stats_printer.py +2 -4
  81. ccproxy/observability/storage/duckdb_simple.py +1 -1
  82. ccproxy/observability/storage/models.py +0 -1
  83. ccproxy/pricing/cache.py +0 -1
  84. ccproxy/pricing/loader.py +5 -21
  85. ccproxy/pricing/updater.py +0 -1
  86. ccproxy/scheduler/__init__.py +1 -0
  87. ccproxy/scheduler/core.py +6 -6
  88. ccproxy/scheduler/manager.py +35 -7
  89. ccproxy/scheduler/registry.py +1 -1
  90. ccproxy/scheduler/tasks.py +127 -2
  91. ccproxy/services/claude_sdk_service.py +220 -328
  92. ccproxy/services/credentials/manager.py +0 -1
  93. ccproxy/services/credentials/oauth_client.py +1 -2
  94. ccproxy/services/proxy_service.py +93 -222
  95. ccproxy/testing/config.py +1 -1
  96. ccproxy/testing/mock_responses.py +0 -1
  97. ccproxy/utils/model_mapping.py +197 -0
  98. ccproxy/utils/models_provider.py +150 -0
  99. ccproxy/utils/simple_request_logger.py +284 -0
  100. ccproxy/utils/version_checker.py +184 -0
  101. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
  102. ccproxy_api-0.1.3.dist-info/RECORD +166 -0
  103. ccproxy/cli/commands/permission.py +0 -128
  104. ccproxy_api-0.1.2.dist-info/RECORD +0 -150
  105. /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
  106. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
  107. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +0 -0
  108. {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 FastAPI, HTTPException
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.info(
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.info("auth_token_valid", credentials_path=str(validation.path))
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
- claude_path, found_in_path = settings.claude.find_claude_cli()
115
- if claude_path:
116
- logger.info(
117
- "claude_binary_found",
118
- path=claude_path,
119
- found_in_path=found_in_path,
120
- message=f"Claude CLI binary found at: {claude_path}",
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
- else:
123
- searched_paths = settings.claude.get_searched_paths()
124
- logger.warning(
125
- "claude_binary_not_found",
126
- message="Claude CLI binary not found. Please install Claude CLI to use SDK features.",
127
- searched_paths=searched_paths,
128
- install_command="npm install -g @anthropic-ai/claude-code",
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 Exception as e:
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("scheduler_stopped")
171
- except Exception as e:
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
- has_file_handler = any(
206
- isinstance(h, logging.FileHandler) for h in root_logger.handlers
207
- )
208
-
209
- if not structlog.is_configured() or not has_file_handler:
210
- # Only setup logging if not already configured with file handler
211
- # Always use console output
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
- # Don't override file logging if it was already configured
214
- if not has_file_handler:
215
- setup_logging(json_logs=json_logs, log_level=settings.server.log_level)
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 custom access log middleware first (will run second due to middleware order)
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 second (will run first to initialize context)
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
 
@@ -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, get_auth_manager
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(get_settings)]
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(get_claude_service)]
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)
@@ -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
- if logger.isEnabledFor(logging.DEBUG):
586
- import traceback
581
+ # For structlog, we can always include traceback since structlog handles filtering
582
+ import traceback
587
583
 
588
- stack_trace = traceback.format_exc()
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, Optional
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 = get_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"