ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__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 (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -9,25 +9,34 @@ Key features:
9
9
  - Accurate timing measurement using time.perf_counter()
10
10
  - Request correlation with unique IDs
11
11
  - Structured logging integration
12
- - Async-safe context management
12
+ - Async-safe context management with contextvars
13
13
  - Exception handling and error tracking
14
14
  """
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import json
19
20
  import time
20
21
  import uuid
21
22
  from collections.abc import AsyncGenerator
22
23
  from contextlib import asynccontextmanager
24
+ from contextvars import ContextVar, Token
23
25
  from dataclasses import dataclass, field
24
26
  from datetime import UTC, datetime
25
27
  from typing import Any
26
28
 
27
29
  import structlog
28
30
 
31
+ from ccproxy.core.logging import TraceBoundLogger, get_logger
29
32
 
30
- logger = structlog.get_logger(__name__)
33
+
34
+ logger = get_logger(__name__)
35
+
36
+ # Context variable for async-safe request context propagation
37
+ request_context_var: ContextVar[RequestContext | None] = ContextVar(
38
+ "request_context", default=None
39
+ )
31
40
 
32
41
 
33
42
  @dataclass
@@ -41,10 +50,12 @@ class RequestContext:
41
50
 
42
51
  request_id: str
43
52
  start_time: float
44
- logger: structlog.BoundLogger
53
+ logger: structlog.stdlib.BoundLogger | TraceBoundLogger
45
54
  metadata: dict[str, Any] = field(default_factory=dict)
46
55
  storage: Any | None = None # Optional DuckDB storage instance
47
56
  log_timestamp: datetime | None = None # Datetime for consistent logging filenames
57
+ metrics: dict[str, Any] = field(default_factory=dict) # Request metrics storage
58
+ format_chain: list[str] | None = None # Format conversion chain
48
59
 
49
60
  @property
50
61
  def duration_ms(self) -> float:
@@ -80,6 +91,92 @@ class RequestContext:
80
91
  # Fallback to current time if not set
81
92
  return datetime.now(UTC).strftime("%Y%m%d%H%M%S")
82
93
 
94
+ def set_current(self) -> Token[RequestContext | None]:
95
+ """Set this context as the current request context.
96
+
97
+ Returns:
98
+ Token that can be used to restore the previous context
99
+ """
100
+ return request_context_var.set(self)
101
+
102
+ @staticmethod
103
+ def get_current() -> RequestContext | None:
104
+ """Get the current request context from async context.
105
+
106
+ Returns:
107
+ The current RequestContext or None if not set
108
+ """
109
+ return request_context_var.get()
110
+
111
+ def clear_current(self, token: Token[RequestContext | None]) -> None:
112
+ """Clear the current context and restore the previous one.
113
+
114
+ Args:
115
+ token: The token returned by set_current()
116
+ """
117
+ request_context_var.reset(token)
118
+
119
+ def to_dict(self) -> dict[str, Any]:
120
+ """Serialize the context to a dictionary for JSON logging.
121
+
122
+ Returns all context data including:
123
+ - Request ID and timing information
124
+ - All metadata (costs, tokens, model, etc.)
125
+ - All metrics
126
+ - Computed properties (duration_ms, duration_seconds)
127
+
128
+ Excludes non-serializable fields like logger and storage.
129
+ """
130
+ # Start with basic fields
131
+ data = {
132
+ "request_id": self.request_id,
133
+ "start_time": self.start_time,
134
+ }
135
+
136
+ # Add computed timing properties
137
+ try:
138
+ data["duration_ms"] = self.duration_ms
139
+ data["duration_seconds"] = self.duration_seconds
140
+ except Exception:
141
+ pass
142
+
143
+ # Add log timestamp if present
144
+ if self.log_timestamp:
145
+ try:
146
+ data["log_timestamp"] = self.log_timestamp.isoformat()
147
+ except Exception:
148
+ data["log_timestamp"] = str(self.log_timestamp)
149
+
150
+ # Add all metadata (includes costs, tokens, model info, etc.)
151
+ if self.metadata:
152
+ # Try to deep copy metadata to avoid reference issues
153
+ try:
154
+ # Ensure metadata is JSON serializable
155
+ data["metadata"] = json.loads(json.dumps(self.metadata, default=str))
156
+ except Exception:
157
+ data["metadata"] = self.metadata
158
+
159
+ # Add all metrics
160
+ if self.metrics:
161
+ try:
162
+ # Ensure metrics is JSON serializable
163
+ data["metrics"] = json.loads(json.dumps(self.metrics, default=str))
164
+ except Exception:
165
+ data["metrics"] = self.metrics
166
+
167
+ return data
168
+
169
+
170
+ async def get_request_event_stream() -> AsyncGenerator[dict[str, Any], None]:
171
+ """Async generator for request events used by analytics streaming.
172
+
173
+ This is a lightweight stub for type-checking and optional runtime use.
174
+ Integrations can replace or wrap this to provide actual event streams.
175
+ """
176
+ # Empty async generator
177
+ for _ in ():
178
+ yield {}
179
+
83
180
 
84
181
  @asynccontextmanager
85
182
  async def request_context(
@@ -125,8 +222,7 @@ async def request_context(
125
222
  "request_start", request_id=request_id, timestamp=time.time(), **initial_context
126
223
  )
127
224
 
128
- # Emit SSE event for real-time dashboard updates
129
- await _emit_request_start_event(request_id, initial_context)
225
+ # SSE events removed - functionality moved to plugins
130
226
 
131
227
  # Increment active requests if metrics provided
132
228
  if metrics:
@@ -142,31 +238,31 @@ async def request_context(
142
238
  log_timestamp=log_timestamp,
143
239
  )
144
240
 
241
+ # Set as current context for async propagation
242
+ token = ctx.set_current()
243
+
145
244
  try:
146
245
  yield ctx
147
246
 
148
247
  # Log successful completion with comprehensive access log
149
248
  duration_ms = ctx.duration_ms
150
249
 
151
- # Use the new unified access logger for comprehensive logging
152
- from ccproxy.observability.access_logger import log_request_access
250
+ # Also keep the original request_success event for debugging
251
+ # Merge metadata, avoiding duplicates
252
+ success_log_data = {
253
+ "request_id": request_id,
254
+ "duration_ms": duration_ms,
255
+ "duration_seconds": ctx.duration_seconds,
256
+ }
153
257
 
154
- await log_request_access(
155
- context=ctx,
156
- # Extract client info from metadata if available
157
- client_ip=ctx.metadata.get("client_ip"),
158
- user_agent=ctx.metadata.get("user_agent"),
159
- query=ctx.metadata.get("query"),
160
- storage=ctx.storage, # Pass storage from context
161
- )
258
+ # Add metadata, avoiding duplicates
259
+ for key, value in ctx.metadata.items():
260
+ if key not in ("duration_ms", "duration_seconds", "request_id"):
261
+ success_log_data[key] = value
162
262
 
163
- # Also keep the original request_success event for debugging
164
263
  request_logger.debug(
165
264
  "request_success",
166
- request_id=request_id,
167
- duration_ms=duration_ms,
168
- duration_seconds=ctx.duration_seconds,
169
- **ctx.metadata,
265
+ **success_log_data,
170
266
  )
171
267
 
172
268
  except Exception as e:
@@ -174,22 +270,34 @@ async def request_context(
174
270
  duration_ms = ctx.duration_ms
175
271
  error_type = type(e).__name__
176
272
 
273
+ # Merge metadata but ensure no duplicate duration fields
274
+ log_data = {
275
+ "request_id": request_id,
276
+ "duration_ms": duration_ms,
277
+ "duration_seconds": ctx.duration_seconds,
278
+ "error_type": error_type,
279
+ "error_message": str(e),
280
+ }
281
+
282
+ # Add metadata, avoiding duplicates
283
+ for key, value in ctx.metadata.items():
284
+ if key not in ("duration_ms", "duration_seconds"):
285
+ log_data[key] = value
286
+
177
287
  request_logger.error(
178
288
  "request_error",
179
- request_id=request_id,
180
- duration_ms=duration_ms,
181
- duration_seconds=ctx.duration_seconds,
182
- error_type=error_type,
183
- error_message=str(e),
184
- **ctx.metadata,
289
+ exc_info=e,
290
+ **log_data,
185
291
  )
186
292
 
187
- # Emit SSE event for real-time dashboard updates
188
- await _emit_request_error_event(request_id, error_type, str(e), ctx.metadata)
293
+ # SSE events removed - functionality moved to plugins
189
294
 
190
295
  # Re-raise the exception
191
296
  raise
192
297
  finally:
298
+ # Clear the current context
299
+ ctx.clear_current(token)
300
+
193
301
  # Decrement active requests if metrics provided
194
302
  if metrics:
195
303
  metrics.dec_active_requests()
@@ -273,6 +381,7 @@ async def timed_operation(
273
381
  duration_ms=duration_ms,
274
382
  error_type=error_type,
275
383
  error_message=str(e),
384
+ exc_info=e,
276
385
  **{
277
386
  k: v for k, v in op_context.items() if k not in ("logger", "start_time")
278
387
  },
@@ -395,69 +504,3 @@ async def tracked_request_context(
395
504
  finally:
396
505
  # Remove from tracker
397
506
  await tracker.remove_context(ctx.request_id)
398
-
399
-
400
- async def _emit_request_start_event(
401
- request_id: str, initial_context: dict[str, Any]
402
- ) -> None:
403
- """Emit SSE event for request start."""
404
- try:
405
- from ccproxy.observability.sse_events import emit_sse_event
406
-
407
- # Create event data for SSE
408
- sse_data = {
409
- "request_id": request_id,
410
- "method": initial_context.get("method"),
411
- "path": initial_context.get("path"),
412
- "client_ip": initial_context.get("client_ip"),
413
- "user_agent": initial_context.get("user_agent"),
414
- "query": initial_context.get("query"),
415
- }
416
-
417
- # Remove None values
418
- sse_data = {k: v for k, v in sse_data.items() if v is not None}
419
-
420
- await emit_sse_event("request_start", sse_data)
421
-
422
- except Exception as e:
423
- # Log error but don't fail the request
424
- logger.debug(
425
- "sse_emit_failed",
426
- event_type="request_start",
427
- error=str(e),
428
- request_id=request_id,
429
- )
430
-
431
-
432
- async def _emit_request_error_event(
433
- request_id: str, error_type: str, error_message: str, metadata: dict[str, Any]
434
- ) -> None:
435
- """Emit SSE event for request error."""
436
- try:
437
- from ccproxy.observability.sse_events import emit_sse_event
438
-
439
- # Create event data for SSE
440
- sse_data = {
441
- "request_id": request_id,
442
- "error_type": error_type,
443
- "error_message": error_message,
444
- "method": metadata.get("method"),
445
- "path": metadata.get("path"),
446
- "client_ip": metadata.get("client_ip"),
447
- "user_agent": metadata.get("user_agent"),
448
- "query": metadata.get("query"),
449
- }
450
-
451
- # Remove None values
452
- sse_data = {k: v for k, v in sse_data.items() if v is not None}
453
-
454
- await emit_sse_event("request_error", sse_data)
455
-
456
- except Exception as e:
457
- # Log error but don't fail the request
458
- logger.debug(
459
- "sse_emit_failed",
460
- event_type="request_error",
461
- error=str(e),
462
- request_id=request_id,
463
- )
@@ -0,0 +1,211 @@
1
+ """Helpers for collecting status information used across interfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ from ccproxy.config.settings import Settings
11
+ from ccproxy.core.logging import get_logger
12
+ from ccproxy.core.plugins.discovery import discover_and_load_plugins
13
+
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DirectoryStatus:
20
+ """Represents availability of a directory."""
21
+
22
+ path: Path
23
+ exists: bool
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class SystemSnapshot:
28
+ """Summary of system-level settings."""
29
+
30
+ host: str
31
+ port: int
32
+ log_level: str
33
+ auth_token_configured: bool
34
+ plugins_enabled: bool
35
+ plugin_directories: tuple[DirectoryStatus, ...]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ConfigSource:
40
+ """Represents a potential configuration file location."""
41
+
42
+ path: Path
43
+ exists: bool
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ConfigSnapshot:
48
+ """Collected configuration source information."""
49
+
50
+ sources: tuple[ConfigSource, ...]
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class PluginInfo:
55
+ """Represents the status of an individual plugin."""
56
+
57
+ name: str
58
+ state: Literal["enabled", "error"]
59
+ version: str | None
60
+ description: str | None
61
+ error: str | None = None
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class PluginSnapshot:
66
+ """Collection of plugin discovery results."""
67
+
68
+ plugin_system_enabled: bool
69
+ enabled_plugins: tuple[PluginInfo, ...]
70
+ disabled_plugins: tuple[str, ...]
71
+ configuration_notes: tuple[str, ...]
72
+
73
+ @property
74
+ def enabled_count(self) -> int:
75
+ return len(self.enabled_plugins)
76
+
77
+ @property
78
+ def disabled_count(self) -> int:
79
+ return len(self.disabled_plugins)
80
+
81
+ @property
82
+ def total_count(self) -> int:
83
+ return self.enabled_count + self.disabled_count
84
+
85
+
86
+ def collect_system_snapshot(settings: Settings) -> SystemSnapshot:
87
+ """Build a system snapshot from settings."""
88
+ directories: list[DirectoryStatus] = []
89
+ for directory in settings.plugin_discovery.directories:
90
+ dir_path = Path(directory)
91
+ directories.append(DirectoryStatus(path=dir_path, exists=dir_path.exists()))
92
+
93
+ return SystemSnapshot(
94
+ host=str(settings.server.host),
95
+ port=int(settings.server.port),
96
+ log_level=settings.logging.level.upper(),
97
+ auth_token_configured=bool(settings.security.auth_token),
98
+ plugins_enabled=bool(settings.enable_plugins),
99
+ plugin_directories=tuple(directories),
100
+ )
101
+
102
+
103
+ def collect_config_snapshot(*, cwd: Path | None = None) -> ConfigSnapshot:
104
+ """Inspect common configuration locations relative to the current working directory."""
105
+ effective_cwd = cwd or Path.cwd()
106
+ candidates: Iterable[Path] = (
107
+ effective_cwd / ".ccproxy.toml",
108
+ effective_cwd / "ccproxy.toml",
109
+ Path.home() / ".ccproxy" / "config.toml",
110
+ )
111
+
112
+ sources = tuple(
113
+ ConfigSource(path=path, exists=path.exists()) for path in candidates
114
+ )
115
+ return ConfigSnapshot(sources=sources)
116
+
117
+
118
+ def collect_plugin_snapshot(settings: Settings) -> PluginSnapshot:
119
+ """Discover plugins and report basic status information."""
120
+ if not settings.enable_plugins:
121
+ notes = _collect_plugin_configuration_notes(settings)
122
+ return PluginSnapshot(
123
+ plugin_system_enabled=False,
124
+ enabled_plugins=(),
125
+ disabled_plugins=(),
126
+ configuration_notes=notes,
127
+ )
128
+
129
+ plugin_infos: list[PluginInfo] = []
130
+ try:
131
+ plugin_factories = discover_and_load_plugins(settings)
132
+ except Exception as exc: # pragma: no cover - defensive guard
133
+ logger.error("plugin_discovery_failed", error=str(exc), exc_info=exc)
134
+ return PluginSnapshot(
135
+ plugin_system_enabled=True,
136
+ enabled_plugins=(),
137
+ disabled_plugins=(),
138
+ configuration_notes=_collect_plugin_configuration_notes(settings),
139
+ )
140
+
141
+ for name, factory in sorted(plugin_factories.items()):
142
+ try:
143
+ manifest = factory.get_manifest()
144
+ plugin_infos.append(
145
+ PluginInfo(
146
+ name=name,
147
+ state="enabled",
148
+ version=manifest.version,
149
+ description=manifest.description,
150
+ )
151
+ )
152
+ except Exception as exc:
153
+ error_text = str(exc)
154
+ logger.error(
155
+ "plugin_manifest_failed", plugin=name, error=error_text, exc_info=exc
156
+ )
157
+ plugin_infos.append(
158
+ PluginInfo(
159
+ name=name,
160
+ state="error",
161
+ version=None,
162
+ description=None,
163
+ error=error_text,
164
+ )
165
+ )
166
+
167
+ disabled_plugins = tuple(
168
+ sorted(_find_disabled_plugins(settings, set(plugin_factories.keys())))
169
+ )
170
+
171
+ return PluginSnapshot(
172
+ plugin_system_enabled=True,
173
+ enabled_plugins=tuple(plugin_infos),
174
+ disabled_plugins=disabled_plugins,
175
+ configuration_notes=_collect_plugin_configuration_notes(settings),
176
+ )
177
+
178
+
179
+ def _collect_plugin_configuration_notes(settings: Settings) -> tuple[str, ...]:
180
+ notes: list[str] = []
181
+ if settings.plugins_disable_local_discovery:
182
+ notes.append("Local discovery disabled")
183
+ if settings.disabled_plugins:
184
+ notes.append(f"Explicitly disabled: {len(settings.disabled_plugins)}")
185
+ if settings.enabled_plugins:
186
+ notes.append(f"Allow-list active: {len(settings.enabled_plugins)} allowed")
187
+ return tuple(notes)
188
+
189
+
190
+ def _find_disabled_plugins(settings: Settings, enabled_plugins: set[str]) -> set[str]:
191
+ """Find plugins that exist but are disabled in the configuration."""
192
+ disabled: set[str] = set()
193
+
194
+ if settings.plugins_disable_local_discovery:
195
+ return disabled
196
+
197
+ for plugin_dir_path in settings.plugin_discovery.directories:
198
+ plugin_dir = Path(plugin_dir_path)
199
+ if not plugin_dir.exists():
200
+ continue
201
+
202
+ for item in plugin_dir.iterdir():
203
+ if not item.is_dir() or item.name.startswith("_"):
204
+ continue
205
+ plugin_file = item / "plugin.py"
206
+ if not plugin_file.exists():
207
+ continue
208
+ if item.name not in enabled_plugins:
209
+ disabled.add(item.name)
210
+
211
+ return disabled
@@ -1,10 +1,10 @@
1
1
  """Core transformer abstractions for request/response transformation."""
2
2
 
3
+ import time
3
4
  from abc import ABC, abstractmethod
4
5
  from typing import TYPE_CHECKING, Any, Protocol, TypeVar, runtime_checkable
5
6
 
6
- from structlog import get_logger
7
-
7
+ from ccproxy.core.logging import get_logger
8
8
  from ccproxy.core.types import ProxyRequest, ProxyResponse, TransformContext
9
9
 
10
10
 
@@ -65,8 +65,8 @@ class BaseTransformer(ABC):
65
65
 
66
66
  try:
67
67
  # Calculate data sizes
68
- input_size = self._calculate_data_size(input_data)
69
- output_size = self._calculate_data_size(output_data) if output_data else 0
68
+ # input_size = self._calculate_data_size(input_data)
69
+ # output_size = self._calculate_data_size(output_data) if output_data else 0
70
70
 
71
71
  # Create a unique request ID for this transformation
72
72
  request_id = (
@@ -80,6 +80,15 @@ class BaseTransformer(ABC):
80
80
  processing_time=duration_ms,
81
81
  )
82
82
 
83
+ except (AttributeError, TypeError) as e:
84
+ # Don't let metrics collection fail the transformation
85
+ logger = get_logger(__name__)
86
+ # logger = logging.getLogger(__name__)
87
+ logger.debug(
88
+ "transformation_metrics_attribute_error",
89
+ error=str(e),
90
+ exc_info=e,
91
+ )
83
92
  except Exception as e:
84
93
  # Don't let metrics collection fail the transformation
85
94
  logger = get_logger(__name__)
@@ -131,8 +140,6 @@ class RequestTransformer(BaseTransformer):
131
140
  Returns:
132
141
  The transformed request
133
142
  """
134
- import time
135
-
136
143
  start_time = time.perf_counter()
137
144
  error_msg = None
138
145
  result = None
@@ -186,8 +193,6 @@ class ResponseTransformer(BaseTransformer):
186
193
  Returns:
187
194
  The transformed response
188
195
  """
189
- import time
190
-
191
196
  start_time = time.perf_counter()
192
197
  error_msg = None
193
198
  result = None