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,763 @@
1
+ """Hook-based access log implementation."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from ccproxy.core.logging import get_plugin_logger
7
+ from ccproxy.core.plugins.hooks import Hook
8
+ from ccproxy.core.plugins.hooks.base import HookContext
9
+ from ccproxy.core.plugins.hooks.events import HookEvent
10
+
11
+ from .config import AccessLogConfig
12
+ from .formatter import AccessLogFormatter
13
+ from .writer import AccessLogWriter
14
+
15
+
16
+ logger = get_plugin_logger(__name__)
17
+
18
+
19
+ class AccessLogHook(Hook):
20
+ """Hook-based access logger implementation.
21
+
22
+ This hook listens to request/response lifecycle events and logs them
23
+ according to the configured format (common, combined, or structured).
24
+ """
25
+
26
+ name = "access_log"
27
+ events = [
28
+ HookEvent.REQUEST_STARTED,
29
+ HookEvent.REQUEST_COMPLETED,
30
+ HookEvent.REQUEST_FAILED,
31
+ HookEvent.PROVIDER_REQUEST_PREPARED,
32
+ HookEvent.PROVIDER_RESPONSE_RECEIVED,
33
+ HookEvent.PROVIDER_ERROR,
34
+ HookEvent.PROVIDER_STREAM_END,
35
+ ]
36
+ priority = (
37
+ 750 # HookLayer.OBSERVATION + 50 - Access logging last to capture all data
38
+ )
39
+
40
+ def __init__(self, config: AccessLogConfig | None = None) -> None:
41
+ """Initialize the access log hook.
42
+
43
+ Args:
44
+ config: Access log configuration
45
+ """
46
+ self.config = config or AccessLogConfig()
47
+ self.formatter = AccessLogFormatter()
48
+
49
+ # Create writers based on configuration
50
+ self.client_writer: AccessLogWriter | None = None
51
+ self.provider_writer: AccessLogWriter | None = None
52
+
53
+ if self.config.client_enabled:
54
+ self.client_writer = AccessLogWriter(
55
+ self.config.client_log_file,
56
+ self.config.buffer_size,
57
+ self.config.flush_interval,
58
+ )
59
+
60
+ if self.config.provider_enabled:
61
+ self.provider_writer = AccessLogWriter(
62
+ self.config.provider_log_file,
63
+ self.config.buffer_size,
64
+ self.config.flush_interval,
65
+ )
66
+
67
+ # Track in-flight requests
68
+ self.client_requests: dict[str, dict[str, Any]] = {}
69
+ self.provider_requests: dict[str, dict[str, Any]] = {}
70
+ # Store streaming metrics until REQUEST_COMPLETED fires
71
+ self._streaming_metrics: dict[str, dict[str, Any]] = {}
72
+
73
+ self.ingest_service: Any | None = None
74
+
75
+ logger.trace(
76
+ "access_log_hook_initialized",
77
+ enabled=self.config.enabled,
78
+ client_enabled=self.config.client_enabled,
79
+ client_format=self.config.client_format,
80
+ provider_enabled=self.config.provider_enabled,
81
+ )
82
+
83
+ async def __call__(self, context: HookContext) -> None:
84
+ """Handle hook events for access logging.
85
+
86
+ Args:
87
+ context: Hook context with event data
88
+ """
89
+ if not self.config.enabled:
90
+ return
91
+
92
+ # Map hook events to handler methods
93
+ handlers = {
94
+ HookEvent.REQUEST_STARTED: self._handle_request_start,
95
+ HookEvent.REQUEST_COMPLETED: self._handle_request_complete,
96
+ HookEvent.REQUEST_FAILED: self._handle_request_failed,
97
+ HookEvent.PROVIDER_REQUEST_PREPARED: self._handle_provider_request,
98
+ HookEvent.PROVIDER_RESPONSE_RECEIVED: self._handle_provider_response,
99
+ HookEvent.PROVIDER_ERROR: self._handle_provider_error,
100
+ HookEvent.PROVIDER_STREAM_END: self._handle_provider_stream_end,
101
+ }
102
+
103
+ handler = handlers.get(context.event)
104
+ if handler:
105
+ try:
106
+ await handler(context)
107
+ except Exception as e:
108
+ logger.error(
109
+ "access_log_hook_error",
110
+ hook_event=context.event.value if context.event else "unknown",
111
+ error=str(e),
112
+ exc_info=e,
113
+ )
114
+
115
+ async def _handle_request_start(self, context: HookContext) -> None:
116
+ """Handle REQUEST_STARTED event."""
117
+ if not self.config.client_enabled:
118
+ return
119
+
120
+ # Extract request data from context
121
+ request_id = context.data.get("request_id", "unknown")
122
+ method = context.data.get("method", "UNKNOWN")
123
+
124
+ # Handle both path and url fields
125
+ path = context.data.get("path", "")
126
+ if not path and "url" in context.data:
127
+ # Extract path from URL
128
+ url = context.data.get("url", "")
129
+ path = self._extract_path(url)
130
+
131
+ query = context.data.get("query", "")
132
+
133
+ # Try to get client_ip from various sources
134
+ client_ip = context.data.get("client_ip", "-")
135
+ if client_ip == "-" and context.request and hasattr(context.request, "client"):
136
+ # Try to get from request object
137
+ client_ip = (
138
+ getattr(context.request.client, "host", "-")
139
+ if context.request.client
140
+ else "-"
141
+ )
142
+
143
+ # Try to get user_agent from headers
144
+ user_agent = context.data.get("user_agent", "-")
145
+ if user_agent == "-":
146
+ headers = context.data.get("headers", {})
147
+ user_agent = headers.get("user-agent", "-")
148
+
149
+ # Check path filters
150
+ if self._should_exclude_path(path):
151
+ return
152
+
153
+ # Store request data for later
154
+ # Get current time for timestamp
155
+ current_time = time.time()
156
+
157
+ # Store request data with additional context fields
158
+ request_data = {
159
+ "timestamp": current_time, # Store as float for formatter compatibility
160
+ "method": method,
161
+ "path": path,
162
+ "query": query,
163
+ "client_ip": client_ip,
164
+ "user_agent": user_agent,
165
+ "start_time": current_time,
166
+ }
167
+
168
+ # Add additional context fields if available
169
+ additional_fields = [
170
+ "endpoint",
171
+ "service_type",
172
+ "provider",
173
+ "model",
174
+ "session_id",
175
+ "session_type",
176
+ "streaming",
177
+ ]
178
+ for field in additional_fields:
179
+ value = context.data.get(field)
180
+ if value is not None:
181
+ request_data[field] = value
182
+
183
+ self.client_requests[request_id] = request_data
184
+
185
+ async def _handle_request_complete(self, context: HookContext) -> None:
186
+ """Handle REQUEST_COMPLETED event."""
187
+ if not self.config.client_enabled:
188
+ return
189
+
190
+ request_id = context.data.get("request_id", "unknown")
191
+
192
+ # Check if we have the request data
193
+ if request_id not in self.client_requests:
194
+ return
195
+
196
+ # Check if this is a streaming response by looking for streaming flag
197
+ # For streaming responses, we'll handle logging in PROVIDER_STREAM_END
198
+ # to ensure we have all metrics
199
+ is_streaming = (
200
+ context.data.get("streaming_completed", False)
201
+ or context.data.get("streaming", False)
202
+ or self.client_requests.get(request_id, {}).get("streaming", False)
203
+ )
204
+
205
+ if is_streaming:
206
+ # Check if we have metrics in metadata (non-streaming response wrapped as streaming)
207
+ has_metrics = False
208
+ if context.metadata:
209
+ # Check if we have token metrics available
210
+ has_metrics = any(
211
+ context.metadata.get(field) is not None
212
+ for field in ["tokens_input", "tokens_output", "cost_usd"]
213
+ )
214
+
215
+ if not has_metrics:
216
+ # True streaming response - wait for PROVIDER_STREAM_END
217
+ # Just mark that we got the completion
218
+ if request_id in self.client_requests:
219
+ self.client_requests[request_id]["completion_time"] = time.time()
220
+ self.client_requests[request_id]["status_code"] = context.data.get(
221
+ "response_status", 200
222
+ )
223
+ return
224
+ # If we have metrics, continue to log immediately (non-streaming wrapped as streaming)
225
+
226
+ # For non-streaming responses, log immediately
227
+ # Get and remove request data
228
+ request_data = self.client_requests.pop(request_id)
229
+
230
+ # Calculate duration
231
+ duration_ms = (time.time() - request_data["start_time"]) * 1000
232
+
233
+ # Extract response data
234
+ status_code = context.data.get("status_code", 200)
235
+ body_size = context.data.get("body_size", 0)
236
+
237
+ # Check if we have usage metrics in context metadata
238
+ # These might be available from RequestContext metadata
239
+ usage_metrics = {}
240
+ if context.metadata:
241
+ # Extract any token/cost metrics from metadata
242
+ token_fields = [
243
+ "tokens_input",
244
+ "tokens_output",
245
+ "cache_read_tokens",
246
+ "cache_write_tokens",
247
+ "cost_usd",
248
+ "model",
249
+ ]
250
+ for field in token_fields:
251
+ value = context.metadata.get(field)
252
+ if value is not None:
253
+ usage_metrics[field] = value
254
+
255
+ # Merge request and response data
256
+ log_data = {
257
+ **request_data,
258
+ "request_id": request_id,
259
+ "status_code": status_code,
260
+ "body_size": body_size,
261
+ "duration_ms": duration_ms,
262
+ "error": None,
263
+ **usage_metrics, # Include any usage metrics found
264
+ }
265
+
266
+ # Format and write
267
+ if self.client_writer:
268
+ formatted = self.formatter.format_client(
269
+ log_data, self.config.client_format
270
+ )
271
+ await self.client_writer.write(formatted)
272
+
273
+ # Also log to structured logger
274
+ await self._log_to_structured_logger(log_data, "client")
275
+
276
+ # Ingest into analytics if available
277
+ await self._maybe_ingest(log_data)
278
+
279
+ async def _handle_request_failed(self, context: HookContext) -> None:
280
+ """Handle REQUEST_FAILED event."""
281
+ if not self.config.client_enabled:
282
+ return
283
+
284
+ request_id = context.data.get("request_id", "unknown")
285
+
286
+ # Check if we have the request data
287
+ if request_id not in self.client_requests:
288
+ return
289
+
290
+ # Get and remove request data
291
+ request_data = self.client_requests.pop(request_id)
292
+
293
+ # Calculate duration
294
+ duration_ms = (time.time() - request_data["start_time"]) * 1000
295
+
296
+ # Extract error information
297
+ error = context.error
298
+ error_message = str(error) if error else "Unknown error"
299
+ status_code = context.data.get("status_code", 500)
300
+
301
+ # Merge request and error data
302
+ log_data = {
303
+ **request_data,
304
+ "request_id": request_id,
305
+ "status_code": status_code,
306
+ "body_size": 0,
307
+ "duration_ms": duration_ms,
308
+ "error": error_message,
309
+ }
310
+
311
+ # Format and write
312
+ if self.client_writer:
313
+ formatted = self.formatter.format_client(
314
+ log_data, self.config.client_format
315
+ )
316
+ await self.client_writer.write(formatted)
317
+
318
+ # Also log to structured logger
319
+ await self._log_to_structured_logger(log_data, "client", error=error_message)
320
+
321
+ # Ingest into analytics if available
322
+ await self._maybe_ingest(log_data)
323
+
324
+ async def _handle_provider_request(self, context: HookContext) -> None:
325
+ """Handle PROVIDER_REQUEST_PREPARED event."""
326
+ if not self.config.provider_enabled:
327
+ return
328
+
329
+ request_id = context.metadata.get("request_id", "unknown")
330
+ provider = context.provider or "unknown"
331
+ url = context.data.get("url", "")
332
+ method = context.data.get("method", "UNKNOWN")
333
+
334
+ # Store request data for later
335
+ # Get current time for timestamp
336
+ current_time = time.time()
337
+
338
+ self.provider_requests[request_id] = {
339
+ "timestamp": current_time, # Store as float for formatter compatibility
340
+ "provider": provider,
341
+ "method": method,
342
+ "url": url,
343
+ "start_time": current_time,
344
+ }
345
+
346
+ async def _handle_provider_response(self, context: HookContext) -> None:
347
+ """Handle PROVIDER_RESPONSE_RECEIVED event."""
348
+ if not self.config.provider_enabled:
349
+ return
350
+
351
+ request_id = context.metadata.get("request_id", "unknown")
352
+
353
+ # Check if we have the request data
354
+ if request_id not in self.provider_requests:
355
+ return
356
+
357
+ # Get and remove request data
358
+ request_data = self.provider_requests.pop(request_id)
359
+
360
+ # Calculate duration if not provided
361
+ duration_ms = context.data.get("duration_ms", 0)
362
+ if duration_ms == 0:
363
+ duration_ms = (time.time() - request_data["start_time"]) * 1000
364
+
365
+ # Extract response data
366
+ status_code = context.data.get("status_code", 200)
367
+ tokens_input = context.data.get("tokens_input", 0)
368
+ tokens_output = context.data.get("tokens_output", 0)
369
+ cache_read_tokens = context.data.get("cache_read_tokens", 0)
370
+ cache_write_tokens = context.data.get("cache_write_tokens", 0)
371
+ cost_usd = context.data.get("cost_usd", 0.0)
372
+ model = context.data.get("model", "")
373
+
374
+ # Merge request and response data
375
+ log_data = {
376
+ **request_data,
377
+ "request_id": request_id,
378
+ "status_code": status_code,
379
+ "duration_ms": duration_ms,
380
+ "tokens_input": tokens_input,
381
+ "tokens_output": tokens_output,
382
+ "cache_read_tokens": cache_read_tokens,
383
+ "cache_write_tokens": cache_write_tokens,
384
+ "cost_usd": cost_usd,
385
+ "model": model,
386
+ }
387
+
388
+ # Format and write
389
+ if self.provider_writer:
390
+ formatted = self.formatter.format_provider(log_data)
391
+ await self.provider_writer.write(formatted)
392
+
393
+ # Also log to structured logger
394
+ await self._log_to_structured_logger(log_data, "provider")
395
+
396
+ async def _handle_provider_error(self, context: HookContext) -> None:
397
+ """Handle PROVIDER_ERROR event."""
398
+ if not self.config.provider_enabled:
399
+ return
400
+
401
+ request_id = context.metadata.get("request_id", "unknown")
402
+
403
+ # Check if we have the request data
404
+ if request_id not in self.provider_requests:
405
+ return
406
+
407
+ # Get and remove request data
408
+ request_data = self.provider_requests.pop(request_id)
409
+
410
+ # Calculate duration
411
+ duration_ms = (time.time() - request_data["start_time"]) * 1000
412
+
413
+ # Extract error information
414
+ error = context.error
415
+ error_message = str(error) if error else "Unknown error"
416
+ status_code = context.data.get("status_code", 500)
417
+
418
+ # Merge request and error data
419
+ log_data = {
420
+ **request_data,
421
+ "request_id": request_id,
422
+ "status_code": status_code,
423
+ "duration_ms": duration_ms,
424
+ "tokens_input": 0,
425
+ "tokens_output": 0,
426
+ "cache_read_tokens": 0,
427
+ "cache_write_tokens": 0,
428
+ "cost_usd": 0.0,
429
+ "model": "",
430
+ "error": error_message,
431
+ }
432
+
433
+ # Format and write
434
+ if self.provider_writer:
435
+ formatted = self.formatter.format_provider(log_data)
436
+ await self.provider_writer.write(formatted)
437
+
438
+ # Also log to structured logger
439
+ await self._log_to_structured_logger(log_data, "provider", error=error_message)
440
+
441
+ async def _handle_provider_stream_end(self, context: HookContext) -> None:
442
+ """Handle PROVIDER_STREAM_END event to capture complete streaming metrics."""
443
+ if not self.config.provider_enabled and not self.config.client_enabled:
444
+ return
445
+
446
+ request_id = context.metadata.get("request_id", "unknown")
447
+
448
+ # Extract usage metrics from the event
449
+ usage_metrics = context.data.get("usage_metrics", {})
450
+
451
+ # Store metrics for logging
452
+ self._streaming_metrics[request_id] = {
453
+ "usage_metrics": usage_metrics,
454
+ "provider": context.provider or context.data.get("provider", "unknown"),
455
+ "url": context.data.get("url", ""),
456
+ "method": context.data.get("method", "POST"),
457
+ "total_chunks": context.data.get("total_chunks", 0),
458
+ "total_bytes": context.data.get("total_bytes", 0),
459
+ }
460
+
461
+ # If we have client request data for this streaming request, log it now with metrics
462
+ if self.config.client_enabled and request_id in self.client_requests:
463
+ request_data = self.client_requests.pop(request_id)
464
+
465
+ # Calculate duration
466
+ completion_time = request_data.get("completion_time", time.time())
467
+ duration_ms = (completion_time - request_data["start_time"]) * 1000
468
+
469
+ # Extract metrics (handle both naming conventions)
470
+ tokens_input = usage_metrics.get(
471
+ "input_tokens", usage_metrics.get("tokens_input", 0)
472
+ )
473
+ tokens_output = usage_metrics.get(
474
+ "output_tokens", usage_metrics.get("tokens_output", 0)
475
+ )
476
+ cache_read_tokens = usage_metrics.get(
477
+ "cache_read_input_tokens", usage_metrics.get("cache_read_tokens", 0)
478
+ )
479
+ cache_write_tokens = usage_metrics.get(
480
+ "cache_creation_input_tokens",
481
+ usage_metrics.get("cache_write_tokens", 0),
482
+ )
483
+ cost_usd = usage_metrics.get("cost_usd", 0.0)
484
+ model = usage_metrics.get("model") or request_data.get("model", "")
485
+
486
+ # Build complete log data
487
+ client_log_data = {
488
+ **request_data,
489
+ "request_id": request_id,
490
+ "status_code": request_data.get("status_code", 200),
491
+ "duration_ms": duration_ms,
492
+ "tokens_input": tokens_input,
493
+ "tokens_output": tokens_output,
494
+ "cache_read_tokens": cache_read_tokens,
495
+ "cache_write_tokens": cache_write_tokens,
496
+ "cost_usd": cost_usd,
497
+ "model": model,
498
+ "streaming": True,
499
+ "total_chunks": context.data.get("total_chunks", 0),
500
+ "total_bytes": context.data.get("total_bytes", 0),
501
+ "error": None,
502
+ }
503
+
504
+ # Format and write client log
505
+ if self.client_writer:
506
+ formatted = self.formatter.format_client(
507
+ client_log_data, self.config.client_format
508
+ )
509
+ await self.client_writer.write(formatted)
510
+
511
+ # Log to structured logger
512
+ await self._log_to_structured_logger(client_log_data, "client")
513
+
514
+ # Ingest into analytics with full client details (includes IP/UA)
515
+ await self._maybe_ingest(client_log_data)
516
+
517
+ # Extract complete metrics from usage_metrics (handle both naming conventions)
518
+ tokens_input = usage_metrics.get(
519
+ "input_tokens", usage_metrics.get("tokens_input", 0)
520
+ )
521
+ tokens_output = usage_metrics.get(
522
+ "output_tokens", usage_metrics.get("tokens_output", 0)
523
+ )
524
+ cache_read_tokens = usage_metrics.get(
525
+ "cache_read_input_tokens", usage_metrics.get("cache_read_tokens", 0)
526
+ )
527
+ cache_write_tokens = usage_metrics.get(
528
+ "cache_creation_input_tokens", usage_metrics.get("cache_write_tokens", 0)
529
+ )
530
+ cost_usd = usage_metrics.get("cost_usd", 0.0)
531
+ model = usage_metrics.get("model", "")
532
+
533
+ # Get other data from context
534
+ provider = context.provider or context.data.get("provider", "unknown")
535
+ url = context.data.get("url", "")
536
+ method = context.data.get("method", "POST")
537
+ total_chunks = context.data.get("total_chunks", 0)
538
+ total_bytes = context.data.get("total_bytes", 0)
539
+
540
+ # Create log data for streaming complete
541
+ log_data = {
542
+ "timestamp": time.time(),
543
+ "request_id": request_id,
544
+ "provider": provider,
545
+ "method": method,
546
+ "url": url,
547
+ "status_code": 200, # Streaming completion implies success
548
+ "tokens_input": tokens_input,
549
+ "tokens_output": tokens_output,
550
+ "cache_read_tokens": cache_read_tokens,
551
+ "cache_write_tokens": cache_write_tokens,
552
+ "cost_usd": cost_usd,
553
+ "model": model,
554
+ "total_chunks": total_chunks,
555
+ "total_bytes": total_bytes,
556
+ "streaming": True,
557
+ "event_type": "streaming_complete",
558
+ }
559
+
560
+ # Format and write to provider log
561
+ if self.provider_writer and self.config.provider_enabled:
562
+ formatted = self.formatter.format_provider(log_data)
563
+ await self.provider_writer.write(formatted)
564
+
565
+ # Log provider streaming metrics captured (for debugging)
566
+ logger.debug(
567
+ "access_log_provider_stream_end_captured",
568
+ request_id=request_id,
569
+ tokens_input=tokens_input,
570
+ tokens_output=tokens_output,
571
+ cost_usd=cost_usd,
572
+ )
573
+
574
+ # If client request details were not available earlier, we skip ingestion here
575
+ # to avoid emitting incomplete records with missing IP/User-Agent.
576
+
577
+ def _extract_path(self, url: str) -> str:
578
+ """Extract path from URL.
579
+
580
+ Args:
581
+ url: Full URL or path
582
+
583
+ Returns:
584
+ The path portion of the URL
585
+ """
586
+ if "://" in url:
587
+ # Full URL - extract path
588
+ parts = url.split("/", 3)
589
+ return "/" + parts[3] if len(parts) > 3 else "/"
590
+ return url
591
+
592
+ def _should_exclude_path(self, path: str) -> bool:
593
+ """Check if a path should be excluded from logging.
594
+
595
+ Args:
596
+ path: The request path
597
+
598
+ Returns:
599
+ True if the path should be excluded, False otherwise
600
+ """
601
+ return any(path.startswith(excluded) for excluded in self.config.exclude_paths)
602
+
603
+ async def _maybe_ingest(self, log_data: dict[str, Any]) -> None:
604
+ """Ingest log data into analytics storage if service is available."""
605
+ try:
606
+ if self.ingest_service and hasattr(self.ingest_service, "ingest"):
607
+ await self.ingest_service.ingest(log_data)
608
+ except Exception as e: # pragma: no cover - non-fatal
609
+ logger.debug("access_log_ingest_failed", error=str(e))
610
+
611
+ async def _log_to_structured_logger(
612
+ self,
613
+ log_data: dict[str, Any],
614
+ log_type: str,
615
+ error: str | None = None,
616
+ ) -> None:
617
+ """Log to structured logger (stdout/stderr).
618
+
619
+ Args:
620
+ log_data: Log data dictionary
621
+ log_type: Type of log ("client" or "provider")
622
+ error: Error message if applicable
623
+ """
624
+ # Prepare structured log entry with all available fields
625
+ structured_data = {
626
+ "log_type": log_type,
627
+ "request_id": log_data.get("request_id"),
628
+ "method": log_data.get("method"),
629
+ "path": log_data.get("path"),
630
+ "status_code": log_data.get("status_code"),
631
+ "duration_ms": log_data.get("duration_ms"),
632
+ "client_ip": log_data.get("client_ip"),
633
+ "user_agent": log_data.get("user_agent"),
634
+ }
635
+
636
+ # Add token and cost metrics (available for both client and provider logs)
637
+ token_fields = [
638
+ "tokens_input",
639
+ "tokens_output",
640
+ "cache_read_tokens",
641
+ "cache_write_tokens",
642
+ "cost_usd",
643
+ "model",
644
+ ]
645
+
646
+ for field in token_fields:
647
+ value = log_data.get(field)
648
+ if value is not None:
649
+ structured_data[field] = value
650
+
651
+ # Add streaming-specific fields if present
652
+ streaming_fields = ["streaming", "total_chunks", "total_bytes", "event_type"]
653
+ for field in streaming_fields:
654
+ value = log_data.get(field)
655
+ if value is not None:
656
+ structured_data[field] = value
657
+
658
+ # Add service and endpoint info
659
+ service_fields = ["endpoint", "service_type", "provider"]
660
+ for field in service_fields:
661
+ value = log_data.get(field)
662
+ if value is not None:
663
+ structured_data[field] = value
664
+
665
+ # Add session context metadata if available
666
+ session_fields = [
667
+ "session_id",
668
+ "session_type",
669
+ "session_status",
670
+ "session_age_seconds",
671
+ "session_message_count",
672
+ "session_pool_enabled",
673
+ "session_idle_seconds",
674
+ "session_error_count",
675
+ "session_is_new",
676
+ ]
677
+ for field in session_fields:
678
+ value = log_data.get(field)
679
+ if value is not None:
680
+ structured_data[field] = value
681
+
682
+ # Add provider-specific URL if this is a provider log
683
+ if log_type == "provider" and "url" not in structured_data:
684
+ url = log_data.get("url")
685
+ if url:
686
+ structured_data["url"] = url
687
+
688
+ # Remove None values to keep log clean
689
+ structured_data = {k: v for k, v in structured_data.items() if v is not None}
690
+
691
+ # Log with appropriate level - event is passed as first argument to logger methods
692
+ if error:
693
+ logger.warning("access_log", error=error, **structured_data)
694
+ else:
695
+ logger.info("access_log", **structured_data)
696
+
697
+ async def _log_streaming_complete(
698
+ self, request_id: str, context: HookContext
699
+ ) -> None:
700
+ """Log streaming completion with full metrics.
701
+
702
+ This is called when REQUEST_COMPLETED fires for a streaming response,
703
+ using the metrics we stored from PROVIDER_STREAM_END.
704
+ """
705
+ if request_id not in self.client_requests:
706
+ return
707
+
708
+ # Get stored metrics
709
+ metrics_data = self._streaming_metrics.pop(request_id, {})
710
+ usage_metrics = metrics_data.get("usage_metrics", {})
711
+
712
+ # Get the original request data
713
+ request_data = self.client_requests.pop(request_id)
714
+
715
+ # Calculate duration
716
+ duration_ms = (time.time() - request_data["start_time"]) * 1000
717
+
718
+ # Extract metrics
719
+ tokens_input = usage_metrics.get("tokens_input", 0)
720
+ tokens_output = usage_metrics.get("tokens_output", 0)
721
+ cache_read_tokens = usage_metrics.get("cache_read_tokens", 0)
722
+ cache_write_tokens = usage_metrics.get("cache_write_tokens", 0)
723
+ cost_usd = usage_metrics.get("cost_usd", 0.0)
724
+ model = usage_metrics.get("model", "")
725
+
726
+ # Merge request data with streaming metrics
727
+ client_log_data = {
728
+ **request_data,
729
+ "request_id": request_id,
730
+ "status_code": 200,
731
+ "duration_ms": duration_ms,
732
+ "tokens_input": tokens_input,
733
+ "tokens_output": tokens_output,
734
+ "cache_read_tokens": cache_read_tokens,
735
+ "cache_write_tokens": cache_write_tokens,
736
+ "cost_usd": cost_usd,
737
+ "model": model,
738
+ "streaming": True,
739
+ "total_chunks": metrics_data.get("total_chunks", 0),
740
+ "total_bytes": metrics_data.get("total_bytes", 0),
741
+ "error": None,
742
+ }
743
+
744
+ # Format and write client log
745
+ if self.client_writer:
746
+ formatted = self.formatter.format_client(
747
+ client_log_data, self.config.client_format
748
+ )
749
+ await self.client_writer.write(formatted)
750
+
751
+ # Log to structured logger for client
752
+ await self._log_to_structured_logger(client_log_data, "client")
753
+
754
+ logger.info(
755
+ "access_log", **{k: v for k, v in client_log_data.items() if v is not None}
756
+ )
757
+
758
+ async def close(self) -> None:
759
+ """Close writers and flush any pending data."""
760
+ if self.client_writer:
761
+ await self.client_writer.close()
762
+ if self.provider_writer:
763
+ await self.provider_writer.close()