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,415 @@
1
+ """Hook-based request tracer implementation for REQUEST_* events only."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import aiofiles
9
+
10
+ from ccproxy.core.logging import get_plugin_logger
11
+ from ccproxy.core.plugins.hooks import Hook
12
+ from ccproxy.core.plugins.hooks.base import HookContext
13
+ from ccproxy.core.plugins.hooks.events import HookEvent
14
+
15
+ from .config import RequestTracerConfig
16
+
17
+
18
+ logger = get_plugin_logger(__name__)
19
+
20
+
21
+ class RequestTracerHook(Hook):
22
+ """Simplified hook-based request tracer implementation.
23
+
24
+ This hook only handles REQUEST_* events since HTTP_* events are now
25
+ handled by the core HTTPTracerHook. This eliminates duplication and
26
+ follows the single responsibility principle.
27
+
28
+ The plugin now focuses purely on request lifecycle logging without
29
+ attempting to capture HTTP request/response bodies.
30
+ """
31
+
32
+ name = "request_tracer"
33
+ events = [
34
+ HookEvent.REQUEST_STARTED,
35
+ HookEvent.REQUEST_COMPLETED,
36
+ HookEvent.REQUEST_FAILED,
37
+ HookEvent.PROVIDER_REQUEST_PREPARED,
38
+ HookEvent.PROVIDER_RESPONSE_RECEIVED,
39
+ HookEvent.PROVIDER_ERROR,
40
+ HookEvent.PROVIDER_STREAM_START,
41
+ HookEvent.PROVIDER_STREAM_CHUNK,
42
+ HookEvent.PROVIDER_STREAM_END,
43
+ ]
44
+ priority = 300 # HookLayer.ENRICHMENT - Capture/enrich request context early
45
+
46
+ def __init__(
47
+ self,
48
+ config: RequestTracerConfig | None = None,
49
+ ) -> None:
50
+ """Initialize the request tracer hook.
51
+
52
+ Args:
53
+ config: Request tracer configuration
54
+ """
55
+ self.config = config or RequestTracerConfig()
56
+
57
+ # Storage for streaming chunks per request
58
+ self._streaming_chunks: dict[str, list[bytes]] = {}
59
+ self._streaming_metadata: dict[str, dict[str, Any]] = {}
60
+
61
+ logger.debug(
62
+ "request_tracer_hook_initialized",
63
+ enabled=self.config.enabled,
64
+ )
65
+
66
+ async def __call__(self, context: HookContext) -> None:
67
+ """Handle hook events for request tracing.
68
+
69
+ Args:
70
+ context: Hook context with event data
71
+ """
72
+ # Debug logging for CLI hook calls
73
+ # logger.trace(
74
+ # "request_tracer_hook_called",
75
+ # hook_event=context.event.value if context.event else "unknown",
76
+ # enabled=self.config.enabled,
77
+ # data_keys=list(context.data.keys()) if context.data else [],
78
+ # )
79
+
80
+ if not self.config.enabled:
81
+ return
82
+
83
+ # Map hook events to handler methods
84
+ handlers = {
85
+ HookEvent.REQUEST_STARTED: self._handle_request_start,
86
+ HookEvent.REQUEST_COMPLETED: self._handle_request_complete,
87
+ HookEvent.REQUEST_FAILED: self._handle_request_failed,
88
+ HookEvent.PROVIDER_REQUEST_PREPARED: self._handle_provider_request,
89
+ HookEvent.PROVIDER_RESPONSE_RECEIVED: self._handle_provider_response,
90
+ HookEvent.PROVIDER_ERROR: self._handle_provider_error,
91
+ HookEvent.PROVIDER_STREAM_START: self._handle_stream_start,
92
+ HookEvent.PROVIDER_STREAM_CHUNK: self._handle_stream_chunk,
93
+ HookEvent.PROVIDER_STREAM_END: self._handle_stream_end,
94
+ }
95
+
96
+ handler = handlers.get(context.event)
97
+ if handler:
98
+ try:
99
+ await handler(context)
100
+ except Exception as e:
101
+ logger.error(
102
+ "request_tracer_hook_error",
103
+ hook_event=context.event.value if context.event else "unknown",
104
+ error=str(e),
105
+ exc_info=e,
106
+ )
107
+
108
+ async def _handle_request_start(self, context: HookContext) -> None:
109
+ """Handle REQUEST_STARTED event."""
110
+ if not self.config.log_client_request:
111
+ return
112
+
113
+ # Extract request data from context
114
+ request_id = context.data.get("request_id", "unknown")
115
+ method = context.data.get("method", "UNKNOWN")
116
+ url = context.data.get("url", "")
117
+ path = context.data.get("path", url) # Use direct path if available
118
+
119
+ # Check path filters
120
+ if self._should_exclude_path(path):
121
+ return
122
+
123
+ logger.trace(
124
+ "request_started",
125
+ request_id=request_id,
126
+ method=method,
127
+ url=url,
128
+ note="Request body logged by core HTTPTracerHook",
129
+ )
130
+
131
+ async def _handle_request_complete(self, context: HookContext) -> None:
132
+ """Handle REQUEST_COMPLETED event."""
133
+ if not self.config.log_client_response:
134
+ return
135
+
136
+ request_id = context.data.get("request_id", "unknown")
137
+ status_code = context.data.get("status_code", 200)
138
+ duration_ms = context.data.get("duration_ms", 0)
139
+
140
+ # Check path filters
141
+ url = context.data.get("url", "")
142
+ path = self._extract_path(url)
143
+ if self._should_exclude_path(path):
144
+ return
145
+
146
+ logger.trace(
147
+ "request_completed",
148
+ request_id=request_id,
149
+ status_code=status_code,
150
+ duration_ms=duration_ms,
151
+ note="Response body logged by core HTTPTracerHook",
152
+ )
153
+
154
+ async def _handle_request_failed(self, context: HookContext) -> None:
155
+ """Handle REQUEST_FAILED event."""
156
+ request_id = context.data.get("request_id", "unknown")
157
+ error = context.error
158
+ duration = context.data.get("duration", 0)
159
+
160
+ logger.trace(
161
+ "request_failed",
162
+ request_id=request_id,
163
+ error=str(error) if error else "unknown",
164
+ duration=duration,
165
+ )
166
+
167
+ async def _handle_provider_request(self, context: HookContext) -> None:
168
+ """Handle PROVIDER_REQUEST_PREPARED event."""
169
+ if not self.config.log_provider_request:
170
+ return
171
+
172
+ request_id = context.metadata.get("request_id", "unknown")
173
+ url = context.data.get("url", "")
174
+ method = context.data.get("method", "UNKNOWN")
175
+ provider = context.provider or "unknown"
176
+
177
+ logger.trace(
178
+ "provider_request_prepared",
179
+ request_id=request_id,
180
+ provider=provider,
181
+ method=method,
182
+ url=url,
183
+ note="Request body logged by core HTTPTracerHook",
184
+ )
185
+
186
+ async def _handle_provider_response(self, context: HookContext) -> None:
187
+ """Handle PROVIDER_RESPONSE_RECEIVED event."""
188
+ if not self.config.log_provider_response:
189
+ return
190
+
191
+ request_id = context.metadata.get("request_id", "unknown")
192
+ status_code = context.data.get("status_code", 200)
193
+ provider = context.provider or "unknown"
194
+ is_streaming = context.data.get("is_streaming", False)
195
+
196
+ logger.trace(
197
+ "provider_response_received",
198
+ request_id=request_id,
199
+ provider=provider,
200
+ status_code=status_code,
201
+ is_streaming=is_streaming,
202
+ )
203
+
204
+ async def _handle_provider_error(self, context: HookContext) -> None:
205
+ """Handle PROVIDER_ERROR event."""
206
+ request_id = context.metadata.get("request_id", "unknown")
207
+ provider = context.provider or "unknown"
208
+ error = context.error
209
+
210
+ logger.error(
211
+ "provider_error",
212
+ request_id=request_id,
213
+ provider=provider,
214
+ error=str(error) if error else "unknown",
215
+ )
216
+
217
+ async def _handle_stream_start(self, context: HookContext) -> None:
218
+ """Handle PROVIDER_STREAM_START event."""
219
+ request_id = context.data.get("request_id") or context.metadata.get(
220
+ "request_id", "unknown"
221
+ )
222
+ provider = context.provider or "unknown"
223
+
224
+ logger.debug(
225
+ "stream_start_handler_called",
226
+ chunk_logging_enabled=self.config.log_streaming_chunks,
227
+ json_logs_enabled=self.config.json_logs_enabled,
228
+ request_id=request_id,
229
+ provider=provider,
230
+ )
231
+
232
+ if self.config.json_logs_enabled:
233
+ # Initialize chunk collection for this request when JSON logs are enabled
234
+ self._streaming_chunks[request_id] = []
235
+ self._streaming_metadata[request_id] = {
236
+ "provider": provider,
237
+ "start_time": datetime.now(),
238
+ "url": context.data.get("url", ""),
239
+ "method": context.data.get("method", "UNKNOWN"),
240
+ "buffered_mode": context.data.get("buffered_mode", False),
241
+ "upstream_stream_text": None,
242
+ }
243
+
244
+ logger.debug(
245
+ "stream_started",
246
+ request_id=request_id,
247
+ provider=provider,
248
+ )
249
+
250
+ async def _handle_stream_chunk(self, context: HookContext) -> None:
251
+ """Handle PROVIDER_STREAM_CHUNK event."""
252
+ request_id = context.data.get("request_id", "unknown")
253
+ chunk = context.data.get("chunk")
254
+
255
+ if (
256
+ chunk
257
+ and self.config.json_logs_enabled
258
+ and request_id in self._streaming_chunks
259
+ ):
260
+ # Collect the chunk
261
+ self._streaming_chunks[request_id].append(chunk)
262
+
263
+ if not self.config.log_streaming_chunks:
264
+ return
265
+
266
+ # Optional: Log chunk info for debugging
267
+ chunk_number = context.data.get("chunk_number", 0)
268
+ chunk_size = context.data.get("chunk_size", len(chunk) if chunk else 0)
269
+
270
+ logger.trace(
271
+ "stream_chunk_collected",
272
+ request_id=request_id,
273
+ chunk_number=chunk_number,
274
+ chunk_size=chunk_size,
275
+ )
276
+
277
+ async def _handle_stream_end(self, context: HookContext) -> None:
278
+ """Handle PROVIDER_STREAM_END event."""
279
+ request_id = context.data.get("request_id", "unknown")
280
+ provider = context.provider or "unknown"
281
+ total_chunks = context.data.get("total_chunks", 0)
282
+ total_bytes = context.data.get("total_bytes", 0)
283
+ usage_metrics = context.data.get("usage_metrics", {})
284
+
285
+ # Write collected chunks to response file
286
+ if self.config.json_logs_enabled:
287
+ if (
288
+ request_id in self._streaming_chunks
289
+ and self._streaming_chunks[request_id]
290
+ ):
291
+ metadata = self._streaming_metadata.get(request_id, {})
292
+ chunks = self._streaming_chunks[request_id]
293
+
294
+ # Add end time and metrics to metadata
295
+ metadata.update(
296
+ {
297
+ "end_time": datetime.now(),
298
+ "total_chunks": total_chunks,
299
+ "total_bytes": total_bytes,
300
+ "usage_metrics": usage_metrics,
301
+ "upstream_stream_text": context.data.get(
302
+ "upstream_stream_text"
303
+ ),
304
+ }
305
+ )
306
+
307
+ # Write response file
308
+ await self._write_streaming_response_file(request_id, chunks, metadata)
309
+
310
+ # Clean up memory regardless of whether we had chunks
311
+ self._streaming_chunks.pop(request_id, None)
312
+ self._streaming_metadata.pop(request_id, None)
313
+
314
+ logger.trace(
315
+ "stream_ended",
316
+ request_id=request_id,
317
+ provider=provider,
318
+ total_chunks=total_chunks,
319
+ total_bytes=total_bytes,
320
+ usage_metrics=usage_metrics,
321
+ )
322
+
323
+ def _extract_path(self, url: str) -> str:
324
+ """Extract path from URL."""
325
+ if "://" in url:
326
+ # Full URL
327
+ parts = url.split("/", 3)
328
+ return "/" + parts[3] if len(parts) > 3 else "/"
329
+ return url
330
+
331
+ def _should_exclude_path(self, path: str) -> bool:
332
+ """Check if path should be excluded from logging."""
333
+ # Check include paths first (if specified)
334
+ if self.config.include_paths:
335
+ return not any(path.startswith(p) for p in self.config.include_paths)
336
+
337
+ # Check exclude paths
338
+ if self.config.exclude_paths:
339
+ return any(path.startswith(p) for p in self.config.exclude_paths)
340
+
341
+ return False
342
+
343
+ def _generate_stream_response_file_path(
344
+ self, request_id: str, provider: str
345
+ ) -> Path:
346
+ """Generate file path for streaming response file."""
347
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
348
+ filename = f"{request_id}_{timestamp}_{provider}_streaming_response.json"
349
+ return Path(self.config.get_json_log_dir()) / filename
350
+
351
+ async def _write_streaming_response_file(
352
+ self, request_id: str, chunks: list[bytes], metadata: dict[str, Any]
353
+ ) -> None:
354
+ """Write collected streaming chunks to a response file."""
355
+ try:
356
+ # Combine all chunks
357
+ combined_data = b"".join(chunks)
358
+
359
+ # Try to decode as text for JSON parsing
360
+ try:
361
+ response_text = combined_data.decode("utf-8", errors="replace")
362
+ except Exception:
363
+ response_text = str(combined_data)
364
+
365
+ # Build response data
366
+ upstream_stream_text = metadata.get("upstream_stream_text")
367
+ response_data = {
368
+ "request_id": request_id,
369
+ "provider": metadata.get("provider", "unknown"),
370
+ "method": metadata.get("method", "UNKNOWN"),
371
+ "url": metadata.get("url", ""),
372
+ "start_time": metadata.get("start_time", datetime.now()).isoformat(),
373
+ "end_time": datetime.now().isoformat(),
374
+ "total_chunks": len(chunks),
375
+ "total_bytes": len(combined_data),
376
+ "buffered_mode": metadata.get("buffered_mode", False),
377
+ "usage_metrics": metadata.get("usage_metrics"),
378
+ # "response_text": response_text[: self.config.truncate_body_preview]
379
+ # if len(response_text) > self.config.truncate_body_preview
380
+ # else response_text,
381
+ # "response_truncated": len(response_text)
382
+ # > self.config.truncate_body_preview,
383
+ "response_text": response_text,
384
+ }
385
+
386
+ if upstream_stream_text is not None:
387
+ response_data["upstream_stream_text"] = upstream_stream_text
388
+
389
+ # Generate file path
390
+ file_path = self._generate_stream_response_file_path(
391
+ request_id, metadata.get("provider", "unknown")
392
+ )
393
+
394
+ # Ensure directory exists
395
+ file_path.parent.mkdir(parents=True, exist_ok=True)
396
+
397
+ # Write JSON file
398
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
399
+ await f.write(json.dumps(response_data, indent=2, ensure_ascii=False))
400
+
401
+ logger.debug(
402
+ "streaming_response_file_written",
403
+ request_id=request_id,
404
+ file_path=str(file_path),
405
+ total_chunks=len(chunks),
406
+ total_bytes=len(combined_data),
407
+ )
408
+
409
+ except Exception as e:
410
+ logger.error(
411
+ "streaming_response_file_write_failed",
412
+ request_id=request_id,
413
+ error=str(e),
414
+ exc_info=e,
415
+ )
@@ -0,0 +1,255 @@
1
+ """Request Tracer plugin implementation - after refactoring."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from ccproxy.core.logging import get_plugin_logger
7
+ from ccproxy.core.plugins import (
8
+ PluginManifest,
9
+ SystemPluginFactory,
10
+ SystemPluginRuntime,
11
+ )
12
+ from ccproxy.core.plugins.hooks import HookRegistry
13
+ from ccproxy.core.plugins.hooks.implementations import HTTPTracerHook
14
+ from ccproxy.core.plugins.hooks.implementations.formatters import (
15
+ JSONFormatter,
16
+ RawHTTPFormatter,
17
+ )
18
+
19
+ from .config import RequestTracerConfig
20
+ from .hook import RequestTracerHook
21
+
22
+
23
+ logger = get_plugin_logger()
24
+
25
+
26
+ class RequestTracerRuntime(SystemPluginRuntime):
27
+ """Runtime for the request tracer plugin.
28
+
29
+ Handles only REQUEST_* events via a hook.
30
+ HTTP events are managed by the core HTTPTracerHook.
31
+ """
32
+
33
+ def __init__(self, manifest: PluginManifest):
34
+ """Initialize runtime."""
35
+ super().__init__(manifest)
36
+ self.config: RequestTracerConfig | None = None
37
+ self.hook: RequestTracerHook | None = None
38
+ self.http_tracer_hook: HTTPTracerHook | None = None
39
+
40
+ async def _on_initialize(self) -> None:
41
+ """Initialize the request tracer."""
42
+ if not self.context:
43
+ raise RuntimeError("Context not set")
44
+
45
+ # Get configuration
46
+ config = self.context.get("config")
47
+ if not isinstance(config, RequestTracerConfig):
48
+ config = RequestTracerConfig()
49
+
50
+ self.config = config
51
+
52
+ # Debug log the actual configuration being used
53
+ logger.debug(
54
+ "plugin_configuration_loaded",
55
+ enabled=config.enabled,
56
+ json_logs_enabled=config.json_logs_enabled,
57
+ verbose_api=config.verbose_api,
58
+ log_dir=config.log_dir,
59
+ exclude_paths=config.exclude_paths,
60
+ log_client_request=config.log_client_request,
61
+ log_client_response=config.log_client_response,
62
+ note="HTTP events handled by core HTTPTracerHook",
63
+ )
64
+
65
+ # Validate configuration
66
+ validation_errors = self._validate_config(config)
67
+ if validation_errors:
68
+ logger.error(
69
+ "plugin_config_validation_failed",
70
+ errors=validation_errors,
71
+ config=config.model_dump()
72
+ if hasattr(config, "model_dump")
73
+ else str(config),
74
+ )
75
+ for error in validation_errors:
76
+ logger.warning("config_validation_warning", issue=error)
77
+
78
+ # Try to get hook registry from context
79
+ hook_registry: HookRegistry | None = self.context.get("hook_registry")
80
+
81
+ # If not found, try app state
82
+ if not hook_registry:
83
+ app = self.context.get("app")
84
+ if app and hasattr(app.state, "hook_registry"):
85
+ hook_registry = app.state.hook_registry
86
+
87
+ if not hook_registry or not isinstance(hook_registry, HookRegistry):
88
+ logger.warning(
89
+ "hook_registry_not_available",
90
+ mode="hooks",
91
+ fallback="disabled",
92
+ )
93
+ return
94
+
95
+ settings = self.context.get("settings") if self.context else None
96
+
97
+ json_formatter, raw_formatter = self._build_formatters(settings)
98
+
99
+ self.http_tracer_hook = HTTPTracerHook(
100
+ json_formatter=json_formatter,
101
+ raw_formatter=raw_formatter,
102
+ enabled=True,
103
+ )
104
+ hook_registry.register(self.http_tracer_hook)
105
+ logger.debug(
106
+ "core_http_tracer_registered",
107
+ mode="hooks",
108
+ json_logs_enabled=self.config.json_logs_enabled,
109
+ raw_http_enabled=(raw_formatter is not None),
110
+ log_dir=json_formatter.log_dir if json_formatter else None,
111
+ )
112
+
113
+ # Register REQUEST_* hook for contextual logging
114
+ self.hook = RequestTracerHook(self.config)
115
+ hook_registry.register(self.hook)
116
+ logger.debug(
117
+ "request_tracer_hook_registered",
118
+ mode="hooks",
119
+ json_logs=self.config.json_logs_enabled,
120
+ verbose_api=self.config.verbose_api,
121
+ note="HTTP events handled by core HTTPTracerHook",
122
+ )
123
+
124
+ logger.debug(
125
+ "request_tracer_enabled",
126
+ log_dir=self.config.log_dir,
127
+ json_logs=self.config.json_logs_enabled,
128
+ exclude_paths=self.config.exclude_paths,
129
+ architecture="hooks_only",
130
+ )
131
+
132
+ def _validate_config(self, config: RequestTracerConfig) -> list[str]:
133
+ """Validate plugin configuration.
134
+
135
+ Returns:
136
+ List of validation error messages (empty if valid)
137
+ """
138
+ errors: list[str] = []
139
+
140
+ if not config.enabled:
141
+ return errors # No validation needed if disabled
142
+
143
+ # Basic path validation
144
+ try:
145
+ log_path = Path(config.log_dir)
146
+ if not log_path.parent.exists():
147
+ errors.append(
148
+ f"Parent directory of log_dir does not exist: {log_path.parent}"
149
+ )
150
+ except Exception as e:
151
+ errors.append(f"Invalid log_dir path: {e}")
152
+
153
+ # Configuration consistency checks
154
+ if not config.json_logs_enabled and not config.verbose_api:
155
+ errors.append(
156
+ "No logging output enabled (json_logs_enabled=False, verbose_api=False)"
157
+ )
158
+
159
+ if config.max_body_size < 0:
160
+ errors.append("max_body_size cannot be negative")
161
+
162
+ return errors
163
+
164
+ async def _on_shutdown(self) -> None:
165
+ """Cleanup resources."""
166
+ if self.hook:
167
+ logger.debug("shutting_down_request_tracer_hook")
168
+ self.hook = None
169
+ self.http_tracer_hook = None
170
+ logger.debug("request_tracer_plugin_shutdown_complete")
171
+
172
+ def _build_formatters(
173
+ self,
174
+ settings: Any,
175
+ ) -> tuple[JSONFormatter | None, RawHTTPFormatter | None]:
176
+ """Construct formatters based on plugin and global logging settings."""
177
+
178
+ if not self.config:
179
+ return (None, None)
180
+
181
+ # Determine logging base directory overrides from global settings
182
+ json_log_dir = Path(self.config.get_json_log_dir())
183
+ raw_log_dir = Path(self.config.get_raw_log_dir())
184
+
185
+ override_base: Path | None = None
186
+ if settings and getattr(settings, "logging", None):
187
+ plugin_base = getattr(settings.logging, "plugin_log_base_dir", None)
188
+ if plugin_base:
189
+ override_base = Path(plugin_base) / "tracer"
190
+
191
+ fields_set: set[str] = getattr(self.config, "model_fields_set", set())
192
+ if override_base:
193
+ if "request_log_dir" not in fields_set and "log_dir" not in fields_set:
194
+ json_log_dir = override_base
195
+ if "raw_log_dir" not in fields_set and "log_dir" not in fields_set:
196
+ raw_log_dir = override_base
197
+
198
+ json_formatter: JSONFormatter | None = None
199
+ if self.config.json_logs_enabled:
200
+ json_formatter = JSONFormatter(
201
+ log_dir=str(json_log_dir),
202
+ verbose_api=self.config.verbose_api,
203
+ json_logs_enabled=self.config.json_logs_enabled,
204
+ redact_sensitive=self.config.redact_sensitive,
205
+ truncate_body_preview=self.config.truncate_body_preview,
206
+ )
207
+
208
+ raw_formatter: RawHTTPFormatter | None = None
209
+ raw_logging_enabled = self.config.raw_http_enabled
210
+ if raw_logging_enabled:
211
+ raw_formatter = RawHTTPFormatter(
212
+ log_dir=str(raw_log_dir),
213
+ enabled=True,
214
+ log_client_request=self.config.log_client_request,
215
+ log_client_response=self.config.log_client_response,
216
+ log_provider_request=self.config.log_provider_request,
217
+ log_provider_response=self.config.log_provider_response,
218
+ max_body_size=self.config.max_body_size,
219
+ exclude_headers=self.config.exclude_headers,
220
+ )
221
+
222
+ return (json_formatter, raw_formatter)
223
+
224
+
225
+ class RequestTracerFactory(SystemPluginFactory):
226
+ """factory for request tracer plugin."""
227
+
228
+ def __init__(self) -> None:
229
+ """Initialize factory with manifest."""
230
+ # Create manifest with static declarations ( from original)
231
+ manifest = PluginManifest(
232
+ name="request_tracer",
233
+ version="0.1.0", # Standardized initial plugin version
234
+ description=" request tracing for REQUEST_* events only",
235
+ is_provider=False,
236
+ config_class=RequestTracerConfig,
237
+ )
238
+
239
+ # Initialize with manifest
240
+ super().__init__(manifest)
241
+
242
+ def create_runtime(self) -> RequestTracerRuntime:
243
+ """Create runtime instance."""
244
+ return RequestTracerRuntime(self.manifest)
245
+
246
+ def create_context(self, core_services: Any) -> Any:
247
+ """Create context for the plugin."""
248
+ # Get base context from parent
249
+ context = super().create_context(core_services)
250
+
251
+ return context
252
+
253
+
254
+ # Export the factory instance for entry points
255
+ factory = RequestTracerFactory()
@@ -17,24 +17,12 @@ Key components:
17
17
  """
18
18
 
19
19
  from .core import Scheduler
20
- from .registry import TaskRegistry, register_task
21
- from .tasks import (
22
- BaseScheduledTask,
23
- PricingCacheUpdateTask,
24
- PushgatewayTask,
25
- StatsPrintingTask,
26
- )
20
+ from .registry import TaskRegistry
21
+ from .tasks import BaseScheduledTask
27
22
 
28
23
 
29
- # Task registration is now handled in manager.py during scheduler startup
30
- # to avoid side effects during module imports (e.g., CLI help display)
31
-
32
24
  __all__ = [
33
25
  "Scheduler",
34
26
  "TaskRegistry",
35
- "register_task",
36
27
  "BaseScheduledTask",
37
- "PushgatewayTask",
38
- "StatsPrintingTask",
39
- "PricingCacheUpdateTask",
40
28
  ]