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,97 @@
1
+ """Access log schema and payload definitions (owned by analytics)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+ from sqlmodel import Field, SQLModel
8
+ from typing_extensions import TypedDict
9
+
10
+
11
+ class AccessLog(SQLModel, table=True):
12
+ """Access log model for storing request/response data."""
13
+
14
+ __tablename__ = "access_logs"
15
+
16
+ # Core request identification
17
+ request_id: str = Field(primary_key=True)
18
+ timestamp: datetime = Field(default_factory=datetime.now, index=True)
19
+
20
+ # Request details
21
+ method: str
22
+ endpoint: str
23
+ path: str
24
+ query: str = Field(default="")
25
+ client_ip: str
26
+ user_agent: str
27
+
28
+ # Service and model info
29
+ service_type: str
30
+ provider: str = Field(default="")
31
+ model: str
32
+ streaming: bool = Field(default=False)
33
+
34
+ # Response details
35
+ status_code: int
36
+ duration_ms: float
37
+ duration_seconds: float
38
+
39
+ # Token and cost tracking
40
+ tokens_input: int = Field(default=0)
41
+ tokens_output: int = Field(default=0)
42
+ cache_read_tokens: int = Field(default=0)
43
+ cache_write_tokens: int = Field(default=0)
44
+ cost_usd: float = Field(default=0.0)
45
+ cost_sdk_usd: float = Field(default=0.0)
46
+ num_turns: int = Field(default=0)
47
+
48
+ # Session context metadata
49
+ session_type: str = Field(default="")
50
+ session_status: str = Field(default="")
51
+ session_age_seconds: float = Field(default=0.0)
52
+ session_message_count: int = Field(default=0)
53
+ session_client_id: str = Field(default="")
54
+ session_pool_enabled: bool = Field(default=False)
55
+ session_idle_seconds: float = Field(default=0.0)
56
+ session_error_count: int = Field(default=0)
57
+ session_is_new: bool = Field(default=True)
58
+
59
+ # SQLModel provides its own config typing; avoid overriding with Pydantic ConfigDict
60
+ # from_attributes=True is not required for SQLModel usage here
61
+ # Keep default SQLModel config to satisfy mypy type expectations
62
+
63
+
64
+ class AccessLogPayload(TypedDict, total=False):
65
+ """TypedDict for access log data payloads."""
66
+
67
+ request_id: str
68
+ timestamp: int | float | datetime
69
+ method: str
70
+ endpoint: str
71
+ path: str
72
+ query: str
73
+ client_ip: str
74
+ user_agent: str
75
+ service_type: str
76
+ provider: str
77
+ model: str
78
+ streaming: bool
79
+ status_code: int
80
+ duration_ms: float
81
+ duration_seconds: float
82
+ tokens_input: int
83
+ tokens_output: int
84
+ cache_read_tokens: int
85
+ cache_write_tokens: int
86
+ cost_usd: float
87
+ cost_sdk_usd: float
88
+ num_turns: int
89
+ session_type: str
90
+ session_status: str
91
+ session_age_seconds: float
92
+ session_message_count: int
93
+ session_client_id: str
94
+ session_pool_enabled: bool
95
+ session_idle_seconds: float
96
+ session_error_count: int
97
+ session_is_new: bool
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ from ccproxy.core.logging import get_plugin_logger
4
+ from ccproxy.core.plugins import (
5
+ PluginManifest,
6
+ RouteSpec,
7
+ SystemPluginFactory,
8
+ SystemPluginRuntime,
9
+ )
10
+
11
+ from .config import AnalyticsPluginConfig
12
+
13
+
14
+ logger = get_plugin_logger()
15
+
16
+
17
+ class AnalyticsRuntime(SystemPluginRuntime):
18
+ async def _on_initialize(self) -> None:
19
+ # Ensure AccessLog model is registered and table exists on the engine.
20
+ from sqlmodel import SQLModel
21
+
22
+ # Import models to register with SQLModel metadata
23
+ try:
24
+ from . import models as _models # noqa: F401
25
+ except Exception as e: # pragma: no cover - defensive
26
+ logger.error("analytics_models_import_failed", error=str(e))
27
+ raise
28
+
29
+ # Assert model registration in metadata
30
+ table = SQLModel.metadata.tables.get("access_logs")
31
+ if table is None:
32
+ logger.error("access_logs_table_not_in_metadata")
33
+ raise RuntimeError("AccessLog model not registered in SQLModel metadata")
34
+
35
+ # Try to get storage engine via plugin registry service
36
+ engine = None
37
+ try:
38
+ registry = self.context.get("plugin_registry") if self.context else None
39
+ if registry:
40
+ storage = registry.get_service("log_storage")
41
+ engine = getattr(storage, "_engine", None)
42
+
43
+ # Fallback to app.state if needed
44
+ if (engine is None) and self.context and self.context.get("app"):
45
+ app = self.context["app"]
46
+ storage = getattr(app.state, "log_storage", None)
47
+ engine = getattr(storage, "_engine", None)
48
+ except Exception as e: # pragma: no cover - defensive
49
+ logger.warning("analytics_engine_lookup_failed", error=str(e))
50
+
51
+ # If we have an engine, assert table is created (idempotent create_all)
52
+ if engine is not None:
53
+ try:
54
+ SQLModel.metadata.create_all(engine)
55
+ logger.debug("analytics_table_ready", table="access_logs")
56
+ except Exception as e:
57
+ logger.error("analytics_table_create_failed", error=str(e))
58
+ raise
59
+ else:
60
+ logger.warning(
61
+ "analytics_no_engine_available",
62
+ message="Storage engine not available during analytics init; table creation skipped",
63
+ )
64
+
65
+ # Register ingest service for access_log hook to call
66
+ try:
67
+ if self.context:
68
+ registry = self.context.get("plugin_registry")
69
+ storage = None
70
+ if registry:
71
+ # Get storage service without importing DuckDB-specific classes
72
+ storage = registry.get_service("log_storage")
73
+ if not storage and self.context.get("app"):
74
+ storage = getattr(self.context["app"].state, "log_storage", None)
75
+
76
+ if storage:
77
+ engine = getattr(storage, "_engine", None)
78
+ else:
79
+ engine = None
80
+
81
+ if engine is not None:
82
+ from .ingest import AnalyticsIngestService
83
+
84
+ ingest_service = AnalyticsIngestService(engine)
85
+ if registry:
86
+ registry.register_service(
87
+ "analytics_ingest", ingest_service, self.manifest.name
88
+ )
89
+ logger.debug("analytics_ingest_service_registered")
90
+ else:
91
+ logger.warning(
92
+ "analytics_ingest_registration_skipped",
93
+ reason="no_engine_available",
94
+ )
95
+ except Exception as e: # pragma: no cover - defensive
96
+ logger.warning("analytics_ingest_registration_failed", error=str(e))
97
+
98
+ logger.debug("analytics_plugin_initialized")
99
+
100
+
101
+ class AnalyticsFactory(SystemPluginFactory):
102
+ def __init__(self) -> None:
103
+ from .routes import router as analytics_router
104
+
105
+ manifest = PluginManifest(
106
+ name="analytics",
107
+ version="0.1.0",
108
+ description="Logs query, analytics, and streaming endpoints",
109
+ is_provider=False,
110
+ config_class=AnalyticsPluginConfig,
111
+ provides=["analytics_ingest"],
112
+ dependencies=["duckdb_storage"],
113
+ routes=[RouteSpec(router=analytics_router, prefix="/logs", tags=["logs"])],
114
+ )
115
+ super().__init__(manifest)
116
+
117
+ def create_runtime(self) -> AnalyticsRuntime:
118
+ return AnalyticsRuntime(self.manifest)
119
+
120
+
121
+ factory = AnalyticsFactory()
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import AsyncGenerator
5
+ from typing import Annotated, Any
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
8
+ from fastapi.responses import StreamingResponse
9
+
10
+ from ccproxy.auth.dependencies import ConditionalAuthDep
11
+ from ccproxy.core.request_context import get_request_event_stream
12
+ from ccproxy.plugins.duckdb_storage.storage import SimpleDuckDBStorage
13
+
14
+ from .service import AnalyticsService
15
+
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ @router.get("/query")
21
+ async def query_logs(
22
+ storage: DuckDBStorageDep,
23
+ auth: ConditionalAuthDep,
24
+ limit: int = Query(1000, ge=1, le=10000, description="Maximum number of results"),
25
+ start_time: float | None = Query(None, description="Start timestamp filter"),
26
+ end_time: float | None = Query(None, description="End timestamp filter"),
27
+ model: str | None = Query(None, description="Model filter"),
28
+ service_type: str | None = Query(None, description="Service type filter"),
29
+ cursor: float | None = Query(
30
+ None, description="Timestamp cursor for pagination (Unix time)"
31
+ ),
32
+ order: str = Query(
33
+ "desc", pattern="^(?i)(asc|desc)$", description="Sort order: asc or desc"
34
+ ),
35
+ ) -> dict[str, Any]:
36
+ if not storage:
37
+ raise HTTPException(status_code=503, detail="Storage backend not available")
38
+ if not getattr(storage, "_engine", None):
39
+ raise HTTPException(status_code=503, detail="Storage engine not available")
40
+
41
+ try:
42
+ svc = AnalyticsService(storage._engine)
43
+ return svc.query_logs(
44
+ limit=limit,
45
+ start_time=start_time,
46
+ end_time=end_time,
47
+ model=model,
48
+ service_type=service_type,
49
+ cursor=cursor,
50
+ order=order,
51
+ )
52
+ except Exception as e:
53
+ raise HTTPException(status_code=500, detail=f"Query failed: {str(e)}") from e
54
+
55
+
56
+ @router.get("/analytics")
57
+ async def get_logs_analytics(
58
+ storage: DuckDBStorageDep,
59
+ auth: ConditionalAuthDep,
60
+ start_time: float | None = Query(None, description="Start timestamp (Unix time)"),
61
+ end_time: float | None = Query(None, description="End timestamp (Unix time)"),
62
+ model: str | None = Query(None, description="Filter by model name"),
63
+ service_type: str | None = Query(
64
+ None,
65
+ description="Filter by service type. Supports comma-separated values and !negation",
66
+ ),
67
+ hours: int | None = Query(24, ge=1, le=168, description="Hours of data to analyze"),
68
+ ) -> dict[str, Any]:
69
+ if not storage:
70
+ raise HTTPException(status_code=503, detail="Storage backend not available")
71
+ if not getattr(storage, "_engine", None):
72
+ raise HTTPException(status_code=503, detail="Storage engine not available")
73
+
74
+ try:
75
+ svc = AnalyticsService(storage._engine)
76
+ analytics = svc.get_analytics(
77
+ start_time=start_time,
78
+ end_time=end_time,
79
+ model=model,
80
+ service_type=service_type,
81
+ hours=hours,
82
+ )
83
+ analytics["query_params"] = {
84
+ "start_time": start_time,
85
+ "end_time": end_time,
86
+ "model": model,
87
+ "service_type": service_type,
88
+ "hours": hours,
89
+ }
90
+ return analytics
91
+ except Exception as e:
92
+ raise HTTPException(
93
+ status_code=500, detail=f"Analytics query failed: {str(e)}"
94
+ ) from e
95
+
96
+
97
+ @router.get("/stream")
98
+ async def stream_logs(
99
+ request: Request,
100
+ auth: ConditionalAuthDep,
101
+ model: str | None = Query(None, description="Filter by model name"),
102
+ service_type: str | None = Query(None, description="Filter by service type"),
103
+ min_duration_ms: float | None = Query(None, description="Min duration (ms)"),
104
+ max_duration_ms: float | None = Query(None, description="Max duration (ms)"),
105
+ status_code_min: int | None = Query(None, description="Min status code"),
106
+ status_code_max: int | None = Query(None, description="Max status code"),
107
+ ) -> StreamingResponse:
108
+ async def event_generator() -> AsyncGenerator[str, None]:
109
+ try:
110
+ async for event in get_request_event_stream():
111
+ data = event
112
+ if model and data.get("model") != model:
113
+ continue
114
+ if service_type and data.get("service_type") != service_type:
115
+ continue
116
+ if min_duration_ms and data.get("duration_ms", 0) < min_duration_ms:
117
+ continue
118
+ if max_duration_ms and data.get("duration_ms", 0) > max_duration_ms:
119
+ continue
120
+ if status_code_min and data.get("status_code", 0) < status_code_min:
121
+ continue
122
+ if status_code_max and data.get("status_code", 0) > status_code_max:
123
+ continue
124
+
125
+ yield f"data: {data}\n\n"
126
+ except Exception as e: # pragma: no cover - stream errors aren't fatal
127
+ yield f"event: error\ndata: {str(e)}\n\n"
128
+
129
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
130
+
131
+
132
+ @router.post("/reset")
133
+ async def reset_logs(
134
+ storage: DuckDBStorageDep,
135
+ auth: ConditionalAuthDep,
136
+ ) -> dict[str, Any]:
137
+ if not storage:
138
+ raise HTTPException(status_code=503, detail="Storage backend not available")
139
+ if not hasattr(storage, "reset_data"):
140
+ raise HTTPException(
141
+ status_code=501, detail="Reset not supported by storage backend"
142
+ )
143
+
144
+ ok = await storage.reset_data()
145
+ if not ok:
146
+ raise HTTPException(status_code=500, detail="Failed to reset logs data")
147
+ return {
148
+ "status": "success",
149
+ "message": "All logs data has been reset",
150
+ "timestamp": time.time(),
151
+ "backend": "duckdb",
152
+ }
153
+
154
+
155
+ async def get_duckdb_storage(request: Request) -> SimpleDuckDBStorage | None:
156
+ """Get DuckDB storage service from app state.
157
+
158
+ The duckdb_storage plugin registers the storage as app.state.log_storage.
159
+ """
160
+ return getattr(request.app.state, "log_storage", None)
161
+
162
+
163
+ DuckDBStorageDep = Annotated[SimpleDuckDBStorage | None, Depends(get_duckdb_storage)]
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from datetime import datetime as dt
5
+ from typing import Any
6
+
7
+ from sqlmodel import Session, col, func, select
8
+
9
+ from .models import AccessLog
10
+
11
+
12
+ class AnalyticsService:
13
+ """Encapsulates analytics queries over the AccessLog table."""
14
+
15
+ def __init__(self, engine: Any):
16
+ self._engine = engine
17
+
18
+ def query_logs(
19
+ self,
20
+ limit: int = 1000,
21
+ start_time: float | None = None,
22
+ end_time: float | None = None,
23
+ model: str | None = None,
24
+ service_type: str | None = None,
25
+ cursor: float | None = None,
26
+ order: str = "desc",
27
+ ) -> dict[str, Any]:
28
+ with Session(self._engine) as session:
29
+ statement = select(AccessLog)
30
+
31
+ start_dt = dt.fromtimestamp(start_time) if start_time else None
32
+ end_dt = dt.fromtimestamp(end_time) if end_time else None
33
+ cursor_dt = dt.fromtimestamp(cursor) if cursor else None
34
+
35
+ if start_dt:
36
+ statement = statement.where(AccessLog.timestamp >= start_dt)
37
+ if end_dt:
38
+ statement = statement.where(AccessLog.timestamp <= end_dt)
39
+ if model:
40
+ statement = statement.where(AccessLog.model == model)
41
+ if service_type:
42
+ statement = statement.where(AccessLog.service_type == service_type)
43
+
44
+ # Cursor-based pagination using timestamp
45
+ # For descending order (newest first): use timestamp < cursor
46
+ # For ascending order (oldest first): use timestamp > cursor
47
+ if cursor_dt:
48
+ if order.lower() == "asc":
49
+ statement = statement.where(AccessLog.timestamp > cursor_dt)
50
+ else:
51
+ statement = statement.where(AccessLog.timestamp < cursor_dt)
52
+
53
+ if order.lower() == "asc":
54
+ statement = statement.order_by(col(AccessLog.timestamp).asc()).limit(
55
+ limit
56
+ )
57
+ else:
58
+ statement = statement.order_by(col(AccessLog.timestamp).desc()).limit(
59
+ limit
60
+ )
61
+ results = session.exec(statement).all()
62
+ payload = [log.model_dump() for log in results]
63
+
64
+ # Compute next cursor from last item in current page
65
+ next_cursor = None
66
+ if results:
67
+ last = results[-1]
68
+ next_cursor = last.timestamp.timestamp()
69
+
70
+ return {
71
+ "results": payload,
72
+ "limit": limit,
73
+ "count": len(results),
74
+ "order": order.lower(),
75
+ "cursor": cursor,
76
+ "next_cursor": next_cursor,
77
+ "has_more": len(results) == limit,
78
+ "query_time": time.time(),
79
+ "backend": "sqlmodel",
80
+ }
81
+
82
+ def get_analytics(
83
+ self,
84
+ start_time: float | None = None,
85
+ end_time: float | None = None,
86
+ model: str | None = None,
87
+ service_type: str | None = None,
88
+ hours: int | None = 24,
89
+ ) -> dict[str, Any]:
90
+ if start_time is None and end_time is None and hours:
91
+ end_time = time.time()
92
+ start_time = end_time - (hours * 3600)
93
+
94
+ start_dt = dt.fromtimestamp(start_time) if start_time else None
95
+ end_dt = dt.fromtimestamp(end_time) if end_time else None
96
+
97
+ def build_filters() -> list[Any]:
98
+ conditions: list[Any] = []
99
+ if start_dt:
100
+ conditions.append(AccessLog.timestamp >= start_dt)
101
+ if end_dt:
102
+ conditions.append(AccessLog.timestamp <= end_dt)
103
+ if model:
104
+ conditions.append(AccessLog.model == model)
105
+ if service_type:
106
+ parts = [s.strip() for s in service_type.split(",")]
107
+ include = [p for p in parts if not p.startswith("!")]
108
+ exclude = [p[1:] for p in parts if p.startswith("!")]
109
+ if include:
110
+ conditions.append(col(AccessLog.service_type).in_(include))
111
+ if exclude:
112
+ conditions.append(~col(AccessLog.service_type).in_(exclude))
113
+ return conditions
114
+
115
+ with Session(self._engine) as session:
116
+ filters = build_filters()
117
+
118
+ total_requests = session.exec(
119
+ select(func.count()).select_from(AccessLog).where(*filters)
120
+ ).first()
121
+ total_successful_requests = session.exec(
122
+ select(func.count())
123
+ .select_from(AccessLog)
124
+ .where(
125
+ *filters, AccessLog.status_code >= 200, AccessLog.status_code < 400
126
+ )
127
+ ).first()
128
+ total_error_requests = session.exec(
129
+ select(func.count())
130
+ .select_from(AccessLog)
131
+ .where(*filters, AccessLog.status_code >= 400)
132
+ ).first()
133
+ avg_duration = session.exec(
134
+ select(func.avg(AccessLog.duration_ms))
135
+ .select_from(AccessLog)
136
+ .where(*filters)
137
+ ).first()
138
+ total_cost = session.exec(
139
+ select(func.sum(AccessLog.cost_usd))
140
+ .select_from(AccessLog)
141
+ .where(*filters)
142
+ ).first()
143
+ total_tokens_input = session.exec(
144
+ select(func.sum(AccessLog.tokens_input))
145
+ .select_from(AccessLog)
146
+ .where(*filters)
147
+ ).first()
148
+ total_tokens_output = session.exec(
149
+ select(func.sum(AccessLog.tokens_output))
150
+ .select_from(AccessLog)
151
+ .where(*filters)
152
+ ).first()
153
+ total_cache_read_tokens = session.exec(
154
+ select(func.sum(AccessLog.cache_read_tokens))
155
+ .select_from(AccessLog)
156
+ .where(*filters)
157
+ ).first()
158
+ total_cache_write_tokens = session.exec(
159
+ select(func.sum(AccessLog.cache_write_tokens))
160
+ .select_from(AccessLog)
161
+ .where(*filters)
162
+ ).first()
163
+
164
+ services = session.exec(
165
+ select(AccessLog.service_type).distinct().where(*filters)
166
+ ).all()
167
+ breakdown: dict[str, Any] = {}
168
+ for svc in services:
169
+ svc_filters = filters + [AccessLog.service_type == svc]
170
+ svc_count = session.exec(
171
+ select(func.count()).select_from(AccessLog).where(*svc_filters)
172
+ ).first()
173
+ svc_success = session.exec(
174
+ select(func.count())
175
+ .select_from(AccessLog)
176
+ .where(
177
+ *svc_filters,
178
+ AccessLog.status_code >= 200,
179
+ AccessLog.status_code < 400,
180
+ )
181
+ ).first()
182
+ svc_error = session.exec(
183
+ select(func.count())
184
+ .select_from(AccessLog)
185
+ .where(*svc_filters, AccessLog.status_code >= 400)
186
+ ).first()
187
+ svc_avg = session.exec(
188
+ select(func.avg(AccessLog.duration_ms))
189
+ .select_from(AccessLog)
190
+ .where(*svc_filters)
191
+ ).first()
192
+ svc_cost = session.exec(
193
+ select(func.sum(AccessLog.cost_usd))
194
+ .select_from(AccessLog)
195
+ .where(*svc_filters)
196
+ ).first()
197
+ svc_in = session.exec(
198
+ select(func.sum(AccessLog.tokens_input))
199
+ .select_from(AccessLog)
200
+ .where(*svc_filters)
201
+ ).first()
202
+ svc_out = session.exec(
203
+ select(func.sum(AccessLog.tokens_output))
204
+ .select_from(AccessLog)
205
+ .where(*svc_filters)
206
+ ).first()
207
+ svc_cr = session.exec(
208
+ select(func.sum(AccessLog.cache_read_tokens))
209
+ .select_from(AccessLog)
210
+ .where(*svc_filters)
211
+ ).first()
212
+ svc_cw = session.exec(
213
+ select(func.sum(AccessLog.cache_write_tokens))
214
+ .select_from(AccessLog)
215
+ .where(*svc_filters)
216
+ ).first()
217
+
218
+ breakdown[str(svc)] = {
219
+ "request_count": svc_count or 0,
220
+ "successful_requests": svc_success or 0,
221
+ "error_requests": svc_error or 0,
222
+ "success_rate": (svc_success or 0) / (svc_count or 1) * 100
223
+ if svc_count
224
+ else 0,
225
+ "error_rate": (svc_error or 0) / (svc_count or 1) * 100
226
+ if svc_count
227
+ else 0,
228
+ "avg_duration_ms": svc_avg or 0,
229
+ "total_cost_usd": svc_cost or 0,
230
+ "total_tokens_input": svc_in or 0,
231
+ "total_tokens_output": svc_out or 0,
232
+ "total_cache_read_tokens": svc_cr or 0,
233
+ "total_cache_write_tokens": svc_cw or 0,
234
+ "total_tokens_all": (svc_in or 0)
235
+ + (svc_out or 0)
236
+ + (svc_cr or 0)
237
+ + (svc_cw or 0),
238
+ }
239
+
240
+ return {
241
+ "summary": {
242
+ "total_requests": total_requests or 0,
243
+ "total_successful_requests": total_successful_requests or 0,
244
+ "total_error_requests": total_error_requests or 0,
245
+ "avg_duration_ms": avg_duration or 0,
246
+ "total_cost_usd": total_cost or 0,
247
+ "total_tokens_input": total_tokens_input or 0,
248
+ "total_tokens_output": total_tokens_output or 0,
249
+ "total_cache_read_tokens": total_cache_read_tokens or 0,
250
+ "total_cache_write_tokens": total_cache_write_tokens or 0,
251
+ "total_tokens_all": (total_tokens_input or 0)
252
+ + (total_tokens_output or 0)
253
+ + (total_cache_read_tokens or 0)
254
+ + (total_cache_write_tokens or 0),
255
+ },
256
+ "token_analytics": {
257
+ "input_tokens": total_tokens_input or 0,
258
+ "output_tokens": total_tokens_output or 0,
259
+ "cache_read_tokens": total_cache_read_tokens or 0,
260
+ "cache_write_tokens": total_cache_write_tokens or 0,
261
+ "total_tokens": (total_tokens_input or 0)
262
+ + (total_tokens_output or 0)
263
+ + (total_cache_read_tokens or 0)
264
+ + (total_cache_write_tokens or 0),
265
+ },
266
+ "request_analytics": {
267
+ "total_requests": total_requests or 0,
268
+ "successful_requests": total_successful_requests or 0,
269
+ "error_requests": total_error_requests or 0,
270
+ "success_rate": (total_successful_requests or 0)
271
+ / (total_requests or 1)
272
+ * 100
273
+ if total_requests
274
+ else 0,
275
+ "error_rate": (total_error_requests or 0)
276
+ / (total_requests or 1)
277
+ * 100
278
+ if total_requests
279
+ else 0,
280
+ },
281
+ "service_type_breakdown": breakdown,
282
+ "query_time": time.time(),
283
+ "backend": "sqlmodel",
284
+ }
@@ -0,0 +1,29 @@
1
+ # Claude API Plugin
2
+
3
+ Connects CCProxy to Anthropic's Claude HTTP API with detection, health, and metrics.
4
+
5
+ ## Highlights
6
+ - Wraps `ClaudeAPIAdapter` for chat, tool, and streaming requests
7
+ - Uses the detection service to discover CLI headers and available models
8
+ - Emits streaming metrics and standardized health checks via the hook registry
9
+
10
+ ## Configuration
11
+ - `ClaudeAPISettings` defines base URLs, model cards, and auth manager name
12
+ - Works with `oauth_claude` or the credential balancer for token management
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin claude_api --config-class ClaudeAPISettings`
15
+
16
+ ```toml
17
+ [plugins.claude_api]
18
+ # enabled = true
19
+ # base_url = "https://api.anthropic.com"
20
+ # auth_type = "oauth"
21
+ # supports_streaming = true
22
+ # include_sdk_content_as_xml = false
23
+ # system_prompt_injection_mode = "minimal"
24
+ ```
25
+
26
+ ## Related Components
27
+ - `adapter.py`: HTTP client for Anthropic endpoints
28
+ - `detection_service.py`: CLI and capability discovery helpers
29
+ - `routes.py`: FastAPI router mounted under `/claude/api`