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
@@ -1,753 +0,0 @@
1
- """
2
- Stats collector and printer for periodic metrics summary.
3
-
4
- This module provides functionality to collect and print periodic statistics
5
- from the observability system, including Prometheus metrics and DuckDB storage.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import time
12
- from dataclasses import dataclass
13
- from datetime import datetime
14
- from typing import Any
15
-
16
- import structlog
17
-
18
- from ccproxy.config.observability import ObservabilitySettings
19
-
20
-
21
- logger = structlog.get_logger(__name__)
22
-
23
-
24
- @dataclass
25
- class StatsSnapshot:
26
- """Snapshot of current statistics."""
27
-
28
- timestamp: datetime
29
- requests_total: int
30
- requests_last_minute: int
31
- avg_response_time_ms: float
32
- avg_response_time_last_minute_ms: float
33
- tokens_input_total: int
34
- tokens_output_total: int
35
- tokens_input_last_minute: int
36
- tokens_output_last_minute: int
37
- cost_total_usd: float
38
- cost_last_minute_usd: float
39
- errors_total: int
40
- errors_last_minute: int
41
- active_requests: int
42
- top_model: str
43
- top_model_percentage: float
44
-
45
-
46
- class StatsCollector:
47
- """
48
- Collects and formats metrics statistics for periodic printing.
49
-
50
- Integrates with both Prometheus metrics and DuckDB storage to provide
51
- comprehensive statistics about the API performance.
52
- """
53
-
54
- def __init__(
55
- self,
56
- settings: ObservabilitySettings,
57
- metrics_instance: Any | None = None,
58
- storage_instance: Any | None = None,
59
- ):
60
- """
61
- Initialize stats collector.
62
-
63
- Args:
64
- settings: Observability configuration settings
65
- metrics_instance: Prometheus metrics instance
66
- storage_instance: DuckDB storage instance
67
- """
68
- self.settings = settings
69
- self._metrics_instance = metrics_instance
70
- self._storage_instance = storage_instance
71
- self._last_snapshot: StatsSnapshot | None = None
72
- self._last_collection_time = time.time()
73
-
74
- async def collect_stats(self) -> StatsSnapshot:
75
- """
76
- Collect current statistics from all available sources.
77
-
78
- Returns:
79
- StatsSnapshot with current metrics
80
- """
81
- current_time = time.time()
82
- timestamp = datetime.now()
83
-
84
- # Initialize default values
85
- stats_data: dict[str, Any] = {
86
- "timestamp": timestamp,
87
- "requests_total": 0,
88
- "requests_last_minute": 0,
89
- "avg_response_time_ms": 0.0,
90
- "avg_response_time_last_minute_ms": 0.0,
91
- "tokens_input_total": 0,
92
- "tokens_output_total": 0,
93
- "tokens_input_last_minute": 0,
94
- "tokens_output_last_minute": 0,
95
- "cost_total_usd": 0.0,
96
- "cost_last_minute_usd": 0.0,
97
- "errors_total": 0,
98
- "errors_last_minute": 0,
99
- "active_requests": 0,
100
- "top_model": "unknown",
101
- "top_model_percentage": 0.0,
102
- }
103
-
104
- # Collect from Prometheus metrics if available
105
- if self._metrics_instance and self._metrics_instance.is_enabled():
106
- try:
107
- await self._collect_from_prometheus(stats_data)
108
- except Exception as e:
109
- logger.warning(
110
- "Failed to collect from Prometheus metrics", error=str(e)
111
- )
112
-
113
- # Collect from DuckDB storage if available
114
- if self._storage_instance and self._storage_instance.is_enabled():
115
- try:
116
- await self._collect_from_duckdb(stats_data, current_time)
117
- except Exception as e:
118
- logger.warning("Failed to collect from DuckDB storage", error=str(e))
119
-
120
- snapshot = StatsSnapshot(
121
- timestamp=stats_data["timestamp"],
122
- requests_total=int(stats_data["requests_total"]),
123
- requests_last_minute=int(stats_data["requests_last_minute"]),
124
- avg_response_time_ms=float(stats_data["avg_response_time_ms"]),
125
- avg_response_time_last_minute_ms=float(
126
- stats_data["avg_response_time_last_minute_ms"]
127
- ),
128
- tokens_input_total=int(stats_data["tokens_input_total"]),
129
- tokens_output_total=int(stats_data["tokens_output_total"]),
130
- tokens_input_last_minute=int(stats_data["tokens_input_last_minute"]),
131
- tokens_output_last_minute=int(stats_data["tokens_output_last_minute"]),
132
- cost_total_usd=float(stats_data["cost_total_usd"]),
133
- cost_last_minute_usd=float(stats_data["cost_last_minute_usd"]),
134
- errors_total=int(stats_data["errors_total"]),
135
- errors_last_minute=int(stats_data["errors_last_minute"]),
136
- active_requests=int(stats_data["active_requests"]),
137
- top_model=str(stats_data["top_model"]),
138
- top_model_percentage=float(stats_data["top_model_percentage"]),
139
- )
140
- self._last_snapshot = snapshot
141
- self._last_collection_time = current_time
142
-
143
- return snapshot
144
-
145
- async def _collect_from_prometheus(self, stats_data: dict[str, Any]) -> None:
146
- """Collect statistics from Prometheus metrics."""
147
- if not self._metrics_instance:
148
- return
149
-
150
- try:
151
- logger.debug(
152
- "prometheus_collection_starting",
153
- metrics_available=bool(self._metrics_instance),
154
- )
155
-
156
- # Get active requests from gauge
157
- if hasattr(self._metrics_instance, "active_requests"):
158
- active_value = self._metrics_instance.active_requests._value._value
159
- stats_data["active_requests"] = int(active_value)
160
- logger.debug(
161
- "prometheus_active_requests_collected", active_requests=active_value
162
- )
163
-
164
- # Get request counts from counter
165
- if hasattr(self._metrics_instance, "request_counter"):
166
- request_counter = self._metrics_instance.request_counter
167
- # Sum all request counts across all labels
168
- total_requests = 0
169
- for metric in request_counter.collect():
170
- for sample in metric.samples:
171
- if sample.name.endswith("_total"):
172
- total_requests += sample.value
173
- stats_data["requests_total"] = int(total_requests)
174
-
175
- # Calculate last minute requests (difference from last snapshot)
176
- if self._last_snapshot:
177
- last_minute_requests = (
178
- total_requests - self._last_snapshot.requests_total
179
- )
180
- stats_data["requests_last_minute"] = max(
181
- 0, int(last_minute_requests)
182
- )
183
- else:
184
- stats_data["requests_last_minute"] = int(total_requests)
185
-
186
- logger.debug(
187
- "prometheus_requests_collected",
188
- total_requests=total_requests,
189
- requests_last_minute=stats_data["requests_last_minute"],
190
- )
191
-
192
- # Get response times from histogram
193
- if hasattr(self._metrics_instance, "response_time"):
194
- response_time = self._metrics_instance.response_time
195
- # Get total count and sum for average calculation
196
- total_count = 0
197
- total_sum = 0
198
- for metric in response_time.collect():
199
- for sample in metric.samples:
200
- if sample.name.endswith("_count"):
201
- total_count += sample.value
202
- elif sample.name.endswith("_sum"):
203
- total_sum += sample.value
204
-
205
- if total_count > 0:
206
- avg_response_time_seconds = total_sum / total_count
207
- stats_data["avg_response_time_ms"] = (
208
- avg_response_time_seconds * 1000
209
- )
210
-
211
- # Calculate last minute average response time
212
- if self._last_snapshot and self._last_snapshot.requests_total > 0:
213
- last_minute_count = (
214
- total_count - self._last_snapshot.requests_total
215
- )
216
- if last_minute_count > 0:
217
- # Calculate the sum for just the last minute
218
- last_minute_sum = total_sum - (
219
- self._last_snapshot.requests_total
220
- * self._last_snapshot.avg_response_time_ms
221
- / 1000
222
- )
223
- last_minute_avg = (
224
- last_minute_sum / last_minute_count
225
- ) * 1000
226
- stats_data["avg_response_time_last_minute_ms"] = float(
227
- last_minute_avg
228
- )
229
- else:
230
- stats_data["avg_response_time_last_minute_ms"] = 0.0
231
- else:
232
- stats_data["avg_response_time_last_minute_ms"] = stats_data[
233
- "avg_response_time_ms"
234
- ]
235
-
236
- # Get token counts from counter
237
- if hasattr(self._metrics_instance, "token_counter"):
238
- token_counter = self._metrics_instance.token_counter
239
- tokens_input = 0
240
- tokens_output = 0
241
- for metric in token_counter.collect():
242
- for sample in metric.samples:
243
- if sample.name.endswith("_total"):
244
- token_type = sample.labels.get("type", "")
245
- if token_type == "input":
246
- tokens_input += sample.value
247
- elif token_type == "output":
248
- tokens_output += sample.value
249
-
250
- stats_data["tokens_input_total"] = int(tokens_input)
251
- stats_data["tokens_output_total"] = int(tokens_output)
252
-
253
- # Calculate last minute tokens
254
- if self._last_snapshot:
255
- last_minute_input = (
256
- tokens_input - self._last_snapshot.tokens_input_total
257
- )
258
- last_minute_output = (
259
- tokens_output - self._last_snapshot.tokens_output_total
260
- )
261
- stats_data["tokens_input_last_minute"] = max(
262
- 0, int(last_minute_input)
263
- )
264
- stats_data["tokens_output_last_minute"] = max(
265
- 0, int(last_minute_output)
266
- )
267
- else:
268
- stats_data["tokens_input_last_minute"] = int(tokens_input)
269
- stats_data["tokens_output_last_minute"] = int(tokens_output)
270
-
271
- # Get cost from counter
272
- if hasattr(self._metrics_instance, "cost_counter"):
273
- cost_counter = self._metrics_instance.cost_counter
274
- total_cost = 0
275
- for metric in cost_counter.collect():
276
- for sample in metric.samples:
277
- if sample.name.endswith("_total"):
278
- total_cost += sample.value
279
- stats_data["cost_total_usd"] = float(total_cost)
280
-
281
- # Calculate last minute cost
282
- if self._last_snapshot:
283
- last_minute_cost = total_cost - self._last_snapshot.cost_total_usd
284
- stats_data["cost_last_minute_usd"] = max(
285
- 0.0, float(last_minute_cost)
286
- )
287
- else:
288
- stats_data["cost_last_minute_usd"] = float(total_cost)
289
-
290
- # Get error counts from counter
291
- if hasattr(self._metrics_instance, "error_counter"):
292
- error_counter = self._metrics_instance.error_counter
293
- total_errors = 0
294
- for metric in error_counter.collect():
295
- for sample in metric.samples:
296
- if sample.name.endswith("_total"):
297
- total_errors += sample.value
298
- stats_data["errors_total"] = int(total_errors)
299
-
300
- # Calculate last minute errors
301
- if self._last_snapshot:
302
- last_minute_errors = total_errors - self._last_snapshot.errors_total
303
- stats_data["errors_last_minute"] = max(0, int(last_minute_errors))
304
- else:
305
- stats_data["errors_last_minute"] = int(total_errors)
306
-
307
- logger.debug(
308
- "prometheus_stats_collected",
309
- requests_total=stats_data["requests_total"],
310
- requests_last_minute=stats_data["requests_last_minute"],
311
- avg_response_time_ms=stats_data["avg_response_time_ms"],
312
- tokens_input_total=stats_data["tokens_input_total"],
313
- tokens_output_total=stats_data["tokens_output_total"],
314
- cost_total_usd=stats_data["cost_total_usd"],
315
- errors_total=stats_data["errors_total"],
316
- active_requests=stats_data["active_requests"],
317
- )
318
-
319
- except Exception as e:
320
- logger.debug("Failed to get metrics from Prometheus", error=str(e))
321
-
322
- async def _collect_from_duckdb(
323
- self, stats_data: dict[str, Any], current_time: float
324
- ) -> None:
325
- """Collect statistics from DuckDB storage."""
326
- if not self._storage_instance:
327
- return
328
-
329
- try:
330
- # Get overall analytics
331
- overall_analytics = await self._storage_instance.get_analytics()
332
- if overall_analytics and "summary" in overall_analytics:
333
- summary = overall_analytics["summary"]
334
- stats_data["requests_total"] = summary.get("total_requests", 0)
335
- stats_data["avg_response_time_ms"] = summary.get("avg_duration_ms", 0.0)
336
- stats_data["tokens_input_total"] = summary.get("total_tokens_input", 0)
337
- stats_data["tokens_output_total"] = summary.get(
338
- "total_tokens_output", 0
339
- )
340
- stats_data["cost_total_usd"] = summary.get("total_cost_usd", 0.0)
341
-
342
- # Get last minute analytics
343
- one_minute_ago = current_time - 60
344
- last_minute_analytics = await self._storage_instance.get_analytics(
345
- start_time=one_minute_ago,
346
- end_time=current_time,
347
- )
348
-
349
- if last_minute_analytics and "summary" in last_minute_analytics:
350
- last_minute_summary = last_minute_analytics["summary"]
351
- stats_data["requests_last_minute"] = last_minute_summary.get(
352
- "total_requests", 0
353
- )
354
- stats_data["avg_response_time_last_minute_ms"] = (
355
- last_minute_summary.get("avg_duration_ms", 0.0)
356
- )
357
- stats_data["tokens_input_last_minute"] = last_minute_summary.get(
358
- "total_tokens_input", 0
359
- )
360
- stats_data["tokens_output_last_minute"] = last_minute_summary.get(
361
- "total_tokens_output", 0
362
- )
363
- stats_data["cost_last_minute_usd"] = last_minute_summary.get(
364
- "total_cost_usd", 0.0
365
- )
366
-
367
- # Get top model from last minute data
368
- await self._get_top_model(stats_data, one_minute_ago, current_time)
369
-
370
- except Exception as e:
371
- logger.debug("Failed to collect from DuckDB", error=str(e))
372
-
373
- async def _get_top_model(
374
- self, stats_data: dict[str, Any], start_time: float, end_time: float
375
- ) -> None:
376
- """Get the most used model in the time period."""
377
- if not self._storage_instance:
378
- return
379
-
380
- try:
381
- # Query for model usage
382
- sql = """
383
- SELECT model, COUNT(*) as request_count
384
- FROM access_logs
385
- WHERE timestamp >= ? AND timestamp <= ?
386
- GROUP BY model
387
- ORDER BY request_count DESC
388
- LIMIT 1
389
- """
390
-
391
- start_dt = datetime.fromtimestamp(start_time)
392
- end_dt = datetime.fromtimestamp(end_time)
393
-
394
- results = await self._storage_instance.query(
395
- sql, [start_dt, end_dt], limit=1
396
- )
397
-
398
- if results:
399
- top_model_data = results[0]
400
- stats_data["top_model"] = top_model_data.get("model", "unknown")
401
- request_count = top_model_data.get("request_count", 0)
402
-
403
- if stats_data["requests_last_minute"] > 0:
404
- stats_data["top_model_percentage"] = (
405
- request_count / stats_data["requests_last_minute"]
406
- ) * 100
407
- else:
408
- stats_data["top_model_percentage"] = 0.0
409
-
410
- except Exception as e:
411
- logger.debug("Failed to get top model", error=str(e))
412
-
413
- def _has_meaningful_activity(self, snapshot: StatsSnapshot) -> bool:
414
- """
415
- Check if there is meaningful activity to report.
416
-
417
- Args:
418
- snapshot: Stats snapshot to check
419
-
420
- Returns:
421
- True if there is meaningful activity, False otherwise
422
- """
423
- # Show stats if there are requests in the last minute
424
- if snapshot.requests_last_minute > 0:
425
- return True
426
-
427
- # Show stats if there are currently active requests
428
- if snapshot.active_requests > 0:
429
- return True
430
-
431
- # Show stats if there are any errors in the last minute
432
- if snapshot.errors_last_minute > 0:
433
- return True
434
-
435
- # Show stats if there are any total requests (for the first time)
436
- return snapshot.requests_total > 0 and self._last_snapshot is None
437
-
438
- def format_stats(self, snapshot: StatsSnapshot) -> str:
439
- """
440
- Format stats snapshot for display.
441
-
442
- Args:
443
- snapshot: Stats snapshot to format
444
-
445
- Returns:
446
- Formatted stats string
447
- """
448
- format_type = self.settings.stats_printing_format
449
-
450
- if format_type == "json":
451
- return self._format_json(snapshot)
452
- elif format_type == "rich":
453
- return self._format_rich(snapshot)
454
- elif format_type == "log":
455
- return self._format_log(snapshot)
456
- else: # console (default)
457
- return self._format_console(snapshot)
458
-
459
- def _format_console(self, snapshot: StatsSnapshot) -> str:
460
- """Format stats for console output."""
461
- timestamp_str = snapshot.timestamp.strftime("%Y-%m-%d %H:%M:%S")
462
-
463
- # Format response times
464
- avg_response_str = f"{snapshot.avg_response_time_ms:.1f}ms"
465
- avg_response_last_min_str = f"{snapshot.avg_response_time_last_minute_ms:.1f}ms"
466
-
467
- # Format costs
468
- cost_total_str = f"${snapshot.cost_total_usd:.4f}"
469
- cost_last_min_str = f"${snapshot.cost_last_minute_usd:.4f}"
470
-
471
- # Format top model percentage
472
- top_model_str = f"{snapshot.top_model} ({snapshot.top_model_percentage:.1f}%)"
473
-
474
- return f"""[{timestamp_str}] METRICS SUMMARY
475
- ├─ Requests: {snapshot.requests_last_minute} (last min) / {snapshot.requests_total} (total)
476
- ├─ Avg Response: {avg_response_last_min_str} (last min) / {avg_response_str} (overall)
477
- ├─ Tokens: {snapshot.tokens_input_last_minute:,} in / {snapshot.tokens_output_last_minute:,} out (last min)
478
- ├─ Cost: {cost_last_min_str} (last min) / {cost_total_str} (total)
479
- ├─ Errors: {snapshot.errors_last_minute} (last min) / {snapshot.errors_total} (total)
480
- ├─ Active: {snapshot.active_requests} requests
481
- └─ Top Model: {top_model_str}"""
482
-
483
- def _format_json(self, snapshot: StatsSnapshot) -> str:
484
- """Format stats for JSON output."""
485
- data = {
486
- "timestamp": snapshot.timestamp.isoformat(),
487
- "requests": {
488
- "last_minute": snapshot.requests_last_minute,
489
- "total": snapshot.requests_total,
490
- },
491
- "response_time_ms": {
492
- "last_minute": snapshot.avg_response_time_last_minute_ms,
493
- "overall": snapshot.avg_response_time_ms,
494
- },
495
- "tokens": {
496
- "input_last_minute": snapshot.tokens_input_last_minute,
497
- "output_last_minute": snapshot.tokens_output_last_minute,
498
- "input_total": snapshot.tokens_input_total,
499
- "output_total": snapshot.tokens_output_total,
500
- },
501
- "cost_usd": {
502
- "last_minute": snapshot.cost_last_minute_usd,
503
- "total": snapshot.cost_total_usd,
504
- },
505
- "errors": {
506
- "last_minute": snapshot.errors_last_minute,
507
- "total": snapshot.errors_total,
508
- },
509
- "active_requests": snapshot.active_requests,
510
- "top_model": {
511
- "name": snapshot.top_model,
512
- "percentage": snapshot.top_model_percentage,
513
- },
514
- }
515
- return json.dumps(data, indent=2)
516
-
517
- def _format_rich(self, snapshot: StatsSnapshot) -> str:
518
- """Format stats for rich console output with colors and styling."""
519
- try:
520
- # Try to import rich for enhanced formatting
521
- from io import StringIO
522
-
523
- from rich import box
524
- from rich.console import Console
525
- from rich.table import Table
526
-
527
- output_buffer = StringIO()
528
- console = Console(file=output_buffer, width=80, force_terminal=True)
529
- timestamp_str = snapshot.timestamp.strftime("%Y-%m-%d %H:%M:%S")
530
-
531
- # Create main stats table
532
- table = Table(title=f"METRICS SUMMARY - {timestamp_str}", box=box.ROUNDED)
533
- table.add_column("Metric", style="cyan", no_wrap=True)
534
- table.add_column("Last Minute", style="yellow", justify="right")
535
- table.add_column("Total", style="green", justify="right")
536
-
537
- # Add rows with formatted data
538
- table.add_row(
539
- "Requests",
540
- f"{snapshot.requests_last_minute:,}",
541
- f"{snapshot.requests_total:,}",
542
- )
543
-
544
- table.add_row(
545
- "Avg Response",
546
- f"{snapshot.avg_response_time_last_minute_ms:.1f}ms",
547
- f"{snapshot.avg_response_time_ms:.1f}ms",
548
- )
549
-
550
- table.add_row(
551
- "Tokens In",
552
- f"{snapshot.tokens_input_last_minute:,}",
553
- f"{snapshot.tokens_input_total:,}",
554
- )
555
-
556
- table.add_row(
557
- "Tokens Out",
558
- f"{snapshot.tokens_output_last_minute:,}",
559
- f"{snapshot.tokens_output_total:,}",
560
- )
561
-
562
- table.add_row(
563
- "Cost",
564
- f"${snapshot.cost_last_minute_usd:.4f}",
565
- f"${snapshot.cost_total_usd:.4f}",
566
- )
567
-
568
- table.add_row(
569
- "Errors",
570
- f"{snapshot.errors_last_minute}",
571
- f"{snapshot.errors_total}",
572
- )
573
-
574
- # Add single-column rows
575
- table.add_row("", "", "") # Separator
576
- table.add_row("Active Requests", f"{snapshot.active_requests}", "")
577
-
578
- table.add_row(
579
- "Top Model",
580
- f"{snapshot.top_model}",
581
- f"({snapshot.top_model_percentage:.1f}%)",
582
- )
583
-
584
- console.print(table)
585
- output = output_buffer.getvalue()
586
- output_buffer.close()
587
-
588
- return output.strip()
589
-
590
- except ImportError:
591
- # Fallback to console format if rich is not available
592
- logger.warning("Rich not available, falling back to console format")
593
- return self._format_console(snapshot)
594
- except Exception as e:
595
- logger.warning(
596
- f"Rich formatting failed: {e}, falling back to console format"
597
- )
598
- return self._format_console(snapshot)
599
-
600
- def _format_log(self, snapshot: StatsSnapshot) -> str:
601
- """Format stats for structured logging output."""
602
- timestamp_str = snapshot.timestamp.strftime("%Y-%m-%d %H:%M:%S")
603
-
604
- # Create a structured log entry
605
- log_data = {
606
- "timestamp": timestamp_str,
607
- "event": "metrics_summary",
608
- "requests": {
609
- "last_minute": snapshot.requests_last_minute,
610
- "total": snapshot.requests_total,
611
- },
612
- "response_time_ms": {
613
- "last_minute_avg": snapshot.avg_response_time_last_minute_ms,
614
- "overall_avg": snapshot.avg_response_time_ms,
615
- },
616
- "tokens": {
617
- "input_last_minute": snapshot.tokens_input_last_minute,
618
- "output_last_minute": snapshot.tokens_output_last_minute,
619
- "input_total": snapshot.tokens_input_total,
620
- "output_total": snapshot.tokens_output_total,
621
- },
622
- "cost_usd": {
623
- "last_minute": snapshot.cost_last_minute_usd,
624
- "total": snapshot.cost_total_usd,
625
- },
626
- "errors": {
627
- "last_minute": snapshot.errors_last_minute,
628
- "total": snapshot.errors_total,
629
- },
630
- "active_requests": snapshot.active_requests,
631
- "top_model": {
632
- "name": snapshot.top_model,
633
- "percentage": snapshot.top_model_percentage,
634
- },
635
- }
636
-
637
- # Format as a log line with key=value pairs
638
- log_parts = [f"[{timestamp_str}]", "event=metrics_summary"]
639
-
640
- log_parts.extend(
641
- [
642
- f"requests_last_min={snapshot.requests_last_minute}",
643
- f"requests_total={snapshot.requests_total}",
644
- f"avg_response_ms={snapshot.avg_response_time_ms:.1f}",
645
- f"avg_response_last_min_ms={snapshot.avg_response_time_last_minute_ms:.1f}",
646
- f"tokens_in_last_min={snapshot.tokens_input_last_minute}",
647
- f"tokens_out_last_min={snapshot.tokens_output_last_minute}",
648
- f"tokens_in_total={snapshot.tokens_input_total}",
649
- f"tokens_out_total={snapshot.tokens_output_total}",
650
- f"cost_last_min_usd={snapshot.cost_last_minute_usd:.4f}",
651
- f"cost_total_usd={snapshot.cost_total_usd:.4f}",
652
- f"errors_last_min={snapshot.errors_last_minute}",
653
- f"errors_total={snapshot.errors_total}",
654
- f"active_requests={snapshot.active_requests}",
655
- f"top_model={snapshot.top_model}",
656
- f"top_model_pct={snapshot.top_model_percentage:.1f}",
657
- ]
658
- )
659
-
660
- return " ".join(log_parts)
661
-
662
- async def print_stats(self) -> None:
663
- """Collect and print current statistics."""
664
- try:
665
- snapshot = await self.collect_stats()
666
-
667
- # Only print stats if there is meaningful activity
668
- if self._has_meaningful_activity(snapshot):
669
- formatted_stats = self.format_stats(snapshot)
670
-
671
- # Print to stdout for console visibility
672
- print(formatted_stats)
673
-
674
- # Also log for structured logging
675
- logger.info(
676
- "stats_printed",
677
- requests_last_minute=snapshot.requests_last_minute,
678
- requests_total=snapshot.requests_total,
679
- avg_response_time_ms=snapshot.avg_response_time_ms,
680
- cost_total_usd=snapshot.cost_total_usd,
681
- active_requests=snapshot.active_requests,
682
- top_model=snapshot.top_model,
683
- )
684
- else:
685
- logger.debug(
686
- "stats_skipped_no_activity",
687
- requests_last_minute=snapshot.requests_last_minute,
688
- requests_total=snapshot.requests_total,
689
- active_requests=snapshot.active_requests,
690
- )
691
-
692
- except Exception as e:
693
- logger.error("Failed to print stats", error=str(e), exc_info=True)
694
-
695
-
696
- # Global stats collector instance
697
- _global_stats_collector: StatsCollector | None = None
698
-
699
-
700
- def get_stats_collector(
701
- settings: ObservabilitySettings | None = None,
702
- metrics_instance: Any | None = None,
703
- storage_instance: Any | None = None,
704
- ) -> StatsCollector:
705
- """
706
- Get or create global stats collector instance.
707
-
708
- Args:
709
- settings: Observability settings
710
- metrics_instance: Metrics instance for dependency injection
711
- storage_instance: Storage instance for dependency injection
712
-
713
- Returns:
714
- StatsCollector instance
715
- """
716
- global _global_stats_collector
717
-
718
- if _global_stats_collector is None:
719
- if settings is None:
720
- from ccproxy.config.settings import get_settings
721
-
722
- settings = get_settings().observability
723
-
724
- if metrics_instance is None:
725
- try:
726
- from .metrics import get_metrics
727
-
728
- metrics_instance = get_metrics()
729
- except Exception as e:
730
- logger.warning("Failed to get metrics instance", error=str(e))
731
-
732
- if storage_instance is None:
733
- try:
734
- from .storage.duckdb_simple import SimpleDuckDBStorage
735
-
736
- storage_instance = SimpleDuckDBStorage(settings.duckdb_path)
737
- # Note: Storage needs to be initialized before use
738
- except Exception as e:
739
- logger.warning("Failed to get storage instance", error=str(e))
740
-
741
- _global_stats_collector = StatsCollector(
742
- settings=settings,
743
- metrics_instance=metrics_instance,
744
- storage_instance=storage_instance,
745
- )
746
-
747
- return _global_stats_collector
748
-
749
-
750
- def reset_stats_collector() -> None:
751
- """Reset global stats collector instance (mainly for testing)."""
752
- global _global_stats_collector
753
- _global_stats_collector = None