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
@@ -0,0 +1,254 @@
1
+ """Utility functions for comprehensive access logging.
2
+
3
+ This module provides logging utilities adapted from the observability
4
+ module for use within the access_log plugin.
5
+ """
6
+
7
+ import time
8
+ from typing import Any
9
+
10
+ from ccproxy.core.logging import get_plugin_logger
11
+
12
+
13
+ logger = get_plugin_logger(__name__)
14
+
15
+
16
+ async def log_request_access(
17
+ request_id: str,
18
+ method: str | None = None,
19
+ path: str | None = None,
20
+ status_code: int | None = None,
21
+ duration_ms: float | None = None,
22
+ client_ip: str | None = None,
23
+ user_agent: str | None = None,
24
+ query: str | None = None,
25
+ error_message: str | None = None,
26
+ **additional_metadata: Any,
27
+ ) -> None:
28
+ """Log comprehensive access information for a request.
29
+
30
+ This function generates a unified access log entry with complete request
31
+ metadata including timing, tokens, costs, and any additional context.
32
+
33
+ Args:
34
+ request_id: Request identifier
35
+ method: HTTP method
36
+ path: Request path
37
+ status_code: HTTP status code
38
+ duration_ms: Request duration in milliseconds
39
+ client_ip: Client IP address
40
+ user_agent: User agent string
41
+ query: Query parameters
42
+ error_message: Error message if applicable
43
+ **additional_metadata: Any additional fields to include
44
+ """
45
+ # Prepare basic log data (always included)
46
+ log_data: dict[str, Any] = {
47
+ "request_id": request_id,
48
+ "method": method,
49
+ "path": path,
50
+ "query": query,
51
+ "client_ip": client_ip,
52
+ "user_agent": user_agent,
53
+ }
54
+
55
+ # Add response-specific fields
56
+ log_data.update(
57
+ {
58
+ "status_code": status_code,
59
+ "duration_ms": duration_ms,
60
+ "duration_seconds": duration_ms / 1000 if duration_ms else None,
61
+ "error_message": error_message,
62
+ }
63
+ )
64
+
65
+ # Add token and cost metrics if available in metadata
66
+ token_fields = [
67
+ "tokens_input",
68
+ "tokens_output",
69
+ "cache_read_tokens",
70
+ "cache_write_tokens",
71
+ "cost_usd",
72
+ "num_turns",
73
+ ]
74
+
75
+ for field in token_fields:
76
+ value = additional_metadata.get(field)
77
+ if value is not None:
78
+ log_data[field] = value
79
+
80
+ # Add service and endpoint info
81
+ service_fields = ["endpoint", "model", "streaming", "service_type", "provider"]
82
+
83
+ for field in service_fields:
84
+ value = additional_metadata.get(field)
85
+ if value is not None:
86
+ log_data[field] = value
87
+
88
+ # Add session context metadata if available
89
+ session_fields = [
90
+ "session_id",
91
+ "session_type",
92
+ "session_status",
93
+ "session_age_seconds",
94
+ "session_message_count",
95
+ "session_pool_enabled",
96
+ "session_idle_seconds",
97
+ "session_error_count",
98
+ "session_is_new",
99
+ ]
100
+
101
+ for field in session_fields:
102
+ value = additional_metadata.get(field)
103
+ if value is not None:
104
+ log_data[field] = value
105
+
106
+ # Add rate limit headers if available
107
+ rate_limit_fields = [
108
+ "x-ratelimit-limit",
109
+ "x-ratelimit-remaining",
110
+ "x-ratelimit-reset",
111
+ "anthropic-ratelimit-requests-limit",
112
+ "anthropic-ratelimit-requests-remaining",
113
+ "anthropic-ratelimit-requests-reset",
114
+ "anthropic-ratelimit-tokens-limit",
115
+ "anthropic-ratelimit-tokens-remaining",
116
+ "anthropic-ratelimit-tokens-reset",
117
+ "anthropic_request_id",
118
+ ]
119
+
120
+ for field in rate_limit_fields:
121
+ value = additional_metadata.get(field)
122
+ if value is not None:
123
+ log_data[field] = value
124
+
125
+ # Add any additional metadata provided
126
+ log_data.update(additional_metadata)
127
+
128
+ # Remove None values to keep log clean
129
+ log_data = {k: v for k, v in log_data.items() if v is not None}
130
+
131
+ # Log with appropriate level
132
+ bound_logger = logger.bind(**log_data)
133
+
134
+ if error_message:
135
+ bound_logger.warning("access_log", exc_info=additional_metadata.get("error"))
136
+ else:
137
+ is_streaming = additional_metadata.get("streaming", False)
138
+ is_streaming_complete = (
139
+ additional_metadata.get("event_type", "") == "streaming_complete"
140
+ )
141
+
142
+ if not is_streaming or is_streaming_complete:
143
+ bound_logger.info("access_log")
144
+ else:
145
+ # If streaming is true, and not streaming_complete log as debug
146
+ bound_logger.info("access_log_streaming_start")
147
+
148
+
149
+ def log_request_start(
150
+ request_id: str,
151
+ method: str,
152
+ path: str,
153
+ client_ip: str | None = None,
154
+ user_agent: str | None = None,
155
+ query: str | None = None,
156
+ **additional_metadata: Any,
157
+ ) -> None:
158
+ """Log request start event with basic information.
159
+
160
+ This is used for early/hook logging when full context isn't available yet.
161
+
162
+ Args:
163
+ request_id: Request identifier
164
+ method: HTTP method
165
+ path: Request path
166
+ client_ip: Client IP address
167
+ user_agent: User agent string
168
+ query: Query parameters
169
+ **additional_metadata: Any additional fields to include
170
+ """
171
+ log_data: dict[str, Any] = {
172
+ "request_id": request_id,
173
+ "method": method,
174
+ "path": path,
175
+ "client_ip": client_ip,
176
+ "user_agent": user_agent,
177
+ "query": query,
178
+ "event_type": "request_start",
179
+ "timestamp": time.time(),
180
+ }
181
+
182
+ # Add any additional metadata
183
+ log_data.update(additional_metadata)
184
+
185
+ # Remove None values
186
+ log_data = {k: v for k, v in log_data.items() if v is not None}
187
+
188
+ logger.debug("access_log_start", **log_data)
189
+
190
+
191
+ async def log_provider_access(
192
+ request_id: str,
193
+ provider: str,
194
+ method: str,
195
+ url: str,
196
+ status_code: int | None = None,
197
+ duration_ms: float | None = None,
198
+ error_message: str | None = None,
199
+ **additional_metadata: Any,
200
+ ) -> None:
201
+ """Log provider access information.
202
+
203
+ Args:
204
+ request_id: Request identifier
205
+ provider: Provider name
206
+ method: HTTP method
207
+ url: Provider URL
208
+ status_code: Response status code
209
+ duration_ms: Request duration in milliseconds
210
+ error_message: Error message if applicable
211
+ **additional_metadata: Any additional fields to include
212
+ """
213
+ log_data: dict[str, Any] = {
214
+ "request_id": request_id,
215
+ "provider": provider,
216
+ "method": method,
217
+ "url": url,
218
+ "status_code": status_code,
219
+ "duration_ms": duration_ms,
220
+ "duration_seconds": duration_ms / 1000 if duration_ms else None,
221
+ "error_message": error_message,
222
+ "event_type": "provider_access",
223
+ }
224
+
225
+ # Add token and cost metrics if available
226
+ token_fields = [
227
+ "tokens_input",
228
+ "tokens_output",
229
+ "cache_read_tokens",
230
+ "cache_write_tokens",
231
+ "cost_usd",
232
+ "model",
233
+ ]
234
+
235
+ for field in token_fields:
236
+ value = additional_metadata.get(field)
237
+ if value is not None:
238
+ log_data[field] = value
239
+
240
+ # Add any additional metadata
241
+ log_data.update(additional_metadata)
242
+
243
+ # Remove None values
244
+ log_data = {k: v for k, v in log_data.items() if v is not None}
245
+
246
+ # Log with appropriate level
247
+ bound_logger = logger.bind(**log_data)
248
+
249
+ if error_message:
250
+ bound_logger.warning(
251
+ "provider_access_log", exc_info=additional_metadata.get("error")
252
+ )
253
+ else:
254
+ bound_logger.info("provider_access_log")
@@ -0,0 +1,137 @@
1
+ from typing import Any
2
+
3
+ from ccproxy.core.logging import get_plugin_logger
4
+ from ccproxy.core.plugins import (
5
+ PluginManifest,
6
+ SystemPluginFactory,
7
+ SystemPluginRuntime,
8
+ )
9
+ from ccproxy.core.plugins.hooks import HookRegistry
10
+ from ccproxy.plugins.analytics.ingest import AnalyticsIngestService
11
+ from ccproxy.services.container import ServiceContainer
12
+
13
+ from .config import AccessLogConfig
14
+ from .hook import AccessLogHook
15
+
16
+
17
+ logger = get_plugin_logger()
18
+
19
+
20
+ class AccessLogRuntime(SystemPluginRuntime):
21
+ """Runtime for access log plugin.
22
+
23
+ Integrates with the Hook system to receive and log events.
24
+ """
25
+
26
+ def __init__(self, manifest: PluginManifest):
27
+ super().__init__(manifest)
28
+ self.hook: AccessLogHook | None = None
29
+ self.config: AccessLogConfig | None = None
30
+
31
+ async def _on_initialize(self) -> None:
32
+ """Initialize the access logger."""
33
+ if not self.context:
34
+ raise RuntimeError("Context not set")
35
+
36
+ # Get configuration
37
+ config: AccessLogConfig | None = self.context.get("config")
38
+ if config is None or not isinstance(config, AccessLogConfig):
39
+ config = AccessLogConfig()
40
+ self.config = config
41
+
42
+ if not config.enabled:
43
+ return
44
+
45
+ self.hook = AccessLogHook(config)
46
+
47
+ hook_registry = self.context.get(HookRegistry)
48
+
49
+ if hook_registry is None or not isinstance(hook_registry, HookRegistry):
50
+ raise RuntimeError("Hook registry not found in context")
51
+
52
+ hook_registry.register(self.hook)
53
+
54
+ # Try to wire analytics ingest service if available
55
+ try:
56
+ registry = self.context.get(ServiceContainer)
57
+ self.hook.ingest_service = registry.get_service(AnalyticsIngestService)
58
+ if not self.hook.ingest_service:
59
+ # optional service
60
+ logger.debug("access_log_analytics_service_not_found")
61
+ except Exception as e:
62
+ logger.warning(
63
+ "access_log_ingest_service_connect_failed", error=str(e), exc_info=e
64
+ )
65
+ #
66
+ # Consolidated ready summary at INFO
67
+ logger.trace(
68
+ "access_log_ready",
69
+ client_enabled=config.client_enabled,
70
+ provider_enabled=config.provider_enabled,
71
+ client_format=config.client_format,
72
+ client_log_file=config.client_log_file,
73
+ provider_log_file=config.provider_log_file,
74
+ )
75
+
76
+ async def _on_shutdown(self) -> None:
77
+ """Cleanup on shutdown."""
78
+ # Unregister hook from registry
79
+ if self.hook:
80
+ # Try to get hook registry
81
+ hook_registry = None
82
+ if self.context:
83
+ hook_registry = self.context.get("hook_registry")
84
+ if not hook_registry:
85
+ app = self.context.get("app")
86
+ if (
87
+ app
88
+ and hasattr(app, "state")
89
+ and hasattr(app.state, "hook_registry")
90
+ ):
91
+ hook_registry = app.state.hook_registry
92
+
93
+ if hook_registry and isinstance(hook_registry, HookRegistry):
94
+ hook_registry.unregister(self.hook)
95
+ logger.trace("access_log_hook_unregistered")
96
+
97
+ # Close hook (flushes writers)
98
+ await self.hook.close()
99
+ logger.trace("access_log_shutdown")
100
+
101
+ async def _get_health_details(self) -> dict[str, Any]:
102
+ """Get health check details."""
103
+ config = self.config
104
+
105
+ return {
106
+ "type": "system",
107
+ "initialized": self.initialized,
108
+ "enabled": config.enabled if config else False,
109
+ "client_enabled": config.client_enabled if config else False,
110
+ "provider_enabled": config.provider_enabled if config else False,
111
+ }
112
+
113
+ def get_hook(self) -> AccessLogHook | None:
114
+ """Get the hook instance (for testing or manual integration)."""
115
+ return self.hook
116
+
117
+
118
+ class AccessLogFactory(SystemPluginFactory):
119
+ """Factory for access log plugin."""
120
+
121
+ def __init__(self) -> None:
122
+ manifest = PluginManifest(
123
+ name="access_log",
124
+ version="0.1.0",
125
+ description="Simple access logging with Common, Combined, and Structured formats",
126
+ is_provider=False,
127
+ config_class=AccessLogConfig,
128
+ # dependencies=["analytics"], # optional, handled at runtime
129
+ )
130
+ super().__init__(manifest)
131
+
132
+ def create_runtime(self) -> AccessLogRuntime:
133
+ return AccessLogRuntime(self.manifest)
134
+
135
+
136
+ # Export the factory instance
137
+ factory = AccessLogFactory()
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+ import time
3
+ from pathlib import Path
4
+
5
+ import aiofiles
6
+
7
+ from ccproxy.core.logging import get_plugin_logger
8
+
9
+
10
+ logger = get_plugin_logger(__name__)
11
+
12
+
13
+ class AccessLogWriter:
14
+ """Simple async file writer for access logs.
15
+
16
+ Features:
17
+ - Async file I/O for performance
18
+ - Optional buffering to reduce I/O operations
19
+ - Thread-safe with asyncio.Lock
20
+ - Auto-creates parent directories
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ log_file: str,
26
+ buffer_size: int = 100,
27
+ flush_interval: float = 1.0,
28
+ ):
29
+ """Initialize the writer.
30
+
31
+ Args:
32
+ log_file: Path to the log file
33
+ buffer_size: Number of entries to buffer before writing
34
+ flush_interval: Time in seconds between automatic flushes
35
+ """
36
+ self.log_file = Path(log_file)
37
+ self.buffer_size = buffer_size
38
+ self.flush_interval = flush_interval
39
+
40
+ self._buffer: list[str] = []
41
+ self._lock = asyncio.Lock()
42
+ self._flush_task: asyncio.Task[None] | None = None
43
+ self._last_flush = time.time()
44
+
45
+ # Ensure parent directory exists
46
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
47
+
48
+ async def write(self, line: str) -> None:
49
+ """Write a line to the log file.
50
+
51
+ Lines are buffered and written in batches for performance.
52
+
53
+ Args:
54
+ line: The formatted log line to write
55
+ """
56
+ async with self._lock:
57
+ self._buffer.append(line)
58
+
59
+ # Flush if buffer is full
60
+ if len(self._buffer) >= self.buffer_size:
61
+ await self._flush()
62
+ else:
63
+ # Schedule a flush if not already scheduled
64
+ self._schedule_flush()
65
+
66
+ async def _flush(self) -> None:
67
+ """Flush the buffer to disk.
68
+
69
+ This method assumes the lock is already held.
70
+ """
71
+ if not self._buffer:
72
+ return
73
+
74
+ try:
75
+ # Write all buffered lines at once
76
+ async with aiofiles.open(self.log_file, "a") as f:
77
+ await f.write("\n".join(self._buffer) + "\n")
78
+
79
+ self._buffer.clear()
80
+ self._last_flush = time.time()
81
+
82
+ except Exception as e:
83
+ logger.error(
84
+ "access_log_write_error",
85
+ error=str(e),
86
+ log_file=str(self.log_file),
87
+ buffer_size=len(self._buffer),
88
+ )
89
+
90
+ def _schedule_flush(self) -> None:
91
+ """Schedule an automatic flush after the flush interval."""
92
+ if self._flush_task and not self._flush_task.done():
93
+ return # Already scheduled
94
+
95
+ self._flush_task = asyncio.create_task(self._auto_flush())
96
+
97
+ async def _auto_flush(self) -> None:
98
+ """Automatically flush the buffer after the flush interval."""
99
+ await asyncio.sleep(self.flush_interval)
100
+ async with self._lock:
101
+ await self._flush()
102
+
103
+ async def close(self) -> None:
104
+ """Close the writer and flush any remaining data."""
105
+ async with self._lock:
106
+ await self._flush()
107
+
108
+ if self._flush_task and not self._flush_task.done():
109
+ self._flush_task.cancel()
@@ -0,0 +1,24 @@
1
+ # Analytics Plugin
2
+
3
+ Persists structured access logs and serves query APIs for observability data.
4
+
5
+ ## Highlights
6
+ - Ensures DuckDB schemas exist and registers the `access_logs` SQLModel table
7
+ - Publishes an ingest service consumed by the access log hook
8
+ - Adds `/logs` routes for querying, streaming, and inspecting request history
9
+
10
+ ## Configuration
11
+ - `AnalyticsPluginConfig` toggles collection, retention, and debug logging
12
+ - Requires the `duckdb_storage` plugin to supply the underlying engine
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin analytics --config-class AnalyticsPluginConfig`
15
+
16
+ ```toml
17
+ [plugins.analytics]
18
+ # enabled = true
19
+ ```
20
+
21
+ ## Related Components
22
+ - `plugin.py`: runtime initialization and service registration
23
+ - `ingest.py`: writes events into DuckDB using SQLModel
24
+ - `routes.py`: FastAPI router for analytics and log queries
@@ -0,0 +1 @@
1
+ """Analytics plugin (logs query/analytics/stream endpoints)."""
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class AnalyticsPluginConfig(BaseModel):
5
+ enabled: bool = Field(default=True, description="Enable analytics routes")
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from sqlmodel import Session
9
+
10
+ from .models import AccessLog
11
+
12
+
13
+ class AnalyticsIngestService:
14
+ """Ingest access logs directly via SQLModel.
15
+
16
+ This service accepts a SQLAlchemy/SQLModel engine and writes AccessLog rows
17
+ without delegating to a storage-specific `store_request` API.
18
+ """
19
+
20
+ def __init__(self, engine: Any | None):
21
+ self._engine = engine
22
+
23
+ async def ingest(self, log_data: dict[str, Any]) -> bool:
24
+ """Normalize payload and persist using SQLModel.
25
+
26
+ Args:
27
+ log_data: Access log fields captured by hooks
28
+
29
+ Returns:
30
+ True on success, False otherwise
31
+ """
32
+ if not self._engine:
33
+ return False
34
+
35
+ # Normalize timestamp to datetime
36
+ ts_value = log_data.get("timestamp", time.time())
37
+ if isinstance(ts_value, int | float):
38
+ ts_dt = datetime.fromtimestamp(ts_value)
39
+ else:
40
+ ts_dt = ts_value
41
+
42
+ # Prefer explicit endpoint then path
43
+ endpoint = log_data.get("endpoint", log_data.get("path", ""))
44
+
45
+ # Map incoming dict to AccessLog fields; defaults keep schema stable
46
+ row = AccessLog(
47
+ request_id=str(log_data.get("request_id", "")),
48
+ timestamp=ts_dt,
49
+ method=str(log_data.get("method", "")),
50
+ endpoint=str(endpoint),
51
+ path=str(log_data.get("path", "")),
52
+ query=str(log_data.get("query", "")),
53
+ client_ip=str(log_data.get("client_ip", "")),
54
+ user_agent=str(log_data.get("user_agent", "")),
55
+ service_type=str(log_data.get("service_type", "access_log")),
56
+ provider=str(log_data.get("provider", "")),
57
+ model=str(log_data.get("model", "")),
58
+ streaming=bool(log_data.get("streaming", False)),
59
+ status_code=int(log_data.get("status_code", 200)),
60
+ duration_ms=float(log_data.get("duration_ms", 0.0)),
61
+ duration_seconds=float(
62
+ log_data.get("duration_seconds", log_data.get("duration_ms", 0.0))
63
+ )
64
+ / 1000.0
65
+ if "duration_seconds" not in log_data
66
+ else float(log_data.get("duration_seconds", 0.0)),
67
+ tokens_input=int(log_data.get("tokens_input", 0)),
68
+ tokens_output=int(log_data.get("tokens_output", 0)),
69
+ cache_read_tokens=int(log_data.get("cache_read_tokens", 0)),
70
+ cache_write_tokens=int(log_data.get("cache_write_tokens", 0)),
71
+ cost_usd=float(log_data.get("cost_usd", 0.0)),
72
+ cost_sdk_usd=float(log_data.get("cost_sdk_usd", 0.0)),
73
+ )
74
+
75
+ try:
76
+ # Execute the DB write in a thread to avoid blocking the event loop
77
+ return await asyncio.to_thread(self._insert_sync, row)
78
+ except Exception:
79
+ return False
80
+
81
+ def _insert_sync(self, row: AccessLog) -> bool:
82
+ with Session(self._engine) as session:
83
+ session.add(row)
84
+ session.commit()
85
+ return True