ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,552 @@
1
+ """JSON formatter for structured request/response logging."""
2
+
3
+ import base64
4
+ import json
5
+ import logging
6
+ import time
7
+ import uuid
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import structlog
13
+ from structlog.contextvars import get_merged_contextvars
14
+
15
+ from ccproxy.core.plugins.hooks.types import HookHeaders
16
+
17
+
18
+ try:
19
+ from ccproxy.core.logging import TRACE_LEVEL
20
+ except ImportError:
21
+ TRACE_LEVEL = 5 # Fallback
22
+
23
+ logger = structlog.get_logger(__name__)
24
+
25
+
26
+ class JSONFormatter:
27
+ """Formats requests/responses as structured JSON for observability."""
28
+
29
+ def __init__(
30
+ self,
31
+ log_dir: str = "/tmp/ccproxy/traces",
32
+ verbose_api: bool = True,
33
+ json_logs_enabled: bool = True,
34
+ redact_sensitive: bool = True,
35
+ truncate_body_preview: int = 1024,
36
+ ) -> None:
37
+ """Initialize with configuration.
38
+
39
+ Args:
40
+ log_dir: Directory for log files
41
+ verbose_api: Enable verbose API logging
42
+ json_logs_enabled: Enable JSON file logging
43
+ redact_sensitive: Redact sensitive headers
44
+ truncate_body_preview: Max body preview size
45
+ """
46
+ self.log_dir = log_dir
47
+ self.verbose_api = verbose_api
48
+ self.json_logs_enabled = json_logs_enabled
49
+ self.redact_sensitive = redact_sensitive
50
+ self.truncate_body_preview = truncate_body_preview
51
+
52
+ # Check if TRACE level is enabled
53
+ current_level = (
54
+ logger._context.get("_level", logging.INFO)
55
+ if hasattr(logger, "_context")
56
+ else logging.INFO
57
+ )
58
+ self.trace_enabled = self.verbose_api or current_level <= TRACE_LEVEL
59
+
60
+ # Setup log directory if file logging is enabled
61
+ self.request_log_dir = None
62
+ if self.json_logs_enabled:
63
+ self.request_log_dir = Path(log_dir)
64
+ self.request_log_dir.mkdir(parents=True, exist_ok=True)
65
+
66
+ @classmethod
67
+ def from_config(cls, config: Any) -> "JSONFormatter":
68
+ """Create JSONFormatter from a RequestTracerConfig.
69
+
70
+ Args:
71
+ config: RequestTracerConfig instance
72
+
73
+ Returns:
74
+ JSONFormatter instance
75
+ """
76
+ return cls(
77
+ log_dir=config.get_json_log_dir(),
78
+ verbose_api=config.verbose_api,
79
+ json_logs_enabled=config.json_logs_enabled,
80
+ redact_sensitive=config.redact_sensitive,
81
+ truncate_body_preview=config.truncate_body_preview,
82
+ )
83
+
84
+ def _current_cmd_id(self) -> str | None:
85
+ """Return current cmd_id from structlog contextvars or env."""
86
+ try:
87
+ ctx = get_merged_contextvars(logger) or {}
88
+ cmd_id = ctx.get("cmd_id")
89
+ except Exception:
90
+ cmd_id = None
91
+
92
+ return str(cmd_id) if cmd_id else None
93
+
94
+ def _compose_file_id(self, request_id: str | None) -> str:
95
+ """Build filename ID using cmd_id and request_id per rules.
96
+
97
+ - If both cmd_id and request_id exist: "{cmd_id}_{request_id}"
98
+ - If only request_id exists: request_id
99
+ - If only cmd_id exists: cmd_id
100
+ - If neither exists: generate a UUID4
101
+ """
102
+ try:
103
+ ctx = get_merged_contextvars(logger) or {}
104
+ cmd_id = ctx.get("cmd_id")
105
+ except Exception:
106
+ cmd_id = None
107
+
108
+ if cmd_id and request_id:
109
+ return f"{cmd_id}_{request_id}"
110
+ if request_id:
111
+ return request_id
112
+ if cmd_id:
113
+ return str(cmd_id)
114
+ return str(uuid.uuid4())
115
+
116
+ def _compose_file_id_with_timestamp(self, request_id: str | None) -> str:
117
+ """Build filename ID with timestamp suffix for better organization.
118
+
119
+ Format: {base_id}_{timestamp}_{sequence}
120
+ Where timestamp is in format: YYYYMMDD_HHMMSS_microseconds
121
+ And sequence is a counter to prevent collisions
122
+ """
123
+ base_id = self._compose_file_id(request_id)
124
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
125
+
126
+ # Add a high-resolution timestamp with nanoseconds for uniqueness
127
+ nanos = time.time_ns() % 1000000 # Get nanosecond portion
128
+ return f"{base_id}_{timestamp}_{nanos:06d}"
129
+
130
+ @staticmethod
131
+ def redact_headers(headers: dict[str, str]) -> dict[str, str]:
132
+ """Redact sensitive headers for safe logging.
133
+
134
+ - Replaces authorization, x-api-key, cookie values with [REDACTED]
135
+ - Preserves header names for debugging
136
+ - Returns new dict without modifying original
137
+ """
138
+ sensitive_headers = {
139
+ "authorization",
140
+ "x-api-key",
141
+ "api-key",
142
+ "cookie",
143
+ "x-auth-token",
144
+ "x-secret-key",
145
+ }
146
+
147
+ redacted = {}
148
+ for key, value in headers.items():
149
+ if key.lower() in sensitive_headers:
150
+ redacted[key] = "[REDACTED]"
151
+ else:
152
+ redacted[key] = value
153
+ return redacted
154
+
155
+ async def log_request(
156
+ self,
157
+ request_id: str,
158
+ method: str,
159
+ url: str,
160
+ headers: HookHeaders | dict[str, str],
161
+ body: bytes | None,
162
+ request_type: str = "provider", # "client" or "provider"
163
+ context: Any = None, # RequestContext
164
+ hook_type: str | None = None, # Hook type for filename (e.g., "tracer", "http")
165
+ ) -> None:
166
+ """Log structured request data.
167
+
168
+ - Logs at TRACE level with redacted headers
169
+ - Writes to request log file with complete data (if configured)
170
+ """
171
+ if not self.trace_enabled:
172
+ return
173
+
174
+ # Normalize headers (preserve order/case if dict-like)
175
+ headers_dict = (
176
+ headers.to_dict() if hasattr(headers, "to_dict") else dict(headers)
177
+ )
178
+
179
+ # Log at TRACE level with redacted headers
180
+ log_headers = (
181
+ self.redact_headers(headers_dict) if self.redact_sensitive else headers_dict
182
+ )
183
+
184
+ if hasattr(logger, "trace"):
185
+ logger.trace(
186
+ "api_request",
187
+ category="http",
188
+ request_id=request_id,
189
+ method=method,
190
+ url=url,
191
+ headers=log_headers,
192
+ body_size=len(body) if body else 0,
193
+ )
194
+ elif self.verbose_api:
195
+ # Fallback for backward compatibility
196
+ logger.info(
197
+ "api_request",
198
+ category="http",
199
+ request_id=request_id,
200
+ method=method,
201
+ url=url,
202
+ headers=log_headers,
203
+ body_size=len(body) if body else 0,
204
+ )
205
+
206
+ # Write to file if configured
207
+ if self.request_log_dir and self.json_logs_enabled:
208
+ # Build file suffix with hook type
209
+ base_suffix = (
210
+ f"{request_type}_request" if request_type != "provider" else "request"
211
+ )
212
+ if hook_type:
213
+ file_suffix = f"{base_suffix}_{hook_type}"
214
+ else:
215
+ file_suffix = base_suffix
216
+
217
+ base_id = self._compose_file_id_with_timestamp(request_id)
218
+ request_file = self.request_log_dir / f"{base_id}_{file_suffix}.json"
219
+
220
+ # Handle body content - could be bytes, dict/list (from JSON), or string
221
+ body_content = None
222
+ if body is not None:
223
+ if isinstance(body, dict | list):
224
+ # Already parsed JSON object from hook context
225
+ body_content = body
226
+ elif isinstance(body, bytes):
227
+ # Raw bytes - try to parse as JSON first, then string, then base64
228
+ try:
229
+ # First try to decode as UTF-8 string
230
+ body_str = body.decode("utf-8")
231
+ # Then try to parse as JSON
232
+ body_content = json.loads(body_str)
233
+ except (json.JSONDecodeError, UnicodeDecodeError):
234
+ # Not JSON, try plain string
235
+ try:
236
+ body_content = body.decode("utf-8", errors="replace")
237
+ except Exception:
238
+ # Last resort: encode as base64
239
+ body_content = {
240
+ "_type": "base64",
241
+ "data": base64.b64encode(body).decode("ascii"),
242
+ }
243
+ elif isinstance(body, str):
244
+ # String body - try to parse as JSON, otherwise keep as string
245
+ try:
246
+ body_content = json.loads(body)
247
+ except json.JSONDecodeError:
248
+ body_content = body
249
+ else:
250
+ # Other type - convert to string
251
+ body_content = str(body)
252
+
253
+ request_data = {
254
+ "request_id": request_id,
255
+ "method": method,
256
+ "url": url,
257
+ "headers": headers_dict, # Full headers in file
258
+ "body": body_content,
259
+ "type": request_type,
260
+ }
261
+
262
+ # Add cmd_id for CLI correlation if present
263
+ cmd_id = self._current_cmd_id()
264
+ if cmd_id:
265
+ request_data["cmd_id"] = cmd_id
266
+
267
+ # Add context data if available
268
+ if context and hasattr(context, "to_dict"):
269
+ try:
270
+ context_data = context.to_dict()
271
+ if context_data:
272
+ request_data["context"] = context_data
273
+ except Exception as e:
274
+ logger.debug(
275
+ "context_serialization_error",
276
+ error=str(e),
277
+ request_id=request_id,
278
+ )
279
+
280
+ request_file.write_text(json.dumps(request_data, indent=2, default=str))
281
+
282
+ async def log_response(
283
+ self,
284
+ request_id: str,
285
+ status: int,
286
+ headers: HookHeaders | dict[str, str],
287
+ body: bytes,
288
+ response_type: str = "provider", # "client" or "provider"
289
+ context: Any = None, # RequestContext
290
+ hook_type: str | None = None, # Hook type for filename (e.g., "tracer", "http")
291
+ ) -> None:
292
+ """Log structured response data.
293
+
294
+ - Logs at TRACE level
295
+ - Truncates body preview for console
296
+ - Handles binary data gracefully
297
+ """
298
+ if not self.trace_enabled:
299
+ return
300
+
301
+ body_preview = self._get_body_preview(body)
302
+
303
+ # Normalize headers (preserve order/case if dict-like)
304
+ headers_dict = (
305
+ headers.to_dict() if hasattr(headers, "to_dict") else dict(headers)
306
+ )
307
+
308
+ # Log at TRACE level
309
+ if hasattr(logger, "trace"):
310
+ logger.trace(
311
+ "api_response",
312
+ category="http",
313
+ request_id=request_id,
314
+ status=status,
315
+ headers=headers_dict,
316
+ body_preview=body_preview,
317
+ body_size=len(body),
318
+ )
319
+ else:
320
+ # Fallback for backward compatibility
321
+ logger.info(
322
+ "api_response",
323
+ category="http",
324
+ request_id=request_id,
325
+ status=status,
326
+ headers=headers_dict,
327
+ body_preview=body_preview,
328
+ body_size=len(body),
329
+ )
330
+
331
+ # Write to file if configured
332
+ if self.request_log_dir and self.json_logs_enabled:
333
+ # Build file suffix with hook type
334
+ base_suffix = (
335
+ f"{response_type}_response"
336
+ if response_type != "provider"
337
+ else "response"
338
+ )
339
+ if hook_type:
340
+ file_suffix = f"{base_suffix}_{hook_type}"
341
+ else:
342
+ file_suffix = base_suffix
343
+ logger.debug(
344
+ "Writing response JSON file",
345
+ request_id=request_id,
346
+ status=status,
347
+ response_type=response_type,
348
+ file_suffix=file_suffix,
349
+ body_type=type(body).__name__,
350
+ body_size=len(body) if body else 0,
351
+ body_preview=body[:100] if body else None,
352
+ )
353
+ base_id = self._compose_file_id_with_timestamp(request_id)
354
+ response_file = self.request_log_dir / f"{base_id}_{file_suffix}.json"
355
+
356
+ # Try to parse body as JSON first, then string, then base64
357
+ body_content: str | dict[str, Any] = ""
358
+ if body:
359
+ try:
360
+ # First try to decode as UTF-8 string
361
+ body_str = body.decode("utf-8")
362
+ # Then try to parse as JSON
363
+ body_content = json.loads(body_str)
364
+ except (json.JSONDecodeError, UnicodeDecodeError):
365
+ # Not JSON, try plain string
366
+ try:
367
+ body_content = body.decode("utf-8", errors="replace")
368
+ except Exception:
369
+ # Last resort: encode as base64
370
+ import base64
371
+
372
+ body_content = {
373
+ "_type": "base64",
374
+ "data": base64.b64encode(body).decode("ascii"),
375
+ }
376
+
377
+ response_data = {
378
+ "request_id": request_id,
379
+ "status": status,
380
+ "headers": headers_dict,
381
+ "body": body_content,
382
+ "type": response_type,
383
+ }
384
+
385
+ # Add cmd_id for CLI correlation if present
386
+ cmd_id = self._current_cmd_id()
387
+ if cmd_id:
388
+ response_data["cmd_id"] = cmd_id
389
+
390
+ # Add context data if available (including cost/metrics)
391
+ if context and hasattr(context, "to_dict"):
392
+ try:
393
+ context_data = context.to_dict()
394
+ if context_data:
395
+ response_data["context"] = context_data
396
+ except Exception as e:
397
+ logger.debug(
398
+ "context_serialization_error",
399
+ error=str(e),
400
+ request_id=request_id,
401
+ )
402
+
403
+ response_file.write_text(json.dumps(response_data, indent=2, default=str))
404
+
405
+ def _get_body_preview(self, body: bytes) -> str:
406
+ """Extract readable preview from body bytes.
407
+
408
+ - Decodes UTF-8 with error replacement
409
+ - Truncates to max_length
410
+ - Returns '<binary data>' for non-text content
411
+ """
412
+ max_length = self.truncate_body_preview
413
+
414
+ try:
415
+ text = body.decode("utf-8", errors="replace")
416
+
417
+ # Try to parse as JSON for better formatting
418
+ try:
419
+ json_data = json.loads(text)
420
+ formatted = json.dumps(json_data, indent=2)
421
+ if len(formatted) > max_length:
422
+ return formatted[:max_length] + "..."
423
+ return formatted
424
+ except json.JSONDecodeError:
425
+ # Not JSON, return as plain text
426
+ if len(text) > max_length:
427
+ return text[:max_length] + "..."
428
+ return text
429
+ except UnicodeDecodeError:
430
+ return "<binary data>"
431
+ except Exception as e:
432
+ logger.debug("text_formatting_unexpected_error", error=str(e))
433
+ return "<binary data>"
434
+
435
+ # Streaming methods
436
+ async def log_stream_chunk(
437
+ self, request_id: str, chunk: bytes, chunk_number: int
438
+ ) -> None:
439
+ """Record individual stream chunk (optional, for deep debugging)."""
440
+ logger.debug(
441
+ "stream_chunk",
442
+ category="streaming",
443
+ request_id=request_id,
444
+ chunk_number=chunk_number,
445
+ chunk_size=len(chunk),
446
+ )
447
+
448
+ async def log_error(
449
+ self,
450
+ request_id: str,
451
+ error: Exception | None,
452
+ duration: float | None = None,
453
+ provider: str | None = None,
454
+ ) -> None:
455
+ """Log error information."""
456
+ if not self.verbose_api:
457
+ return
458
+
459
+ error_data: dict[str, Any] = {
460
+ "request_id": request_id,
461
+ "error": str(error) if error else "unknown",
462
+ "category": "error",
463
+ }
464
+
465
+ if duration is not None:
466
+ error_data["duration"] = duration
467
+ if provider:
468
+ error_data["provider"] = provider
469
+
470
+ logger.error("request_error", **error_data)
471
+
472
+ # Legacy compatibility methods
473
+ async def log_provider_request(
474
+ self,
475
+ request_id: str,
476
+ provider: str,
477
+ method: str,
478
+ url: str,
479
+ headers: dict[str, str],
480
+ body: bytes | None,
481
+ ) -> None:
482
+ """Log provider request."""
483
+ await self.log_request(
484
+ request_id=request_id,
485
+ method=method,
486
+ url=url,
487
+ headers=headers,
488
+ body=body,
489
+ request_type="provider",
490
+ )
491
+
492
+ async def log_provider_response(
493
+ self,
494
+ request_id: str,
495
+ provider: str,
496
+ status_code: int,
497
+ headers: dict[str, str],
498
+ body: bytes | None,
499
+ ) -> None:
500
+ """Log provider response."""
501
+ await self.log_response(
502
+ request_id=request_id,
503
+ status=status_code,
504
+ headers=headers,
505
+ body=body or b"",
506
+ response_type="provider",
507
+ )
508
+
509
+ async def log_stream_start(
510
+ self,
511
+ request_id: str,
512
+ provider: str | None = None,
513
+ ) -> None:
514
+ """Log stream start."""
515
+ if not self.verbose_api:
516
+ return
517
+
518
+ log_data: dict[str, Any] = {
519
+ "request_id": request_id,
520
+ "category": "streaming",
521
+ }
522
+ if provider:
523
+ log_data["provider"] = provider
524
+
525
+ logger.info("stream_start", **log_data)
526
+
527
+ async def log_stream_complete(
528
+ self,
529
+ request_id: str,
530
+ provider: str | None = None,
531
+ total_chunks: int | None = None,
532
+ total_bytes: int | None = None,
533
+ usage_metrics: dict[str, Any] | None = None,
534
+ ) -> None:
535
+ """Log stream completion with metrics."""
536
+ if not self.verbose_api:
537
+ return
538
+
539
+ log_data: dict[str, Any] = {
540
+ "request_id": request_id,
541
+ "category": "streaming",
542
+ }
543
+ if provider:
544
+ log_data["provider"] = provider
545
+ if total_chunks is not None:
546
+ log_data["total_chunks"] = total_chunks
547
+ if total_bytes is not None:
548
+ log_data["total_bytes"] = total_bytes
549
+ if usage_metrics:
550
+ log_data["usage_metrics"] = usage_metrics
551
+
552
+ logger.info("stream_complete", **log_data)