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
@@ -1,1029 +0,0 @@
1
- """Metrics endpoints for CCProxy API Server."""
2
-
3
- import time
4
- from datetime import datetime as dt
5
- from typing import Any, cast
6
-
7
- from fastapi import APIRouter, HTTPException, Query, Request, Response
8
- from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
9
- from sqlmodel import Session, col, desc, func, select
10
- from typing_extensions import TypedDict
11
-
12
- from ccproxy.api.dependencies import (
13
- DuckDBStorageDep,
14
- ObservabilityMetricsDep,
15
- SettingsDep,
16
- )
17
- from ccproxy.observability.storage.models import AccessLog
18
-
19
-
20
- class AnalyticsSummary(TypedDict):
21
- """TypedDict for analytics summary data."""
22
-
23
- total_requests: int
24
- total_successful_requests: int
25
- total_error_requests: int
26
- avg_duration_ms: float
27
- total_cost_usd: float
28
- total_tokens_input: int
29
- total_tokens_output: int
30
- total_cache_read_tokens: int
31
- total_cache_write_tokens: int
32
- total_tokens_all: int
33
-
34
-
35
- class TokenAnalytics(TypedDict):
36
- """TypedDict for token analytics data."""
37
-
38
- input_tokens: int
39
- output_tokens: int
40
- cache_read_tokens: int
41
- cache_write_tokens: int
42
- total_tokens: int
43
-
44
-
45
- class RequestAnalytics(TypedDict):
46
- """TypedDict for request analytics data."""
47
-
48
- total_requests: int
49
- successful_requests: int
50
- error_requests: int
51
- success_rate: float
52
- error_rate: float
53
-
54
-
55
- class ServiceBreakdown(TypedDict):
56
- """TypedDict for service type breakdown data."""
57
-
58
- request_count: int
59
- successful_requests: int
60
- error_requests: int
61
- success_rate: float
62
- error_rate: float
63
- avg_duration_ms: float
64
- total_cost_usd: float
65
- total_tokens_input: int
66
- total_tokens_output: int
67
- total_cache_read_tokens: int
68
- total_cache_write_tokens: int
69
- total_tokens_all: int
70
-
71
-
72
- class AnalyticsResult(TypedDict):
73
- """TypedDict for complete analytics result."""
74
-
75
- summary: AnalyticsSummary
76
- token_analytics: TokenAnalytics
77
- request_analytics: RequestAnalytics
78
- service_type_breakdown: dict[str, ServiceBreakdown]
79
- query_time: float
80
- backend: str
81
- query_params: dict[str, Any]
82
-
83
-
84
- # Create separate routers for different concerns
85
- prometheus_router = APIRouter(tags=["metrics"])
86
- logs_router = APIRouter(tags=["logs"])
87
- dashboard_router = APIRouter(tags=["dashboard"])
88
-
89
-
90
- @logs_router.get("/status")
91
- async def logs_status(metrics: ObservabilityMetricsDep) -> dict[str, str]:
92
- """Get observability system status."""
93
- return {
94
- "status": "healthy",
95
- "prometheus_enabled": str(metrics.is_enabled()),
96
- "observability_system": "hybrid_prometheus_structlog",
97
- }
98
-
99
-
100
- @dashboard_router.get("/dashboard")
101
- async def get_metrics_dashboard() -> HTMLResponse:
102
- """Serve the metrics dashboard SPA entry point."""
103
- from pathlib import Path
104
-
105
- # Get the path to the dashboard folder
106
- current_file = Path(__file__)
107
- project_root = (
108
- current_file.parent.parent.parent.parent
109
- ) # ccproxy/api/routes/metrics.py -> project root
110
- dashboard_folder = project_root / "ccproxy" / "static" / "dashboard"
111
- dashboard_index = dashboard_folder / "index.html"
112
-
113
- # Check if dashboard folder and index.html exist
114
- if not dashboard_folder.exists():
115
- raise HTTPException(
116
- status_code=404,
117
- detail="Dashboard not found. Please build the dashboard first using 'cd dashboard && bun run build:prod'",
118
- )
119
-
120
- if not dashboard_index.exists():
121
- raise HTTPException(
122
- status_code=404,
123
- detail="Dashboard index.html not found. Please rebuild the dashboard using 'cd dashboard && bun run build:prod'",
124
- )
125
-
126
- # Read the HTML content
127
- try:
128
- with dashboard_index.open(encoding="utf-8") as f:
129
- html_content = f.read()
130
-
131
- return HTMLResponse(
132
- content=html_content,
133
- status_code=200,
134
- headers={
135
- "Cache-Control": "no-cache, no-store, must-revalidate",
136
- "Pragma": "no-cache",
137
- "Expires": "0",
138
- "Content-Type": "text/html; charset=utf-8",
139
- },
140
- )
141
- except Exception as e:
142
- raise HTTPException(
143
- status_code=500, detail=f"Failed to serve dashboard: {str(e)}"
144
- ) from e
145
-
146
-
147
- @dashboard_router.get("/dashboard/favicon.svg")
148
- async def get_dashboard_favicon() -> FileResponse:
149
- """Serve the dashboard favicon."""
150
- from pathlib import Path
151
-
152
- # Get the path to the favicon
153
- current_file = Path(__file__)
154
- project_root = (
155
- current_file.parent.parent.parent.parent
156
- ) # ccproxy/api/routes/metrics.py -> project root
157
- favicon_path = project_root / "ccproxy" / "static" / "dashboard" / "favicon.svg"
158
-
159
- if not favicon_path.exists():
160
- raise HTTPException(status_code=404, detail="Favicon not found")
161
-
162
- return FileResponse(
163
- path=str(favicon_path),
164
- media_type="image/svg+xml",
165
- headers={"Cache-Control": "public, max-age=3600"},
166
- )
167
-
168
-
169
- @prometheus_router.get("/metrics")
170
- async def get_prometheus_metrics(metrics: ObservabilityMetricsDep) -> Response:
171
- """Export metrics in Prometheus format using native prometheus_client.
172
-
173
- This endpoint exposes operational metrics collected by the hybrid observability
174
- system for Prometheus scraping.
175
-
176
- Args:
177
- metrics: Observability metrics dependency
178
-
179
- Returns:
180
- Prometheus-formatted metrics text
181
- """
182
- try:
183
- # Check if prometheus_client is available
184
- try:
185
- from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
186
- except ImportError as err:
187
- raise HTTPException(
188
- status_code=503,
189
- detail="Prometheus client not available. Install with: pip install prometheus-client",
190
- ) from err
191
-
192
- if not metrics.is_enabled():
193
- raise HTTPException(
194
- status_code=503,
195
- detail="Prometheus metrics not enabled. Ensure prometheus-client is installed.",
196
- )
197
-
198
- # Generate prometheus format using the registry
199
- from prometheus_client import REGISTRY
200
-
201
- # Use the global registry if metrics.registry is None (default behavior)
202
- registry = metrics.registry if metrics.registry is not None else REGISTRY
203
- prometheus_data = generate_latest(registry)
204
-
205
- # Return the metrics data with proper content type
206
- from fastapi import Response
207
-
208
- return Response(
209
- content=prometheus_data,
210
- media_type=CONTENT_TYPE_LATEST,
211
- headers={
212
- "Cache-Control": "no-cache, no-store, must-revalidate",
213
- "Pragma": "no-cache",
214
- "Expires": "0",
215
- },
216
- )
217
-
218
- except HTTPException:
219
- raise
220
- except Exception as e:
221
- raise HTTPException(
222
- status_code=500, detail=f"Failed to generate Prometheus metrics: {str(e)}"
223
- ) from e
224
-
225
-
226
- @logs_router.get("/query")
227
- async def query_logs(
228
- storage: DuckDBStorageDep,
229
- settings: SettingsDep,
230
- limit: int = Query(1000, ge=1, le=10000, description="Maximum number of results"),
231
- start_time: float | None = Query(None, description="Start timestamp filter"),
232
- end_time: float | None = Query(None, description="End timestamp filter"),
233
- model: str | None = Query(None, description="Model filter"),
234
- service_type: str | None = Query(None, description="Service type filter"),
235
- ) -> dict[str, Any]:
236
- """
237
- Query access logs with filters.
238
-
239
- Returns access log entries with optional filtering by time range, model, and service type.
240
- """
241
- try:
242
- if not settings.observability.logs_collection_enabled:
243
- raise HTTPException(
244
- status_code=503,
245
- detail="Logs collection is disabled. Enable with logs_collection_enabled=true",
246
- )
247
- if not storage:
248
- raise HTTPException(
249
- status_code=503,
250
- detail="Storage backend not available. Ensure DuckDB is installed and pipeline is running.",
251
- )
252
-
253
- # Use SQLModel for querying
254
- if hasattr(storage, "_engine") and storage._engine:
255
- try:
256
- with Session(storage._engine) as session:
257
- # Build base query
258
- statement = select(AccessLog)
259
-
260
- # Add filters - convert Unix timestamps to datetime
261
- start_dt = dt.fromtimestamp(start_time) if start_time else None
262
- end_dt = dt.fromtimestamp(end_time) if end_time else None
263
-
264
- if start_dt:
265
- statement = statement.where(AccessLog.timestamp >= start_dt)
266
- if end_dt:
267
- statement = statement.where(AccessLog.timestamp <= end_dt)
268
- if model:
269
- statement = statement.where(AccessLog.model == model)
270
- if service_type:
271
- statement = statement.where(
272
- AccessLog.service_type == service_type
273
- )
274
-
275
- # Apply limit and order
276
- statement = statement.order_by(desc(AccessLog.timestamp)).limit(
277
- limit
278
- )
279
-
280
- # Execute query
281
- results = session.exec(statement).all()
282
-
283
- # Convert to dict format
284
- entries = [log.dict() for log in results]
285
-
286
- return {
287
- "results": entries,
288
- "count": len(entries),
289
- "limit": limit,
290
- "filters": {
291
- "start_time": start_time,
292
- "end_time": end_time,
293
- "model": model,
294
- "service_type": service_type,
295
- },
296
- "timestamp": time.time(),
297
- }
298
-
299
- except Exception as e:
300
- import structlog
301
-
302
- logger = structlog.get_logger(__name__)
303
- logger.error("sqlmodel_query_error", error=str(e))
304
- raise HTTPException(
305
- status_code=500, detail=f"Query execution failed: {str(e)}"
306
- ) from e
307
- else:
308
- raise HTTPException(
309
- status_code=503,
310
- detail="Storage engine not available",
311
- )
312
-
313
- except HTTPException:
314
- raise
315
- except Exception as e:
316
- raise HTTPException(
317
- status_code=500, detail=f"Query execution failed: {str(e)}"
318
- ) from e
319
-
320
-
321
- @logs_router.get("/analytics")
322
- async def get_logs_analytics(
323
- storage: DuckDBStorageDep,
324
- settings: SettingsDep,
325
- start_time: float | None = Query(None, description="Start timestamp (Unix time)"),
326
- end_time: float | None = Query(None, description="End timestamp (Unix time)"),
327
- model: str | None = Query(None, description="Filter by model name"),
328
- service_type: str | None = Query(
329
- None,
330
- description="Filter by service type. Supports comma-separated values (e.g., 'proxy_service,sdk_service') and negation with ! prefix (e.g., '!access_log,!sdk_service')",
331
- ),
332
- hours: int | None = Query(
333
- 24, ge=1, le=168, description="Hours of data to analyze (default: 24)"
334
- ),
335
- ) -> AnalyticsResult:
336
- """
337
- Get comprehensive analytics for metrics data.
338
-
339
- Returns summary statistics, hourly trends, and model breakdowns.
340
- """
341
- try:
342
- if not settings.observability.logs_collection_enabled:
343
- raise HTTPException(
344
- status_code=503,
345
- detail="Logs collection is disabled. Enable with logs_collection_enabled=true",
346
- )
347
- if not storage:
348
- raise HTTPException(
349
- status_code=503,
350
- detail="Storage backend not available. Ensure DuckDB is installed and pipeline is running.",
351
- )
352
-
353
- # Default time range if not provided
354
- if start_time is None and end_time is None and hours:
355
- end_time = time.time()
356
- start_time = end_time - (hours * 3600)
357
-
358
- # Use SQLModel for analytics
359
- if hasattr(storage, "_engine") and storage._engine:
360
- try:
361
- with Session(storage._engine) as session:
362
- # Build base query
363
- statement = select(AccessLog)
364
-
365
- # Add filters - convert Unix timestamps to datetime
366
- start_dt = dt.fromtimestamp(start_time) if start_time else None
367
- end_dt = dt.fromtimestamp(end_time) if end_time else None
368
-
369
- # Helper function to build filter conditions
370
- def build_filter_conditions() -> list[Any]:
371
- conditions: list[Any] = []
372
- if start_dt:
373
- conditions.append(AccessLog.timestamp >= start_dt)
374
- if end_dt:
375
- conditions.append(AccessLog.timestamp <= end_dt)
376
- if model:
377
- conditions.append(AccessLog.model == model)
378
-
379
- # Apply service type filtering with comma-separated values and negation
380
- if service_type:
381
- service_filters = [
382
- s.strip() for s in service_type.split(",")
383
- ]
384
- include_filters = [
385
- f for f in service_filters if not f.startswith("!")
386
- ]
387
- exclude_filters = [
388
- f[1:] for f in service_filters if f.startswith("!")
389
- ]
390
-
391
- if include_filters:
392
- conditions.append(
393
- col(AccessLog.service_type).in_(include_filters)
394
- )
395
- if exclude_filters:
396
- conditions.append(
397
- ~col(AccessLog.service_type).in_(exclude_filters)
398
- )
399
-
400
- return conditions
401
-
402
- # Get summary statistics using individual queries to avoid overload issues
403
- # Reuse datetime variables defined above
404
-
405
- filter_conditions = build_filter_conditions()
406
-
407
- total_requests = session.exec(
408
- select(func.count())
409
- .select_from(AccessLog)
410
- .where(*filter_conditions)
411
- ).first()
412
-
413
- avg_duration = session.exec(
414
- select(func.avg(AccessLog.duration_ms))
415
- .select_from(AccessLog)
416
- .where(*filter_conditions)
417
- ).first()
418
-
419
- total_cost = session.exec(
420
- select(func.sum(AccessLog.cost_usd))
421
- .select_from(AccessLog)
422
- .where(*filter_conditions)
423
- ).first()
424
-
425
- total_tokens_input = session.exec(
426
- select(func.sum(AccessLog.tokens_input))
427
- .select_from(AccessLog)
428
- .where(*filter_conditions)
429
- ).first()
430
-
431
- total_tokens_output = session.exec(
432
- select(func.sum(AccessLog.tokens_output))
433
- .select_from(AccessLog)
434
- .where(*filter_conditions)
435
- ).first()
436
-
437
- # Token analytics - all token types
438
- total_cache_read_tokens = session.exec(
439
- select(func.sum(AccessLog.cache_read_tokens))
440
- .select_from(AccessLog)
441
- .where(*filter_conditions)
442
- ).first()
443
-
444
- total_cache_write_tokens = session.exec(
445
- select(func.sum(AccessLog.cache_write_tokens))
446
- .select_from(AccessLog)
447
- .where(*filter_conditions)
448
- ).first()
449
-
450
- # Success and error request analytics
451
- success_conditions = filter_conditions + [
452
- AccessLog.status_code >= 200,
453
- AccessLog.status_code < 400,
454
- ]
455
- total_successful_requests = session.exec(
456
- select(func.count())
457
- .select_from(AccessLog)
458
- .where(*success_conditions)
459
- ).first()
460
-
461
- error_conditions = filter_conditions + [
462
- AccessLog.status_code >= 400,
463
- ]
464
- total_error_requests = session.exec(
465
- select(func.count())
466
- .select_from(AccessLog)
467
- .where(*error_conditions)
468
- ).first()
469
-
470
- # Summary results are already computed individually above
471
-
472
- # Get service type breakdown - simplified approach
473
- service_breakdown = {}
474
- # Get unique service types first
475
- unique_services = session.exec(
476
- select(AccessLog.service_type)
477
- .distinct()
478
- .where(*filter_conditions)
479
- ).all()
480
-
481
- # For each service type, get its statistics
482
- for service in unique_services:
483
- if service: # Skip None values
484
- # Build service-specific filter conditions
485
- service_conditions = []
486
- if start_dt:
487
- service_conditions.append(
488
- AccessLog.timestamp >= start_dt
489
- )
490
- if end_dt:
491
- service_conditions.append(AccessLog.timestamp <= end_dt)
492
- if model:
493
- service_conditions.append(AccessLog.model == model)
494
- service_conditions.append(AccessLog.service_type == service)
495
-
496
- service_count = session.exec(
497
- select(func.count())
498
- .select_from(AccessLog)
499
- .where(*service_conditions)
500
- ).first()
501
-
502
- service_avg_duration = session.exec(
503
- select(func.avg(AccessLog.duration_ms))
504
- .select_from(AccessLog)
505
- .where(*service_conditions)
506
- ).first()
507
-
508
- service_total_cost = session.exec(
509
- select(func.sum(AccessLog.cost_usd))
510
- .select_from(AccessLog)
511
- .where(*service_conditions)
512
- ).first()
513
-
514
- service_total_tokens_input = session.exec(
515
- select(func.sum(AccessLog.tokens_input))
516
- .select_from(AccessLog)
517
- .where(*service_conditions)
518
- ).first()
519
-
520
- service_total_tokens_output = session.exec(
521
- select(func.sum(AccessLog.tokens_output))
522
- .select_from(AccessLog)
523
- .where(*service_conditions)
524
- ).first()
525
-
526
- service_cache_read_tokens = session.exec(
527
- select(func.sum(AccessLog.cache_read_tokens))
528
- .select_from(AccessLog)
529
- .where(*service_conditions)
530
- ).first()
531
-
532
- service_cache_write_tokens = session.exec(
533
- select(func.sum(AccessLog.cache_write_tokens))
534
- .select_from(AccessLog)
535
- .where(*service_conditions)
536
- ).first()
537
-
538
- service_success_conditions = service_conditions + [
539
- AccessLog.status_code >= 200,
540
- AccessLog.status_code < 400,
541
- ]
542
- service_success_count = session.exec(
543
- select(func.count())
544
- .select_from(AccessLog)
545
- .where(*service_success_conditions)
546
- ).first()
547
-
548
- service_error_conditions = service_conditions + [
549
- AccessLog.status_code >= 400,
550
- ]
551
- service_error_count = session.exec(
552
- select(func.count())
553
- .select_from(AccessLog)
554
- .where(*service_error_conditions)
555
- ).first()
556
-
557
- service_breakdown[service] = {
558
- "request_count": service_count or 0,
559
- "successful_requests": service_success_count or 0,
560
- "error_requests": service_error_count or 0,
561
- "success_rate": (service_success_count or 0)
562
- / (service_count or 1)
563
- * 100
564
- if service_count
565
- else 0,
566
- "error_rate": (service_error_count or 0)
567
- / (service_count or 1)
568
- * 100
569
- if service_count
570
- else 0,
571
- "avg_duration_ms": service_avg_duration or 0,
572
- "total_cost_usd": service_total_cost or 0,
573
- "total_tokens_input": service_total_tokens_input or 0,
574
- "total_tokens_output": service_total_tokens_output or 0,
575
- "total_cache_read_tokens": service_cache_read_tokens
576
- or 0,
577
- "total_cache_write_tokens": service_cache_write_tokens
578
- or 0,
579
- "total_tokens_all": (service_total_tokens_input or 0)
580
- + (service_total_tokens_output or 0)
581
- + (service_cache_read_tokens or 0)
582
- + (service_cache_write_tokens or 0),
583
- }
584
-
585
- analytics = {
586
- "summary": {
587
- "total_requests": total_requests or 0,
588
- "total_successful_requests": total_successful_requests or 0,
589
- "total_error_requests": total_error_requests or 0,
590
- "avg_duration_ms": avg_duration or 0,
591
- "total_cost_usd": total_cost or 0,
592
- "total_tokens_input": total_tokens_input or 0,
593
- "total_tokens_output": total_tokens_output or 0,
594
- "total_cache_read_tokens": total_cache_read_tokens or 0,
595
- "total_cache_write_tokens": total_cache_write_tokens or 0,
596
- "total_tokens_all": (total_tokens_input or 0)
597
- + (total_tokens_output or 0)
598
- + (total_cache_read_tokens or 0)
599
- + (total_cache_write_tokens or 0),
600
- },
601
- "token_analytics": {
602
- "input_tokens": total_tokens_input or 0,
603
- "output_tokens": total_tokens_output or 0,
604
- "cache_read_tokens": total_cache_read_tokens or 0,
605
- "cache_write_tokens": total_cache_write_tokens or 0,
606
- "total_tokens": (total_tokens_input or 0)
607
- + (total_tokens_output or 0)
608
- + (total_cache_read_tokens or 0)
609
- + (total_cache_write_tokens or 0),
610
- },
611
- "request_analytics": {
612
- "total_requests": total_requests or 0,
613
- "successful_requests": total_successful_requests or 0,
614
- "error_requests": total_error_requests or 0,
615
- "success_rate": (total_successful_requests or 0)
616
- / (total_requests or 1)
617
- * 100
618
- if total_requests
619
- else 0,
620
- "error_rate": (total_error_requests or 0)
621
- / (total_requests or 1)
622
- * 100
623
- if total_requests
624
- else 0,
625
- },
626
- "service_type_breakdown": service_breakdown,
627
- "query_time": time.time(),
628
- "backend": "sqlmodel",
629
- }
630
-
631
- # Add metadata
632
- analytics["query_params"] = {
633
- "start_time": start_time,
634
- "end_time": end_time,
635
- "model": model,
636
- "service_type": service_type,
637
- "hours": hours,
638
- }
639
-
640
- return cast(AnalyticsResult, analytics)
641
-
642
- except Exception as e:
643
- import structlog
644
-
645
- logger = structlog.get_logger(__name__)
646
- logger.error("sqlmodel_analytics_error", error=str(e))
647
- raise HTTPException(
648
- status_code=500, detail=f"Analytics query failed: {str(e)}"
649
- ) from e
650
- else:
651
- raise HTTPException(
652
- status_code=503,
653
- detail="Storage engine not available",
654
- )
655
-
656
- except HTTPException:
657
- raise
658
- except Exception as e:
659
- raise HTTPException(
660
- status_code=500, detail=f"Analytics generation failed: {str(e)}"
661
- ) from e
662
-
663
-
664
- @logs_router.get("/stream")
665
- async def stream_logs(
666
- request: Request,
667
- model: str | None = Query(None, description="Filter by model name"),
668
- service_type: str | None = Query(
669
- None,
670
- description="Filter by service type. Supports comma-separated values (e.g., 'proxy_service,sdk_service') and negation with ! prefix (e.g., '!access_log,!sdk_service')",
671
- ),
672
- min_duration_ms: float | None = Query(
673
- None, description="Filter by minimum duration in milliseconds"
674
- ),
675
- max_duration_ms: float | None = Query(
676
- None, description="Filter by maximum duration in milliseconds"
677
- ),
678
- status_code_min: int | None = Query(
679
- None, description="Filter by minimum status code"
680
- ),
681
- status_code_max: int | None = Query(
682
- None, description="Filter by maximum status code"
683
- ),
684
- ) -> StreamingResponse:
685
- """
686
- Stream real-time metrics and request logs via Server-Sent Events.
687
-
688
- Returns a continuous stream of request events using event-driven SSE
689
- instead of polling. Events are emitted in real-time when requests
690
- start, complete, or error. Supports filtering similar to analytics and entries endpoints.
691
- """
692
- import asyncio
693
- import uuid
694
- from collections.abc import AsyncIterator
695
-
696
- # Get request ID from request state
697
- request_id = getattr(request.state, "request_id", None)
698
-
699
- if request and hasattr(request, "state") and hasattr(request.state, "context"):
700
- # Use existing context from middleware
701
- ctx = request.state.context
702
- # Set streaming flag for access log
703
- ctx.add_metadata(streaming=True)
704
- ctx.add_metadata(event_type="streaming_complete")
705
-
706
- # Build filter criteria for event filtering
707
- filter_criteria = {
708
- "model": model,
709
- "service_type": service_type,
710
- "min_duration_ms": min_duration_ms,
711
- "max_duration_ms": max_duration_ms,
712
- "status_code_min": status_code_min,
713
- "status_code_max": status_code_max,
714
- }
715
- # Remove None values
716
- filter_criteria = {k: v for k, v in filter_criteria.items() if v is not None}
717
-
718
- def should_include_event(event_data: dict[str, Any]) -> bool:
719
- """Check if event matches filter criteria."""
720
- if not filter_criteria:
721
- return True
722
-
723
- data = event_data.get("data", {})
724
-
725
- # Model filter
726
- if "model" in filter_criteria and data.get("model") != filter_criteria["model"]:
727
- return False
728
-
729
- # Service type filter with comma-separated and negation support
730
- if "service_type" in filter_criteria:
731
- service_type_filter = filter_criteria["service_type"]
732
- if isinstance(service_type_filter, str):
733
- service_filters = [s.strip() for s in service_type_filter.split(",")]
734
- else:
735
- # Handle non-string types by converting to string
736
- service_filters = [str(service_type_filter).strip()]
737
- include_filters = [f for f in service_filters if not f.startswith("!")]
738
- exclude_filters = [f[1:] for f in service_filters if f.startswith("!")]
739
-
740
- data_service_type = data.get("service_type")
741
- if include_filters and data_service_type not in include_filters:
742
- return False
743
- if exclude_filters and data_service_type in exclude_filters:
744
- return False
745
-
746
- # Duration filters
747
- duration_ms = data.get("duration_ms")
748
- if duration_ms is not None:
749
- if (
750
- "min_duration_ms" in filter_criteria
751
- and duration_ms < filter_criteria["min_duration_ms"]
752
- ):
753
- return False
754
- if (
755
- "max_duration_ms" in filter_criteria
756
- and duration_ms > filter_criteria["max_duration_ms"]
757
- ):
758
- return False
759
-
760
- # Status code filters
761
- status_code = data.get("status_code")
762
- if status_code is not None:
763
- if (
764
- "status_code_min" in filter_criteria
765
- and status_code < filter_criteria["status_code_min"]
766
- ):
767
- return False
768
- if (
769
- "status_code_max" in filter_criteria
770
- and status_code > filter_criteria["status_code_max"]
771
- ):
772
- return False
773
-
774
- return True
775
-
776
- async def event_stream() -> AsyncIterator[str]:
777
- """Generate Server-Sent Events for real-time metrics."""
778
- from ccproxy.observability.sse_events import get_sse_manager
779
-
780
- # Get SSE manager
781
- sse_manager = get_sse_manager()
782
-
783
- # Create unique connection ID
784
- connection_id = str(uuid.uuid4())
785
-
786
- try:
787
- # Use SSE manager for event-driven streaming
788
- async for event_data in sse_manager.add_connection(
789
- connection_id, request_id
790
- ):
791
- # Parse event data to check for filtering
792
- if event_data.startswith("data: "):
793
- try:
794
- import json
795
-
796
- json_str = event_data[6:].strip()
797
- if json_str:
798
- event_obj = json.loads(json_str)
799
-
800
- # Apply filters for data events (not connection/system events)
801
- if (
802
- event_obj.get("type")
803
- in ["request_complete", "request_start"]
804
- and filter_criteria
805
- ) and not should_include_event(event_obj):
806
- continue # Skip this event
807
-
808
- except (json.JSONDecodeError, KeyError):
809
- # If we can't parse, pass through (system events)
810
- pass
811
-
812
- yield event_data
813
-
814
- except asyncio.CancelledError:
815
- # Connection was cancelled, cleanup handled by SSE manager
816
- pass
817
- except Exception as e:
818
- # Send error event
819
- import json
820
-
821
- error_event = {
822
- "type": "error",
823
- "message": str(e),
824
- "timestamp": time.time(),
825
- }
826
- yield f"data: {json.dumps(error_event)}\n\n"
827
-
828
- return StreamingResponse(
829
- event_stream(),
830
- media_type="text/event-stream",
831
- headers={
832
- "Cache-Control": "no-cache",
833
- "Connection": "keep-alive",
834
- "Access-Control-Allow-Origin": "*",
835
- "Access-Control-Allow-Headers": "Cache-Control",
836
- },
837
- )
838
-
839
-
840
- @logs_router.get("/entries")
841
- async def get_logs_entries(
842
- storage: DuckDBStorageDep,
843
- settings: SettingsDep,
844
- limit: int = Query(
845
- 50, ge=1, le=1000, description="Maximum number of entries to return"
846
- ),
847
- offset: int = Query(0, ge=0, description="Number of entries to skip"),
848
- order_by: str = Query(
849
- "timestamp",
850
- description="Column to order by (timestamp, duration_ms, cost_usd, model, service_type, status_code)",
851
- ),
852
- order_desc: bool = Query(False, description="Order in descending order"),
853
- service_type: str | None = Query(
854
- None,
855
- description="Filter by service type. Supports comma-separated values (e.g., 'proxy_service,sdk_service') and negation with ! prefix (e.g., '!access_log,!sdk_service')",
856
- ),
857
- ) -> dict[str, Any]:
858
- """
859
- Get the last n database entries from the access logs.
860
-
861
- Returns individual request entries with full details for analysis.
862
- """
863
- try:
864
- if not settings.observability.logs_collection_enabled:
865
- raise HTTPException(
866
- status_code=503,
867
- detail="Logs collection is disabled. Enable with logs_collection_enabled=true",
868
- )
869
- if not storage:
870
- raise HTTPException(
871
- status_code=503,
872
- detail="Storage backend not available. Ensure DuckDB is installed and pipeline is running.",
873
- )
874
-
875
- # Use SQLModel for entries
876
- if hasattr(storage, "_engine") and storage._engine:
877
- try:
878
- with Session(storage._engine) as session:
879
- # Validate order_by parameter using SQLModel
880
- valid_columns = list(AccessLog.model_fields.keys())
881
- if order_by not in valid_columns:
882
- order_by = "timestamp"
883
-
884
- # Build SQLModel query
885
- order_attr = getattr(AccessLog, order_by)
886
- order_clause = order_attr.desc() if order_desc else order_attr.asc()
887
-
888
- statement = select(AccessLog)
889
-
890
- # Apply service type filtering with comma-separated values and negation
891
- if service_type:
892
- service_filters = [s.strip() for s in service_type.split(",")]
893
- include_filters = [
894
- f for f in service_filters if not f.startswith("!")
895
- ]
896
- exclude_filters = [
897
- f[1:] for f in service_filters if f.startswith("!")
898
- ]
899
-
900
- if include_filters:
901
- statement = statement.where(
902
- col(AccessLog.service_type).in_(include_filters)
903
- )
904
- if exclude_filters:
905
- statement = statement.where(
906
- ~col(AccessLog.service_type).in_(exclude_filters)
907
- )
908
-
909
- statement = (
910
- statement.order_by(order_clause).offset(offset).limit(limit)
911
- )
912
- results = session.exec(statement).all()
913
-
914
- # Get total count with same filters
915
- count_statement = select(func.count()).select_from(AccessLog)
916
-
917
- # Apply same service type filtering to count
918
- if service_type:
919
- service_filters = [s.strip() for s in service_type.split(",")]
920
- include_filters = [
921
- f for f in service_filters if not f.startswith("!")
922
- ]
923
- exclude_filters = [
924
- f[1:] for f in service_filters if f.startswith("!")
925
- ]
926
-
927
- if include_filters:
928
- count_statement = count_statement.where(
929
- col(AccessLog.service_type).in_(include_filters)
930
- )
931
- if exclude_filters:
932
- count_statement = count_statement.where(
933
- ~col(AccessLog.service_type).in_(exclude_filters)
934
- )
935
-
936
- total_count = session.exec(count_statement).first()
937
-
938
- # Convert to dict format
939
- entries = [log.dict() for log in results]
940
-
941
- return {
942
- "entries": entries,
943
- "total_count": total_count,
944
- "limit": limit,
945
- "offset": offset,
946
- "order_by": order_by,
947
- "order_desc": order_desc,
948
- "service_type": service_type,
949
- "page": (offset // limit) + 1,
950
- "total_pages": ((total_count or 0) + limit - 1) // limit,
951
- "backend": "sqlmodel",
952
- }
953
-
954
- except Exception as e:
955
- import structlog
956
-
957
- logger = structlog.get_logger(__name__)
958
- logger.error("sqlmodel_entries_error", error=str(e))
959
- raise HTTPException(
960
- status_code=500, detail=f"Failed to retrieve entries: {str(e)}"
961
- ) from e
962
- else:
963
- raise HTTPException(
964
- status_code=503,
965
- detail="Storage engine not available",
966
- )
967
-
968
- except HTTPException:
969
- raise
970
- except Exception as e:
971
- raise HTTPException(
972
- status_code=500, detail=f"Failed to retrieve database entries: {str(e)}"
973
- ) from e
974
-
975
-
976
- @logs_router.post("/reset")
977
- async def reset_logs_data(
978
- storage: DuckDBStorageDep, settings: SettingsDep
979
- ) -> dict[str, Any]:
980
- """
981
- Reset all data in the logs storage.
982
-
983
- This endpoint clears all access logs from the database.
984
- Use with caution - this action cannot be undone.
985
-
986
- Returns:
987
- Dictionary with reset status and timestamp
988
- """
989
- try:
990
- if not settings.observability.logs_collection_enabled:
991
- raise HTTPException(
992
- status_code=503,
993
- detail="Logs collection is disabled. Enable with logs_collection_enabled=true",
994
- )
995
- if not storage:
996
- raise HTTPException(
997
- status_code=503,
998
- detail="Storage backend not available. Ensure DuckDB is installed.",
999
- )
1000
-
1001
- # Check if storage has reset_data method
1002
- if not hasattr(storage, "reset_data"):
1003
- raise HTTPException(
1004
- status_code=501,
1005
- detail="Reset operation not supported by current storage backend",
1006
- )
1007
-
1008
- # Perform the reset
1009
- success = await storage.reset_data()
1010
-
1011
- if success:
1012
- return {
1013
- "status": "success",
1014
- "message": "All logs data has been reset",
1015
- "timestamp": time.time(),
1016
- "backend": "duckdb",
1017
- }
1018
- else:
1019
- raise HTTPException(
1020
- status_code=500,
1021
- detail="Reset operation failed",
1022
- )
1023
-
1024
- except HTTPException:
1025
- raise
1026
- except Exception as e:
1027
- raise HTTPException(
1028
- status_code=500, detail=f"Reset operation failed: {str(e)}"
1029
- ) from e