ccproxy-api 0.1.6__py3-none-any.whl → 0.2.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 (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  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 +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  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 +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  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 +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.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 -1231
  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 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  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.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.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.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,431 @@
1
+ """Core HTTP request tracer hook implementation."""
2
+
3
+ import json
4
+ import uuid
5
+ from typing import Any
6
+
7
+ import structlog
8
+
9
+ from ccproxy.core.plugins.hooks import Hook
10
+ from ccproxy.core.plugins.hooks.base import HookContext
11
+ from ccproxy.core.plugins.hooks.events import HookEvent
12
+
13
+
14
+ logger = structlog.get_logger(__name__)
15
+
16
+
17
+ class HTTPTracerHook(Hook):
18
+ """Core hook for tracing all HTTP requests and responses.
19
+
20
+ This hook captures HTTP_REQUEST, HTTP_RESPONSE, and HTTP_ERROR events
21
+ for both client-side (CCProxy → providers) and server-side (client → CCProxy)
22
+ HTTP traffic. It uses injected formatters for consistent logging.
23
+ """
24
+
25
+ name = "core_http_tracer"
26
+ events = [
27
+ HookEvent.HTTP_REQUEST,
28
+ HookEvent.HTTP_RESPONSE,
29
+ HookEvent.HTTP_ERROR,
30
+ ]
31
+ priority = 100 # Run early to capture raw data
32
+
33
+ def __init__(
34
+ self,
35
+ json_formatter: Any = None,
36
+ raw_formatter: Any = None,
37
+ enabled: bool = True,
38
+ ) -> None:
39
+ """Initialize the HTTP tracer hook.
40
+
41
+ Args:
42
+ json_formatter: JSONFormatter instance for structured logging
43
+ raw_formatter: RawHTTPFormatter instance for raw HTTP logging
44
+ enabled: Whether the hook is enabled
45
+ """
46
+ self.enabled = enabled
47
+ self.json_formatter = json_formatter
48
+ self.raw_formatter = raw_formatter
49
+
50
+ if self.enabled:
51
+ logger.debug(
52
+ "core_http_tracer_hook_initialized",
53
+ json_logs=json_formatter is not None,
54
+ raw_http=raw_formatter is not None,
55
+ )
56
+
57
+ async def __call__(self, context: HookContext) -> None:
58
+ """Process HTTP events and log them.
59
+
60
+ Args:
61
+ context: Hook context with event data
62
+ """
63
+ if not self.enabled:
64
+ return
65
+
66
+ event = context.event
67
+ try:
68
+ if event == HookEvent.HTTP_REQUEST:
69
+ await self._log_http_request(context)
70
+ elif event == HookEvent.HTTP_RESPONSE:
71
+ await self._log_http_response(context)
72
+ elif event == HookEvent.HTTP_ERROR:
73
+ await self._log_http_error(context)
74
+ except Exception as e:
75
+ logger.error(
76
+ "core_http_tracer_hook_error",
77
+ hook_event=event.value if hasattr(event, "value") else str(event),
78
+ error=str(e),
79
+ exc_info=e,
80
+ )
81
+
82
+ async def _log_http_request(self, context: HookContext) -> None:
83
+ """Log an HTTP request.
84
+
85
+ Args:
86
+ context: Hook context with request data
87
+ """
88
+ method = context.data.get("method", "UNKNOWN")
89
+ url = context.data.get("url", "")
90
+ headers_any = context.data.get("headers", {})
91
+ headers_pairs = self._normalize_header_pairs(headers_any)
92
+ body = context.data.get("body")
93
+ is_json = context.data.get("is_json", False)
94
+
95
+ # Use existing request ID from context or generate new one
96
+ request_id = (
97
+ context.data.get("request_id")
98
+ or context.metadata.get("request_id")
99
+ or str(uuid.uuid4())
100
+ )
101
+
102
+ # Store request ID in context for response correlation
103
+ context.data["request_id"] = request_id
104
+
105
+ # Determine if this is a provider request
106
+ # First check explicit context markers, then fall back to URL analysis
107
+ if context.data.get("is_provider_request"):
108
+ is_provider_request = True
109
+ elif context.data.get("is_client_request"):
110
+ is_provider_request = False
111
+ else:
112
+ # Fall back to URL analysis for backward compatibility
113
+ is_provider_request = self._is_provider_request(url)
114
+
115
+ logger.debug(
116
+ "core_http_request",
117
+ request_id=request_id,
118
+ method=method,
119
+ url=url,
120
+ is_provider_request=is_provider_request,
121
+ headers=headers_pairs,
122
+ )
123
+
124
+ # Log with JSON formatter
125
+ if self.json_formatter:
126
+ await self.json_formatter.log_request(
127
+ request_id=request_id,
128
+ method=method,
129
+ url=url,
130
+ headers=headers_any,
131
+ body=body, # Pass original body data directly
132
+ request_type="provider" if is_provider_request else "http",
133
+ hook_type="core_http", # Indicate this came from core HTTPTracerHook
134
+ )
135
+
136
+ # Log with raw HTTP formatter
137
+ if self.raw_formatter:
138
+ # Build raw HTTP request
139
+ raw_request = self._build_raw_http_request(
140
+ method, url, headers_pairs, body, is_json
141
+ )
142
+
143
+ # Use appropriate logging method based on request type
144
+ if is_provider_request:
145
+ await self.raw_formatter.log_provider_request(
146
+ request_id=request_id,
147
+ raw_data=raw_request,
148
+ hook_type="core_http", # Indicate this came from core HTTPTracerHook
149
+ )
150
+ else:
151
+ await self.raw_formatter.log_client_request(
152
+ request_id=request_id,
153
+ raw_data=raw_request,
154
+ hook_type="core_http", # Indicate this came from core HTTPTracerHook
155
+ )
156
+
157
+ async def _log_http_response(self, context: HookContext) -> None:
158
+ """Log an HTTP response.
159
+
160
+ Args:
161
+ context: Hook context with response data
162
+ """
163
+ request_id = context.data.get("request_id", str(uuid.uuid4()))
164
+ status_code = context.data.get("status_code", 0)
165
+ headers_any = context.data.get("response_headers", {})
166
+ headers_pairs = self._normalize_header_pairs(headers_any)
167
+ body_any = context.data.get("response_body")
168
+ url = context.data.get("url", "")
169
+
170
+ # Determine if this is a provider response
171
+ # First check explicit context markers, then fall back to URL analysis
172
+ if context.data.get("is_provider_response"):
173
+ is_provider_response = True
174
+ elif context.data.get("is_client_response"):
175
+ is_provider_response = False
176
+ else:
177
+ # Fall back to URL analysis for backward compatibility
178
+ is_provider_response = self._is_provider_request(url)
179
+
180
+ logger.debug(
181
+ "core_http_response",
182
+ request_id=request_id,
183
+ status_code=status_code,
184
+ is_provider_response=is_provider_response,
185
+ )
186
+
187
+ # Log with JSON formatter
188
+ if self.json_formatter:
189
+ # Normalize body to bytes for formatter typing
190
+ if body_any is None:
191
+ body_bytes = b""
192
+ elif isinstance(body_any, bytes):
193
+ body_bytes = body_any
194
+ elif isinstance(body_any, str):
195
+ body_bytes = body_any.encode("utf-8")
196
+ else:
197
+ body_bytes = json.dumps(body_any).encode("utf-8")
198
+
199
+ await self.json_formatter.log_response(
200
+ request_id=request_id,
201
+ status=status_code,
202
+ headers=headers_any,
203
+ body=body_bytes,
204
+ response_type="provider" if is_provider_response else "http",
205
+ hook_type="core_http", # Indicate this came from core HTTPTracerHook
206
+ )
207
+
208
+ # Log with raw HTTP formatter
209
+ if self.raw_formatter:
210
+ # Build raw HTTP response
211
+ raw_response = self._build_raw_http_response(
212
+ status_code, headers_pairs, body_any
213
+ )
214
+
215
+ try:
216
+ # Use appropriate logging method based on response type
217
+ if is_provider_response:
218
+ await self.raw_formatter.log_provider_response(
219
+ request_id=request_id,
220
+ raw_data=raw_response,
221
+ hook_type="core_http", # Indicate this came from core HTTPTracerHook
222
+ )
223
+ else:
224
+ await self.raw_formatter.log_client_response(
225
+ request_id=request_id,
226
+ raw_data=raw_response,
227
+ hook_type="core_http", # Indicate this came from core HTTPTracerHook
228
+ )
229
+ except Exception as e:
230
+ logger.error(
231
+ "core_http_tracer_hook_response_logging_error",
232
+ request_id=request_id,
233
+ error=str(e),
234
+ exc_info=e,
235
+ )
236
+
237
+ async def _log_http_error(self, context: HookContext) -> None:
238
+ """Log an HTTP error.
239
+
240
+ Args:
241
+ context: Hook context with error data
242
+ """
243
+ request_id = context.data.get("request_id", str(uuid.uuid4()))
244
+ error_type = context.data.get("error_type", "unknown")
245
+ error_detail = context.data.get("error_detail", "")
246
+ status_code = context.data.get("status_code", 0)
247
+ response_body = context.data.get("response_body", "")
248
+ url = context.data.get("url", "")
249
+
250
+ # Determine if this is a provider error
251
+ is_provider_error = self._is_provider_request(url)
252
+
253
+ logger.error(
254
+ "core_http_error",
255
+ request_id=request_id,
256
+ error_type=error_type,
257
+ status_code=status_code,
258
+ error_detail=error_detail,
259
+ is_provider_error=is_provider_error,
260
+ )
261
+
262
+ # Log error response with formatters
263
+ if self.json_formatter:
264
+ await self.json_formatter.log_error(
265
+ request_id=request_id,
266
+ error=Exception(f"{error_type}: {error_detail}"),
267
+ )
268
+
269
+ if self.raw_formatter and status_code > 0:
270
+ # Build error response
271
+ raw_response = f"HTTP/1.1 {status_code} Error\r\n\r\n{response_body}"
272
+
273
+ # Use appropriate logging method based on error type
274
+ if is_provider_error:
275
+ await self.raw_formatter.log_provider_response(
276
+ request_id=request_id,
277
+ raw_data=raw_response.encode(),
278
+ )
279
+ else:
280
+ await self.raw_formatter.log_client_response(
281
+ request_id=request_id,
282
+ raw_data=raw_response.encode(),
283
+ )
284
+
285
+ def _build_raw_http_request(
286
+ self,
287
+ method: str,
288
+ url: str,
289
+ headers_pairs: list[tuple[str, str]] | Any,
290
+ body: Any,
291
+ is_json: bool,
292
+ ) -> bytes:
293
+ """Build raw HTTP request for logging.
294
+
295
+ Args:
296
+ method: HTTP method
297
+ url: Request URL
298
+ headers: Request headers
299
+ body: Request body
300
+ is_json: Whether body is JSON
301
+
302
+ Returns:
303
+ Raw HTTP request bytes
304
+ """
305
+ # Parse URL to get path
306
+ from urllib.parse import urlparse
307
+
308
+ parsed = urlparse(url)
309
+ path = parsed.path or "/"
310
+ if parsed.query:
311
+ path += f"?{parsed.query}"
312
+
313
+ # Build request line
314
+ lines = [f"{method} {path} HTTP/1.1"]
315
+
316
+ headers_list = self._normalize_header_pairs(headers_pairs)
317
+ # Add Host header only if not already present in headers
318
+ has_host = any(k.lower() == "host" for k, _ in headers_list)
319
+ if parsed.netloc and not has_host:
320
+ lines.append(f"Host: {parsed.netloc}")
321
+
322
+ # Add other headers (preserve input order, duplicates allowed)
323
+ for key, value in headers_list:
324
+ lines.append(f"{key}: {value}")
325
+
326
+ # Add body
327
+ body_str = ""
328
+ if body:
329
+ if is_json and isinstance(body, dict):
330
+ body_str = json.dumps(body)
331
+ elif isinstance(body, bytes):
332
+ try:
333
+ body_str = body.decode()
334
+ except (UnicodeDecodeError, AttributeError):
335
+ body_str = str(body)
336
+ else:
337
+ body_str = str(body)
338
+
339
+ # Add Content-Length only if not already present in headers
340
+ has_cl = any(k.lower() == "content-length" for k, _ in headers_list)
341
+ if not has_cl:
342
+ lines.append(f"Content-Length: {len(body_str)}")
343
+ lines.append("")
344
+ lines.append(body_str)
345
+ else:
346
+ lines.append("")
347
+
348
+ return "\r\n".join(lines).encode()
349
+
350
+ def _build_raw_http_response(
351
+ self,
352
+ status_code: int,
353
+ headers_pairs: list[tuple[str, str]] | Any,
354
+ body: Any,
355
+ ) -> bytes:
356
+ """Build raw HTTP response for logging.
357
+
358
+ Args:
359
+ status_code: HTTP status code
360
+ headers: Response headers
361
+ body: Response body
362
+
363
+ Returns:
364
+ Raw HTTP response bytes
365
+ """
366
+ # Build status line
367
+ lines = [f"HTTP/1.1 {status_code} OK"]
368
+
369
+ # Add headers (preserve order and duplicates)
370
+ headers_list = self._normalize_header_pairs(headers_pairs)
371
+ for key, value in headers_list:
372
+ lines.append(f"{key}: {value}")
373
+
374
+ # Add body
375
+ if body:
376
+ if isinstance(body, bytes):
377
+ try:
378
+ body_str = body.decode("utf-8")
379
+ except UnicodeDecodeError:
380
+ body_str = body.decode("utf-8", errors="replace")
381
+ elif isinstance(body, dict):
382
+ body_str = json.dumps(body, indent=2)
383
+ else:
384
+ body_str = str(body)
385
+
386
+ # Add Content-Length only if not already present in headers
387
+ has_cl = any(k.lower() == "content-length" for k, _ in headers_list)
388
+ if not has_cl:
389
+ lines.append(f"Content-Length: {len(body_str)}")
390
+ lines.append("")
391
+ lines.append(body_str)
392
+ else:
393
+ lines.append("")
394
+
395
+ return "\r\n".join(lines).encode()
396
+
397
+ def _is_provider_request(self, url: str) -> bool:
398
+ """Determine if this is a request to a provider API.
399
+
400
+ Args:
401
+ url: The request URL
402
+
403
+ Returns:
404
+ True if this is a provider request, False for client requests
405
+ """
406
+ # Known provider domains
407
+ provider_domains = [
408
+ "api.anthropic.com",
409
+ "claude.ai",
410
+ "api.openai.com",
411
+ "chatgpt.com",
412
+ ]
413
+
414
+ # Check if URL contains any provider domain
415
+ url_lower = url.lower()
416
+ return any(domain in url_lower for domain in provider_domains)
417
+
418
+ def _normalize_header_pairs(self, headers: Any) -> list[tuple[str, str]]:
419
+ """Normalize headers to a list of pairs preserving order and duplicates.
420
+
421
+ Accepts dict (items()), dict-like objects, or any iterable of pairs.
422
+ """
423
+ try:
424
+ if headers is None:
425
+ return []
426
+ if hasattr(headers, "items") and callable(headers.items):
427
+ return [(str(k), str(v)) for k, v in headers.items()]
428
+ # Already a sequence of pairs
429
+ return [(str(k), str(v)) for k, v in headers]
430
+ except Exception:
431
+ return []
@@ -0,0 +1,44 @@
1
+ """Standard hook execution layers for priority ordering."""
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class HookLayer(IntEnum):
7
+ """Standard hook execution priority layers.
8
+
9
+ Hooks execute in priority order from lowest to highest value.
10
+ Within the same priority, hooks execute in registration order.
11
+ """
12
+
13
+ # Pre-processing: Core system setup
14
+ CRITICAL = 0 # System-critical hooks (request ID generation, core context)
15
+ VALIDATION = 100 # Input validation and sanitization
16
+
17
+ # Context building: Authentication and enrichment
18
+ AUTH = 200 # Authentication and authorization
19
+ ENRICHMENT = 300 # Context enrichment (session data, user info, metadata)
20
+
21
+ # Core processing: Business logic
22
+ PROCESSING = 500 # Main request/response processing
23
+
24
+ # Observation: Metrics and logging
25
+ OBSERVATION = 700 # Metrics collection, access logging, tracing
26
+
27
+ # Post-processing: Cleanup and finalization
28
+ CLEANUP = 900 # Resource cleanup, connection management
29
+ FINALIZATION = 1000 # Final operations before response
30
+
31
+
32
+ # Convenience aliases for common use cases
33
+ BEFORE_AUTH = HookLayer.AUTH - 10
34
+ AFTER_AUTH = HookLayer.AUTH + 10
35
+
36
+ BEFORE_PROCESSING = HookLayer.PROCESSING - 10
37
+ AFTER_PROCESSING = HookLayer.PROCESSING + 10
38
+
39
+ # Observation layer ordering (metrics first, logging last)
40
+ METRICS = HookLayer.OBSERVATION # 700: Collect metrics
41
+ TRACING = HookLayer.OBSERVATION + 20 # 720: Request tracing
42
+ ACCESS_LOGGING = (
43
+ HookLayer.OBSERVATION + 50
44
+ ) # 750: Access logs (last to capture all data)
@@ -0,0 +1,186 @@
1
+ """Hook execution manager for CCProxy.
2
+
3
+ This module provides the HookManager class which handles the execution of hooks
4
+ for various events in the system. It ensures proper error isolation and supports
5
+ both async and sync hooks.
6
+ """
7
+
8
+ import asyncio
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ import structlog
13
+
14
+ from .base import Hook, HookContext
15
+ from .events import HookEvent
16
+ from .registry import HookRegistry
17
+ from .thread_manager import BackgroundHookThreadManager
18
+
19
+
20
+ class HookManager:
21
+ """Manages hook execution with error isolation and async/sync support.
22
+
23
+ The HookManager is responsible for emitting events to registered hooks
24
+ and ensuring that hook failures don't crash the system. It handles both
25
+ async and sync hooks by running sync hooks in a thread pool.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ registry: HookRegistry,
31
+ background_manager: BackgroundHookThreadManager | None = None,
32
+ ):
33
+ """Initialize the hook manager.
34
+
35
+ Args:
36
+ registry: The hook registry to get hooks from
37
+ background_manager: Optional background thread manager for fire-and-forget execution
38
+ """
39
+ self._registry = registry
40
+ self._background_manager = background_manager
41
+ self._logger = structlog.get_logger(__name__)
42
+
43
+ async def emit(
44
+ self,
45
+ event: HookEvent,
46
+ data: dict[str, Any] | None = None,
47
+ fire_and_forget: bool = True,
48
+ **kwargs: Any,
49
+ ) -> None:
50
+ """Emit an event to all registered hooks.
51
+
52
+ Creates a HookContext with the provided data and emits it to all
53
+ hooks registered for the given event. Handles errors gracefully
54
+ to ensure one failing hook doesn't affect others.
55
+
56
+ Args:
57
+ event: The event to emit
58
+ data: Optional data dictionary to include in context
59
+ fire_and_forget: If True, execute hooks in background thread (default)
60
+ **kwargs: Additional context fields (request, response, provider, etc.)
61
+ """
62
+ context = HookContext(
63
+ event=event,
64
+ timestamp=datetime.utcnow(),
65
+ data=data or {},
66
+ metadata={},
67
+ **kwargs,
68
+ )
69
+
70
+ if fire_and_forget and self._background_manager:
71
+ # Execute in background thread - non-blocking
72
+ self._background_manager.emit_async(context, self._registry)
73
+ return
74
+ elif fire_and_forget and not self._background_manager:
75
+ # No background manager available, log warning and fall back to sync
76
+ self._logger.warning(
77
+ "fire_and_forget_requested_but_no_background_manager_available"
78
+ )
79
+ # Fall through to synchronous execution
80
+
81
+ # Synchronous execution (legacy behavior)
82
+ hooks = self._registry.get(event)
83
+ if not hooks:
84
+ return
85
+
86
+ # Log execution order if debug logging enabled
87
+ self._logger.debug(
88
+ "hook_execution_order",
89
+ hook_event=event.value if hasattr(event, "value") else str(event),
90
+ hooks=[
91
+ {"name": h.name, "priority": getattr(h, "priority", 500)} for h in hooks
92
+ ],
93
+ )
94
+
95
+ # Execute all hooks in priority order, catching errors
96
+ for hook in hooks:
97
+ try:
98
+ await self._execute_hook(hook, context)
99
+ except Exception as e:
100
+ self._logger.error(
101
+ "hook_execution_failed",
102
+ hook=hook.name,
103
+ hook_event=event.value if hasattr(event, "value") else str(event),
104
+ priority=getattr(hook, "priority", 500),
105
+ error=str(e),
106
+ )
107
+ # Continue executing other hooks
108
+
109
+ async def emit_with_context(
110
+ self, context: HookContext, fire_and_forget: bool = True
111
+ ) -> None:
112
+ """Emit an event using a pre-built HookContext.
113
+
114
+ This is useful when you need to build the context with specific metadata
115
+ before emitting the event.
116
+
117
+ Args:
118
+ context: The HookContext to emit
119
+ fire_and_forget: If True, execute hooks in background thread (default)
120
+ """
121
+ if fire_and_forget and self._background_manager:
122
+ # Execute in background thread - non-blocking
123
+ self._background_manager.emit_async(context, self._registry)
124
+ return
125
+ elif fire_and_forget and not self._background_manager:
126
+ # No background manager available, log warning and fall back to sync
127
+ self._logger.warning(
128
+ "fire_and_forget_requested_but_no_background_manager_available"
129
+ )
130
+ # Fall through to synchronous execution
131
+
132
+ # Synchronous execution (legacy behavior)
133
+ hooks = self._registry.get(context.event)
134
+ if not hooks:
135
+ return
136
+
137
+ # Log execution order if debug logging enabled
138
+ self._logger.debug(
139
+ "hook_execution_order",
140
+ hook_event=context.event.value
141
+ if hasattr(context.event, "value")
142
+ else str(context.event),
143
+ hooks=[
144
+ {"name": h.name, "priority": getattr(h, "priority", 500)} for h in hooks
145
+ ],
146
+ )
147
+
148
+ # Execute all hooks in priority order, catching errors
149
+ for hook in hooks:
150
+ try:
151
+ await self._execute_hook(hook, context)
152
+ except Exception as e:
153
+ self._logger.error(
154
+ "hook_execution_failed",
155
+ hook=hook.name,
156
+ hook_event=context.event.value
157
+ if hasattr(context.event, "value")
158
+ else str(context.event),
159
+ priority=getattr(hook, "priority", 500),
160
+ error=str(e),
161
+ )
162
+ # Continue executing other hooks
163
+
164
+ async def _execute_hook(self, hook: Hook, context: HookContext) -> None:
165
+ """Execute a single hook with proper async/sync handling.
166
+
167
+ Determines if the hook is async or sync and executes it appropriately.
168
+ Sync hooks are run in a thread pool to avoid blocking the async event loop.
169
+
170
+ Args:
171
+ hook: The hook to execute
172
+ context: The context to pass to the hook
173
+ """
174
+ result = hook(context)
175
+ if asyncio.iscoroutine(result):
176
+ await result
177
+ # If result is None, it was a sync hook and we're done
178
+
179
+ def shutdown(self) -> None:
180
+ """Shutdown the background hook processing.
181
+
182
+ This method should be called during application shutdown to ensure
183
+ proper cleanup of the background thread.
184
+ """
185
+ if self._background_manager:
186
+ self._background_manager.stop()