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,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
58
-
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
- )
47
+ path: Request URL path
67
48
 
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
- )
123
-
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
- )
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
139
59
 
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
- )
157
60
 
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
- )
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.
172
65
 
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
- )
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()}
237
103
 
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
- },
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",
254
112
  )
255
113
 
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),
268
- )
114
+ # Fallback to default format
115
+ return default_content
269
116
 
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
- )
287
-
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
- },
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"
380
176
  )
381
177
 
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),
393
- )
394
-
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."""
418
- 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),
425
- )
426
-
427
- # Record error in metrics
428
- if metrics:
429
- 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."""
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
450
200
  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),
201
+ f"{error_type.replace('_', ' ').title()}",
202
+ **log_kwargs,
203
+ category="middleware",
457
204
  )
458
205
 
459
206
  # Record error in metrics
460
207
  if metrics:
461
208
  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,6 +348,11 @@ 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"
@@ -585,7 +361,6 @@ def setup_error_handlers(app: FastAPI) -> None:
585
361
 
586
362
  # Don't log stack trace for expected errors (404, 401)
587
363
  if exc.status_code in (404, 401):
588
- log_level = "debug" if exc.status_code == 404 else "warning"
589
364
  log_func = logger.debug if exc.status_code == 404 else logger.warning
590
365
 
591
366
  log_func(
@@ -595,13 +370,10 @@ def setup_error_handlers(app: FastAPI) -> None:
595
370
  status_code=exc.status_code,
596
371
  request_method=request.method,
597
372
  request_url=str(request.url.path),
373
+ category="middleware",
598
374
  )
599
375
  else:
600
376
  # Log with basic stack trace (no local variables)
601
- stack_trace = None
602
- # For structlog, we can always include traceback since structlog handles filtering
603
- import traceback
604
-
605
377
  stack_trace = traceback.format_exc(limit=5) # Limit to 5 frames
606
378
 
607
379
  logger.error(
@@ -612,6 +384,7 @@ def setup_error_handlers(app: FastAPI) -> None:
612
384
  request_method=request.method,
613
385
  request_url=str(request.url.path),
614
386
  stack_trace=stack_trace,
387
+ category="middleware",
615
388
  )
616
389
 
617
390
  # Record error in metrics
@@ -629,15 +402,43 @@ def setup_error_handlers(app: FastAPI) -> None:
629
402
  service_type="middleware",
630
403
  )
631
404
 
632
- # 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
+
633
438
  return JSONResponse(
634
439
  status_code=exc.status_code,
635
- content={
636
- "error": {
637
- "type": "http_error",
638
- "message": exc.detail,
639
- }
640
- },
440
+ content=error_content,
441
+ headers=headers,
641
442
  )
642
443
 
643
444
  @app.exception_handler(StarletteHTTPException)
@@ -645,6 +446,11 @@ def setup_error_handlers(app: FastAPI) -> None:
645
446
  request: Request, exc: StarletteHTTPException
646
447
  ) -> JSONResponse:
647
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
+
648
454
  # Don't log stack trace for 404 errors as they're expected
649
455
  if exc.status_code == 404:
650
456
  logger.debug(
@@ -654,6 +460,7 @@ def setup_error_handlers(app: FastAPI) -> None:
654
460
  status_code=404,
655
461
  request_method=request.method,
656
462
  request_url=str(request.url.path),
463
+ category="middleware",
657
464
  )
658
465
  else:
659
466
  logger.error(
@@ -663,6 +470,7 @@ def setup_error_handlers(app: FastAPI) -> None:
663
470
  status_code=exc.status_code,
664
471
  request_method=request.method,
665
472
  request_url=str(request.url.path),
473
+ category="middleware",
666
474
  )
667
475
 
668
476
  # Record error in metrics
@@ -678,14 +486,42 @@ def setup_error_handlers(app: FastAPI) -> None:
678
486
  model=None,
679
487
  service_type="middleware",
680
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
+
681
521
  return JSONResponse(
682
522
  status_code=exc.status_code,
683
- content={
684
- "error": {
685
- "type": "http_error",
686
- "message": exc.detail,
687
- }
688
- },
523
+ content=error_content,
524
+ headers=headers,
689
525
  )
690
526
 
691
527
  # Global exception handler
@@ -694,6 +530,11 @@ def setup_error_handlers(app: FastAPI) -> None:
694
530
  request: Request, exc: Exception
695
531
  ) -> JSONResponse:
696
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
+
697
538
  # Store status code in request state for access logging
698
539
  if hasattr(request.state, "context") and hasattr(
699
540
  request.state.context, "metadata"
@@ -708,6 +549,7 @@ def setup_error_handlers(app: FastAPI) -> None:
708
549
  request_method=request.method,
709
550
  request_url=str(request.url.path),
710
551
  exc_info=True,
552
+ category="middleware",
711
553
  )
712
554
 
713
555
  # Record error in metrics
@@ -718,14 +560,36 @@ def setup_error_handlers(app: FastAPI) -> None:
718
560
  model=None,
719
561
  service_type="middleware",
720
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
+
721
589
  return JSONResponse(
722
590
  status_code=500,
723
- content={
724
- "error": {
725
- "type": "internal_server_error",
726
- "message": "An internal server error occurred",
727
- }
728
- },
591
+ content=error_content,
592
+ headers=headers,
729
593
  )
730
594
 
731
- logger.debug("error_handlers_setup_completed")
595
+ logger.debug("error_handlers_setup_completed", category="lifecycle")