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,484 @@
1
+ """Base scheduled task classes and task implementations."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import random
6
+ import time
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+ from .exceptions import TaskExecutionError
13
+
14
+
15
+ logger = structlog.get_logger(__name__)
16
+
17
+
18
+ class BaseScheduledTask(ABC):
19
+ """
20
+ Abstract base class for all scheduled tasks.
21
+
22
+ Provides common functionality for task lifecycle management, error handling,
23
+ and exponential backoff for failed executions.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ interval_seconds: float,
30
+ enabled: bool = True,
31
+ max_backoff_seconds: float = 300.0,
32
+ jitter_factor: float = 0.25,
33
+ ):
34
+ """
35
+ Initialize scheduled task.
36
+
37
+ Args:
38
+ name: Human-readable task name
39
+ interval_seconds: Interval between task executions in seconds
40
+ enabled: Whether the task is enabled
41
+ max_backoff_seconds: Maximum backoff delay for failed tasks
42
+ jitter_factor: Jitter factor for backoff randomization (0.0-1.0)
43
+ """
44
+ self.name = name
45
+ self.interval_seconds = max(1.0, interval_seconds)
46
+ self.enabled = enabled
47
+ self.max_backoff_seconds = max_backoff_seconds
48
+ self.jitter_factor = min(1.0, max(0.0, jitter_factor))
49
+
50
+ self._consecutive_failures = 0
51
+ self._last_run_time: float = 0
52
+ self._running = False
53
+ self._task: asyncio.Task[Any] | None = None
54
+
55
+ @abstractmethod
56
+ async def run(self) -> bool:
57
+ """
58
+ Execute the scheduled task.
59
+
60
+ Returns:
61
+ True if execution was successful, False otherwise
62
+ """
63
+ pass
64
+
65
+ async def setup(self) -> None:
66
+ """
67
+ Perform any setup required before task execution starts.
68
+
69
+ Called once when the task is first started. Override if needed.
70
+ Default implementation does nothing.
71
+ """
72
+ # Default implementation - subclasses can override if needed
73
+ return
74
+
75
+ async def cleanup(self) -> None:
76
+ """
77
+ Perform any cleanup required after task execution stops.
78
+
79
+ Called once when the task is stopped. Override if needed.
80
+ Default implementation does nothing.
81
+ """
82
+ # Default implementation - subclasses can override if needed
83
+ return
84
+
85
+ def calculate_next_delay(self) -> float:
86
+ """
87
+ Calculate the delay before the next task execution.
88
+
89
+ Returns exponential backoff delay for failed tasks, or normal interval
90
+ for successful tasks, with optional jitter.
91
+
92
+ Returns:
93
+ Delay in seconds before next execution
94
+ """
95
+ if self._consecutive_failures == 0:
96
+ base_delay = self.interval_seconds
97
+ else:
98
+ # Exponential backoff: interval * (2 ^ failures)
99
+ base_delay = self.interval_seconds * (2**self._consecutive_failures)
100
+ base_delay = min(base_delay, self.max_backoff_seconds)
101
+
102
+ # Add jitter to prevent thundering herd
103
+ if self.jitter_factor > 0:
104
+ jitter = base_delay * self.jitter_factor * (random.random() - 0.5)
105
+ base_delay += jitter
106
+
107
+ return max(1.0, base_delay)
108
+
109
+ async def start(self) -> None:
110
+ """Start the scheduled task execution loop."""
111
+ if self._running or not self.enabled:
112
+ return
113
+
114
+ self._running = True
115
+ logger.debug("task_starting", task_name=self.name)
116
+
117
+ try:
118
+ await self.setup()
119
+ self._task = asyncio.create_task(self._run_loop())
120
+ logger.debug("task_started", task_name=self.name)
121
+ except Exception as e:
122
+ self._running = False
123
+ logger.error(
124
+ "task_start_failed",
125
+ task_name=self.name,
126
+ error=str(e),
127
+ error_type=type(e).__name__,
128
+ )
129
+ raise
130
+
131
+ async def stop(self) -> None:
132
+ """Stop the scheduled task execution loop."""
133
+ if not self._running:
134
+ return
135
+
136
+ self._running = False
137
+ logger.debug("task_stopping", task_name=self.name)
138
+
139
+ # Cancel the running task
140
+ if self._task and not self._task.done():
141
+ self._task.cancel()
142
+ with contextlib.suppress(asyncio.CancelledError):
143
+ await self._task
144
+
145
+ try:
146
+ await self.cleanup()
147
+ logger.debug("task_stopped", task_name=self.name)
148
+ except Exception as e:
149
+ logger.error(
150
+ "task_cleanup_failed",
151
+ task_name=self.name,
152
+ error=str(e),
153
+ error_type=type(e).__name__,
154
+ )
155
+
156
+ async def _run_loop(self) -> None:
157
+ """Main execution loop for the scheduled task."""
158
+ while self._running:
159
+ try:
160
+ start_time = time.time()
161
+
162
+ # Execute the task
163
+ success = await self.run()
164
+
165
+ execution_time = time.time() - start_time
166
+
167
+ if success:
168
+ self._consecutive_failures = 0
169
+ logger.debug(
170
+ "task_execution_success",
171
+ task_name=self.name,
172
+ execution_time=execution_time,
173
+ )
174
+ else:
175
+ self._consecutive_failures += 1
176
+ logger.warning(
177
+ "task_execution_failed",
178
+ task_name=self.name,
179
+ consecutive_failures=self._consecutive_failures,
180
+ execution_time=execution_time,
181
+ )
182
+
183
+ self._last_run_time = time.time()
184
+
185
+ # Calculate delay before next execution
186
+ delay = self.calculate_next_delay()
187
+
188
+ if not success and self._consecutive_failures > 1:
189
+ logger.info(
190
+ "task_backoff_delay",
191
+ task_name=self.name,
192
+ consecutive_failures=self._consecutive_failures,
193
+ delay=delay,
194
+ max_backoff=self.max_backoff_seconds,
195
+ )
196
+
197
+ # Wait for next execution or cancellation
198
+ await asyncio.sleep(delay)
199
+
200
+ except asyncio.CancelledError:
201
+ logger.debug("task_cancelled", task_name=self.name)
202
+ break
203
+ except Exception as e:
204
+ self._consecutive_failures += 1
205
+ logger.error(
206
+ "task_execution_error",
207
+ task_name=self.name,
208
+ error=str(e),
209
+ error_type=type(e).__name__,
210
+ consecutive_failures=self._consecutive_failures,
211
+ )
212
+
213
+ # Use backoff delay for exceptions too
214
+ backoff_delay = self.calculate_next_delay()
215
+ await asyncio.sleep(backoff_delay)
216
+
217
+ @property
218
+ def is_running(self) -> bool:
219
+ """Check if the task is currently running."""
220
+ return self._running
221
+
222
+ @property
223
+ def consecutive_failures(self) -> int:
224
+ """Get the number of consecutive failures."""
225
+ return self._consecutive_failures
226
+
227
+ @property
228
+ def last_run_time(self) -> float:
229
+ """Get the timestamp of the last execution."""
230
+ return self._last_run_time
231
+
232
+ def get_status(self) -> dict[str, Any]:
233
+ """
234
+ Get current task status information.
235
+
236
+ Returns:
237
+ Dictionary with task status details
238
+ """
239
+ return {
240
+ "name": self.name,
241
+ "enabled": self.enabled,
242
+ "running": self.is_running,
243
+ "interval_seconds": self.interval_seconds,
244
+ "consecutive_failures": self.consecutive_failures,
245
+ "last_run_time": self.last_run_time,
246
+ "next_delay": self.calculate_next_delay() if self.is_running else None,
247
+ }
248
+
249
+
250
+ class PushgatewayTask(BaseScheduledTask):
251
+ """Task for pushing metrics to Pushgateway periodically."""
252
+
253
+ def __init__(
254
+ self,
255
+ name: str,
256
+ interval_seconds: float,
257
+ enabled: bool = True,
258
+ max_backoff_seconds: float = 300.0,
259
+ ):
260
+ """
261
+ Initialize pushgateway task.
262
+
263
+ Args:
264
+ name: Task name
265
+ interval_seconds: Interval between pushgateway operations
266
+ enabled: Whether task is enabled
267
+ max_backoff_seconds: Maximum backoff delay for failures
268
+ """
269
+ super().__init__(
270
+ name=name,
271
+ interval_seconds=interval_seconds,
272
+ enabled=enabled,
273
+ max_backoff_seconds=max_backoff_seconds,
274
+ )
275
+ self._metrics_instance: Any | None = None
276
+
277
+ async def setup(self) -> None:
278
+ """Initialize metrics instance for pushgateway operations."""
279
+ try:
280
+ from ccproxy.observability.metrics import get_metrics
281
+
282
+ self._metrics_instance = get_metrics()
283
+ logger.debug("pushgateway_task_setup_complete", task_name=self.name)
284
+ except Exception as e:
285
+ logger.error(
286
+ "pushgateway_task_setup_failed",
287
+ task_name=self.name,
288
+ error=str(e),
289
+ error_type=type(e).__name__,
290
+ )
291
+ raise
292
+
293
+ async def run(self) -> bool:
294
+ """Execute pushgateway metrics push."""
295
+ try:
296
+ if not self._metrics_instance:
297
+ logger.warning("pushgateway_no_metrics_instance", task_name=self.name)
298
+ return False
299
+
300
+ if not self._metrics_instance.is_pushgateway_enabled():
301
+ logger.debug("pushgateway_disabled", task_name=self.name)
302
+ return True # Not an error, just disabled
303
+
304
+ success = bool(self._metrics_instance.push_to_gateway())
305
+
306
+ if success:
307
+ logger.debug("pushgateway_push_success", task_name=self.name)
308
+ else:
309
+ logger.warning("pushgateway_push_failed", task_name=self.name)
310
+
311
+ return success
312
+
313
+ except Exception as e:
314
+ logger.error(
315
+ "pushgateway_task_error",
316
+ task_name=self.name,
317
+ error=str(e),
318
+ error_type=type(e).__name__,
319
+ )
320
+ return False
321
+
322
+
323
+ class StatsPrintingTask(BaseScheduledTask):
324
+ """Task for printing stats summary periodically."""
325
+
326
+ def __init__(
327
+ self,
328
+ name: str,
329
+ interval_seconds: float,
330
+ enabled: bool = True,
331
+ ):
332
+ """
333
+ Initialize stats printing task.
334
+
335
+ Args:
336
+ name: Task name
337
+ interval_seconds: Interval between stats printing
338
+ enabled: Whether task is enabled
339
+ """
340
+ super().__init__(
341
+ name=name,
342
+ interval_seconds=interval_seconds,
343
+ enabled=enabled,
344
+ )
345
+ self._stats_collector_instance: Any | None = None
346
+ self._metrics_instance: Any | None = None
347
+
348
+ async def setup(self) -> None:
349
+ """Initialize stats collector and metrics instances."""
350
+ try:
351
+ from ccproxy.config.settings import get_settings
352
+ from ccproxy.observability.metrics import get_metrics
353
+ from ccproxy.observability.stats_printer import get_stats_collector
354
+
355
+ self._metrics_instance = get_metrics()
356
+ settings = get_settings()
357
+ self._stats_collector_instance = get_stats_collector(
358
+ settings=settings.observability,
359
+ metrics_instance=self._metrics_instance,
360
+ )
361
+ logger.debug("stats_printing_task_setup_complete", task_name=self.name)
362
+ except Exception as e:
363
+ logger.error(
364
+ "stats_printing_task_setup_failed",
365
+ task_name=self.name,
366
+ error=str(e),
367
+ error_type=type(e).__name__,
368
+ )
369
+ raise
370
+
371
+ async def run(self) -> bool:
372
+ """Execute stats printing."""
373
+ try:
374
+ if not self._stats_collector_instance:
375
+ logger.warning("stats_printing_no_collector", task_name=self.name)
376
+ return False
377
+
378
+ await self._stats_collector_instance.print_stats()
379
+ logger.debug("stats_printing_success", task_name=self.name)
380
+ return True
381
+
382
+ except Exception as e:
383
+ logger.error(
384
+ "stats_printing_task_error",
385
+ task_name=self.name,
386
+ error=str(e),
387
+ error_type=type(e).__name__,
388
+ )
389
+ return False
390
+
391
+
392
+ class PricingCacheUpdateTask(BaseScheduledTask):
393
+ """Task for updating pricing cache periodically."""
394
+
395
+ def __init__(
396
+ self,
397
+ name: str,
398
+ interval_seconds: float,
399
+ enabled: bool = True,
400
+ force_refresh_on_startup: bool = False,
401
+ pricing_updater: Any | None = None,
402
+ ):
403
+ """
404
+ Initialize pricing cache update task.
405
+
406
+ Args:
407
+ name: Task name
408
+ interval_seconds: Interval between pricing updates
409
+ enabled: Whether task is enabled
410
+ force_refresh_on_startup: Whether to force refresh on first run
411
+ pricing_updater: Injected pricing updater instance
412
+ """
413
+ super().__init__(
414
+ name=name,
415
+ interval_seconds=interval_seconds,
416
+ enabled=enabled,
417
+ )
418
+ self.force_refresh_on_startup = force_refresh_on_startup
419
+ self._pricing_updater = pricing_updater
420
+ self._first_run = True
421
+
422
+ async def setup(self) -> None:
423
+ """Initialize pricing updater instance if not injected."""
424
+ if self._pricing_updater is None:
425
+ try:
426
+ from ccproxy.config.pricing import PricingSettings
427
+ from ccproxy.pricing.cache import PricingCache
428
+ from ccproxy.pricing.updater import PricingUpdater
429
+
430
+ # Create pricing components with dependency injection
431
+ settings = PricingSettings()
432
+ cache = PricingCache(settings)
433
+ self._pricing_updater = PricingUpdater(cache, settings)
434
+ logger.debug("pricing_update_task_setup_complete", task_name=self.name)
435
+ except Exception as e:
436
+ logger.error(
437
+ "pricing_update_task_setup_failed",
438
+ task_name=self.name,
439
+ error=str(e),
440
+ error_type=type(e).__name__,
441
+ )
442
+ raise
443
+ else:
444
+ logger.debug(
445
+ "pricing_update_task_using_injected_updater", task_name=self.name
446
+ )
447
+
448
+ async def run(self) -> bool:
449
+ """Execute pricing cache update."""
450
+ try:
451
+ if not self._pricing_updater:
452
+ logger.warning("pricing_update_no_updater", task_name=self.name)
453
+ return False
454
+
455
+ # Force refresh on first run if configured
456
+ force_refresh = self._first_run and self.force_refresh_on_startup
457
+ self._first_run = False
458
+
459
+ if force_refresh:
460
+ logger.info("pricing_update_force_refresh_startup", task_name=self.name)
461
+ refresh_result = await self._pricing_updater.force_refresh()
462
+ success = bool(refresh_result)
463
+ else:
464
+ # Regular update check
465
+ pricing_data = await self._pricing_updater.get_current_pricing(
466
+ force_refresh=False
467
+ )
468
+ success = pricing_data is not None
469
+
470
+ if success:
471
+ logger.debug("pricing_update_success", task_name=self.name)
472
+ else:
473
+ logger.warning("pricing_update_failed", task_name=self.name)
474
+
475
+ return success
476
+
477
+ except Exception as e:
478
+ logger.error(
479
+ "pricing_update_task_error",
480
+ task_name=self.name,
481
+ error=str(e),
482
+ error_type=type(e).__name__,
483
+ )
484
+ return False
@@ -0,0 +1,10 @@
1
+ """Services module for Claude Proxy API Server."""
2
+
3
+ # NOTE: Imports removed to avoid circular dependency issues.
4
+ # Import services directly from their modules as needed.
5
+
6
+ __all__ = [
7
+ "ClaudeSDKService",
8
+ "MetricsService",
9
+ "ProxyService",
10
+ ]