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
@@ -0,0 +1,30 @@
1
+ """
2
+ Application bootstrapping and dependency injection container setup.
3
+
4
+ This module is responsible for the initial setup of the application's core services,
5
+ including configuration loading and service container initialization. It acts as the
6
+ main entry point for assembling the application's components before the web server
7
+ starts.
8
+ """
9
+
10
+ from ccproxy.config.settings import Settings
11
+ from ccproxy.services.container import ServiceContainer
12
+
13
+
14
+ def create_service_container(settings: Settings | None = None) -> ServiceContainer:
15
+ """
16
+ Create and configure the service container.
17
+
18
+ Args:
19
+ settings: Optional pre-loaded settings instance. If not provided,
20
+ settings will be loaded from config files/environment.
21
+
22
+ Returns:
23
+ The initialized service container.
24
+ """
25
+ if settings is None:
26
+ settings = Settings.from_config()
27
+
28
+ container = ServiceContainer(settings)
29
+
30
+ return container
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from collections.abc import Awaitable, Callable
6
+ from functools import wraps
7
+ from typing import ParamSpec, TypeVar
8
+
9
+ from fastapi import Request
10
+
11
+ from ccproxy.core.logging import get_logger as _get_logger
12
+ from ccproxy.core.request_context import RequestContext
13
+
14
+
15
+ P = ParamSpec("P")
16
+ R = TypeVar("R")
17
+
18
+
19
+ def format_chain(
20
+ *formats: str,
21
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
22
+ """Existing simple decorator to attach a format chain to a route handler.
23
+
24
+ This attaches a __format_chain__ attribute used by validation and helpers.
25
+ """
26
+
27
+ def decorator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
28
+ func.__format_chain__ = list(formats) # type: ignore[attr-defined]
29
+
30
+ @wraps(func)
31
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
32
+ return await func(*args, **kwargs)
33
+
34
+ return wrapper
35
+
36
+ return decorator
37
+
38
+
39
+ def with_format_chain(
40
+ formats: list[str], *, endpoint: str | None = None
41
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
42
+ """Decorator to set format chain and optional endpoint metadata on a route.
43
+
44
+ - Attaches __format_chain__ to the endpoint for upstream processing/validation
45
+ - Ensures request.state.context exists and sets context.format_chain
46
+ - Optionally sets context.metadata["endpoint"] to the provided upstream endpoint path
47
+ """
48
+
49
+ def decorator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
50
+ func.__format_chain__ = list(formats) # type: ignore[attr-defined]
51
+
52
+ @wraps(func)
53
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
54
+ # Find Request in args/kwargs
55
+ request: Request | None = None
56
+ for arg in args:
57
+ if isinstance(arg, Request):
58
+ request = arg
59
+ break
60
+ if request is None:
61
+ req = kwargs.get("request")
62
+ if isinstance(req, Request):
63
+ request = req
64
+
65
+ if request is not None:
66
+ # Ensure a context exists
67
+ if (
68
+ not hasattr(request.state, "context")
69
+ or request.state.context is None
70
+ ):
71
+ request.state.context = RequestContext(
72
+ request_id=str(uuid.uuid4()),
73
+ start_time=time.perf_counter(),
74
+ logger=_get_logger(__name__),
75
+ )
76
+ # Set chain and endpoint metadata
77
+ request.state.context.format_chain = list(formats)
78
+ if endpoint:
79
+ request.state.context.metadata["endpoint"] = endpoint
80
+
81
+ return await func(*args, **kwargs)
82
+
83
+ return wrapper
84
+
85
+ return decorator
@@ -2,214 +2,190 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Annotated
5
+ from collections.abc import Callable
6
+ from typing import TYPE_CHECKING, Annotated, Any, TypeVar
6
7
 
7
- from fastapi import Depends, Request
8
- from structlog import get_logger
8
+ import httpx
9
+ from fastapi import Depends, HTTPException, Request
9
10
 
10
- from ccproxy.config.settings import Settings, get_settings
11
- from ccproxy.core.http import BaseProxyClient
12
- from ccproxy.observability import PrometheusMetrics, get_metrics
13
- from ccproxy.observability.storage.duckdb_simple import SimpleDuckDBStorage
14
- from ccproxy.services.claude_sdk_service import ClaudeSDKService
15
- from ccproxy.services.credentials.manager import CredentialsManager
16
- from ccproxy.services.proxy_service import ProxyService
11
+ from ccproxy.config.settings import Settings
12
+ from ccproxy.core.logging import get_logger
13
+ from ccproxy.core.plugins import PluginRegistry, ProviderPluginRuntime
14
+ from ccproxy.core.plugins.hooks import HookManager
15
+ from ccproxy.services.adapters.base import BaseAdapter
16
+ from ccproxy.services.container import ServiceContainer
17
17
 
18
18
 
19
+ if TYPE_CHECKING:
20
+ pass
21
+
19
22
  logger = get_logger(__name__)
20
23
 
24
+ T = TypeVar("T")
21
25
 
22
- def get_cached_settings(request: Request) -> Settings:
23
- """Get cached settings from app state.
24
26
 
25
- This avoids recomputing settings on every request by using the
26
- settings instance computed during application startup.
27
+ def get_service(service_type: type[T]) -> Callable[[Request], T]:
28
+ """Return a dependency callable that fetches a service from the container."""
27
29
 
28
- Args:
29
- request: FastAPI request object
30
+ def _get_service(request: Request) -> T:
31
+ """Get a service from the container."""
32
+ container: ServiceContainer | None = getattr(
33
+ request.app.state, "service_container", None
34
+ )
35
+ if container is None:
36
+ logger.error(
37
+ "service_container_missing_on_app_state",
38
+ category="lifecycle",
39
+ )
40
+ raise HTTPException(
41
+ status_code=503, detail="Service container not initialized"
42
+ )
43
+ return container.get_service(service_type)
30
44
 
31
- Returns:
32
- Settings instance from app state
45
+ return _get_service
33
46
 
34
- Raises:
35
- RuntimeError: If settings are not available in app state
36
- """
37
- settings = getattr(request.app.state, "settings", None)
38
- if settings is None:
39
- # Fallback to get_settings() for safety, but this should not happen
40
- # in normal operation after lifespan startup
41
- logger.warning(
42
- "Settings not found in app state, falling back to get_settings()"
43
- )
44
- settings = get_settings()
45
- return settings
46
47
 
48
+ def _resolve_service_container(request: Request) -> ServiceContainer | None:
49
+ """Resolve a service container from the request or global context."""
47
50
 
48
- def get_cached_claude_service(request: Request) -> ClaudeSDKService:
49
- """Get cached ClaudeSDKService from app state.
51
+ container: ServiceContainer | None = getattr(
52
+ request.app.state, "service_container", None
53
+ )
54
+ if container is not None:
55
+ return container
56
+
57
+ try:
58
+ return ServiceContainer.get_current(strict=False)
59
+ except RuntimeError:
60
+ # Should not happen with strict=False but guard defensively
61
+ return None
50
62
 
51
- This avoids recreating the ClaudeSDKService on every request by using the
52
- service instance created during application startup.
53
63
 
54
- Args:
55
- request: FastAPI request object
64
+ def get_cached_settings(request: Request) -> Settings:
65
+ """Get cached settings from app state.
66
+
67
+ Raises a 503 HTTPException if no service container is available,
68
+ preserving the existing behaviour for required dependencies.
69
+ """
70
+ return get_service(Settings)(request)
71
+
56
72
 
57
- Returns:
58
- ClaudeSDKService instance from app state
73
+ def get_optional_settings(request: Request) -> Settings | None:
74
+ """Best-effort retrieval of settings for optional dependencies.
59
75
 
60
- Raises:
61
- RuntimeError: If ClaudeSDKService is not available in app state
76
+ Returns a Settings instance if a service container is available.
77
+ Falls back to a new Settings object (with defaults) when running in
78
+ lightweight test contexts where the container is not initialised.
62
79
  """
63
- claude_service = getattr(request.app.state, "claude_service", None)
64
- if claude_service is None:
65
- # Fallback to get_claude_service() for safety, but this should not happen
66
- # in normal operation after lifespan startup
80
+
81
+ container = _resolve_service_container(request)
82
+ if container is not None:
83
+ try:
84
+ return container.get_service(Settings)
85
+ except ValueError:
86
+ logger.debug(
87
+ "settings_not_registered_in_container",
88
+ category="config",
89
+ )
90
+
91
+ try:
92
+ return Settings()
93
+ except Exception as exc: # pragma: no cover - defensive guard
67
94
  logger.warning(
68
- "ClaudeSDKService not found in app state, falling back to get_claude_service()"
95
+ "optional_settings_initialization_failed",
96
+ error=str(exc),
97
+ category="config",
69
98
  )
70
- # Get dependencies manually for fallback
71
- settings = get_cached_settings(request)
99
+ return None
72
100
 
73
- claude_service = get_claude_service(settings)
74
- return claude_service
75
101
 
102
+ async def get_http_client(request: Request) -> httpx.AsyncClient:
103
+ """Get container-managed HTTP client from the service container."""
104
+ return get_service(httpx.AsyncClient)(request)
76
105
 
77
- # Type aliases for dependency injection
78
- SettingsDep = Annotated[Settings, Depends(get_cached_settings)]
79
106
 
107
+ def get_hook_manager(request: Request) -> HookManager:
108
+ """Get HookManager from the service container.
80
109
 
81
- def get_claude_service(
82
- settings: SettingsDep,
83
- ) -> ClaudeSDKService:
84
- """Get Claude SDK service instance.
110
+ This dependency is required; if the hook system has not been initialized
111
+ the request will fail with 503 to reflect misconfigured startup order.
112
+ """
113
+ return get_service(HookManager)(request)
85
114
 
86
- Args:
87
- settings: Application settings dependency
88
115
 
89
- Returns:
90
- Claude SDK service instance
91
- """
92
- logger.debug("Creating Claude SDK service instance")
93
- # Get global metrics instance
94
- metrics = get_metrics()
95
-
96
- # Check if pooling should be enabled from configuration
97
- use_pool = settings.claude.sdk_session_pool.enabled
98
- session_manager = None
99
-
100
- if use_pool:
101
- logger.info(
102
- "claude_sdk_pool_enabled",
103
- message="Using Claude SDK client pooling for improved performance",
104
- pool_size=settings.claude.sdk_session_pool.max_sessions,
105
- max_pool_size=settings.claude.sdk_session_pool.max_sessions,
106
- )
107
- # Note: Session manager should be created in the lifespan function, not here
108
- # This dependency function should not create stateful resources
116
+ def get_plugin_adapter(plugin_name: str) -> Any:
117
+ """Create a dependency function for a specific plugin's adapter."""
109
118
 
110
- return ClaudeSDKService(
111
- metrics=metrics,
112
- settings=settings,
113
- session_manager=session_manager,
114
- )
119
+ def _get_adapter(request: Request) -> BaseAdapter:
120
+ """Get adapter for the specified plugin."""
121
+ if not hasattr(request.app.state, "plugin_registry"):
122
+ raise HTTPException(
123
+ status_code=503, detail="Plugin registry not initialized"
124
+ )
115
125
 
126
+ registry: PluginRegistry = request.app.state.plugin_registry
127
+ runtime = registry.get_runtime(plugin_name)
116
128
 
117
- def get_credentials_manager(
118
- settings: SettingsDep,
119
- ) -> CredentialsManager:
120
- """Get credentials manager instance.
129
+ if not runtime:
130
+ raise HTTPException(
131
+ status_code=503, detail=f"Plugin {plugin_name} not initialized"
132
+ )
121
133
 
122
- Args:
123
- settings: Application settings dependency
134
+ if not isinstance(runtime, ProviderPluginRuntime):
135
+ raise HTTPException(
136
+ status_code=503, detail=f"Plugin {plugin_name} is not a provider plugin"
137
+ )
124
138
 
125
- Returns:
126
- Credentials manager instance
127
- """
128
- logger.debug("Creating credentials manager instance")
129
- return CredentialsManager(config=settings.auth)
130
-
131
-
132
- def get_proxy_service(
133
- request: Request,
134
- settings: SettingsDep,
135
- credentials_manager: Annotated[
136
- CredentialsManager, Depends(get_credentials_manager)
137
- ],
138
- ) -> ProxyService:
139
- """Get proxy service instance.
140
-
141
- Args:
142
- request: FastAPI request object (for app state access)
143
- settings: Application settings dependency
144
- credentials_manager: Credentials manager dependency
145
-
146
- Returns:
147
- Proxy service instance
148
- """
149
- logger.debug("get_proxy_service")
150
- # Create HTTP client for proxy
151
- from ccproxy.core.http import HTTPXClient
152
-
153
- http_client = HTTPXClient()
154
- proxy_client = BaseProxyClient(http_client)
155
-
156
- # Get global metrics instance
157
- metrics = get_metrics()
158
-
159
- return ProxyService(
160
- proxy_client=proxy_client,
161
- credentials_manager=credentials_manager,
162
- settings=settings,
163
- proxy_mode="full",
164
- target_base_url=settings.reverse_proxy.target_url,
165
- metrics=metrics,
166
- app_state=request.app.state, # Pass app state for detection data access
167
- )
139
+ if not runtime.adapter:
140
+ raise HTTPException(
141
+ status_code=503, detail=f"Plugin {plugin_name} adapter not available"
142
+ )
168
143
 
144
+ adapter: BaseAdapter = runtime.adapter
145
+ return adapter
169
146
 
170
- def get_observability_metrics() -> PrometheusMetrics:
171
- """Get observability metrics instance.
147
+ return _get_adapter
172
148
 
173
- Returns:
174
- PrometheusMetrics instance
175
- """
176
- logger.debug("get_observability_metrics")
177
- return get_metrics()
178
149
 
150
+ def get_provider_config_dependency(
151
+ plugin_name: str, config_type: type[T]
152
+ ) -> Callable[[Request], T]:
153
+ """Return a dependency that fetches a provider plugin's validated config."""
179
154
 
180
- async def get_log_storage(request: Request) -> SimpleDuckDBStorage | None:
181
- """Get log storage from app state.
155
+ def _get_config(request: Request) -> T:
156
+ if not hasattr(request.app.state, "plugin_registry"):
157
+ raise HTTPException(
158
+ status_code=503, detail="Plugin registry not initialized"
159
+ )
182
160
 
183
- Args:
184
- request: FastAPI request object
161
+ registry: PluginRegistry = request.app.state.plugin_registry
162
+ runtime = registry.get_runtime(plugin_name)
185
163
 
186
- Returns:
187
- SimpleDuckDBStorage instance if available, None otherwise
188
- """
189
- return getattr(request.app.state, "log_storage", None)
164
+ if not runtime or not isinstance(runtime, ProviderPluginRuntime):
165
+ raise HTTPException(
166
+ status_code=503, detail=f"Plugin {plugin_name} not initialized"
167
+ )
190
168
 
169
+ context = getattr(runtime, "context", None)
170
+ if not context:
171
+ raise HTTPException(
172
+ status_code=503,
173
+ detail=f"Plugin {plugin_name} configuration unavailable",
174
+ )
191
175
 
192
- async def get_duckdb_storage(request: Request) -> SimpleDuckDBStorage | None:
193
- """Get DuckDB storage from app state (backward compatibility).
176
+ try:
177
+ config = context.get(config_type)
178
+ return config # type: ignore[no-any-return]
179
+ except ValueError as exc: # pragma: no cover - defensive guard
180
+ raise HTTPException(
181
+ status_code=503,
182
+ detail=f"Plugin {plugin_name} configuration not loaded",
183
+ ) from exc
194
184
 
195
- Args:
196
- request: FastAPI request object
185
+ return _get_config
197
186
 
198
- Returns:
199
- SimpleDuckDBStorage instance if available, None otherwise
200
- """
201
- # Try new name first, then fall back to old name for backward compatibility
202
- storage = getattr(request.app.state, "log_storage", None)
203
- if storage is None:
204
- storage = getattr(request.app.state, "duckdb_storage", None)
205
- return storage
206
-
207
-
208
- # Type aliases for service dependencies
209
- ClaudeServiceDep = Annotated[ClaudeSDKService, Depends(get_cached_claude_service)]
210
- ProxyServiceDep = Annotated[ProxyService, Depends(get_proxy_service)]
211
- ObservabilityMetricsDep = Annotated[
212
- PrometheusMetrics, Depends(get_observability_metrics)
213
- ]
214
- LogStorageDep = Annotated[SimpleDuckDBStorage | None, Depends(get_log_storage)]
215
- DuckDBStorageDep = Annotated[SimpleDuckDBStorage | None, Depends(get_duckdb_storage)]
187
+
188
+ SettingsDep = Annotated[Settings, Depends(get_cached_settings)]
189
+ OptionalSettingsDep = Annotated[Settings | None, Depends(get_optional_settings)]
190
+ HTTPClientDep = Annotated[httpx.AsyncClient, Depends(get_http_client)]
191
+ HookManagerDep = Annotated[HookManager, Depends(get_hook_manager)]
@@ -0,0 +1,54 @@
1
+ """Utilities for validating format adapter chains during application startup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import FastAPI
6
+
7
+ from ccproxy.core.logging import TraceBoundLogger
8
+ from ccproxy.services.adapters.chain_validation import (
9
+ validate_chains,
10
+ validate_stream_pairs,
11
+ )
12
+ from ccproxy.services.adapters.format_registry import FormatRegistry
13
+
14
+
15
+ def collect_declared_format_chains(app: FastAPI) -> list[list[str]]:
16
+ """Collect declared format chains from FastAPI routes."""
17
+
18
+ chains: list[list[str]] = []
19
+ for route in app.router.routes:
20
+ endpoint = getattr(route, "endpoint", None)
21
+ chain = getattr(endpoint, "__format_chain__", None)
22
+ if chain:
23
+ chains.append(chain)
24
+ return chains
25
+
26
+
27
+ def validate_route_format_chains(
28
+ *,
29
+ app: FastAPI,
30
+ registry: FormatRegistry,
31
+ logger: TraceBoundLogger,
32
+ ) -> None:
33
+ """Validate format chains declared on routes against the format registry."""
34
+
35
+ try:
36
+ declared_chains = collect_declared_format_chains(app)
37
+ if not declared_chains:
38
+ return
39
+
40
+ missing = validate_chains(registry=registry, chains=declared_chains)
41
+ missing_stream = validate_stream_pairs(
42
+ registry=registry, chains=declared_chains
43
+ )
44
+ if missing or missing_stream:
45
+ logger.error(
46
+ "format_chain_validation_failed",
47
+ missing_adapters=missing,
48
+ missing_stream_adapters=missing_stream,
49
+ )
50
+ except Exception as exc: # pragma: no cover - defensive logging path
51
+ logger.warning("format_registry_setup_skipped", error=str(exc))
52
+
53
+
54
+ __all__ = ["validate_route_format_chains", "collect_declared_format_chains"]
@@ -4,9 +4,9 @@ from typing import Any
4
4
 
5
5
  from fastapi import FastAPI
6
6
  from fastapi.middleware.cors import CORSMiddleware
7
- from structlog import get_logger
8
7
 
9
8
  from ccproxy.config.settings import Settings
9
+ from ccproxy.core.logging import get_logger
10
10
 
11
11
 
12
12
  logger = get_logger(__name__)
@@ -19,7 +19,6 @@ def setup_cors_middleware(app: FastAPI, settings: Settings) -> None:
19
19
  app: FastAPI application instance
20
20
  settings: Application settings containing CORS configuration
21
21
  """
22
- logger.debug("cors_middleware_setup_start")
23
22
 
24
23
  app.add_middleware(
25
24
  CORSMiddleware,
@@ -32,7 +31,11 @@ def setup_cors_middleware(app: FastAPI, settings: Settings) -> None:
32
31
  max_age=settings.cors.max_age,
33
32
  )
34
33
 
35
- logger.debug("cors_middleware_configured", origins=settings.cors.origins)
34
+ logger.debug(
35
+ "cors_middleware_configured",
36
+ origins=settings.cors.origins,
37
+ category="middleware",
38
+ )
36
39
 
37
40
 
38
41
  def get_cors_config(settings: Settings) -> dict[str, Any]: