ccproxy-api 0.1.7__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 +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.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 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,403 @@
1
+ """Hook-based metrics collection implementation."""
2
+
3
+ import time
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins.hooks import Hook
7
+ from ccproxy.core.plugins.hooks.base import HookContext
8
+ from ccproxy.core.plugins.hooks.events import HookEvent
9
+
10
+ from .collector import PrometheusMetrics
11
+ from .config import MetricsConfig
12
+ from .pushgateway import PushgatewayClient
13
+
14
+
15
+ logger = get_plugin_logger(__name__)
16
+
17
+
18
+ class MetricsHook(Hook):
19
+ """Hook-based metrics collection implementation.
20
+
21
+ This hook listens to request/response lifecycle events and updates
22
+ Prometheus metrics accordingly. It provides event-driven metric
23
+ collection without requiring direct metric calls in the code.
24
+ """
25
+
26
+ name = "metrics"
27
+ events = [
28
+ HookEvent.REQUEST_STARTED,
29
+ HookEvent.REQUEST_COMPLETED,
30
+ HookEvent.REQUEST_FAILED,
31
+ HookEvent.PROVIDER_REQUEST_PREPARED,
32
+ HookEvent.PROVIDER_RESPONSE_RECEIVED,
33
+ HookEvent.PROVIDER_ERROR,
34
+ HookEvent.PROVIDER_STREAM_START,
35
+ HookEvent.PROVIDER_STREAM_CHUNK,
36
+ HookEvent.PROVIDER_STREAM_END,
37
+ ]
38
+ priority = 700 # HookLayer.OBSERVATION - Metrics collection first
39
+
40
+ def __init__(self, config: MetricsConfig | None = None) -> None:
41
+ """Initialize the metrics hook.
42
+
43
+ Args:
44
+ config: Metrics configuration
45
+ """
46
+ self.config = config or MetricsConfig()
47
+
48
+ # Initialize collectors based on config using an isolated registry to
49
+ # avoid global REGISTRY collisions in multi-app/test environments.
50
+ if self.config.enabled:
51
+ registry = None
52
+ try:
53
+ from prometheus_client import (
54
+ CollectorRegistry as CollectorRegistry,
55
+ )
56
+
57
+ registry = CollectorRegistry()
58
+ except Exception:
59
+ registry = None
60
+
61
+ self.collector: PrometheusMetrics | None = PrometheusMetrics(
62
+ namespace=self.config.namespace,
63
+ histogram_buckets=self.config.histogram_buckets,
64
+ registry=registry,
65
+ )
66
+ else:
67
+ self.collector = None
68
+
69
+ self.pushgateway: PushgatewayClient | None = (
70
+ PushgatewayClient(self.config)
71
+ if self.config.pushgateway_enabled and self.config.enabled
72
+ else None
73
+ )
74
+
75
+ # Track active requests and their start times
76
+ self._request_start_times: dict[str, float] = {}
77
+
78
+ logger.debug(
79
+ "metrics_configured",
80
+ enabled=self.config.enabled,
81
+ namespace=self.config.namespace,
82
+ pushgateway_enabled=self.config.pushgateway_enabled,
83
+ pushgateway_url=self.config.pushgateway_url,
84
+ )
85
+
86
+ async def __call__(self, context: HookContext) -> None:
87
+ """Handle hook events for metrics collection.
88
+
89
+ Args:
90
+ context: Hook context with event data
91
+ """
92
+ if not self.config.enabled or not self.collector:
93
+ return
94
+
95
+ # Map hook events to handler methods
96
+ handlers = {
97
+ HookEvent.REQUEST_STARTED: self._handle_request_start,
98
+ HookEvent.REQUEST_COMPLETED: self._handle_request_complete,
99
+ HookEvent.REQUEST_FAILED: self._handle_request_failed,
100
+ HookEvent.PROVIDER_REQUEST_PREPARED: self._handle_provider_request,
101
+ HookEvent.PROVIDER_RESPONSE_RECEIVED: self._handle_provider_response,
102
+ HookEvent.PROVIDER_ERROR: self._handle_provider_error,
103
+ HookEvent.PROVIDER_STREAM_START: self._handle_stream_start,
104
+ HookEvent.PROVIDER_STREAM_CHUNK: self._handle_stream_chunk,
105
+ HookEvent.PROVIDER_STREAM_END: self._handle_stream_end,
106
+ }
107
+
108
+ handler = handlers.get(context.event)
109
+ if handler:
110
+ try:
111
+ await handler(context)
112
+ except Exception as e:
113
+ logger.error(
114
+ "metrics_hook_error",
115
+ hook_event=context.event.value if context.event else "unknown",
116
+ error=str(e),
117
+ exc_info=e,
118
+ )
119
+
120
+ async def _handle_request_start(self, context: HookContext) -> None:
121
+ """Handle REQUEST_STARTED event."""
122
+ if not self.config.collect_request_metrics or not self.collector:
123
+ return
124
+
125
+ request_id = context.data.get("request_id", "unknown")
126
+
127
+ # Track request start time
128
+ self._request_start_times[request_id] = time.time()
129
+
130
+ # Increment active requests
131
+ self.collector.inc_active_requests()
132
+
133
+ logger.debug(
134
+ "metrics_request_started",
135
+ request_id=request_id,
136
+ active_requests=len(self._request_start_times),
137
+ )
138
+
139
+ async def _handle_request_complete(self, context: HookContext) -> None:
140
+ """Handle REQUEST_COMPLETED event."""
141
+ if not self.config.collect_request_metrics or not self.collector:
142
+ return
143
+
144
+ request_id = context.data.get("request_id", "unknown")
145
+ method = context.data.get("method", "UNKNOWN")
146
+ endpoint = context.data.get("endpoint", context.data.get("url", "/"))
147
+ model = context.data.get("model")
148
+ status_code = context.data.get(
149
+ "response_status", context.data.get("status_code", 200)
150
+ )
151
+ service_type = context.data.get("service_type", "unknown")
152
+
153
+ # Calculate duration if we have start time
154
+ duration_seconds = 0.0
155
+ if request_id in self._request_start_times:
156
+ start_time = self._request_start_times.pop(request_id)
157
+ duration_seconds = time.time() - start_time
158
+ elif "duration" in context.data:
159
+ # Use provided duration if available
160
+ duration_seconds = context.data["duration"]
161
+
162
+ # Record metrics
163
+ self.collector.record_request(
164
+ method=method,
165
+ endpoint=endpoint,
166
+ model=model,
167
+ status=status_code,
168
+ service_type=service_type,
169
+ )
170
+
171
+ if duration_seconds > 0:
172
+ self.collector.record_response_time(
173
+ duration_seconds=duration_seconds,
174
+ model=model,
175
+ endpoint=endpoint,
176
+ service_type=service_type,
177
+ )
178
+
179
+ # Decrement active requests
180
+ self.collector.dec_active_requests()
181
+
182
+ # Handle token metrics if present
183
+ if self.config.collect_token_metrics:
184
+ usage = context.data.get("usage", {})
185
+ if usage:
186
+ if input_tokens := usage.get("input_tokens"):
187
+ self.collector.record_tokens(
188
+ token_count=input_tokens,
189
+ token_type="input",
190
+ model=model,
191
+ service_type=service_type,
192
+ )
193
+ if output_tokens := usage.get("output_tokens"):
194
+ self.collector.record_tokens(
195
+ token_count=output_tokens,
196
+ token_type="output",
197
+ model=model,
198
+ service_type=service_type,
199
+ )
200
+ if cache_read := usage.get("cache_read_input_tokens"):
201
+ self.collector.record_tokens(
202
+ token_count=cache_read,
203
+ token_type="cache_read",
204
+ model=model,
205
+ service_type=service_type,
206
+ )
207
+ if cache_write := usage.get("cache_creation_input_tokens"):
208
+ self.collector.record_tokens(
209
+ token_count=cache_write,
210
+ token_type="cache_write",
211
+ model=model,
212
+ service_type=service_type,
213
+ )
214
+
215
+ # Handle cost metrics if present
216
+ if self.config.collect_cost_metrics and (cost := context.data.get("cost_usd")):
217
+ self.collector.record_cost(
218
+ cost_usd=cost,
219
+ model=model,
220
+ cost_type="total",
221
+ service_type=service_type,
222
+ )
223
+
224
+ logger.debug(
225
+ "metrics_request_completed",
226
+ request_id=request_id,
227
+ duration_seconds=duration_seconds,
228
+ status_code=status_code,
229
+ model=model,
230
+ )
231
+
232
+ async def _handle_request_failed(self, context: HookContext) -> None:
233
+ """Handle REQUEST_FAILED event."""
234
+ if not self.config.collect_error_metrics or not self.collector:
235
+ return
236
+
237
+ request_id = context.data.get("request_id", "unknown")
238
+ endpoint = context.data.get("endpoint", context.data.get("url", "/"))
239
+ model = context.data.get("model")
240
+ service_type = context.data.get("service_type", "unknown")
241
+ error = context.error
242
+ error_type = type(error).__name__ if error else "unknown"
243
+
244
+ # Record error
245
+ self.collector.record_error(
246
+ error_type=error_type,
247
+ endpoint=endpoint,
248
+ model=model,
249
+ service_type=service_type,
250
+ )
251
+
252
+ # Record as failed request
253
+ self.collector.record_request(
254
+ method=context.data.get("method", "UNKNOWN"),
255
+ endpoint=endpoint,
256
+ model=model,
257
+ status="error",
258
+ service_type=service_type,
259
+ )
260
+
261
+ # Clean up start time and decrement active requests
262
+ self._request_start_times.pop(request_id, None)
263
+ self.collector.dec_active_requests()
264
+
265
+ logger.debug(
266
+ "metrics_request_failed",
267
+ request_id=request_id,
268
+ error_type=error_type,
269
+ endpoint=endpoint,
270
+ )
271
+
272
+ async def _handle_provider_request(self, context: HookContext) -> None:
273
+ """Handle PROVIDER_REQUEST_PREPARED event."""
274
+ if not self.config.collect_request_metrics:
275
+ return
276
+
277
+ provider = context.provider or "unknown"
278
+ request_id = context.metadata.get("request_id", "unknown")
279
+
280
+ logger.debug(
281
+ "metrics_provider_request",
282
+ request_id=request_id,
283
+ provider=provider,
284
+ )
285
+
286
+ async def _handle_provider_response(self, context: HookContext) -> None:
287
+ """Handle PROVIDER_RESPONSE_RECEIVED event."""
288
+ if not self.config.collect_request_metrics:
289
+ return
290
+
291
+ provider = context.provider or "unknown"
292
+ request_id = context.metadata.get("request_id", "unknown")
293
+ status_code = context.data.get("status_code", 200)
294
+
295
+ logger.debug(
296
+ "metrics_provider_response",
297
+ request_id=request_id,
298
+ provider=provider,
299
+ status_code=status_code,
300
+ )
301
+
302
+ async def _handle_provider_error(self, context: HookContext) -> None:
303
+ """Handle PROVIDER_ERROR event."""
304
+ if not self.config.collect_error_metrics or not self.collector:
305
+ return
306
+
307
+ provider = context.provider or "unknown"
308
+ request_id = context.metadata.get("request_id", "unknown")
309
+ error = context.error
310
+ error_type = type(error).__name__ if error else "unknown"
311
+
312
+ # Record provider error
313
+ self.collector.record_error(
314
+ error_type=f"provider_{error_type}",
315
+ endpoint=context.data.get("endpoint", "/"),
316
+ model=context.data.get("model"),
317
+ service_type=provider,
318
+ )
319
+
320
+ logger.debug(
321
+ "metrics_provider_error",
322
+ request_id=request_id,
323
+ provider=provider,
324
+ error_type=error_type,
325
+ )
326
+
327
+ async def _handle_stream_start(self, context: HookContext) -> None:
328
+ """Handle PROVIDER_STREAM_START event."""
329
+ request_id = context.data.get("request_id", "unknown")
330
+ provider = context.provider or "unknown"
331
+
332
+ logger.debug(
333
+ "metrics_stream_started",
334
+ request_id=request_id,
335
+ provider=provider,
336
+ )
337
+
338
+ async def _handle_stream_chunk(self, context: HookContext) -> None:
339
+ """Handle PROVIDER_STREAM_CHUNK event."""
340
+ # We might not want to record metrics for every chunk
341
+ # due to performance considerations
342
+ pass
343
+
344
+ async def _handle_stream_end(self, context: HookContext) -> None:
345
+ """Handle PROVIDER_STREAM_END event."""
346
+ if not self.config.collect_token_metrics or not self.collector:
347
+ return
348
+
349
+ request_id = context.data.get("request_id", "unknown")
350
+ provider = context.provider or "unknown"
351
+ usage_metrics = context.data.get("usage_metrics", {})
352
+ model = context.data.get("model")
353
+
354
+ # Record streaming token metrics
355
+ if usage_metrics:
356
+ if input_tokens := usage_metrics.get("input_tokens"):
357
+ self.collector.record_tokens(
358
+ token_count=input_tokens,
359
+ token_type="input",
360
+ model=model,
361
+ service_type=provider,
362
+ )
363
+ if output_tokens := usage_metrics.get("output_tokens"):
364
+ self.collector.record_tokens(
365
+ token_count=output_tokens,
366
+ token_type="output",
367
+ model=model,
368
+ service_type=provider,
369
+ )
370
+
371
+ logger.debug(
372
+ "metrics_stream_ended",
373
+ request_id=request_id,
374
+ provider=provider,
375
+ usage_metrics=usage_metrics,
376
+ )
377
+
378
+ def get_collector(self) -> PrometheusMetrics | None:
379
+ """Get the Prometheus metrics collector instance.
380
+
381
+ Returns:
382
+ The metrics collector or None if disabled
383
+ """
384
+ return self.collector
385
+
386
+ def get_pushgateway_client(self) -> PushgatewayClient | None:
387
+ """Get the Pushgateway client instance.
388
+
389
+ Returns:
390
+ The pushgateway client or None if disabled
391
+ """
392
+ return self.pushgateway
393
+
394
+ async def push_metrics(self) -> bool:
395
+ """Push current metrics to Pushgateway.
396
+
397
+ Returns:
398
+ True if push succeeded, False otherwise
399
+ """
400
+ if not self.pushgateway or not self.collector or not self.collector.registry:
401
+ return False
402
+
403
+ return self.pushgateway.push_metrics(self.collector.registry)
@@ -0,0 +1,268 @@
1
+ """Metrics plugin implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins import (
7
+ PluginContext,
8
+ PluginManifest,
9
+ SystemPluginFactory,
10
+ SystemPluginRuntime,
11
+ )
12
+ from ccproxy.core.plugins.hooks import HookRegistry
13
+
14
+ from .config import MetricsConfig
15
+ from .hook import MetricsHook
16
+ from .routes import create_metrics_router
17
+
18
+
19
+ logger = get_plugin_logger()
20
+
21
+
22
+ class MetricsRuntime(SystemPluginRuntime):
23
+ """Runtime for metrics plugin."""
24
+
25
+ def __init__(self, manifest: PluginManifest):
26
+ """Initialize runtime."""
27
+ super().__init__(manifest)
28
+ self.config: MetricsConfig | None = None
29
+ self.hook: MetricsHook | None = None
30
+ self.pushgateway_task_name = "metrics_pushgateway"
31
+
32
+ async def _on_initialize(self) -> None:
33
+ """Initialize the metrics plugin."""
34
+ if not self.context:
35
+ raise RuntimeError("Context not set")
36
+
37
+ # Get configuration
38
+ config = self.context.get("config")
39
+ if not isinstance(config, MetricsConfig):
40
+ logger.debug("metrics_config_missing")
41
+ # Use default config if none provided
42
+ config = MetricsConfig()
43
+ logger.debug("metrics_configured")
44
+ self.config = config
45
+
46
+ if self.config.enabled:
47
+ # Create metrics hook
48
+ self.hook = MetricsHook(self.config)
49
+
50
+ # Register hook with registry
51
+ hook_registry = None
52
+
53
+ # Try direct from context first
54
+ hook_registry = self.context.get("hook_registry")
55
+ logger.debug(
56
+ "hook_registry_from_context",
57
+ found=hook_registry is not None,
58
+ context_keys=list(self.context.keys()) if self.context else [],
59
+ )
60
+
61
+ # If not found, try app state
62
+ if not hook_registry:
63
+ app = self.context.get("app")
64
+ if app and hasattr(app.state, "hook_registry"):
65
+ hook_registry = app.state.hook_registry
66
+ logger.debug("hook_registry_from_app_state", found=True)
67
+
68
+ if hook_registry and isinstance(hook_registry, HookRegistry):
69
+ hook_registry.register(self.hook)
70
+ logger.debug(
71
+ "metrics_hook_registered",
72
+ namespace=self.config.namespace,
73
+ pushgateway_enabled=self.config.pushgateway_enabled,
74
+ metrics_endpoint_enabled=self.config.metrics_endpoint_enabled,
75
+ )
76
+ else:
77
+ logger.warning(
78
+ "hook_registry_not_available",
79
+ message="Metrics plugin will not collect metrics via hooks",
80
+ )
81
+
82
+ # Register metrics endpoint if enabled
83
+ if self.config.metrics_endpoint_enabled and self.hook:
84
+ app = self.context.get("app")
85
+ if app:
86
+ # Create and register metrics router
87
+ metrics_router = create_metrics_router(self.hook.get_collector())
88
+ app.include_router(metrics_router, prefix="")
89
+ logger.info(
90
+ "metrics_ready",
91
+ enabled=True,
92
+ endpoint="/metrics",
93
+ namespace=self.config.namespace,
94
+ pushgateway_enabled=self.config.pushgateway_enabled,
95
+ pushgateway_url=self.config.pushgateway_url,
96
+ )
97
+
98
+ # Register pushgateway task with scheduler if enabled
99
+ if self.config.pushgateway_enabled and self.hook:
100
+ scheduler = self.context.get("scheduler")
101
+ if scheduler:
102
+ try:
103
+ # Register the task type if not already registered
104
+ from .tasks import PushgatewayTask
105
+
106
+ # Use scheduler's registry (DI), avoiding globals
107
+ registry = scheduler.task_registry
108
+ if not registry.has(self.pushgateway_task_name):
109
+ registry.register(
110
+ self.pushgateway_task_name, PushgatewayTask
111
+ )
112
+
113
+ # Add task instance to scheduler
114
+ await scheduler.add_task(
115
+ task_name=self.pushgateway_task_name,
116
+ task_type=self.pushgateway_task_name,
117
+ interval_seconds=self.config.pushgateway_push_interval,
118
+ enabled=True,
119
+ max_backoff_seconds=300.0, # Default backoff
120
+ metrics_config=self.config,
121
+ metrics_hook=self.hook,
122
+ )
123
+ logger.info(
124
+ "pushgateway_task_registered",
125
+ task_name=self.pushgateway_task_name,
126
+ url=self.config.pushgateway_url,
127
+ job=self.config.pushgateway_job,
128
+ interval=self.config.pushgateway_push_interval,
129
+ )
130
+ except Exception as e:
131
+ logger.error(
132
+ "pushgateway_task_registration_failed",
133
+ error=str(e),
134
+ exc_info=e,
135
+ )
136
+ else:
137
+ logger.warning(
138
+ "scheduler_not_available",
139
+ message="Pushgateway task will not be scheduled",
140
+ )
141
+
142
+ logger.debug(
143
+ "metrics_plugin_enabled",
144
+ namespace=self.config.namespace,
145
+ collect_request_metrics=self.config.collect_request_metrics,
146
+ collect_token_metrics=self.config.collect_token_metrics,
147
+ collect_cost_metrics=self.config.collect_cost_metrics,
148
+ collect_error_metrics=self.config.collect_error_metrics,
149
+ collect_pool_metrics=self.config.collect_pool_metrics,
150
+ )
151
+ else:
152
+ logger.debug("metrics_plugin_disabled")
153
+
154
+ async def _on_shutdown(self) -> None:
155
+ """Cleanup on shutdown."""
156
+ # Remove pushgateway task from scheduler if registered
157
+ if self.config and self.config.pushgateway_enabled:
158
+ scheduler = None
159
+ if self.context:
160
+ scheduler = self.context.get("scheduler")
161
+
162
+ if scheduler:
163
+ try:
164
+ await scheduler.remove_task(self.pushgateway_task_name)
165
+ logger.debug(
166
+ "pushgateway_task_removed", task_name=self.pushgateway_task_name
167
+ )
168
+ except Exception as e:
169
+ logger.warning(
170
+ "pushgateway_task_removal_failed",
171
+ task_name=self.pushgateway_task_name,
172
+ error=str(e),
173
+ )
174
+
175
+ # Unregister hook from registry
176
+ if self.hook:
177
+ hook_registry = None
178
+ if self.context:
179
+ app = self.context.get("app")
180
+ if app and hasattr(app.state, "hook_registry"):
181
+ hook_registry = app.state.hook_registry
182
+ if not hook_registry:
183
+ hook_registry = self.context.get("hook_registry")
184
+
185
+ if hook_registry and isinstance(hook_registry, HookRegistry):
186
+ hook_registry.unregister(self.hook)
187
+ logger.debug("metrics_hook_unregistered")
188
+
189
+ # Push final metrics if pushgateway is enabled
190
+ if self.config and self.config.pushgateway_enabled and self.hook:
191
+ try:
192
+ await self.hook.push_metrics()
193
+ logger.info("final_metrics_pushed_to_pushgateway")
194
+ except Exception as e:
195
+ logger.error(
196
+ "final_metrics_push_failed",
197
+ error=str(e),
198
+ exc_info=e,
199
+ )
200
+
201
+ async def _get_health_details(self) -> dict[str, Any]:
202
+ """Get health check details."""
203
+ details = {
204
+ "type": "system",
205
+ "initialized": self.initialized,
206
+ "enabled": self.config.enabled if self.config else False,
207
+ }
208
+
209
+ if self.config and self.config.enabled:
210
+ collector_enabled = False
211
+ if self.hook:
212
+ col = self.hook.get_collector()
213
+ collector_enabled = bool(col.is_enabled()) if col else False
214
+
215
+ details.update(
216
+ {
217
+ "namespace": self.config.namespace,
218
+ "metrics_endpoint_enabled": self.config.metrics_endpoint_enabled,
219
+ "pushgateway_enabled": self.config.pushgateway_enabled,
220
+ "pushgateway_url": self.config.pushgateway_url,
221
+ "collector_enabled": collector_enabled,
222
+ }
223
+ )
224
+
225
+ return details
226
+
227
+
228
+ class MetricsFactory(SystemPluginFactory):
229
+ """Factory for metrics plugin."""
230
+
231
+ def __init__(self) -> None:
232
+ """Initialize factory with manifest."""
233
+ # Create manifest
234
+ manifest = PluginManifest(
235
+ name="metrics",
236
+ version="0.1.0",
237
+ description="Prometheus metrics collection and export plugin",
238
+ is_provider=False,
239
+ config_class=MetricsConfig,
240
+ )
241
+
242
+ # Initialize with manifest
243
+ super().__init__(manifest)
244
+
245
+ def create_runtime(self) -> MetricsRuntime:
246
+ """Create runtime instance."""
247
+ return MetricsRuntime(self.manifest)
248
+
249
+ def create_context(self, core_services: Any) -> PluginContext:
250
+ """Create context for the plugin.
251
+
252
+ Args:
253
+ core_services: Core services from the application
254
+
255
+ Returns:
256
+ Plugin context with required services
257
+ """
258
+ # Get base context
259
+ context = super().create_context(core_services)
260
+
261
+ # The metrics plugin doesn't need special context setup
262
+ # It will get hook_registry and app from the base context
263
+
264
+ return context
265
+
266
+
267
+ # Export the factory instance
268
+ factory = MetricsFactory()