ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__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.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.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.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -1 +0,0 @@
1
- """Storage backends for observability data."""
@@ -1,677 +0,0 @@
1
- """Simplified DuckDB storage for low-traffic environments.
2
-
3
- This module provides a simple, direct DuckDB storage implementation without
4
- connection pooling or batch processing. Suitable for dev environments with
5
- low request rates (< 10 req/s).
6
- """
7
-
8
- import asyncio
9
- import time
10
- from collections.abc import Sequence
11
- from datetime import datetime
12
- from pathlib import Path
13
- from typing import Any
14
-
15
- import structlog
16
- from sqlalchemy import text
17
- from sqlalchemy.engine import Engine
18
- from sqlmodel import Session, SQLModel, create_engine, desc, func, select
19
- from typing_extensions import TypedDict
20
-
21
- from .models import AccessLog
22
-
23
-
24
- logger = structlog.get_logger(__name__)
25
-
26
-
27
- class AccessLogPayload(TypedDict, total=False):
28
- """TypedDict for access log data payloads.
29
-
30
- Note: All fields are optional (total=False) to allow partial payloads.
31
- The storage layer will provide sensible defaults for missing fields.
32
- """
33
-
34
- # Core request identification
35
- request_id: str
36
- timestamp: int | float | datetime
37
-
38
- # Request details
39
- method: str
40
- endpoint: str
41
- path: str
42
- query: str
43
- client_ip: str
44
- user_agent: str
45
-
46
- # Service and model info
47
- service_type: str
48
- model: str
49
- streaming: bool
50
-
51
- # Response details
52
- status_code: int
53
- duration_ms: float
54
- duration_seconds: float
55
-
56
- # Token and cost tracking
57
- tokens_input: int
58
- tokens_output: int
59
- cache_read_tokens: int
60
- cache_write_tokens: int
61
- cost_usd: float
62
- cost_sdk_usd: float
63
- num_turns: int # number of conversation turns
64
-
65
- # Session context metadata
66
- session_type: str # "session_pool" or "direct"
67
- session_status: str # active, idle, connecting, etc.
68
- session_age_seconds: float # how long session has been alive
69
- session_message_count: int # number of messages in session
70
- session_client_id: str # unique session client identifier
71
- session_pool_enabled: bool # whether session pooling is enabled
72
- session_idle_seconds: float # how long since last activity
73
- session_error_count: int # number of errors in this session
74
- session_is_new: bool # whether this is a newly created session
75
-
76
-
77
- class SimpleDuckDBStorage:
78
- """Simple DuckDB storage with queue-based writes to prevent deadlocks."""
79
-
80
- def __init__(self, database_path: str | Path = "data/metrics.duckdb"):
81
- """Initialize simple DuckDB storage.
82
-
83
- Args:
84
- database_path: Path to DuckDB database file
85
- """
86
- self.database_path = Path(database_path)
87
- self._engine: Engine | None = None
88
- self._initialized: bool = False
89
- self._write_queue: asyncio.Queue[AccessLogPayload] = asyncio.Queue()
90
- self._background_worker_task: asyncio.Task[None] | None = None
91
- self._shutdown_event = asyncio.Event()
92
-
93
- async def initialize(self) -> None:
94
- """Initialize the storage backend."""
95
- if self._initialized:
96
- return
97
-
98
- try:
99
- # Ensure data directory exists
100
- self.database_path.parent.mkdir(parents=True, exist_ok=True)
101
-
102
- # Create SQLModel engine
103
- self._engine = create_engine(f"duckdb:///{self.database_path}")
104
-
105
- # Create schema using SQLModel (synchronous in main thread)
106
- self._create_schema_sync()
107
-
108
- # Start background worker for queue processing
109
- self._background_worker_task = asyncio.create_task(
110
- self._background_worker()
111
- )
112
-
113
- self._initialized = True
114
- logger.debug(
115
- "simple_duckdb_initialized", database_path=str(self.database_path)
116
- )
117
-
118
- except Exception as e:
119
- logger.error("simple_duckdb_init_error", error=str(e), exc_info=True)
120
- raise
121
-
122
- def _create_schema_sync(self) -> None:
123
- """Create database schema using SQLModel (synchronous)."""
124
- if not self._engine:
125
- return
126
-
127
- try:
128
- # Create tables using SQLModel metadata
129
- SQLModel.metadata.create_all(self._engine)
130
- logger.debug("duckdb_schema_created")
131
-
132
- except Exception as e:
133
- logger.error("simple_duckdb_schema_error", error=str(e))
134
- raise
135
-
136
- async def _ensure_query_column(self) -> None:
137
- """Ensure query column exists in the access_logs table."""
138
- if not self._engine:
139
- return
140
-
141
- try:
142
- with Session(self._engine) as session:
143
- # Check if query column exists
144
- result = session.execute(
145
- text(
146
- "SELECT column_name FROM information_schema.columns WHERE table_name = 'access_logs' AND column_name = 'query'"
147
- )
148
- )
149
- if not result.fetchone():
150
- # Add query column if it doesn't exist
151
- session.execute(
152
- text(
153
- "ALTER TABLE access_logs ADD COLUMN query VARCHAR DEFAULT ''"
154
- )
155
- )
156
- session.commit()
157
- logger.info("Added query column to access_logs table")
158
-
159
- except Exception as e:
160
- logger.warning("Failed to check/add query column", error=str(e))
161
- # Continue without failing - the column might already exist or schema might be different
162
-
163
- async def store_request(self, data: AccessLogPayload) -> bool:
164
- """Store a single request log entry asynchronously via queue.
165
-
166
- Args:
167
- data: Request data to store
168
-
169
- Returns:
170
- True if queued successfully
171
- """
172
- if not self._initialized:
173
- return False
174
-
175
- try:
176
- # Add to queue for background processing
177
- await self._write_queue.put(data)
178
- return True
179
- except Exception as e:
180
- logger.error(
181
- "queue_store_error",
182
- error=str(e),
183
- request_id=data.get("request_id"),
184
- )
185
- return False
186
-
187
- async def _background_worker(self) -> None:
188
- """Background worker to process queued write operations sequentially."""
189
- logger.debug("duckdb_background_worker_started")
190
-
191
- while not self._shutdown_event.is_set():
192
- try:
193
- # Wait for either a queue item or shutdown with timeout
194
- try:
195
- data = await asyncio.wait_for(self._write_queue.get(), timeout=1.0)
196
- except TimeoutError:
197
- continue # Check shutdown event and continue
198
-
199
- # Process the queued write operation synchronously
200
- try:
201
- success = self._store_request_sync(data)
202
- if success:
203
- logger.debug(
204
- "queue_processed_successfully",
205
- request_id=data.get("request_id"),
206
- )
207
- except Exception as e:
208
- logger.error(
209
- "background_worker_error",
210
- error=str(e),
211
- request_id=data.get("request_id"),
212
- exc_info=True,
213
- )
214
- finally:
215
- # Always mark the task as done, regardless of success/failure
216
- self._write_queue.task_done()
217
-
218
- except Exception as e:
219
- logger.error(
220
- "background_worker_unexpected_error",
221
- error=str(e),
222
- exc_info=True,
223
- )
224
- # Continue processing other items
225
-
226
- # Process any remaining items in the queue during shutdown
227
- logger.debug("processing_remaining_queue_items_on_shutdown")
228
- while not self._write_queue.empty():
229
- try:
230
- # Get remaining items without timeout during shutdown
231
- data = self._write_queue.get_nowait()
232
-
233
- # Process the queued write operation synchronously
234
- try:
235
- success = self._store_request_sync(data)
236
- if success:
237
- logger.debug(
238
- "shutdown_queue_processed_successfully",
239
- request_id=data.get("request_id"),
240
- )
241
- except Exception as e:
242
- logger.error(
243
- "shutdown_background_worker_error",
244
- error=str(e),
245
- request_id=data.get("request_id"),
246
- exc_info=True,
247
- )
248
- finally:
249
- # Always mark the task as done, regardless of success/failure
250
- self._write_queue.task_done()
251
-
252
- except asyncio.QueueEmpty:
253
- # No more items to process
254
- break
255
- except Exception as e:
256
- logger.error(
257
- "shutdown_background_worker_unexpected_error",
258
- error=str(e),
259
- exc_info=True,
260
- )
261
- # Continue processing other items
262
-
263
- logger.debug("duckdb_background_worker_stopped")
264
-
265
- def _store_request_sync(self, data: AccessLogPayload) -> bool:
266
- """Synchronous version of store_request for thread pool execution."""
267
- try:
268
- # Convert Unix timestamp to datetime if needed
269
- timestamp_value = data.get("timestamp", time.time())
270
- if isinstance(timestamp_value, int | float):
271
- timestamp_dt = datetime.fromtimestamp(timestamp_value)
272
- else:
273
- timestamp_dt = timestamp_value
274
-
275
- # Create AccessLog object with type validation
276
- access_log = AccessLog(
277
- request_id=data.get("request_id", ""),
278
- timestamp=timestamp_dt,
279
- method=data.get("method", ""),
280
- endpoint=data.get("endpoint", ""),
281
- path=data.get("path", data.get("endpoint", "")),
282
- query=data.get("query", ""),
283
- client_ip=data.get("client_ip", ""),
284
- user_agent=data.get("user_agent", ""),
285
- service_type=data.get("service_type", ""),
286
- model=data.get("model", ""),
287
- streaming=data.get("streaming", False),
288
- status_code=data.get("status_code", 200),
289
- duration_ms=data.get("duration_ms", 0.0),
290
- duration_seconds=data.get("duration_seconds", 0.0),
291
- tokens_input=data.get("tokens_input", 0),
292
- tokens_output=data.get("tokens_output", 0),
293
- cache_read_tokens=data.get("cache_read_tokens", 0),
294
- cache_write_tokens=data.get("cache_write_tokens", 0),
295
- cost_usd=data.get("cost_usd", 0.0),
296
- cost_sdk_usd=data.get("cost_sdk_usd", 0.0),
297
- )
298
-
299
- # Store using SQLModel session
300
- with Session(self._engine) as session:
301
- # Add new log entry (no merge needed as each request is unique)
302
- session.add(access_log)
303
- session.commit()
304
-
305
- logger.info(
306
- "simple_duckdb_store_success",
307
- request_id=data.get("request_id"),
308
- service_type=data.get("service_type", ""),
309
- model=data.get("model", ""),
310
- tokens_input=data.get("tokens_input", 0),
311
- tokens_output=data.get("tokens_output", 0),
312
- cost_usd=data.get("cost_usd", 0.0),
313
- endpoint=data.get("endpoint", ""),
314
- timestamp=timestamp_dt.isoformat() if timestamp_dt else None,
315
- )
316
- return True
317
-
318
- except Exception as e:
319
- logger.error(
320
- "simple_duckdb_store_error",
321
- error=str(e),
322
- request_id=data.get("request_id"),
323
- )
324
- return False
325
-
326
- async def store_batch(self, metrics: Sequence[AccessLogPayload]) -> bool:
327
- """Store a batch of metrics efficiently.
328
-
329
- Args:
330
- metrics: List of metric data to store
331
-
332
- Returns:
333
- True if batch stored successfully
334
- """
335
- if not self._initialized or not metrics or not self._engine:
336
- return False
337
-
338
- try:
339
- # Store using SQLModel with upsert behavior
340
- with Session(self._engine) as session:
341
- for metric in metrics:
342
- # Convert Unix timestamp to datetime if needed
343
- timestamp_value = metric.get("timestamp", time.time())
344
- if isinstance(timestamp_value, int | float):
345
- timestamp_dt = datetime.fromtimestamp(timestamp_value)
346
- else:
347
- timestamp_dt = timestamp_value
348
-
349
- # Create AccessLog object with type validation
350
- access_log = AccessLog(
351
- request_id=metric.get("request_id", ""),
352
- timestamp=timestamp_dt,
353
- method=metric.get("method", ""),
354
- endpoint=metric.get("endpoint", ""),
355
- path=metric.get("path", metric.get("endpoint", "")),
356
- query=metric.get("query", ""),
357
- client_ip=metric.get("client_ip", ""),
358
- user_agent=metric.get("user_agent", ""),
359
- service_type=metric.get("service_type", ""),
360
- model=metric.get("model", ""),
361
- streaming=metric.get("streaming", False),
362
- status_code=metric.get("status_code", 200),
363
- duration_ms=metric.get("duration_ms", 0.0),
364
- duration_seconds=metric.get("duration_seconds", 0.0),
365
- tokens_input=metric.get("tokens_input", 0),
366
- tokens_output=metric.get("tokens_output", 0),
367
- cache_read_tokens=metric.get("cache_read_tokens", 0),
368
- cache_write_tokens=metric.get("cache_write_tokens", 0),
369
- cost_usd=metric.get("cost_usd", 0.0),
370
- cost_sdk_usd=metric.get("cost_sdk_usd", 0.0),
371
- )
372
- # Use merge to handle potential duplicates
373
- session.merge(access_log)
374
-
375
- session.commit()
376
-
377
- logger.info(
378
- "simple_duckdb_batch_store_success",
379
- batch_size=len(metrics),
380
- service_types=[
381
- m.get("service_type", "") for m in metrics[:3]
382
- ], # First 3 for sampling
383
- request_ids=[
384
- m.get("request_id", "") for m in metrics[:3]
385
- ], # First 3 for sampling
386
- )
387
- return True
388
-
389
- except Exception as e:
390
- logger.error(
391
- "simple_duckdb_store_batch_error",
392
- error=str(e),
393
- metric_count=len(metrics),
394
- )
395
- return False
396
-
397
- async def store(self, metric: AccessLogPayload) -> bool:
398
- """Store single metric.
399
-
400
- Args:
401
- metric: Metric data to store
402
-
403
- Returns:
404
- True if stored successfully
405
- """
406
- return await self.store_batch([metric])
407
-
408
- async def query(
409
- self,
410
- sql: str,
411
- params: dict[str, Any] | list[Any] | None = None,
412
- limit: int = 1000,
413
- ) -> list[dict[str, Any]]:
414
- """Execute SQL query and return results.
415
-
416
- Args:
417
- sql: SQL query string
418
- params: Query parameters
419
- limit: Maximum number of results
420
-
421
- Returns:
422
- List of result rows as dictionaries
423
- """
424
- if not self._initialized or not self._engine:
425
- return []
426
-
427
- try:
428
- # Use SQLModel for querying
429
- with Session(self._engine) as session:
430
- # For now, we'll use raw SQL through the engine
431
- # In a full implementation, this would be converted to SQLModel queries
432
-
433
- # Use parameterized query to prevent SQL injection
434
- limited_sql = "SELECT * FROM (" + sql + ") LIMIT :limit"
435
-
436
- query_params = {"limit": limit}
437
- if params:
438
- # Merge user params with limit param
439
- if isinstance(params, dict):
440
- query_params.update(params)
441
- result = session.execute(text(limited_sql), query_params)
442
- else:
443
- # If params is a list, we need to handle it differently
444
- # For now, we'll use the safer approach of not supporting list params with limits
445
- result = session.execute(text(sql), params)
446
- else:
447
- result = session.execute(text(limited_sql), query_params)
448
-
449
- # Convert to list of dictionaries
450
- columns = list(result.keys())
451
- rows = result.fetchall()
452
-
453
- return [dict(zip(columns, row, strict=False)) for row in rows]
454
-
455
- except Exception as e:
456
- logger.error("simple_duckdb_query_error", sql=sql, error=str(e))
457
- return []
458
-
459
- async def get_recent_requests(self, limit: int = 100) -> list[dict[str, Any]]:
460
- """Get recent requests for debugging/monitoring.
461
-
462
- Args:
463
- limit: Number of recent requests to return
464
-
465
- Returns:
466
- List of recent request records
467
- """
468
- if not self._engine:
469
- return []
470
-
471
- try:
472
- with Session(self._engine) as session:
473
- statement = (
474
- select(AccessLog).order_by(desc(AccessLog.timestamp)).limit(limit)
475
- )
476
- results = session.exec(statement).all()
477
- return [log.dict() for log in results]
478
- except Exception as e:
479
- logger.error("sqlmodel_query_error", error=str(e))
480
- return []
481
-
482
- async def get_analytics(
483
- self,
484
- start_time: float | None = None,
485
- end_time: float | None = None,
486
- model: str | None = None,
487
- service_type: str | None = None,
488
- ) -> dict[str, Any]:
489
- """Get analytics using SQLModel.
490
-
491
- Args:
492
- start_time: Start timestamp (Unix time)
493
- end_time: End timestamp (Unix time)
494
- model: Filter by model name
495
- service_type: Filter by service type
496
-
497
- Returns:
498
- Analytics summary data
499
- """
500
- if not self._engine:
501
- return {}
502
-
503
- try:
504
- with Session(self._engine) as session:
505
- # Build base query
506
- statement = select(AccessLog)
507
-
508
- # Add filters - convert Unix timestamps to datetime
509
- if start_time:
510
- start_dt = datetime.fromtimestamp(start_time)
511
- statement = statement.where(AccessLog.timestamp >= start_dt)
512
- if end_time:
513
- end_dt = datetime.fromtimestamp(end_time)
514
- statement = statement.where(AccessLog.timestamp <= end_dt)
515
- if model:
516
- statement = statement.where(AccessLog.model == model)
517
- if service_type:
518
- statement = statement.where(AccessLog.service_type == service_type)
519
-
520
- # Get summary statistics using individual queries to avoid overload issues
521
- base_where_conditions = []
522
- if start_time:
523
- start_dt = datetime.fromtimestamp(start_time)
524
- base_where_conditions.append(AccessLog.timestamp >= start_dt)
525
- if end_time:
526
- end_dt = datetime.fromtimestamp(end_time)
527
- base_where_conditions.append(AccessLog.timestamp <= end_dt)
528
- if model:
529
- base_where_conditions.append(AccessLog.model == model)
530
- if service_type:
531
- base_where_conditions.append(AccessLog.service_type == service_type)
532
-
533
- total_requests = session.exec(
534
- select(func.count())
535
- .select_from(AccessLog)
536
- .where(*base_where_conditions)
537
- ).first()
538
-
539
- avg_duration = session.exec(
540
- select(func.avg(AccessLog.duration_ms))
541
- .select_from(AccessLog)
542
- .where(*base_where_conditions)
543
- ).first()
544
-
545
- total_cost = session.exec(
546
- select(func.sum(AccessLog.cost_usd))
547
- .select_from(AccessLog)
548
- .where(*base_where_conditions)
549
- ).first()
550
-
551
- total_tokens_input = session.exec(
552
- select(func.sum(AccessLog.tokens_input))
553
- .select_from(AccessLog)
554
- .where(*base_where_conditions)
555
- ).first()
556
-
557
- total_tokens_output = session.exec(
558
- select(func.sum(AccessLog.tokens_output))
559
- .select_from(AccessLog)
560
- .where(*base_where_conditions)
561
- ).first()
562
-
563
- return {
564
- "summary": {
565
- "total_requests": total_requests or 0,
566
- "avg_duration_ms": avg_duration or 0,
567
- "total_cost_usd": total_cost or 0,
568
- "total_tokens_input": total_tokens_input or 0,
569
- "total_tokens_output": total_tokens_output or 0,
570
- },
571
- "query_time": time.time(),
572
- }
573
-
574
- except Exception as e:
575
- logger.error("sqlmodel_analytics_error", error=str(e))
576
- return {}
577
-
578
- async def close(self) -> None:
579
- """Close the database connection and stop background worker."""
580
- # Signal shutdown to background worker
581
- self._shutdown_event.set()
582
-
583
- # Wait for background worker to finish
584
- if self._background_worker_task:
585
- try:
586
- await asyncio.wait_for(self._background_worker_task, timeout=5.0)
587
- except TimeoutError:
588
- logger.warning("background_worker_shutdown_timeout")
589
- self._background_worker_task.cancel()
590
- except Exception as e:
591
- logger.error("background_worker_shutdown_error", error=str(e))
592
-
593
- # Process remaining items in queue (with timeout)
594
- try:
595
- await asyncio.wait_for(self._write_queue.join(), timeout=2.0)
596
- except TimeoutError:
597
- logger.warning(
598
- "queue_drain_timeout", remaining_items=self._write_queue.qsize()
599
- )
600
-
601
- if self._engine:
602
- try:
603
- self._engine.dispose()
604
- except Exception as e:
605
- logger.error("simple_duckdb_engine_close_error", error=str(e))
606
- finally:
607
- self._engine = None
608
-
609
- self._initialized = False
610
-
611
- def is_enabled(self) -> bool:
612
- """Check if storage is enabled and available."""
613
- return self._initialized
614
-
615
- async def health_check(self) -> dict[str, Any]:
616
- """Get health status of the storage backend."""
617
- if not self._initialized:
618
- return {
619
- "status": "not_initialized",
620
- "enabled": False,
621
- }
622
-
623
- try:
624
- if self._engine:
625
- with Session(self._engine) as session:
626
- statement = select(func.count()).select_from(AccessLog)
627
- access_log_count = session.exec(statement).first()
628
-
629
- return {
630
- "status": "healthy",
631
- "enabled": True,
632
- "database_path": str(self.database_path),
633
- "access_log_count": access_log_count,
634
- "backend": "sqlmodel",
635
- }
636
- else:
637
- return {
638
- "status": "no_connection",
639
- "enabled": False,
640
- }
641
-
642
- except Exception as e:
643
- return {
644
- "status": "unhealthy",
645
- "enabled": False,
646
- "error": str(e),
647
- }
648
-
649
- async def reset_data(self) -> bool:
650
- """Reset all data in the storage (useful for testing/debugging).
651
-
652
- Returns:
653
- True if reset was successful
654
- """
655
- if not self._initialized or not self._engine:
656
- return False
657
-
658
- try:
659
- # Run the reset operation in a thread pool
660
- return await asyncio.to_thread(self._reset_data_sync)
661
- except Exception as e:
662
- logger.error("simple_duckdb_reset_error", error=str(e))
663
- return False
664
-
665
- def _reset_data_sync(self) -> bool:
666
- """Synchronous version of reset_data for thread pool execution."""
667
- try:
668
- with Session(self._engine) as session:
669
- # Delete all records from access_logs table
670
- session.execute(text("DELETE FROM access_logs"))
671
- session.commit()
672
-
673
- logger.info("simple_duckdb_reset_success")
674
- return True
675
- except Exception as e:
676
- logger.error("simple_duckdb_reset_sync_error", error=str(e))
677
- return False