ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1231
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/api/app.py CHANGED
@@ -1,59 +1,85 @@
1
- """FastAPI application factory for CCProxy API Server."""
1
+ """FastAPI application factory for CCProxy API Server with plugin system."""
2
2
 
3
3
  from collections.abc import AsyncGenerator, Awaitable, Callable
4
4
  from contextlib import asynccontextmanager
5
- from typing import Any, TypedDict
5
+ from enum import Enum
6
+ from typing import Any
6
7
 
7
- from fastapi import APIRouter, FastAPI
8
- from fastapi.staticfiles import StaticFiles
9
- from structlog import get_logger
8
+ import structlog
9
+ from fastapi import FastAPI
10
+ from fastapi.routing import APIRouter
11
+ from typing_extensions import TypedDict
10
12
 
11
- from ccproxy import __version__
13
+ from ccproxy.api.bootstrap import create_service_container
14
+ from ccproxy.api.format_validation import validate_route_format_chains
12
15
  from ccproxy.api.middleware.cors import setup_cors_middleware
13
16
  from ccproxy.api.middleware.errors import setup_error_handlers
14
- from ccproxy.api.middleware.logging import AccessLogMiddleware
15
- from ccproxy.api.middleware.request_content_logging import (
16
- RequestContentLoggingMiddleware,
17
- )
18
- from ccproxy.api.middleware.request_id import RequestIDMiddleware
19
- from ccproxy.api.middleware.server_header import ServerHeaderMiddleware
20
- from ccproxy.api.routes.claude import router as claude_router
21
- from ccproxy.api.routes.codex import router as codex_router
22
17
  from ccproxy.api.routes.health import router as health_router
23
- from ccproxy.api.routes.mcp import setup_mcp
24
- from ccproxy.api.routes.metrics import (
25
- dashboard_router,
26
- logs_router,
27
- prometheus_router,
18
+ from ccproxy.api.routes.plugins import router as plugins_router
19
+ from ccproxy.auth.oauth.router import oauth_router
20
+ from ccproxy.config.settings import Settings
21
+ from ccproxy.core import __version__
22
+ from ccproxy.core.async_task_manager import start_task_manager, stop_task_manager
23
+ from ccproxy.core.logging import TraceBoundLogger, get_logger, setup_logging
24
+ from ccproxy.core.plugins import (
25
+ MiddlewareManager,
26
+ PluginRegistry,
27
+ load_plugin_system,
28
+ setup_default_middleware,
28
29
  )
29
- from ccproxy.api.routes.permissions import router as permissions_router
30
- from ccproxy.api.routes.proxy import router as proxy_router
31
- from ccproxy.auth.oauth.routes import router as oauth_router
32
- from ccproxy.config.settings import Settings, get_settings
33
- from ccproxy.core.logging import setup_logging
34
- from ccproxy.utils.models_provider import get_models_list
30
+ from ccproxy.core.plugins.hooks import HookManager
31
+ from ccproxy.core.plugins.hooks.events import HookEvent
32
+ from ccproxy.services.container import ServiceContainer
35
33
  from ccproxy.utils.startup_helpers import (
36
34
  check_claude_cli_startup,
37
- check_codex_cli_startup,
38
- flush_streaming_batches_shutdown,
39
- initialize_claude_detection_startup,
40
- initialize_claude_sdk_startup,
41
- initialize_codex_detection_startup,
42
- initialize_log_storage_shutdown,
43
- initialize_log_storage_startup,
44
- initialize_permission_service_startup,
45
- setup_permission_service_shutdown,
35
+ check_version_updates_startup,
46
36
  setup_scheduler_shutdown,
47
37
  setup_scheduler_startup,
48
- setup_session_manager_shutdown,
49
- validate_authentication_startup,
50
38
  )
51
39
 
52
40
 
53
- logger = get_logger(__name__)
41
+ logger: TraceBoundLogger = get_logger()
42
+
43
+
44
+ def merge_router_tags(
45
+ router: APIRouter,
46
+ spec_tags: list[str] | None = None,
47
+ default_tags: list[str] | None = None,
48
+ ) -> list[str | Enum] | None:
49
+ """Merge router tags with spec tags, removing duplicates while preserving order.
50
+
51
+ Args:
52
+ router: FastAPI router instance
53
+ spec_tags: Tags from route specification
54
+ default_tags: Fallback tags if no other tags exist
55
+
56
+ Returns:
57
+ Deduplicated list of tags, or None if no tags
58
+ """
59
+ router_tags: list[str | Enum] = list(router.tags) if router.tags else []
60
+ spec_tags_list: list[str | Enum] = list(spec_tags) if spec_tags else []
61
+ default_tags_list: list[str | Enum] = list(default_tags) if default_tags else []
62
+
63
+ # Only use defaults if no other tags exist
64
+ if not router_tags and not spec_tags_list and default_tags_list:
65
+ return default_tags_list
66
+
67
+ # Merge all non-default tags and deduplicate
68
+ all_tags: list[str | Enum] = router_tags + spec_tags_list
69
+ if not all_tags:
70
+ return None
71
+
72
+ # Deduplicate by string value while preserving order
73
+ unique: list[str | Enum] = []
74
+ seen: set[str] = set()
75
+ for t in all_tags:
76
+ s = str(t)
77
+ if s not in seen:
78
+ seen.add(s)
79
+ unique.append(t)
80
+ return unique
54
81
 
55
82
 
56
- # Type definitions for lifecycle components
57
83
  class LifecycleComponent(TypedDict):
58
84
  name: str
59
85
  startup: Callable[[FastAPI, Any], Awaitable[None]] | None
@@ -69,37 +95,159 @@ class ShutdownComponent(TypedDict):
69
95
  shutdown: Callable[[FastAPI], Awaitable[None]] | None
70
96
 
71
97
 
72
- # Define lifecycle components for startup/shutdown organization
98
+ async def setup_task_manager_startup(app: FastAPI, settings: Settings) -> None:
99
+ """Start the async task manager."""
100
+ container: ServiceContainer = app.state.service_container
101
+ await start_task_manager(container=container)
102
+ logger.debug("task_manager_startup_completed", category="lifecycle")
103
+
104
+
105
+ async def setup_task_manager_shutdown(app: FastAPI) -> None:
106
+ """Stop the async task manager."""
107
+ container: ServiceContainer = app.state.service_container
108
+ await stop_task_manager(container=container)
109
+ logger.debug("task_manager_shutdown_completed", category="lifecycle")
110
+
111
+
112
+ async def setup_service_container_shutdown(app: FastAPI) -> None:
113
+ """Close the service container and its resources."""
114
+ if hasattr(app.state, "service_container"):
115
+ service_container = app.state.service_container
116
+ await service_container.shutdown()
117
+
118
+
119
+ async def initialize_plugins_startup(app: FastAPI, settings: Settings) -> None:
120
+ """Initialize plugins during startup (runtime phase)."""
121
+ if not settings.enable_plugins:
122
+ logger.info("plugin_system_disabled", category="lifecycle")
123
+ return
124
+
125
+ if not hasattr(app.state, "plugin_registry"):
126
+ logger.warning("plugin_registry_not_found", category="lifecycle")
127
+ return
128
+
129
+ plugin_registry: PluginRegistry = app.state.plugin_registry
130
+ service_container: ServiceContainer = app.state.service_container
131
+
132
+ hook_registry = service_container.get_hook_registry()
133
+ background_thread_manager = service_container.get_background_hook_thread_manager()
134
+ hook_manager = HookManager(hook_registry, background_thread_manager)
135
+ app.state.hook_registry = hook_registry
136
+ app.state.hook_manager = hook_manager
137
+ service_container.register_service(HookManager, instance=hook_manager)
138
+
139
+ # StreamingHandler now requires HookManager at construction via DI factory,
140
+ # so no post-hoc patching is needed here.
141
+
142
+ # Perform manifest population with access to http_pool_manager
143
+ # This allows plugins to modify their manifests during context creation
144
+ for plugin_name, factory in plugin_registry.factories.items():
145
+ try:
146
+ factory.create_context(service_container)
147
+ except Exception as e:
148
+ logger.warning(
149
+ "plugin_context_creation_failed",
150
+ plugin=plugin_name,
151
+ error=str(e),
152
+ exc_info=e,
153
+ category="plugin",
154
+ )
155
+ # Continue with other plugins
156
+
157
+ await plugin_registry.initialize_all(service_container)
158
+ # A consolidated summary is already emitted by PluginRegistry.initialize_all()
159
+
160
+
161
+ async def shutdown_plugins(app: FastAPI) -> None:
162
+ """Shutdown plugins."""
163
+ if hasattr(app.state, "plugin_registry"):
164
+ plugin_registry: PluginRegistry = app.state.plugin_registry
165
+ await plugin_registry.shutdown_all()
166
+ logger.debug("plugins_shutdown_completed", category="lifecycle")
167
+
168
+
169
+ async def shutdown_hook_system(app: FastAPI) -> None:
170
+ """Shutdown the hook system and background thread."""
171
+ try:
172
+ # Get hook manager from app state - it will shutdown its own background manager
173
+ hook_manager = getattr(app.state, "hook_manager", None)
174
+ if hook_manager:
175
+ hook_manager.shutdown()
176
+
177
+ logger.debug("hook_system_shutdown_completed", category="lifecycle")
178
+ except Exception as e:
179
+ logger.error(
180
+ "hook_system_shutdown_failed",
181
+ error=str(e),
182
+ category="lifecycle",
183
+ )
184
+
185
+
186
+ async def initialize_hooks_startup(app: FastAPI, settings: Settings) -> None:
187
+ """Initialize hook system with plugins."""
188
+ if hasattr(app.state, "hook_registry") and hasattr(app.state, "hook_manager"):
189
+ hook_registry = app.state.hook_registry
190
+ hook_manager = app.state.hook_manager
191
+ logger.debug("hook_system_already_created", category="lifecycle")
192
+ else:
193
+ service_container: ServiceContainer = app.state.service_container
194
+ hook_registry = service_container.get_hook_registry()
195
+ background_thread_manager = (
196
+ service_container.get_background_hook_thread_manager()
197
+ )
198
+ hook_manager = HookManager(hook_registry, background_thread_manager)
199
+ app.state.hook_registry = hook_registry
200
+ app.state.hook_manager = hook_manager
201
+
202
+ # Register plugin hooks
203
+ if hasattr(app.state, "plugin_registry"):
204
+ plugin_registry: PluginRegistry = app.state.plugin_registry
205
+
206
+ for name, factory in plugin_registry.factories.items():
207
+ manifest = factory.get_manifest()
208
+ for hook_spec in manifest.hooks:
209
+ try:
210
+ hook_instance = hook_spec.hook_class(**hook_spec.kwargs)
211
+ hook_registry.register(hook_instance)
212
+ logger.debug(
213
+ "plugin_hook_registered",
214
+ plugin_name=name,
215
+ hook_class=hook_spec.hook_class.__name__,
216
+ category="lifecycle",
217
+ )
218
+ except Exception as e:
219
+ logger.error(
220
+ "plugin_hook_registration_failed",
221
+ plugin_name=name,
222
+ hook_class=hook_spec.hook_class.__name__,
223
+ error=str(e),
224
+ exc_info=e,
225
+ category="lifecycle",
226
+ )
227
+
228
+ try:
229
+ await hook_manager.emit(HookEvent.APP_STARTUP, {"phase": "startup"})
230
+ except Exception as e:
231
+ logger.error(
232
+ "startup_hook_failed", error=str(e), exc_info=e, category="lifecycle"
233
+ )
234
+
235
+
73
236
  LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
74
237
  {
75
- "name": "Authentication",
76
- "startup": validate_authentication_startup,
77
- "shutdown": None, # One-time validation, no cleanup needed
78
- },
79
- {
80
- "name": "Claude CLI",
81
- "startup": check_claude_cli_startup,
82
- "shutdown": None, # Detection only, no cleanup needed
83
- },
84
- {
85
- "name": "Codex CLI",
86
- "startup": check_codex_cli_startup,
87
- "shutdown": None, # Detection only, no cleanup needed
238
+ "name": "Task Manager",
239
+ "startup": setup_task_manager_startup,
240
+ "shutdown": setup_task_manager_shutdown,
88
241
  },
89
242
  {
90
- "name": "Claude Detection",
91
- "startup": initialize_claude_detection_startup,
92
- "shutdown": None, # No cleanup needed
243
+ "name": "Version Check",
244
+ "startup": check_version_updates_startup,
245
+ "shutdown": None,
93
246
  },
94
247
  {
95
- "name": "Codex Detection",
96
- "startup": initialize_codex_detection_startup,
97
- "shutdown": None, # No cleanup needed
98
- },
99
- {
100
- "name": "Claude SDK",
101
- "startup": initialize_claude_sdk_startup,
102
- "shutdown": setup_session_manager_shutdown,
248
+ "name": "Claude CLI",
249
+ "startup": check_claude_cli_startup,
250
+ "shutdown": None,
103
251
  },
104
252
  {
105
253
  "name": "Scheduler",
@@ -107,154 +255,190 @@ LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
107
255
  "shutdown": setup_scheduler_shutdown,
108
256
  },
109
257
  {
110
- "name": "Log Storage",
111
- "startup": initialize_log_storage_startup,
112
- "shutdown": initialize_log_storage_shutdown,
258
+ "name": "Service Container",
259
+ "startup": None,
260
+ "shutdown": setup_service_container_shutdown,
113
261
  },
114
262
  {
115
- "name": "Permission Service",
116
- "startup": initialize_permission_service_startup,
117
- "shutdown": setup_permission_service_shutdown,
263
+ "name": "Plugin System",
264
+ "startup": initialize_plugins_startup,
265
+ "shutdown": shutdown_plugins,
118
266
  },
119
- ]
120
-
121
- # Additional shutdown-only components that need special handling
122
- SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = [
123
267
  {
124
- "name": "Streaming Batches",
125
- "shutdown": flush_streaming_batches_shutdown,
268
+ "name": "Hook System",
269
+ "startup": initialize_hooks_startup,
270
+ "shutdown": shutdown_hook_system,
126
271
  },
127
272
  ]
128
273
 
129
-
130
- # Create shared models router
131
- models_router = APIRouter(tags=["models"])
132
-
133
-
134
- @models_router.get("/v1/models", response_model=None)
135
- async def list_models() -> dict[str, Any]:
136
- """List available models.
137
-
138
- Returns a combined list of Anthropic models and recent OpenAI models.
139
- This endpoint is shared between both SDK and proxy APIs.
140
- """
141
- return get_models_list()
274
+ SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = []
142
275
 
143
276
 
144
277
  @asynccontextmanager
145
278
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
146
279
  """Application lifespan manager using component-based approach."""
147
- settings = get_settings()
148
-
149
- # Store settings in app state for reuse in dependencies
150
- app.state.settings = settings
151
-
152
- # Startup
280
+ service_container: ServiceContainer = app.state.service_container
281
+ settings = service_container.get_service(Settings)
153
282
  logger.info(
154
- "server_start",
283
+ "server_starting",
155
284
  host=settings.server.host,
156
285
  port=settings.server.port,
157
286
  url=f"http://{settings.server.host}:{settings.server.port}",
287
+ category="lifecycle",
158
288
  )
289
+ # Demote granular config detail to DEBUG
159
290
  logger.debug(
160
- "server_configured", host=settings.server.host, port=settings.server.port
291
+ "server_configured",
292
+ host=settings.server.host,
293
+ port=settings.server.port,
294
+ category="config",
161
295
  )
162
296
 
163
- # Log Claude CLI configuration
164
- if settings.claude.cli_path:
165
- logger.debug("claude_cli_configured", cli_path=settings.claude.cli_path)
166
- else:
167
- logger.debug("claude_cli_auto_detect")
168
- logger.debug(
169
- "claude_cli_search_paths", paths=settings.claude.get_searched_paths()
170
- )
171
-
172
- # Execute startup components in order
173
297
  for component in LIFECYCLE_COMPONENTS:
174
298
  if component["startup"]:
175
299
  component_name = component["name"]
176
300
  try:
177
- logger.debug(f"starting_{component_name.lower().replace(' ', '_')}")
301
+ logger.debug(
302
+ f"starting_{component_name.lower().replace(' ', '_')}",
303
+ category="lifecycle",
304
+ )
178
305
  await component["startup"](app, settings)
306
+ except (OSError, PermissionError) as e:
307
+ logger.error(
308
+ f"{component_name.lower().replace(' ', '_')}_startup_io_failed",
309
+ error=str(e),
310
+ component=component_name,
311
+ exc_info=e,
312
+ category="lifecycle",
313
+ )
179
314
  except Exception as e:
180
315
  logger.error(
181
316
  f"{component_name.lower().replace(' ', '_')}_startup_failed",
182
317
  error=str(e),
183
318
  component=component_name,
319
+ exc_info=e,
320
+ category="lifecycle",
184
321
  )
185
- # Continue with graceful degradation
322
+
323
+ # After startup completes (post-yield happens on shutdown); emit ready before yielding
324
+ # Safely derive feature flags from settings which may be models or dicts
325
+ def _get_plugin_enabled(name: str) -> bool:
326
+ plugins_cfg = getattr(settings, "plugins", None)
327
+ if plugins_cfg is None:
328
+ return False
329
+ # dict-like
330
+ if isinstance(plugins_cfg, dict):
331
+ cfg = plugins_cfg.get(name)
332
+ if isinstance(cfg, dict):
333
+ return bool(cfg.get("enabled", False))
334
+ try:
335
+ return bool(getattr(cfg, "enabled", False))
336
+ except Exception:
337
+ return False
338
+ # object-like
339
+ try:
340
+ sub = getattr(plugins_cfg, name, None)
341
+ return bool(getattr(sub, "enabled", False))
342
+ except Exception:
343
+ return False
344
+
345
+ def _get_auth_enabled() -> bool:
346
+ auth_cfg = getattr(settings, "auth", None)
347
+ if auth_cfg is None:
348
+ return False
349
+ if isinstance(auth_cfg, dict):
350
+ return bool(auth_cfg.get("enabled", False))
351
+ return bool(getattr(auth_cfg, "enabled", False))
352
+
353
+ logger.info(
354
+ "server_ready",
355
+ url=f"http://{settings.server.host}:{settings.server.port}",
356
+ version=__version__,
357
+ workers=settings.server.workers,
358
+ reload=settings.server.reload,
359
+ features_enabled={
360
+ "plugins": bool(getattr(settings, "enable_plugins", False)),
361
+ "metrics": _get_plugin_enabled("metrics"),
362
+ "access": _get_plugin_enabled("access_log"),
363
+ "auth": _get_auth_enabled(),
364
+ },
365
+ category="lifecycle",
366
+ )
186
367
 
187
368
  yield
188
369
 
189
- # Shutdown
190
- logger.debug("server_stop")
370
+ logger.debug("server_stop", category="lifecycle")
191
371
 
192
- # Execute shutdown-only components first
193
372
  for shutdown_component in SHUTDOWN_ONLY_COMPONENTS:
194
373
  if shutdown_component["shutdown"]:
195
374
  component_name = shutdown_component["name"]
196
375
  try:
197
- logger.debug(f"stopping_{component_name.lower().replace(' ', '_')}")
376
+ logger.debug(
377
+ f"stopping_{component_name.lower().replace(' ', '_')}",
378
+ category="lifecycle",
379
+ )
198
380
  await shutdown_component["shutdown"](app)
381
+ except (OSError, PermissionError) as e:
382
+ logger.error(
383
+ f"{component_name.lower().replace(' ', '_')}_shutdown_io_failed",
384
+ error=str(e),
385
+ component=component_name,
386
+ exc_info=e,
387
+ category="lifecycle",
388
+ )
199
389
  except Exception as e:
200
390
  logger.error(
201
391
  f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
202
392
  error=str(e),
203
393
  component=component_name,
394
+ exc_info=e,
395
+ category="lifecycle",
204
396
  )
205
397
 
206
- # Execute shutdown components in reverse order
207
398
  for component in reversed(LIFECYCLE_COMPONENTS):
208
399
  if component["shutdown"]:
209
400
  component_name = component["name"]
210
401
  try:
211
- logger.debug(f"stopping_{component_name.lower().replace(' ', '_')}")
212
- # Some shutdown functions need settings, others don't
402
+ logger.debug(
403
+ f"stopping_{component_name.lower().replace(' ', '_')}",
404
+ category="lifecycle",
405
+ )
213
406
  if component_name == "Permission Service":
214
407
  await component["shutdown"](app, settings) # type: ignore
215
408
  else:
216
409
  await component["shutdown"](app) # type: ignore
410
+ except (OSError, PermissionError) as e:
411
+ logger.error(
412
+ f"{component_name.lower().replace(' ', '_')}_shutdown_io_failed",
413
+ error=str(e),
414
+ component=component_name,
415
+ exc_info=e,
416
+ category="lifecycle",
417
+ )
217
418
  except Exception as e:
218
419
  logger.error(
219
420
  f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
220
421
  error=str(e),
221
422
  component=component_name,
423
+ exc_info=e,
424
+ category="lifecycle",
222
425
  )
223
426
 
224
427
 
225
- def create_app(settings: Settings | None = None) -> FastAPI:
226
- """Create and configure the FastAPI application.
227
-
228
- Args:
229
- settings: Optional settings override. If None, uses get_settings().
230
-
231
- Returns:
232
- Configured FastAPI application instance.
233
- """
234
- if settings is None:
235
- settings = get_settings()
236
- # Configure logging based on settings BEFORE any module uses logger
237
- # This is needed for reload mode where the app is re-imported
238
-
239
- import structlog
240
-
241
- # Only configure if not already configured or if no file handler exists
242
- # okay we have the first debug line but after uvicorn start they are not show root_logger = logging.getLogger()
243
- # for h in root_logger.handlers:
244
- # print(h)
245
- # has_file_handler = any(
246
- # isinstance(h, logging.FileHandler) for h in root_logger.handlers
247
- # )
248
-
428
+ def create_app(service_container: ServiceContainer | None = None) -> FastAPI:
429
+ if service_container is None:
430
+ service_container = create_service_container()
431
+ """Create and configure the FastAPI application with plugin system."""
432
+ settings = service_container.get_service(Settings)
249
433
  if not structlog.is_configured():
250
- # Only setup logging if structlog is not configured at all
251
- # Always use console output, but respect file logging from settings
252
- json_logs = False
434
+ json_logs = settings.logging.format == "json"
435
+
253
436
  setup_logging(
254
437
  json_logs=json_logs,
255
- log_level_name=settings.server.log_level,
256
- log_file=settings.server.log_file,
438
+ log_level_name=settings.logging.level,
439
+ log_file=settings.logging.file,
257
440
  )
441
+ logger.trace("settings", category="lifecycle", settings=settings)
258
442
 
259
443
  app = FastAPI(
260
444
  title="CCProxy API Server",
@@ -263,92 +447,135 @@ def create_app(settings: Settings | None = None) -> FastAPI:
263
447
  lifespan=lifespan,
264
448
  )
265
449
 
266
- # Setup middleware
267
- setup_cors_middleware(app, settings)
268
- setup_error_handlers(app)
269
-
270
- # Add request content logging middleware first (will run fourth due to middleware order)
271
- app.add_middleware(RequestContentLoggingMiddleware)
450
+ app.state.service_container = service_container
451
+
452
+ # Make the FastAPI instance available via the service container for plugin contexts
453
+ service_container.register_service(FastAPI, instance=app)
454
+
455
+ app.state.oauth_registry = service_container.get_oauth_registry()
456
+
457
+ plugin_registry = PluginRegistry()
458
+ middleware_manager = MiddlewareManager()
459
+
460
+ if settings.enable_plugins:
461
+ plugin_registry, middleware_manager = load_plugin_system(settings)
462
+
463
+ # Consolidated plugin init summary at INFO
464
+ logger.info(
465
+ "plugins_initialized",
466
+ plugin_count=len(plugin_registry.factories),
467
+ providers=sum(
468
+ 1
469
+ for f in plugin_registry.factories.values()
470
+ if f.get_manifest().is_provider
471
+ ),
472
+ system_plugins=len(plugin_registry.factories)
473
+ - sum(
474
+ 1
475
+ for f in plugin_registry.factories.values()
476
+ if f.get_manifest().is_provider
477
+ ),
478
+ names=list(plugin_registry.factories.keys()),
479
+ category="plugin",
480
+ )
272
481
 
273
- # Add custom access log middleware second (will run third due to middleware order)
274
- app.add_middleware(AccessLogMiddleware)
482
+ # Manifest population will be done during startup when core services are available
483
+
484
+ plugin_middleware_count = 0
485
+ for name, factory in plugin_registry.factories.items():
486
+ manifest = factory.get_manifest()
487
+ if manifest.middleware:
488
+ middleware_manager.add_plugin_middleware(name, manifest.middleware)
489
+ plugin_middleware_count += len(manifest.middleware)
490
+ logger.trace(
491
+ "plugin_middleware_collected",
492
+ plugin=name,
493
+ count=len(manifest.middleware),
494
+ category="lifecycle",
495
+ )
275
496
 
276
- # Add request ID middleware fourth (will run first to initialize context)
277
- app.add_middleware(RequestIDMiddleware)
497
+ if plugin_middleware_count > 0:
498
+ plugins_with_middleware = [
499
+ n
500
+ for n, f in plugin_registry.factories.items()
501
+ if f.get_manifest().middleware
502
+ ]
503
+ logger.debug(
504
+ "plugin_middleware_collection_completed",
505
+ total_middleware=plugin_middleware_count,
506
+ plugins_with_middleware=len(plugins_with_middleware),
507
+ plugin_names=plugins_with_middleware,
508
+ category="lifecycle",
509
+ )
278
510
 
279
- # Add server header middleware (for non-proxy routes)
280
- # You can customize the server name here
281
- app.add_middleware(ServerHeaderMiddleware, server_name="uvicorn")
511
+ for name, factory in plugin_registry.factories.items():
512
+ manifest = factory.get_manifest()
513
+ for route_spec in manifest.routes:
514
+ default_tag = name.replace("_", "-")
515
+ # Merge router tags with spec tags, removing duplicates
516
+ merged_tags = merge_router_tags(
517
+ route_spec.router,
518
+ spec_tags=route_spec.tags,
519
+ default_tags=[default_tag],
520
+ )
282
521
 
283
- # Include health router (always enabled)
284
- app.include_router(health_router, tags=["health"])
522
+ app.include_router(
523
+ route_spec.router,
524
+ prefix=route_spec.prefix,
525
+ tags=merged_tags,
526
+ dependencies=route_spec.dependencies,
527
+ )
528
+ logger.debug(
529
+ "plugin_routes_registered",
530
+ plugin=name,
531
+ prefix=route_spec.prefix,
532
+ category="lifecycle",
533
+ )
285
534
 
286
- # Include observability routers with granular controls
287
- if settings.observability.metrics_endpoint_enabled:
288
- app.include_router(prometheus_router, tags=["metrics"])
535
+ app.state.plugin_registry = plugin_registry
536
+ app.state.middleware_manager = middleware_manager
289
537
 
290
- if settings.observability.logs_endpoints_enabled:
291
- app.include_router(logs_router, prefix="/logs", tags=["logs"])
538
+ app.state.settings = settings
292
539
 
293
- if settings.observability.dashboard_enabled:
294
- app.include_router(dashboard_router, tags=["dashboard"])
540
+ setup_cors_middleware(app, settings)
541
+ setup_error_handlers(app)
295
542
 
296
- app.include_router(oauth_router, prefix="/oauth", tags=["oauth"])
543
+ # Validate format adapters once routes are registered
544
+ try:
545
+ registry = service_container.get_format_registry()
546
+ validate_route_format_chains(app=app, registry=registry, logger=logger)
547
+ except Exception as exc:
548
+ # Best-effort registration/validation; do not block app startup
549
+ logger.warning("format_registry_setup_skipped", error=str(exc))
297
550
 
298
- # Codex routes for OpenAI integration
299
- app.include_router(codex_router, tags=["codex"])
551
+ setup_default_middleware(middleware_manager)
300
552
 
301
- # New /sdk/ routes for Claude SDK endpoints
302
- app.include_router(claude_router, prefix="/sdk", tags=["claude-sdk"])
553
+ middleware_manager.apply_to_app(app)
303
554
 
304
- # New /api/ routes for proxy endpoints (includes OpenAI-compatible /v1/chat/completions)
305
- app.include_router(proxy_router, prefix="/api", tags=["proxy-api"])
555
+ # Core router registrations with tag merging
556
+ app.include_router(
557
+ health_router, tags=merge_router_tags(health_router, default_tags=["health"])
558
+ )
306
559
 
307
- # Shared models endpoints for both SDK and proxy APIs
308
- app.include_router(models_router, prefix="/sdk", tags=["claude-sdk", "models"])
309
- app.include_router(models_router, prefix="/api", tags=["proxy-api", "models"])
560
+ app.include_router(
561
+ oauth_router,
562
+ prefix="/oauth",
563
+ tags=merge_router_tags(oauth_router, default_tags=["oauth"]),
564
+ )
310
565
 
311
- # Confirmation endpoints for SSE streaming and responses (conditional on builtin_permissions)
312
- if settings.claude.builtin_permissions:
566
+ if settings.enable_plugins:
313
567
  app.include_router(
314
- permissions_router, prefix="/permissions", tags=["permissions"]
568
+ plugins_router,
569
+ tags=merge_router_tags(plugins_router, default_tags=["plugins"]),
315
570
  )
316
- setup_mcp(app)
317
-
318
- # Mount static files for dashboard SPA
319
- from pathlib import Path
320
-
321
- # Get the path to the dashboard static files
322
- current_file = Path(__file__)
323
- project_root = (
324
- current_file.parent.parent.parent
325
- ) # ccproxy/api/app.py -> project root
326
- dashboard_static_path = project_root / "ccproxy" / "static" / "dashboard"
327
-
328
- # Mount dashboard static files if they exist
329
- if dashboard_static_path.exists():
330
- # Mount the _app directory for SvelteKit assets at the correct base path
331
- app_path = dashboard_static_path / "_app"
332
- if app_path.exists():
333
- app.mount(
334
- "/dashboard/_app",
335
- StaticFiles(directory=str(app_path)),
336
- name="dashboard-assets",
337
- )
338
-
339
- # Mount favicon.svg at root level
340
- favicon_path = dashboard_static_path / "favicon.svg"
341
- if favicon_path.exists():
342
- # For single files, we'll handle this in the dashboard route or add a specific route
343
- pass
344
571
 
345
572
  return app
346
573
 
347
574
 
348
575
  def get_app() -> FastAPI:
349
- """Get the FastAPI application instance.
576
+ """Get the FastAPI app instance."""
577
+ container = create_service_container()
578
+ return create_app(container)
350
579
 
351
- Returns:
352
- FastAPI application instance.
353
- """
354
- return create_app()
580
+
581
+ __all__ = ["create_app", "get_app"]