ccproxy-api 0.1.6__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 +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  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 +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  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 +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  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 +95 -342
  387. ccproxy/utils/version_checker.py +279 -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.6.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 -1231
  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 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  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.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.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.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/http/hooks.py ADDED
@@ -0,0 +1,642 @@
1
+ """HTTP client with hook support for request/response interception."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import json as jsonlib
6
+ import time
7
+ from collections.abc import AsyncIterator, Sequence
8
+ from typing import Any, cast
9
+
10
+ import httpx
11
+ from httpx._types import (
12
+ HeaderTypes,
13
+ QueryParamTypes,
14
+ RequestContent,
15
+ RequestData,
16
+ RequestFiles,
17
+ )
18
+
19
+ from ccproxy.core.logging import get_logger
20
+ from ccproxy.core.plugins.hooks.events import HookEvent
21
+ from ccproxy.core.request_context import RequestContext
22
+ from ccproxy.utils.headers import (
23
+ extract_response_headers,
24
+ )
25
+
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ MAX_BODY_LOG_CHARS = 2048
31
+
32
+
33
+ def _stringify_body_for_logging(body: Any) -> tuple[str | None, int, bool]:
34
+ """Convert a request/response body into a safe preview for logging."""
35
+
36
+ if body is None:
37
+ return None, 0, False
38
+
39
+ try:
40
+ if isinstance(body, bytes | bytearray | memoryview):
41
+ text = bytes(body).decode("utf-8", errors="replace")
42
+ elif isinstance(body, str):
43
+ text = body
44
+ else:
45
+ text = jsonlib.dumps(body, ensure_ascii=False)
46
+ except Exception:
47
+ text = str(body)
48
+
49
+ length = len(text)
50
+ truncated = length > MAX_BODY_LOG_CHARS
51
+ preview = f"{text[:MAX_BODY_LOG_CHARS]}...[truncated]" if truncated else text
52
+ return preview, length, truncated
53
+
54
+
55
+ class HookableHTTPClient(httpx.AsyncClient):
56
+ """HTTP client wrapper that emits hooks for all requests/responses."""
57
+
58
+ def __init__(self, *args: Any, hook_manager: Any | None = None, **kwargs: Any):
59
+ """Initialize HTTP client with optional hook support.
60
+
61
+ Args:
62
+ *args: Arguments for httpx.AsyncClient
63
+ hook_manager: Optional HookManager instance for emitting hooks
64
+ **kwargs: Keyword arguments for httpx.AsyncClient
65
+ """
66
+ super().__init__(*args, **kwargs)
67
+ self.hook_manager = hook_manager
68
+
69
+ @staticmethod
70
+ def _normalize_header_pairs(
71
+ headers: HeaderTypes | None,
72
+ ) -> list[tuple[str, str]]:
73
+ """Normalize various httpx header types into string pairs.
74
+
75
+ Accepts mapping-like objects, httpx.Headers, or sequences of pairs.
76
+ Ensures keys/values are converted to ``str`` and preserves order.
77
+ """
78
+ if not headers:
79
+ return []
80
+ try:
81
+ if hasattr(headers, "items") and callable(headers.items): # mapping/Headers
82
+ return [(str(k), str(v)) for k, v in cast(Any, headers).items()]
83
+ # Sequence of pairs
84
+ return [
85
+ (str(k), str(v)) for k, v in cast(Sequence[tuple[Any, Any]], headers)
86
+ ]
87
+ except Exception:
88
+ return []
89
+
90
+ async def request(
91
+ self,
92
+ method: str,
93
+ url: httpx.URL | str,
94
+ *,
95
+ content: RequestContent | None = None,
96
+ data: RequestData | None = None,
97
+ files: RequestFiles | None = None,
98
+ params: QueryParamTypes | None = None,
99
+ headers: HeaderTypes | None = None,
100
+ json: Any | None = None,
101
+ **kwargs: Any,
102
+ ) -> httpx.Response:
103
+ """Make an HTTP request with hook emissions.
104
+
105
+ Emits:
106
+ - HTTP_REQUEST before sending
107
+ - HTTP_RESPONSE after receiving response
108
+ - HTTP_ERROR on errors
109
+ """
110
+ # Build request context for hooks
111
+ request_context: dict[str, Any] = {
112
+ "method": method,
113
+ "url": str(url),
114
+ "headers": dict(self._normalize_header_pairs(headers)),
115
+ "is_provider_request": True,
116
+ "origin": "upstream",
117
+ }
118
+
119
+ # Try to get current request ID from RequestContext
120
+ try:
121
+ current_context = RequestContext.get_current()
122
+ if current_context and hasattr(current_context, "request_id"):
123
+ request_context["request_id"] = current_context.request_id
124
+ except Exception:
125
+ # If no request context available, hooks will generate their own ID
126
+ pass
127
+
128
+ # Add body information
129
+ if json is not None:
130
+ request_context["body"] = json
131
+ request_context["is_json"] = True
132
+ preview, length, truncated = _stringify_body_for_logging(json)
133
+ elif data is not None:
134
+ request_context["body"] = data
135
+ request_context["is_json"] = False
136
+ preview, length, truncated = _stringify_body_for_logging(data)
137
+ elif content is not None:
138
+ # Handle content parameter - could be bytes, string, or other
139
+ if isinstance(content, bytes | str):
140
+ try:
141
+ if isinstance(content, bytes):
142
+ content_str = content.decode("utf-8")
143
+ else:
144
+ content_str = content
145
+
146
+ if content_str.strip().startswith(("{", "[")):
147
+ request_context["body"] = jsonlib.loads(content_str)
148
+ request_context["is_json"] = True
149
+ else:
150
+ request_context["body"] = content
151
+ request_context["is_json"] = False
152
+ except Exception:
153
+ # If parsing fails, just include as-is
154
+ request_context["body"] = content
155
+ request_context["is_json"] = False
156
+ else:
157
+ request_context["body"] = content
158
+ request_context["is_json"] = False
159
+ preview, length, truncated = _stringify_body_for_logging(
160
+ request_context["body"]
161
+ )
162
+ else:
163
+ preview, length, truncated = (None, 0, False)
164
+
165
+ start_time = time.perf_counter()
166
+
167
+ logger.info(
168
+ "request_started",
169
+ method=method,
170
+ url=str(url),
171
+ request_id=request_context.get("request_id"),
172
+ origin=request_context.get("origin"),
173
+ has_body=preview is not None,
174
+ body_size=length,
175
+ body_truncated=truncated,
176
+ is_json=request_context.get("is_json", False),
177
+ streaming=False,
178
+ category="http",
179
+ )
180
+
181
+ logger.debug(
182
+ "upstream_http_request",
183
+ method=method,
184
+ url=str(url),
185
+ request_id=request_context.get("request_id"),
186
+ body_preview=preview,
187
+ body_size=length,
188
+ body_truncated=truncated,
189
+ is_json=request_context.get("is_json", False),
190
+ category="http",
191
+ )
192
+
193
+ # Emit pre-request hook
194
+ if self.hook_manager:
195
+ try:
196
+ await self.hook_manager.emit(
197
+ HookEvent.HTTP_REQUEST,
198
+ request_context,
199
+ )
200
+ except Exception as e:
201
+ logger.debug(
202
+ "http_request_hook_error",
203
+ error=str(e),
204
+ method=method,
205
+ url=str(url),
206
+ )
207
+
208
+ final_response: httpx.Response | None = None
209
+
210
+ try:
211
+ response = await super().request(
212
+ method,
213
+ url,
214
+ content=content,
215
+ data=data,
216
+ files=files,
217
+ json=json,
218
+ params=params,
219
+ headers=headers,
220
+ **kwargs,
221
+ )
222
+ final_response = response
223
+
224
+ if self.hook_manager:
225
+ # Read response content FIRST before any other processing
226
+ response_content = response.content
227
+
228
+ response_context = {
229
+ **request_context,
230
+ "status_code": response.status_code,
231
+ "response_headers": extract_response_headers(response),
232
+ "is_provider_response": True,
233
+ }
234
+
235
+ # Include response body from the content we just read
236
+ try:
237
+ content_type = response.headers.get("content-type", "")
238
+ if "application/json" in content_type:
239
+ # Try to parse the raw content as JSON
240
+ try:
241
+ response_context["response_body"] = jsonlib.loads(
242
+ response_content.decode("utf-8")
243
+ )
244
+ except Exception:
245
+ # If JSON parsing fails, include as text
246
+ response_context["response_body"] = response_content.decode(
247
+ "utf-8", errors="replace"
248
+ )
249
+ else:
250
+ # For non-JSON content, include as text
251
+ response_context["response_body"] = response_content.decode(
252
+ "utf-8", errors="replace"
253
+ )
254
+ except Exception:
255
+ # Last resort - include as bytes
256
+ response_context["response_body"] = response_content
257
+
258
+ preview, length, truncated = _stringify_body_for_logging(
259
+ response_context.get("response_body")
260
+ )
261
+ logger.debug(
262
+ "upstream_http_response",
263
+ url=str(url),
264
+ request_id=response_context.get("request_id"),
265
+ status_code=response.status_code,
266
+ body_preview=preview,
267
+ body_size=length,
268
+ body_truncated=truncated,
269
+ category="http",
270
+ )
271
+
272
+ try:
273
+ await self.hook_manager.emit(
274
+ HookEvent.HTTP_RESPONSE,
275
+ response_context,
276
+ )
277
+ except Exception as e:
278
+ logger.debug(
279
+ "http_response_hook_error",
280
+ error=str(e),
281
+ status_code=response.status_code,
282
+ )
283
+
284
+ try:
285
+ recreated_response = httpx.Response(
286
+ status_code=response.status_code,
287
+ headers=response.headers,
288
+ content=response_content,
289
+ request=response.request,
290
+ )
291
+ final_response = recreated_response
292
+ except Exception:
293
+ # If recreation fails, return original (may have empty body)
294
+ logger.debug("response_recreation_failed")
295
+ final_response = response
296
+
297
+ duration_ms = round((time.perf_counter() - start_time) * 1000, 3)
298
+
299
+ logger.info(
300
+ "request_completed",
301
+ method=method,
302
+ url=str(url),
303
+ request_id=request_context.get("request_id"),
304
+ origin=request_context.get("origin"),
305
+ status_code=final_response.status_code if final_response else None,
306
+ duration_ms=duration_ms,
307
+ streaming=False,
308
+ success=True,
309
+ category="http",
310
+ )
311
+
312
+ assert final_response is not None
313
+ return final_response
314
+
315
+ except Exception as error:
316
+ duration_ms = round((time.perf_counter() - start_time) * 1000, 3)
317
+
318
+ # Emit error hook
319
+ if self.hook_manager:
320
+ error_context = {
321
+ **request_context,
322
+ "error_type": type(error).__name__,
323
+ "error_detail": str(error),
324
+ }
325
+
326
+ # Add response info if it's an HTTPStatusError
327
+ if isinstance(error, httpx.HTTPStatusError):
328
+ error_context["status_code"] = error.response.status_code
329
+ error_context["response_body"] = error.response.text
330
+
331
+ try:
332
+ await self.hook_manager.emit(
333
+ HookEvent.HTTP_ERROR,
334
+ error_context,
335
+ )
336
+ except Exception as e:
337
+ logger.debug(
338
+ "http_error_hook_error",
339
+ error=str(e),
340
+ original_error=str(error),
341
+ )
342
+
343
+ status_code = getattr(getattr(error, "response", None), "status_code", None)
344
+
345
+ logger.info(
346
+ "request_completed",
347
+ method=method,
348
+ url=str(url),
349
+ request_id=request_context.get("request_id"),
350
+ origin=request_context.get("origin"),
351
+ status_code=status_code,
352
+ duration_ms=duration_ms,
353
+ streaming=False,
354
+ success=False,
355
+ error_type=type(error).__name__,
356
+ category="http",
357
+ )
358
+
359
+ logger.error(
360
+ "upstream_http_error",
361
+ url=str(url),
362
+ request_id=request_context.get("request_id"),
363
+ error_type=type(error).__name__,
364
+ error_detail=str(error),
365
+ category="http",
366
+ )
367
+
368
+ # Re-raise the original error
369
+ raise
370
+
371
+ @contextlib.asynccontextmanager
372
+ async def stream(
373
+ self,
374
+ method: str,
375
+ url: httpx.URL | str,
376
+ *,
377
+ content: RequestContent | None = None,
378
+ data: RequestData | None = None,
379
+ files: RequestFiles | None = None,
380
+ params: QueryParamTypes | None = None,
381
+ headers: HeaderTypes | None = None,
382
+ json: Any | None = None,
383
+ **kwargs: Any,
384
+ ) -> AsyncIterator[httpx.Response]:
385
+ """Make a streaming HTTP request with hook emissions.
386
+
387
+ This method emits HTTP hooks for streaming requests, capturing the complete
388
+ response body while maintaining streaming behavior.
389
+
390
+ Emits:
391
+ - HTTP_REQUEST before sending
392
+ - HTTP_RESPONSE after receiving complete response
393
+ - HTTP_ERROR on errors
394
+ """
395
+ # Build request context for hooks (same as request() method)
396
+ request_context: dict[str, Any] = {
397
+ "method": method,
398
+ "url": str(url),
399
+ "headers": dict(self._normalize_header_pairs(headers)),
400
+ "is_provider_request": True,
401
+ "origin": "upstream",
402
+ }
403
+
404
+ # Try to get current request ID from RequestContext
405
+ try:
406
+ current_context = RequestContext.get_current()
407
+ if current_context and hasattr(current_context, "request_id"):
408
+ request_context["request_id"] = current_context.request_id
409
+ except Exception:
410
+ # No current context available, that's OK
411
+ pass
412
+
413
+ # Add request body to context if available
414
+ if json is not None:
415
+ request_context["body"] = json
416
+ request_context["is_json"] = True
417
+ elif data is not None:
418
+ request_context["body"] = data
419
+ request_context["is_json"] = False
420
+ elif content is not None:
421
+ request_context["body"] = content
422
+ request_context["is_json"] = False
423
+
424
+ preview, length, truncated = _stringify_body_for_logging(
425
+ request_context.get("body")
426
+ )
427
+ start_time = time.perf_counter()
428
+
429
+ logger.info(
430
+ "sse_connection_started",
431
+ method=method,
432
+ url=str(url),
433
+ request_id=request_context.get("request_id"),
434
+ origin=request_context.get("origin"),
435
+ has_body=preview is not None,
436
+ body_size=length,
437
+ body_truncated=truncated,
438
+ is_json=request_context.get("is_json", False),
439
+ streaming=True,
440
+ category="http",
441
+ )
442
+
443
+ logger.debug(
444
+ "upstream_http_request",
445
+ method=method,
446
+ url=str(url),
447
+ request_id=request_context.get("request_id"),
448
+ body_preview=preview,
449
+ body_size=length,
450
+ body_truncated=truncated,
451
+ is_json=request_context.get("is_json", False),
452
+ streaming=True,
453
+ category="http",
454
+ )
455
+
456
+ # Emit pre-request hook
457
+ if self.hook_manager:
458
+ try:
459
+ await self.hook_manager.emit(
460
+ HookEvent.HTTP_REQUEST,
461
+ request_context,
462
+ )
463
+ except Exception as e:
464
+ logger.debug(
465
+ "http_request_hook_error",
466
+ error=str(e),
467
+ method=method,
468
+ url=str(url),
469
+ )
470
+
471
+ request_error: BaseException | None = None
472
+ status_code: int | None = None
473
+ connection_started = False
474
+ closed_by: str = "server"
475
+
476
+ try:
477
+ async with super().stream(
478
+ method=method,
479
+ url=url,
480
+ content=content,
481
+ data=data,
482
+ files=files,
483
+ params=params,
484
+ headers=headers,
485
+ json=json,
486
+ **kwargs,
487
+ ) as response:
488
+ status_code = response.status_code
489
+ connection_started = True
490
+
491
+ if self.hook_manager:
492
+ try:
493
+ response_context = {
494
+ **request_context,
495
+ "status_code": response.status_code,
496
+ "response_headers": extract_response_headers(response),
497
+ "is_provider_response": True,
498
+ "streaming": True,
499
+ }
500
+ await self.hook_manager.emit(
501
+ HookEvent.HTTP_RESPONSE,
502
+ response_context,
503
+ )
504
+ except Exception as e:
505
+ logger.debug(
506
+ "http_response_hook_error",
507
+ error=str(e),
508
+ status_code=response.status_code,
509
+ )
510
+
511
+ logger.debug(
512
+ "upstream_http_response",
513
+ url=str(url),
514
+ request_id=request_context.get("request_id"),
515
+ status_code=response.status_code,
516
+ streaming=True,
517
+ body_preview=None,
518
+ body_size=0,
519
+ body_truncated=False,
520
+ category="http",
521
+ )
522
+ yield response
523
+
524
+ except asyncio.CancelledError as error:
525
+ request_error = error
526
+ closed_by = "client"
527
+
528
+ # Emit error hook
529
+ if self.hook_manager:
530
+ error_context = {
531
+ **request_context,
532
+ "error": error,
533
+ "error_type": type(error).__name__,
534
+ }
535
+
536
+ try:
537
+ await self.hook_manager.emit(
538
+ HookEvent.HTTP_ERROR,
539
+ error_context,
540
+ )
541
+ except Exception as e:
542
+ logger.debug(
543
+ "http_error_hook_error",
544
+ error=str(e),
545
+ original_error=str(error),
546
+ )
547
+
548
+ logger.debug(
549
+ "upstream_http_cancelled",
550
+ url=str(url),
551
+ request_id=request_context.get("request_id"),
552
+ error_type=type(error).__name__,
553
+ streaming=True,
554
+ category="http",
555
+ )
556
+
557
+ raise
558
+
559
+ except Exception as error:
560
+ request_error = error
561
+ closed_by = "server_error"
562
+
563
+ # Emit error hook
564
+ if self.hook_manager:
565
+ error_context = {
566
+ **request_context,
567
+ "error": error,
568
+ "error_type": type(error).__name__,
569
+ }
570
+
571
+ if isinstance(error, httpx.HTTPStatusError):
572
+ error_context["status_code"] = error.response.status_code
573
+ error_context["response_body"] = error.response.text
574
+
575
+ try:
576
+ await self.hook_manager.emit(
577
+ HookEvent.HTTP_ERROR,
578
+ error_context,
579
+ )
580
+ except Exception as e:
581
+ logger.debug(
582
+ "http_error_hook_error",
583
+ error=str(e),
584
+ original_error=str(error),
585
+ )
586
+
587
+ if not isinstance(error, asyncio.CancelledError):
588
+ logger.error(
589
+ "upstream_http_error",
590
+ url=str(url),
591
+ request_id=request_context.get("request_id"),
592
+ error_type=type(error).__name__,
593
+ error_detail=str(error),
594
+ streaming=True,
595
+ category="http",
596
+ )
597
+
598
+ raise
599
+
600
+ finally:
601
+ duration_ms = round((time.perf_counter() - start_time) * 1000, 3)
602
+ log_fields: dict[str, Any] = {
603
+ "method": method,
604
+ "url": str(url),
605
+ "request_id": request_context.get("request_id"),
606
+ "origin": request_context.get("origin"),
607
+ "status_code": status_code,
608
+ "duration_ms": duration_ms,
609
+ "streaming": True,
610
+ "success": request_error is None and connection_started,
611
+ "closed_by": closed_by if request_error is not None else "server",
612
+ "category": "http",
613
+ }
614
+ stream_metadata = getattr(request_context, "metadata", {})
615
+ closed_override = (
616
+ stream_metadata.get("stream_closed_by") if stream_metadata else None
617
+ )
618
+ if closed_override:
619
+ log_fields["closed_by"] = closed_override
620
+ if closed_override != "server":
621
+ log_fields["success"] = False
622
+ error_reason_override = (
623
+ stream_metadata.get("stream_error_reason") if stream_metadata else None
624
+ )
625
+ error_type_override = (
626
+ stream_metadata.get("stream_error_type") if stream_metadata else None
627
+ )
628
+
629
+ if request_error is not None:
630
+ log_fields["error_type"] = type(request_error).__name__
631
+ error_str = str(request_error)
632
+ if error_str:
633
+ log_fields["error_reason"] = error_str
634
+ elif error_type_override:
635
+ log_fields["error_type"] = error_type_override
636
+ if error_reason_override:
637
+ log_fields["error_reason"] = error_reason_override
638
+
639
+ if error_reason_override and "error_reason" not in log_fields:
640
+ log_fields["error_reason"] = error_reason_override
641
+
642
+ logger.info("sse_connection_ended", **log_fields)