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
@@ -1,180 +0,0 @@
1
- """Access logging middleware for structured HTTP request/response logging."""
2
-
3
- import time
4
- from typing import Any
5
-
6
- import structlog
7
- from fastapi import Request, Response
8
- from starlette.middleware.base import BaseHTTPMiddleware
9
- from starlette.types import ASGIApp
10
-
11
- from ccproxy.api.dependencies import get_cached_settings
12
-
13
-
14
- logger = structlog.get_logger(__name__)
15
-
16
-
17
- class AccessLogMiddleware(BaseHTTPMiddleware):
18
- """Middleware for structured access logging with request/response details."""
19
-
20
- def __init__(self, app: ASGIApp):
21
- """Initialize the access log middleware.
22
-
23
- Args:
24
- app: The ASGI application
25
- """
26
- super().__init__(app)
27
-
28
- async def dispatch(self, request: Request, call_next: Any) -> Response:
29
- """Process the request and log access details.
30
-
31
- Args:
32
- request: The incoming HTTP request
33
- call_next: The next middleware/handler in the chain
34
-
35
- Returns:
36
- The HTTP response
37
- """
38
- # Record start time
39
- start_time = time.perf_counter()
40
-
41
- # Store log storage in request state if collection is enabled
42
-
43
- settings = get_cached_settings(request)
44
-
45
- if settings.observability.logs_collection_enabled and hasattr(
46
- request.app.state, "log_storage"
47
- ):
48
- request.state.log_storage = request.app.state.log_storage
49
-
50
- # Extract client info
51
- client_ip = "unknown"
52
- if request.client:
53
- client_ip = request.client.host
54
-
55
- # Extract request info
56
- method = request.method
57
- path = str(request.url.path)
58
- query = str(request.url.query) if request.url.query else None
59
- user_agent = request.headers.get("user-agent", "unknown")
60
-
61
- # Get request ID from context if available
62
- request_id: str | None = None
63
- try:
64
- if hasattr(request.state, "request_id"):
65
- request_id = request.state.request_id
66
- elif hasattr(request.state, "context"):
67
- # Try to check if it's a RequestContext without importing
68
- context = request.state.context
69
- if hasattr(context, "request_id") and hasattr(context, "metadata"):
70
- request_id = context.request_id
71
- except Exception:
72
- # Ignore any errors getting request_id
73
- pass
74
-
75
- # Process the request
76
- response: Response | None = None
77
- error_message: str | None = None
78
-
79
- try:
80
- response = await call_next(request)
81
- except Exception as e:
82
- # Capture error for logging
83
- error_message = str(e)
84
- # Re-raise to let error handlers process it
85
- raise
86
- finally:
87
- try:
88
- # Calculate duration
89
- duration_seconds = time.perf_counter() - start_time
90
- duration_ms = duration_seconds * 1000
91
-
92
- # Extract response info
93
- if response:
94
- status_code = response.status_code
95
-
96
- # Extract rate limit headers if present
97
- rate_limit_info = {}
98
- anthropic_request_id = None
99
- for header_name, header_value in response.headers.items():
100
- header_lower = header_name.lower()
101
- # Capture x-ratelimit-* headers
102
- if header_lower.startswith(
103
- "x-ratelimit-"
104
- ) or header_lower.startswith("anthropic-ratelimit-"):
105
- rate_limit_info[header_lower] = header_value
106
- # Capture request-id from Anthropic's response
107
- elif header_lower == "request-id":
108
- anthropic_request_id = header_value
109
-
110
- # Add anthropic request ID if present
111
- if anthropic_request_id:
112
- rate_limit_info["anthropic_request_id"] = anthropic_request_id
113
-
114
- headers = request.state.context.metadata.get("headers", {})
115
- headers.update(rate_limit_info)
116
- request.state.context.metadata["headers"] = headers
117
- request.state.context.metadata["status_code"] = status_code
118
- # Extract metadata from context if available
119
- context_metadata = {}
120
- try:
121
- if hasattr(request.state, "context"):
122
- context = request.state.context
123
- # Check if it has the expected attributes of RequestContext
124
- if hasattr(context, "metadata") and isinstance(
125
- context.metadata, dict
126
- ):
127
- # Get all metadata from the context
128
- context_metadata = context.metadata.copy()
129
- # Remove fields we're already logging separately
130
- for key in [
131
- "method",
132
- "path",
133
- "client_ip",
134
- "status_code",
135
- "request_id",
136
- "duration_ms",
137
- "duration_seconds",
138
- "query",
139
- "user_agent",
140
- "error_message",
141
- ]:
142
- context_metadata.pop(key, None)
143
- except Exception:
144
- # Ignore any errors extracting context metadata
145
- pass
146
-
147
- # Use start-only logging - let context handle comprehensive access logging
148
- # Only log basic request start info since context will handle complete access log
149
- from ccproxy.observability.access_logger import log_request_start
150
-
151
- log_request_start(
152
- request_id=request_id or "unknown",
153
- method=method,
154
- path=path,
155
- client_ip=client_ip,
156
- user_agent=user_agent,
157
- query=query,
158
- **rate_limit_info,
159
- )
160
- else:
161
- # Log error case
162
- logger.error(
163
- "access_log_error",
164
- request_id=request_id,
165
- method=method,
166
- path=path,
167
- query=query,
168
- client_ip=client_ip,
169
- user_agent=user_agent,
170
- duration_ms=duration_ms,
171
- duration_seconds=duration_seconds,
172
- error_message=error_message or "No response generated",
173
- exc_info=True,
174
- )
175
- except Exception as log_error:
176
- # If logging fails, don't crash the app
177
- # Use print as a last resort to indicate the issue
178
- print(f"Failed to write access log: {log_error}")
179
-
180
- return response
@@ -1,297 +0,0 @@
1
- """Request content logging middleware for capturing full HTTP request/response data."""
2
-
3
- import json
4
- from collections.abc import AsyncGenerator
5
- from typing import Any
6
-
7
- import structlog
8
- from fastapi import Request, Response
9
- from fastapi.responses import StreamingResponse
10
- from starlette.middleware.base import BaseHTTPMiddleware
11
- from starlette.types import ASGIApp
12
-
13
- from ccproxy.utils.simple_request_logger import (
14
- append_streaming_log,
15
- write_request_log,
16
- )
17
-
18
-
19
- logger = structlog.get_logger(__name__)
20
-
21
-
22
- class RequestContentLoggingMiddleware(BaseHTTPMiddleware):
23
- """Middleware for logging full HTTP request and response content."""
24
-
25
- def __init__(self, app: ASGIApp):
26
- """Initialize the request content logging middleware.
27
-
28
- Args:
29
- app: The ASGI application
30
- """
31
- super().__init__(app)
32
-
33
- async def dispatch(self, request: Request, call_next: Any) -> Any:
34
- """Process the request and log content.
35
-
36
- Args:
37
- request: The incoming HTTP request
38
- call_next: The next middleware/handler in the chain
39
-
40
- Returns:
41
- The HTTP response
42
- """
43
- # Get request ID and timestamp from context if available
44
- request_id = self._get_request_id(request)
45
- timestamp = self._get_timestamp_prefix(request)
46
-
47
- # Log incoming request
48
- await self._log_request(request, request_id, timestamp)
49
-
50
- # Process the request
51
- response = await call_next(request)
52
-
53
- # Log outgoing response
54
- await self._log_response(response, request_id, timestamp)
55
-
56
- return response
57
-
58
- def _get_request_id(self, request: Request) -> str:
59
- """Extract request ID from request state or context.
60
-
61
- Args:
62
- request: The HTTP request
63
-
64
- Returns:
65
- Request ID string or 'unknown' if not found
66
- """
67
- try:
68
- # Try to get from request state
69
- if hasattr(request.state, "request_id"):
70
- return str(request.state.request_id)
71
-
72
- # Try to get from request context
73
- if hasattr(request.state, "context"):
74
- context = request.state.context
75
- if hasattr(context, "request_id"):
76
- return str(context.request_id)
77
-
78
- # Fallback to UUID if available in headers
79
- if "x-request-id" in request.headers:
80
- return request.headers["x-request-id"]
81
-
82
- except Exception:
83
- pass # Ignore errors and use fallback
84
-
85
- return "unknown"
86
-
87
- def _get_timestamp_prefix(self, request: Request) -> str | None:
88
- """Extract timestamp prefix from request context.
89
-
90
- Args:
91
- request: The HTTP request
92
-
93
- Returns:
94
- Timestamp prefix string or None if not found
95
- """
96
- try:
97
- # Try to get from request context
98
- if hasattr(request.state, "context"):
99
- context = request.state.context
100
- if hasattr(context, "get_log_timestamp_prefix"):
101
- result = context.get_log_timestamp_prefix()
102
- return str(result) if result is not None else None
103
- except Exception:
104
- pass # Ignore errors and use fallback
105
-
106
- return None
107
-
108
- async def _log_request(
109
- self, request: Request, request_id: str, timestamp: str | None
110
- ) -> None:
111
- """Log incoming HTTP request content.
112
-
113
- Args:
114
- request: The HTTP request
115
- request_id: Request identifier
116
- timestamp: Timestamp prefix for the log file
117
- """
118
- try:
119
- # Read request body
120
- body = await request.body()
121
-
122
- # Create request log data
123
- request_data = {
124
- "method": request.method,
125
- "url": str(request.url),
126
- "headers": dict(request.headers),
127
- "query_params": dict(request.query_params),
128
- "path_params": dict(request.path_params)
129
- if hasattr(request, "path_params")
130
- else {},
131
- "body_size": len(body) if body else 0,
132
- "body": None,
133
- }
134
-
135
- # Try to parse body as JSON, fallback to string
136
- if body:
137
- try:
138
- request_data["body"] = json.loads(body.decode("utf-8"))
139
- except (json.JSONDecodeError, UnicodeDecodeError):
140
- try:
141
- request_data["body"] = body.decode("utf-8", errors="replace")
142
- except Exception:
143
- request_data["body"] = f"<binary data of length {len(body)}>"
144
-
145
- await write_request_log(
146
- request_id=request_id,
147
- log_type="middleware_request",
148
- data=request_data,
149
- timestamp=timestamp,
150
- )
151
-
152
- except Exception as e:
153
- logger.error(
154
- "failed_to_log_request_content",
155
- request_id=request_id,
156
- error=str(e),
157
- )
158
-
159
- async def _log_response(
160
- self, response: Response, request_id: str, timestamp: str | None
161
- ) -> None:
162
- """Log outgoing HTTP response content.
163
-
164
- Args:
165
- response: The HTTP response
166
- request_id: Request identifier
167
- timestamp: Timestamp prefix for the log file
168
- """
169
- try:
170
- if isinstance(response, StreamingResponse):
171
- # Handle streaming response
172
- await self._log_streaming_response(response, request_id, timestamp)
173
- else:
174
- # Handle regular response
175
- await self._log_regular_response(response, request_id, timestamp)
176
-
177
- except Exception as e:
178
- logger.error(
179
- "failed_to_log_response_content",
180
- request_id=request_id,
181
- error=str(e),
182
- )
183
-
184
- async def _log_regular_response(
185
- self, response: Response, request_id: str, timestamp: str | None
186
- ) -> None:
187
- """Log regular (non-streaming) HTTP response.
188
-
189
- Args:
190
- response: The HTTP response
191
- request_id: Request identifier
192
- timestamp: Timestamp prefix for the log file
193
- """
194
- # Create response log data
195
- response_data = {
196
- "status_code": response.status_code,
197
- "headers": dict(response.headers),
198
- "body": None,
199
- }
200
-
201
- # Try to get response body
202
- if hasattr(response, "body") and response.body:
203
- body = response.body
204
- response_data["body_size"] = len(body)
205
-
206
- try:
207
- # Convert to bytes if needed
208
- body_bytes = bytes(body) if isinstance(body, memoryview) else body
209
- # Try to parse as JSON
210
- response_data["body"] = json.loads(body_bytes.decode("utf-8"))
211
- except (json.JSONDecodeError, UnicodeDecodeError):
212
- try:
213
- # Fallback to string
214
- body_bytes = bytes(body) if isinstance(body, memoryview) else body
215
- response_data["body"] = body_bytes.decode("utf-8", errors="replace")
216
- except Exception:
217
- response_data["body"] = f"<binary data of length {len(body)}>"
218
- else:
219
- response_data["body_size"] = 0
220
-
221
- await write_request_log(
222
- request_id=request_id,
223
- log_type="middleware_response",
224
- data=response_data,
225
- timestamp=timestamp,
226
- )
227
-
228
- async def _log_streaming_response(
229
- self, response: StreamingResponse, request_id: str, timestamp: str | None
230
- ) -> None:
231
- """Log streaming HTTP response by wrapping the stream.
232
-
233
- Args:
234
- response: The streaming HTTP response
235
- request_id: Request identifier
236
- timestamp: Timestamp prefix for the log file
237
- """
238
- # Log response metadata first
239
- response_data = {
240
- "status_code": response.status_code,
241
- "headers": dict(response.headers),
242
- "body_type": "streaming",
243
- "media_type": response.media_type,
244
- }
245
-
246
- await write_request_log(
247
- request_id=request_id,
248
- log_type="middleware_response",
249
- data=response_data,
250
- timestamp=timestamp,
251
- )
252
-
253
- # Wrap the streaming response to capture content
254
- original_body_iterator = response.body_iterator
255
-
256
- def create_logged_body_iterator() -> AsyncGenerator[
257
- str | bytes | memoryview[int], None
258
- ]:
259
- """Create wrapper around the original body iterator to log streaming content."""
260
-
261
- async def logged_body_iterator() -> AsyncGenerator[
262
- str | bytes | memoryview[int], None
263
- ]:
264
- try:
265
- async for chunk in original_body_iterator:
266
- # Log chunk as raw data
267
- if isinstance(chunk, bytes | bytearray):
268
- await append_streaming_log(
269
- request_id=request_id,
270
- log_type="middleware_streaming",
271
- data=bytes(chunk),
272
- timestamp=timestamp,
273
- )
274
- elif isinstance(chunk, str):
275
- await append_streaming_log(
276
- request_id=request_id,
277
- log_type="middleware_streaming",
278
- data=chunk.encode("utf-8"),
279
- timestamp=timestamp,
280
- )
281
-
282
- yield chunk
283
-
284
- except Exception as e:
285
- logger.error(
286
- "error_in_streaming_response_logging",
287
- request_id=request_id,
288
- error=str(e),
289
- )
290
- # Continue with original iterator if logging fails
291
- async for chunk in original_body_iterator:
292
- yield chunk
293
-
294
- return logged_body_iterator()
295
-
296
- # Replace the body iterator with our logged version
297
- response.body_iterator = create_logged_body_iterator()
@@ -1,58 +0,0 @@
1
- """Server header middleware to set a default server header for non-proxy routes."""
2
-
3
- from starlette.types import ASGIApp, Message, Receive, Scope, Send
4
-
5
-
6
- class ServerHeaderMiddleware:
7
- """Middleware to set a default server header for responses.
8
-
9
- This middleware adds a server header to responses that don't already have one.
10
- Proxy responses using ProxyResponse will preserve their upstream server header,
11
- while other routes will get the default header.
12
- """
13
-
14
- def __init__(self, app: ASGIApp, server_name: str = "Claude Code Proxy"):
15
- """Initialize the server header middleware.
16
-
17
- Args:
18
- app: The ASGI application
19
- server_name: The default server name to use
20
- """
21
- self.app = app
22
- self.server_name = server_name
23
-
24
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
25
- """ASGI application entrypoint."""
26
- if scope["type"] != "http":
27
- await self.app(scope, receive, send)
28
- return
29
-
30
- async def send_wrapper(message: Message) -> None:
31
- if message["type"] == "http.response.start":
32
- headers = list(message.get("headers", []))
33
-
34
- # Check if server header already exists
35
- has_server = any(header[0].lower() == b"server" for header in headers)
36
-
37
- # Only add server header for non-proxy routes
38
- # Proxy routes will have their server header preserved from upstream
39
- if not has_server:
40
- # Check if this looks like a proxy response by looking for specific headers
41
- is_proxy_response = any(
42
- header[0].lower()
43
- in [
44
- b"cf-ray",
45
- b"cf-cache-status",
46
- b"anthropic-ratelimit-unified-status",
47
- ]
48
- for header in headers
49
- )
50
-
51
- # Only add our server header if this is NOT a proxy response
52
- if not is_proxy_response:
53
- headers.append((b"server", self.server_name.encode()))
54
- message["headers"] = headers
55
-
56
- await send(message)
57
-
58
- await self.app(scope, receive, send_wrapper)
ccproxy/api/responses.py DELETED
@@ -1,89 +0,0 @@
1
- """Custom response classes for preserving proxy headers."""
2
-
3
- from typing import Any
4
-
5
- from fastapi import Response
6
- from starlette.types import Receive, Scope, Send
7
-
8
-
9
- class ProxyResponse(Response):
10
- """Custom response class that preserves all headers from upstream API.
11
-
12
- This response class ensures that headers like 'server' from the upstream
13
- API are preserved and not overridden by Uvicorn/Starlette.
14
- """
15
-
16
- def __init__(
17
- self,
18
- content: Any = None,
19
- status_code: int = 200,
20
- headers: dict[str, str] | None = None,
21
- media_type: str | None = None,
22
- background: Any = None,
23
- ):
24
- """Initialize the proxy response with preserved headers.
25
-
26
- Args:
27
- content: Response content
28
- status_code: HTTP status code
29
- headers: Headers to preserve from upstream
30
- media_type: Content type
31
- background: Background task
32
- """
33
- super().__init__(
34
- content=content,
35
- status_code=status_code,
36
- headers=headers,
37
- media_type=media_type,
38
- background=background,
39
- )
40
- # Store original headers for preservation
41
- self._preserve_headers = headers or {}
42
-
43
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
44
- """Override the ASGI call to ensure headers are preserved.
45
-
46
- This method intercepts the response sending process to ensure
47
- that our headers are not overridden by the server.
48
- """
49
- # Ensure we include all original headers, including 'server'
50
- headers_list = []
51
- seen_headers = set()
52
-
53
- # Add all headers from the response, but skip content-length
54
- # as we'll recalculate it based on actual body
55
- for name, value in self._preserve_headers.items():
56
- lower_name = name.lower()
57
- # Skip content-length and transfer-encoding as we'll set them correctly
58
- if (
59
- lower_name not in ["content-length", "transfer-encoding"]
60
- and lower_name not in seen_headers
61
- ):
62
- headers_list.append((lower_name.encode(), value.encode()))
63
- seen_headers.add(lower_name)
64
-
65
- # Always set correct content-length based on actual body
66
- if self.body:
67
- headers_list.append((b"content-length", str(len(self.body)).encode()))
68
- else:
69
- headers_list.append((b"content-length", b"0"))
70
-
71
- # Ensure we have content-type
72
- has_content_type = any(h[0] == b"content-type" for h in headers_list)
73
- if not has_content_type and self.media_type:
74
- headers_list.append((b"content-type", self.media_type.encode()))
75
-
76
- await send(
77
- {
78
- "type": "http.response.start",
79
- "status": self.status_code,
80
- "headers": headers_list,
81
- }
82
- )
83
-
84
- await send(
85
- {
86
- "type": "http.response.body",
87
- "body": self.body,
88
- }
89
- )