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
@@ -1,10 +1,19 @@
1
1
  """Error handling middleware for CCProxy API Server."""
2
2
 
3
+ import traceback
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any
6
+
3
7
  from fastapi import FastAPI, HTTPException, Request
8
+ from fastapi.exceptions import RequestValidationError
4
9
  from fastapi.responses import JSONResponse
5
10
  from starlette.exceptions import HTTPException as StarletteHTTPException
6
- from structlog import get_logger
7
11
 
12
+ from ccproxy.core.constants import (
13
+ FORMAT_ANTHROPIC_MESSAGES,
14
+ FORMAT_OPENAI_CHAT,
15
+ FORMAT_OPENAI_RESPONSES,
16
+ )
8
17
  from ccproxy.core.errors import (
9
18
  AuthenticationError,
10
19
  ClaudeProxyError,
@@ -23,552 +32,314 @@ from ccproxy.core.errors import (
23
32
  TransformationError,
24
33
  ValidationError,
25
34
  )
26
- from ccproxy.observability.metrics import get_metrics
35
+ from ccproxy.core.logging import get_logger
36
+ from ccproxy.llms.models import anthropic as anthropic_models
37
+ from ccproxy.llms.models import openai as openai_models
27
38
 
28
39
 
29
40
  logger = get_logger(__name__)
30
41
 
31
42
 
32
- def setup_error_handlers(app: FastAPI) -> None:
33
- """Setup error handlers for the FastAPI application.
43
+ def _detect_format_from_path(path: str) -> str | None:
44
+ """Detect the expected format from the request path.
34
45
 
35
46
  Args:
36
- app: FastAPI application instance
37
- """
38
- logger.debug("error_handlers_setup_start")
39
-
40
- # Get metrics instance for error recording
41
- try:
42
- metrics = get_metrics()
43
- logger.debug("error_handlers_metrics_loaded")
44
- except Exception as e:
45
- logger.warning("error_handlers_metrics_unavailable", error=str(e))
46
- metrics = None
47
-
48
- @app.exception_handler(ClaudeProxyError)
49
- async def claude_proxy_error_handler(
50
- request: Request, exc: ClaudeProxyError
51
- ) -> JSONResponse:
52
- """Handle Claude proxy specific errors."""
53
- # Store status code in request state for access logging
54
- if hasattr(request.state, "context") and hasattr(
55
- request.state.context, "metadata"
56
- ):
57
- request.state.context.metadata["status_code"] = exc.status_code
47
+ path: Request URL path
58
48
 
59
- logger.error(
60
- "Claude proxy error",
61
- error_type="claude_proxy_error",
62
- error_message=str(exc),
63
- status_code=exc.status_code,
64
- request_method=request.method,
65
- request_url=str(request.url.path),
66
- )
67
-
68
- # Record error in metrics
69
- if metrics:
70
- metrics.record_error(
71
- error_type="claude_proxy_error",
72
- endpoint=str(request.url.path),
73
- model=None,
74
- service_type="middleware",
75
- )
76
- return JSONResponse(
77
- status_code=exc.status_code,
78
- content={
79
- "error": {
80
- "type": exc.error_type,
81
- "message": str(exc),
82
- }
83
- },
84
- )
85
-
86
- @app.exception_handler(ValidationError)
87
- async def validation_error_handler(
88
- request: Request, exc: ValidationError
89
- ) -> JSONResponse:
90
- """Handle validation errors."""
91
- # Store status code in request state for access logging
92
- if hasattr(request.state, "context") and hasattr(
93
- request.state.context, "metadata"
94
- ):
95
- request.state.context.metadata["status_code"] = 400
96
-
97
- logger.error(
98
- "Validation error",
99
- error_type="validation_error",
100
- error_message=str(exc),
101
- status_code=400,
102
- request_method=request.method,
103
- request_url=str(request.url.path),
104
- )
105
-
106
- # Record error in metrics
107
- if metrics:
108
- metrics.record_error(
109
- error_type="validation_error",
110
- endpoint=str(request.url.path),
111
- model=None,
112
- service_type="middleware",
113
- )
114
- return JSONResponse(
115
- status_code=400,
116
- content={
117
- "error": {
118
- "type": "validation_error",
119
- "message": str(exc),
120
- }
121
- },
122
- )
49
+ Returns:
50
+ Detected format or None if cannot determine
51
+ """
52
+ if "/chat/completions" in path:
53
+ return FORMAT_OPENAI_CHAT
54
+ elif "/messages" in path:
55
+ return FORMAT_ANTHROPIC_MESSAGES
56
+ elif "/responses" in path:
57
+ return FORMAT_OPENAI_RESPONSES
58
+ return None
123
59
 
124
- @app.exception_handler(AuthenticationError)
125
- async def authentication_error_handler(
126
- request: Request, exc: AuthenticationError
127
- ) -> JSONResponse:
128
- """Handle authentication errors."""
129
- logger.error(
130
- "Authentication error",
131
- error_type="authentication_error",
132
- error_message=str(exc),
133
- status_code=401,
134
- request_method=request.method,
135
- request_url=str(request.url.path),
136
- client_ip=request.client.host if request.client else "unknown",
137
- user_agent=request.headers.get("user-agent", "unknown"),
138
- )
139
60
 
140
- # Record error in metrics
141
- if metrics:
142
- metrics.record_error(
143
- error_type="authentication_error",
144
- endpoint=str(request.url.path),
145
- model=None,
146
- service_type="middleware",
147
- )
148
- return JSONResponse(
149
- status_code=401,
150
- content={
151
- "error": {
152
- "type": "authentication_error",
153
- "message": str(exc),
154
- }
155
- },
156
- )
61
+ def _get_format_aware_error_content(
62
+ error_type: str, message: str, status_code: int, base_format: str | None
63
+ ) -> dict[str, Any]:
64
+ """Create format-aware error response content using proper models.
157
65
 
158
- @app.exception_handler(PermissionError)
159
- async def permission_error_handler(
160
- request: Request, exc: PermissionError
161
- ) -> JSONResponse:
162
- """Handle permission errors."""
163
- logger.error(
164
- "Permission error",
165
- error_type="permission_error",
166
- error_message=str(exc),
167
- status_code=403,
168
- request_method=request.method,
169
- request_url=str(request.url.path),
170
- client_ip=request.client.host if request.client else "unknown",
171
- )
172
-
173
- # Record error in metrics
174
- if metrics:
175
- metrics.record_error(
176
- error_type="permission_error",
177
- endpoint=str(request.url.path),
178
- model=None,
179
- service_type="middleware",
180
- )
181
- return JSONResponse(
182
- status_code=403,
183
- content={
184
- "error": {
185
- "type": "permission_error",
186
- "message": str(exc),
187
- }
188
- },
189
- )
66
+ Args:
67
+ error_type: Type of error for logging
68
+ message: Error message
69
+ status_code: HTTP status code
70
+ base_format: Base format from format_chain[0]
190
71
 
191
- @app.exception_handler(NotFoundError)
192
- async def not_found_error_handler(
193
- request: Request, exc: NotFoundError
194
- ) -> JSONResponse:
195
- """Handle not found errors."""
196
- logger.error(
197
- "Not found error",
198
- error_type="not_found_error",
199
- error_message=str(exc),
200
- status_code=404,
201
- request_method=request.method,
202
- request_url=str(request.url.path),
203
- )
72
+ Returns:
73
+ Formatted error response content using proper models
74
+ """
75
+ # Default CCProxy format
76
+ default_content = {
77
+ "error": {
78
+ "type": error_type,
79
+ "message": message,
80
+ }
81
+ }
204
82
 
205
- # Record error in metrics
206
- if metrics:
207
- metrics.record_error(
208
- error_type="not_found_error",
209
- endpoint=str(request.url.path),
210
- model=None,
211
- service_type="middleware",
83
+ try:
84
+ if base_format in {FORMAT_OPENAI_CHAT, FORMAT_OPENAI_RESPONSES}:
85
+ # Use OpenAI error model
86
+ error_detail = openai_models.ErrorDetail(
87
+ message=message,
88
+ type=error_type,
89
+ code=error_type
90
+ if base_format == FORMAT_OPENAI_RESPONSES
91
+ else str(status_code),
92
+ param=None,
212
93
  )
213
- return JSONResponse(
214
- status_code=404,
215
- content={
216
- "error": {
217
- "type": "not_found_error",
218
- "message": str(exc),
219
- }
220
- },
221
- )
94
+ error_response = openai_models.ErrorResponse(error=error_detail)
95
+ return error_response.model_dump()
222
96
 
223
- @app.exception_handler(RateLimitError)
224
- async def rate_limit_error_handler(
225
- request: Request, exc: RateLimitError
226
- ) -> JSONResponse:
227
- """Handle rate limit errors."""
228
- logger.error(
229
- "Rate limit error",
230
- error_type="rate_limit_error",
231
- error_message=str(exc),
232
- status_code=429,
233
- request_method=request.method,
234
- request_url=str(request.url.path),
235
- client_ip=request.client.host if request.client else "unknown",
236
- )
237
-
238
- # Record error in metrics
239
- if metrics:
240
- metrics.record_error(
241
- error_type="rate_limit_error",
242
- endpoint=str(request.url.path),
243
- model=None,
244
- service_type="middleware",
245
- )
246
- return JSONResponse(
247
- status_code=429,
248
- content={
249
- "error": {
250
- "type": "rate_limit_error",
251
- "message": str(exc),
252
- }
253
- },
254
- )
97
+ elif base_format == FORMAT_ANTHROPIC_MESSAGES:
98
+ # Use Anthropic error model
99
+ # APIError has a fixed type field, so create a generic ErrorDetail instead
100
+ api_error = anthropic_models.ErrorDetail(message=message)
101
+ # Anthropic error format has 'type': 'error' at top level
102
+ return {"type": "error", "error": api_error.model_dump()}
255
103
 
256
- @app.exception_handler(ModelNotFoundError)
257
- async def model_not_found_error_handler(
258
- request: Request, exc: ModelNotFoundError
259
- ) -> JSONResponse:
260
- """Handle model not found errors."""
261
- logger.error(
262
- "Model not found error",
263
- error_type="model_not_found_error",
264
- error_message=str(exc),
265
- status_code=404,
266
- request_method=request.method,
267
- request_url=str(request.url.path),
104
+ except Exception as e:
105
+ # Log the error but don't fail - fallback to default format
106
+ logger.warning(
107
+ "format_aware_error_creation_failed",
108
+ base_format=base_format,
109
+ error_type=error_type,
110
+ fallback_reason=str(e),
111
+ category="middleware",
268
112
  )
269
113
 
270
- # Record error in metrics
271
- if metrics:
272
- metrics.record_error(
273
- error_type="model_not_found_error",
274
- endpoint=str(request.url.path),
275
- model=None,
276
- service_type="middleware",
277
- )
278
- return JSONResponse(
279
- status_code=404,
280
- content={
281
- "error": {
282
- "type": "model_not_found_error",
283
- "message": str(exc),
284
- }
285
- },
286
- )
114
+ # Fallback to default format
115
+ return default_content
287
116
 
288
- @app.exception_handler(TimeoutError)
289
- async def timeout_error_handler(
290
- request: Request, exc: TimeoutError
291
- ) -> JSONResponse:
292
- """Handle timeout errors."""
293
- logger.error(
294
- "Timeout error",
295
- error_type="timeout_error",
296
- error_message=str(exc),
297
- status_code=408,
298
- request_method=request.method,
299
- request_url=str(request.url.path),
300
- )
301
117
 
302
- # Record error in metrics
303
- if metrics:
304
- metrics.record_error(
305
- error_type="timeout_error",
306
- endpoint=str(request.url.path),
307
- model=None,
308
- service_type="middleware",
309
- )
310
- return JSONResponse(
311
- status_code=408,
312
- content={
313
- "error": {
314
- "type": "timeout_error",
315
- "message": str(exc),
316
- }
317
- },
318
- )
118
+ def setup_error_handlers(app: FastAPI) -> None:
119
+ """Setup error handlers for the FastAPI application.
319
120
 
320
- @app.exception_handler(ServiceUnavailableError)
321
- async def service_unavailable_error_handler(
322
- request: Request, exc: ServiceUnavailableError
121
+ Args:
122
+ app: FastAPI application instance
123
+ """
124
+ logger.debug("error_handlers_setup_start", category="lifecycle")
125
+
126
+ # Metrics are now handled by the metrics plugin via hooks
127
+ metrics = None
128
+
129
+ # Define error type mappings with status codes and error types
130
+ ERROR_MAPPINGS: dict[type[Exception], tuple[int | None, str]] = {
131
+ ClaudeProxyError: (None, "claude_proxy_error"), # Uses exc.status_code
132
+ ValidationError: (400, "validation_error"),
133
+ AuthenticationError: (401, "authentication_error"),
134
+ ProxyAuthenticationError: (401, "proxy_authentication_error"),
135
+ PermissionError: (403, "permission_error"),
136
+ NotFoundError: (404, "not_found_error"),
137
+ ModelNotFoundError: (404, "model_not_found_error"),
138
+ TimeoutError: (408, "timeout_error"),
139
+ RateLimitError: (429, "rate_limit_error"),
140
+ ProxyError: (500, "proxy_error"),
141
+ TransformationError: (500, "transformation_error"),
142
+ MiddlewareError: (500, "middleware_error"),
143
+ DockerError: (500, "docker_error"),
144
+ ProxyConnectionError: (502, "proxy_connection_error"),
145
+ ServiceUnavailableError: (503, "service_unavailable_error"),
146
+ ProxyTimeoutError: (504, "proxy_timeout_error"),
147
+ }
148
+
149
+ async def unified_error_handler(
150
+ request: Request,
151
+ exc: Exception,
152
+ status_code: int | None = None,
153
+ error_type: str | None = None,
154
+ include_client_info: bool = False,
323
155
  ) -> JSONResponse:
324
- """Handle service unavailable errors."""
325
- logger.error(
326
- "Service unavailable error",
327
- error_type="service_unavailable_error",
328
- error_message=str(exc),
329
- status_code=503,
330
- request_method=request.method,
331
- request_url=str(request.url.path),
332
- )
156
+ """Unified error handler for all exception types.
333
157
 
334
- # Record error in metrics
335
- if metrics:
336
- metrics.record_error(
337
- error_type="service_unavailable_error",
338
- endpoint=str(request.url.path),
339
- model=None,
340
- service_type="middleware",
341
- )
342
- return JSONResponse(
343
- status_code=503,
344
- content={
345
- "error": {
346
- "type": "service_unavailable_error",
347
- "message": str(exc),
348
- }
349
- },
350
- )
158
+ Args:
159
+ request: The incoming request
160
+ exc: The exception that was raised
161
+ status_code: HTTP status code to return
162
+ error_type: Type of error for logging and response
163
+ include_client_info: Whether to include client IP in logs
164
+ """
165
+ # Get status code from exception if it has one
166
+ if status_code is None:
167
+ status_code = getattr(exc, "status_code", 500)
351
168
 
352
- @app.exception_handler(DockerError)
353
- async def docker_error_handler(request: Request, exc: DockerError) -> JSONResponse:
354
- """Handle Docker errors."""
355
- logger.error(
356
- "Docker error",
357
- error_type="docker_error",
358
- error_message=str(exc),
359
- status_code=500,
360
- request_method=request.method,
361
- request_url=str(request.url.path),
362
- )
169
+ # Determine error type if not provided
170
+ if error_type is None:
171
+ error_type = getattr(exc, "error_type", "unknown_error")
363
172
 
364
- # Record error in metrics
365
- if metrics:
366
- metrics.record_error(
367
- error_type="docker_error",
368
- endpoint=str(request.url.path),
369
- model=None,
370
- service_type="middleware",
371
- )
372
- return JSONResponse(
373
- status_code=500,
374
- content={
375
- "error": {
376
- "type": "docker_error",
377
- "message": str(exc),
378
- }
379
- },
380
- )
381
-
382
- # Core proxy errors
383
- @app.exception_handler(ProxyError)
384
- async def proxy_error_handler(request: Request, exc: ProxyError) -> JSONResponse:
385
- """Handle proxy errors."""
386
- logger.error(
387
- "Proxy error",
388
- error_type="proxy_error",
389
- error_message=str(exc),
390
- status_code=500,
391
- request_method=request.method,
392
- request_url=str(request.url.path),
173
+ # Get request ID from request state or headers
174
+ request_id = getattr(request.state, "request_id", None) or request.headers.get(
175
+ "x-request-id"
393
176
  )
394
177
 
395
- # Record error in metrics
396
- if metrics:
397
- metrics.record_error(
398
- error_type="proxy_error",
399
- endpoint=str(request.url.path),
400
- model=None,
401
- service_type="middleware",
402
- )
403
- return JSONResponse(
404
- status_code=500,
405
- content={
406
- "error": {
407
- "type": "proxy_error",
408
- "message": str(exc),
409
- }
410
- },
411
- )
412
-
413
- @app.exception_handler(TransformationError)
414
- async def transformation_error_handler(
415
- request: Request, exc: TransformationError
416
- ) -> JSONResponse:
417
- """Handle transformation errors."""
178
+ # Store status code in request state for access logging
179
+ if hasattr(request.state, "context") and hasattr(
180
+ request.state.context, "metadata"
181
+ ):
182
+ request.state.context.metadata["status_code"] = status_code
183
+
184
+ # Build log kwargs
185
+ log_kwargs = {
186
+ "error_type": error_type,
187
+ "error_message": str(exc),
188
+ "status_code": status_code,
189
+ "request_method": request.method,
190
+ "request_url": str(request.url.path),
191
+ }
192
+
193
+ # Add client info if needed (for auth errors)
194
+ if include_client_info and request.client:
195
+ log_kwargs["client_ip"] = request.client.host
196
+ if error_type in ("authentication_error", "proxy_authentication_error"):
197
+ log_kwargs["user_agent"] = request.headers.get("user-agent", "unknown")
198
+
199
+ # Log the error
418
200
  logger.error(
419
- "Transformation error",
420
- error_type="transformation_error",
421
- error_message=str(exc),
422
- status_code=500,
423
- request_method=request.method,
424
- request_url=str(request.url.path),
201
+ f"{error_type.replace('_', ' ').title()}",
202
+ **log_kwargs,
203
+ category="middleware",
425
204
  )
426
205
 
427
206
  # Record error in metrics
428
207
  if metrics:
429
208
  metrics.record_error(
430
- error_type="transformation_error",
431
- endpoint=str(request.url.path),
432
- model=None,
433
- service_type="middleware",
434
- )
435
- return JSONResponse(
436
- status_code=500,
437
- content={
438
- "error": {
439
- "type": "transformation_error",
440
- "message": str(exc),
441
- }
442
- },
443
- )
444
-
445
- @app.exception_handler(MiddlewareError)
446
- async def middleware_error_handler(
447
- request: Request, exc: MiddlewareError
448
- ) -> JSONResponse:
449
- """Handle middleware errors."""
450
- logger.error(
451
- "Middleware error",
452
- error_type="middleware_error",
453
- error_message=str(exc),
454
- status_code=500,
455
- request_method=request.method,
456
- request_url=str(request.url.path),
457
- )
458
-
459
- # Record error in metrics
460
- if metrics:
461
- metrics.record_error(
462
- error_type="middleware_error",
209
+ error_type=error_type,
463
210
  endpoint=str(request.url.path),
464
211
  model=None,
465
212
  service_type="middleware",
466
213
  )
467
- return JSONResponse(
468
- status_code=500,
469
- content={
470
- "error": {
471
- "type": "middleware_error",
472
- "message": str(exc),
473
- }
474
- },
475
- )
476
214
 
477
- @app.exception_handler(ProxyConnectionError)
478
- async def proxy_connection_error_handler(
479
- request: Request, exc: ProxyConnectionError
480
- ) -> JSONResponse:
481
- """Handle proxy connection errors."""
482
- logger.error(
483
- "Proxy connection error",
484
- error_type="proxy_connection_error",
485
- error_message=str(exc),
486
- status_code=502,
487
- request_method=request.method,
488
- request_url=str(request.url.path),
489
- )
490
-
491
- # Record error in metrics
492
- if metrics:
493
- metrics.record_error(
494
- error_type="proxy_connection_error",
495
- endpoint=str(request.url.path),
496
- model=None,
497
- service_type="middleware",
498
- )
215
+ # Prepare headers with x-request-id if available
216
+ headers = {}
217
+ if request_id:
218
+ headers["x-request-id"] = request_id
219
+
220
+ # Detect format from request context for format-aware error responses
221
+ base_format = None
222
+ try:
223
+ if hasattr(request.state, "context") and hasattr(
224
+ request.state.context, "format_chain"
225
+ ):
226
+ format_chain = request.state.context.format_chain
227
+ if format_chain and len(format_chain) > 0:
228
+ base_format = format_chain[
229
+ 0
230
+ ] # First format is the client's expected format
231
+ logger.debug(
232
+ "format_aware_error_detected",
233
+ base_format=base_format,
234
+ format_chain=format_chain,
235
+ category="middleware",
236
+ )
237
+ except Exception as e:
238
+ logger.debug("format_detection_failed", error=str(e), category="middleware")
239
+
240
+ # Get format-aware error content
241
+ error_content = _get_format_aware_error_content(
242
+ error_type=error_type,
243
+ message=str(exc),
244
+ status_code=status_code,
245
+ base_format=base_format,
246
+ )
247
+
248
+ # Return JSON response with format-aware content
499
249
  return JSONResponse(
500
- status_code=502,
501
- content={
502
- "error": {
503
- "type": "proxy_connection_error",
504
- "message": str(exc),
505
- }
506
- },
507
- )
508
-
509
- @app.exception_handler(ProxyTimeoutError)
510
- async def proxy_timeout_error_handler(
511
- request: Request, exc: ProxyTimeoutError
250
+ status_code=status_code,
251
+ content=error_content,
252
+ headers=headers,
253
+ )
254
+
255
+ # Register specific error handlers using the unified handler
256
+ for exc_class, (status, err_type) in ERROR_MAPPINGS.items():
257
+ # Determine if this error type should include client info
258
+ include_client = err_type in (
259
+ "authentication_error",
260
+ "proxy_authentication_error",
261
+ "permission_error",
262
+ "rate_limit_error",
263
+ )
264
+
265
+ # Create a closure to capture the specific error configuration
266
+ def make_handler(
267
+ status_code: int | None, error_type: str, include_client_info: bool
268
+ ) -> Callable[[Request, Exception], Awaitable[JSONResponse]]:
269
+ async def handler(request: Request, exc: Exception) -> JSONResponse:
270
+ return await unified_error_handler(
271
+ request, exc, status_code, error_type, include_client_info
272
+ )
273
+
274
+ return handler
275
+
276
+ # Register the handler
277
+ app.exception_handler(exc_class)(make_handler(status, err_type, include_client))
278
+
279
+ # FastAPI validation errors
280
+ @app.exception_handler(RequestValidationError)
281
+ async def validation_exception_handler(
282
+ request: Request, exc: RequestValidationError
512
283
  ) -> JSONResponse:
513
- """Handle proxy timeout errors."""
514
- logger.error(
515
- "Proxy timeout error",
516
- error_type="proxy_timeout_error",
517
- error_message=str(exc),
518
- status_code=504,
284
+ """Handle FastAPI request validation errors with format awareness."""
285
+ # Get request ID from request state or headers
286
+ request_id = getattr(request.state, "request_id", None) or request.headers.get(
287
+ "x-request-id"
288
+ )
289
+
290
+ # Try to get format from request context (set by middleware)
291
+ base_format = None
292
+ try:
293
+ if hasattr(request.state, "context") and hasattr(
294
+ request.state.context, "format_chain"
295
+ ):
296
+ format_chain = request.state.context.format_chain
297
+ if format_chain and len(format_chain) > 0:
298
+ base_format = format_chain[0]
299
+ except Exception:
300
+ pass # Fallback to path detection if needed
301
+
302
+ # Fallback: detect format from path if context isn't available
303
+ if base_format is None:
304
+ base_format = _detect_format_from_path(str(request.url.path))
305
+
306
+ # Create a readable error message from validation errors
307
+ error_details = []
308
+ for error in exc.errors():
309
+ loc = " -> ".join(str(x) for x in error["loc"])
310
+ error_details.append(f"{loc}: {error['msg']}")
311
+
312
+ error_message = "; ".join(error_details)
313
+
314
+ # Log the validation error
315
+ logger.warning(
316
+ "Request validation error",
317
+ error_type="validation_error",
318
+ error_message=error_message,
319
+ status_code=422,
519
320
  request_method=request.method,
520
321
  request_url=str(request.url.path),
322
+ base_format=base_format,
323
+ category="middleware",
521
324
  )
522
325
 
523
- # Record error in metrics
524
- if metrics:
525
- metrics.record_error(
526
- error_type="proxy_timeout_error",
527
- endpoint=str(request.url.path),
528
- model=None,
529
- service_type="middleware",
530
- )
531
- return JSONResponse(
532
- status_code=504,
533
- content={
534
- "error": {
535
- "type": "proxy_timeout_error",
536
- "message": str(exc),
537
- }
538
- },
539
- )
326
+ # Prepare headers with x-request-id if available
327
+ headers = {}
328
+ if request_id:
329
+ headers["x-request-id"] = request_id
540
330
 
541
- @app.exception_handler(ProxyAuthenticationError)
542
- async def proxy_authentication_error_handler(
543
- request: Request, exc: ProxyAuthenticationError
544
- ) -> JSONResponse:
545
- """Handle proxy authentication errors."""
546
- logger.error(
547
- "Proxy authentication error",
548
- error_type="proxy_authentication_error",
549
- error_message=str(exc),
550
- status_code=401,
551
- request_method=request.method,
552
- request_url=str(request.url.path),
553
- client_ip=request.client.host if request.client else "unknown",
331
+ # Get format-aware error content
332
+ error_content = _get_format_aware_error_content(
333
+ error_type="validation_error",
334
+ message=error_message,
335
+ status_code=422,
336
+ base_format=base_format,
554
337
  )
555
338
 
556
- # Record error in metrics
557
- if metrics:
558
- metrics.record_error(
559
- error_type="proxy_authentication_error",
560
- endpoint=str(request.url.path),
561
- model=None,
562
- service_type="middleware",
563
- )
564
339
  return JSONResponse(
565
- status_code=401,
566
- content={
567
- "error": {
568
- "type": "proxy_authentication_error",
569
- "message": str(exc),
570
- }
571
- },
340
+ status_code=422,
341
+ content=error_content,
342
+ headers=headers,
572
343
  )
573
344
 
574
345
  # Standard HTTP exceptions
@@ -577,28 +348,32 @@ def setup_error_handlers(app: FastAPI) -> None:
577
348
  request: Request, exc: HTTPException
578
349
  ) -> JSONResponse:
579
350
  """Handle HTTP exceptions."""
351
+ # Get request ID from request state or headers
352
+ request_id = getattr(request.state, "request_id", None) or request.headers.get(
353
+ "x-request-id"
354
+ )
355
+
580
356
  # Store status code in request state for access logging
581
357
  if hasattr(request.state, "context") and hasattr(
582
358
  request.state.context, "metadata"
583
359
  ):
584
360
  request.state.context.metadata["status_code"] = exc.status_code
585
361
 
586
- # Don't log stack trace for 404 errors as they're expected
587
- if exc.status_code == 404:
588
- logger.debug(
589
- "HTTP 404 error",
590
- error_type="http_404",
362
+ # Don't log stack trace for expected errors (404, 401)
363
+ if exc.status_code in (404, 401):
364
+ log_func = logger.debug if exc.status_code == 404 else logger.warning
365
+
366
+ log_func(
367
+ f"HTTP {exc.status_code} error",
368
+ error_type=f"http_{exc.status_code}",
591
369
  error_message=exc.detail,
592
- status_code=404,
370
+ status_code=exc.status_code,
593
371
  request_method=request.method,
594
372
  request_url=str(request.url.path),
373
+ category="middleware",
595
374
  )
596
375
  else:
597
376
  # Log with basic stack trace (no local variables)
598
- stack_trace = None
599
- # For structlog, we can always include traceback since structlog handles filtering
600
- import traceback
601
-
602
377
  stack_trace = traceback.format_exc(limit=5) # Limit to 5 frames
603
378
 
604
379
  logger.error(
@@ -609,11 +384,17 @@ def setup_error_handlers(app: FastAPI) -> None:
609
384
  request_method=request.method,
610
385
  request_url=str(request.url.path),
611
386
  stack_trace=stack_trace,
387
+ category="middleware",
612
388
  )
613
389
 
614
390
  # Record error in metrics
615
391
  if metrics:
616
- error_type = "http_404" if exc.status_code == 404 else "http_error"
392
+ if exc.status_code == 404:
393
+ error_type = "http_404"
394
+ elif exc.status_code == 401:
395
+ error_type = "http_401"
396
+ else:
397
+ error_type = "http_error"
617
398
  metrics.record_error(
618
399
  error_type=error_type,
619
400
  endpoint=str(request.url.path),
@@ -621,15 +402,43 @@ def setup_error_handlers(app: FastAPI) -> None:
621
402
  service_type="middleware",
622
403
  )
623
404
 
624
- # TODO: Add when in prod hide details in response
405
+ # Prepare headers with x-request-id if available
406
+ headers = {}
407
+ if request_id:
408
+ headers["x-request-id"] = request_id
409
+
410
+ # Detect format from request context for format-aware error responses
411
+ base_format = None
412
+ try:
413
+ if hasattr(request.state, "context") and hasattr(
414
+ request.state.context, "format_chain"
415
+ ):
416
+ format_chain = request.state.context.format_chain
417
+ if format_chain and len(format_chain) > 0:
418
+ base_format = format_chain[0]
419
+ except Exception:
420
+ pass # Ignore format detection errors
421
+
422
+ # Determine error type for format-aware response
423
+ if exc.status_code == 404:
424
+ error_type = "not_found"
425
+ elif exc.status_code == 401:
426
+ error_type = "authentication_error"
427
+ else:
428
+ error_type = "http_error"
429
+
430
+ # Get format-aware error content
431
+ error_content = _get_format_aware_error_content(
432
+ error_type=error_type,
433
+ message=exc.detail,
434
+ status_code=exc.status_code,
435
+ base_format=base_format,
436
+ )
437
+
625
438
  return JSONResponse(
626
439
  status_code=exc.status_code,
627
- content={
628
- "error": {
629
- "type": "http_error",
630
- "message": exc.detail,
631
- }
632
- },
440
+ content=error_content,
441
+ headers=headers,
633
442
  )
634
443
 
635
444
  @app.exception_handler(StarletteHTTPException)
@@ -637,6 +446,11 @@ def setup_error_handlers(app: FastAPI) -> None:
637
446
  request: Request, exc: StarletteHTTPException
638
447
  ) -> JSONResponse:
639
448
  """Handle Starlette HTTP exceptions."""
449
+ # Get request ID from request state or headers
450
+ request_id = getattr(request.state, "request_id", None) or request.headers.get(
451
+ "x-request-id"
452
+ )
453
+
640
454
  # Don't log stack trace for 404 errors as they're expected
641
455
  if exc.status_code == 404:
642
456
  logger.debug(
@@ -646,6 +460,7 @@ def setup_error_handlers(app: FastAPI) -> None:
646
460
  status_code=404,
647
461
  request_method=request.method,
648
462
  request_url=str(request.url.path),
463
+ category="middleware",
649
464
  )
650
465
  else:
651
466
  logger.error(
@@ -655,6 +470,7 @@ def setup_error_handlers(app: FastAPI) -> None:
655
470
  status_code=exc.status_code,
656
471
  request_method=request.method,
657
472
  request_url=str(request.url.path),
473
+ category="middleware",
658
474
  )
659
475
 
660
476
  # Record error in metrics
@@ -670,14 +486,42 @@ def setup_error_handlers(app: FastAPI) -> None:
670
486
  model=None,
671
487
  service_type="middleware",
672
488
  )
489
+
490
+ # Prepare headers with x-request-id if available
491
+ headers = {}
492
+ if request_id:
493
+ headers["x-request-id"] = request_id
494
+
495
+ # Detect format from request context for format-aware error responses
496
+ base_format = None
497
+ try:
498
+ if hasattr(request.state, "context") and hasattr(
499
+ request.state.context, "format_chain"
500
+ ):
501
+ format_chain = request.state.context.format_chain
502
+ if format_chain and len(format_chain) > 0:
503
+ base_format = format_chain[0]
504
+ except Exception:
505
+ pass # Ignore format detection errors
506
+
507
+ # Determine error type for format-aware response
508
+ if exc.status_code == 404:
509
+ error_type = "not_found"
510
+ else:
511
+ error_type = "http_error"
512
+
513
+ # Get format-aware error content
514
+ error_content = _get_format_aware_error_content(
515
+ error_type=error_type,
516
+ message=exc.detail,
517
+ status_code=exc.status_code,
518
+ base_format=base_format,
519
+ )
520
+
673
521
  return JSONResponse(
674
522
  status_code=exc.status_code,
675
- content={
676
- "error": {
677
- "type": "http_error",
678
- "message": exc.detail,
679
- }
680
- },
523
+ content=error_content,
524
+ headers=headers,
681
525
  )
682
526
 
683
527
  # Global exception handler
@@ -686,6 +530,11 @@ def setup_error_handlers(app: FastAPI) -> None:
686
530
  request: Request, exc: Exception
687
531
  ) -> JSONResponse:
688
532
  """Handle all other unhandled exceptions."""
533
+ # Get request ID from request state or headers
534
+ request_id = getattr(request.state, "request_id", None) or request.headers.get(
535
+ "x-request-id"
536
+ )
537
+
689
538
  # Store status code in request state for access logging
690
539
  if hasattr(request.state, "context") and hasattr(
691
540
  request.state.context, "metadata"
@@ -700,6 +549,7 @@ def setup_error_handlers(app: FastAPI) -> None:
700
549
  request_method=request.method,
701
550
  request_url=str(request.url.path),
702
551
  exc_info=True,
552
+ category="middleware",
703
553
  )
704
554
 
705
555
  # Record error in metrics
@@ -710,14 +560,36 @@ def setup_error_handlers(app: FastAPI) -> None:
710
560
  model=None,
711
561
  service_type="middleware",
712
562
  )
563
+
564
+ # Prepare headers with x-request-id if available
565
+ headers = {}
566
+ if request_id:
567
+ headers["x-request-id"] = request_id
568
+
569
+ # Detect format from request context for format-aware error responses
570
+ base_format = None
571
+ try:
572
+ if hasattr(request.state, "context") and hasattr(
573
+ request.state.context, "format_chain"
574
+ ):
575
+ format_chain = request.state.context.format_chain
576
+ if format_chain and len(format_chain) > 0:
577
+ base_format = format_chain[0]
578
+ except Exception:
579
+ pass # Ignore format detection errors
580
+
581
+ # Get format-aware error content for internal server error
582
+ error_content = _get_format_aware_error_content(
583
+ error_type="internal_server_error",
584
+ message="An internal server error occurred",
585
+ status_code=500,
586
+ base_format=base_format,
587
+ )
588
+
713
589
  return JSONResponse(
714
590
  status_code=500,
715
- content={
716
- "error": {
717
- "type": "internal_server_error",
718
- "message": "An internal server error occurred",
719
- }
720
- },
591
+ content=error_content,
592
+ headers=headers,
721
593
  )
722
594
 
723
- logger.debug("error_handlers_setup_completed")
595
+ logger.debug("error_handlers_setup_completed", category="lifecycle")