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
ccproxy/api/app.py CHANGED
@@ -1,61 +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
35
  check_version_updates_startup,
39
- flush_streaming_batches_shutdown,
40
- initialize_claude_detection_startup,
41
- initialize_claude_sdk_startup,
42
- initialize_codex_detection_startup,
43
- initialize_log_storage_shutdown,
44
- initialize_log_storage_startup,
45
- initialize_permission_service_startup,
46
- setup_permission_service_shutdown,
47
36
  setup_scheduler_shutdown,
48
37
  setup_scheduler_startup,
49
- setup_session_manager_shutdown,
50
- validate_claude_authentication_startup,
51
- validate_codex_authentication_startup,
52
38
  )
53
39
 
54
40
 
55
- 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
56
81
 
57
82
 
58
- # Type definitions for lifecycle components
59
83
  class LifecycleComponent(TypedDict):
60
84
  name: str
61
85
  startup: Callable[[FastAPI, Any], Awaitable[None]] | None
@@ -71,47 +95,159 @@ class ShutdownComponent(TypedDict):
71
95
  shutdown: Callable[[FastAPI], Awaitable[None]] | None
72
96
 
73
97
 
74
- # 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
+
75
236
  LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
76
237
  {
77
- "name": "Claude Authentication",
78
- "startup": validate_claude_authentication_startup,
79
- "shutdown": None, # One-time validation, no cleanup needed
80
- },
81
- {
82
- "name": "Codex Authentication",
83
- "startup": validate_codex_authentication_startup,
84
- "shutdown": None, # One-time validation, no cleanup needed
238
+ "name": "Task Manager",
239
+ "startup": setup_task_manager_startup,
240
+ "shutdown": setup_task_manager_shutdown,
85
241
  },
86
242
  {
87
243
  "name": "Version Check",
88
244
  "startup": check_version_updates_startup,
89
- "shutdown": None, # One-time check, no cleanup needed
245
+ "shutdown": None,
90
246
  },
91
247
  {
92
248
  "name": "Claude CLI",
93
249
  "startup": check_claude_cli_startup,
94
- "shutdown": None, # Detection only, no cleanup needed
95
- },
96
- {
97
- "name": "Codex CLI",
98
- "startup": check_codex_cli_startup,
99
- "shutdown": None, # Detection only, no cleanup needed
100
- },
101
- {
102
- "name": "Claude Detection",
103
- "startup": initialize_claude_detection_startup,
104
- "shutdown": None, # No cleanup needed
105
- },
106
- {
107
- "name": "Codex Detection",
108
- "startup": initialize_codex_detection_startup,
109
- "shutdown": None, # No cleanup needed
110
- },
111
- {
112
- "name": "Claude SDK",
113
- "startup": initialize_claude_sdk_startup,
114
- "shutdown": setup_session_manager_shutdown,
250
+ "shutdown": None,
115
251
  },
116
252
  {
117
253
  "name": "Scheduler",
@@ -119,154 +255,190 @@ LIFECYCLE_COMPONENTS: list[LifecycleComponent] = [
119
255
  "shutdown": setup_scheduler_shutdown,
120
256
  },
121
257
  {
122
- "name": "Log Storage",
123
- "startup": initialize_log_storage_startup,
124
- "shutdown": initialize_log_storage_shutdown,
258
+ "name": "Service Container",
259
+ "startup": None,
260
+ "shutdown": setup_service_container_shutdown,
125
261
  },
126
262
  {
127
- "name": "Permission Service",
128
- "startup": initialize_permission_service_startup,
129
- "shutdown": setup_permission_service_shutdown,
263
+ "name": "Plugin System",
264
+ "startup": initialize_plugins_startup,
265
+ "shutdown": shutdown_plugins,
130
266
  },
131
- ]
132
-
133
- # Additional shutdown-only components that need special handling
134
- SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = [
135
267
  {
136
- "name": "Streaming Batches",
137
- "shutdown": flush_streaming_batches_shutdown,
268
+ "name": "Hook System",
269
+ "startup": initialize_hooks_startup,
270
+ "shutdown": shutdown_hook_system,
138
271
  },
139
272
  ]
140
273
 
141
-
142
- # Create shared models router
143
- models_router = APIRouter(tags=["models"])
144
-
145
-
146
- @models_router.get("/v1/models", response_model=None)
147
- async def list_models() -> dict[str, Any]:
148
- """List available models.
149
-
150
- Returns a combined list of Anthropic models and recent OpenAI models.
151
- This endpoint is shared between both SDK and proxy APIs.
152
- """
153
- return get_models_list()
274
+ SHUTDOWN_ONLY_COMPONENTS: list[ShutdownComponent] = []
154
275
 
155
276
 
156
277
  @asynccontextmanager
157
278
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
158
279
  """Application lifespan manager using component-based approach."""
159
- settings = get_settings()
160
-
161
- # Store settings in app state for reuse in dependencies
162
- app.state.settings = settings
163
-
164
- # Startup
280
+ service_container: ServiceContainer = app.state.service_container
281
+ settings = service_container.get_service(Settings)
165
282
  logger.info(
166
- "server_start",
283
+ "server_starting",
167
284
  host=settings.server.host,
168
285
  port=settings.server.port,
169
286
  url=f"http://{settings.server.host}:{settings.server.port}",
287
+ category="lifecycle",
170
288
  )
289
+ # Demote granular config detail to DEBUG
171
290
  logger.debug(
172
- "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",
173
295
  )
174
296
 
175
- # Log Claude CLI configuration
176
- if settings.claude.cli_path:
177
- logger.debug("claude_cli_configured", cli_path=settings.claude.cli_path)
178
- else:
179
- logger.debug("claude_cli_auto_detect")
180
- logger.debug(
181
- "claude_cli_search_paths", paths=settings.claude.get_searched_paths()
182
- )
183
-
184
- # Execute startup components in order
185
297
  for component in LIFECYCLE_COMPONENTS:
186
298
  if component["startup"]:
187
299
  component_name = component["name"]
188
300
  try:
189
- logger.debug(f"starting_{component_name.lower().replace(' ', '_')}")
301
+ logger.debug(
302
+ f"starting_{component_name.lower().replace(' ', '_')}",
303
+ category="lifecycle",
304
+ )
190
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
+ )
191
314
  except Exception as e:
192
315
  logger.error(
193
316
  f"{component_name.lower().replace(' ', '_')}_startup_failed",
194
317
  error=str(e),
195
318
  component=component_name,
319
+ exc_info=e,
320
+ category="lifecycle",
196
321
  )
197
- # 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
+ )
198
367
 
199
368
  yield
200
369
 
201
- # Shutdown
202
- logger.debug("server_stop")
370
+ logger.debug("server_stop", category="lifecycle")
203
371
 
204
- # Execute shutdown-only components first
205
372
  for shutdown_component in SHUTDOWN_ONLY_COMPONENTS:
206
373
  if shutdown_component["shutdown"]:
207
374
  component_name = shutdown_component["name"]
208
375
  try:
209
- logger.debug(f"stopping_{component_name.lower().replace(' ', '_')}")
376
+ logger.debug(
377
+ f"stopping_{component_name.lower().replace(' ', '_')}",
378
+ category="lifecycle",
379
+ )
210
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
+ )
211
389
  except Exception as e:
212
390
  logger.error(
213
391
  f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
214
392
  error=str(e),
215
393
  component=component_name,
394
+ exc_info=e,
395
+ category="lifecycle",
216
396
  )
217
397
 
218
- # Execute shutdown components in reverse order
219
398
  for component in reversed(LIFECYCLE_COMPONENTS):
220
399
  if component["shutdown"]:
221
400
  component_name = component["name"]
222
401
  try:
223
- logger.debug(f"stopping_{component_name.lower().replace(' ', '_')}")
224
- # Some shutdown functions need settings, others don't
402
+ logger.debug(
403
+ f"stopping_{component_name.lower().replace(' ', '_')}",
404
+ category="lifecycle",
405
+ )
225
406
  if component_name == "Permission Service":
226
407
  await component["shutdown"](app, settings) # type: ignore
227
408
  else:
228
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
+ )
229
418
  except Exception as e:
230
419
  logger.error(
231
420
  f"{component_name.lower().replace(' ', '_')}_shutdown_failed",
232
421
  error=str(e),
233
422
  component=component_name,
423
+ exc_info=e,
424
+ category="lifecycle",
234
425
  )
235
426
 
236
427
 
237
- def create_app(settings: Settings | None = None) -> FastAPI:
238
- """Create and configure the FastAPI application.
239
-
240
- Args:
241
- settings: Optional settings override. If None, uses get_settings().
242
-
243
- Returns:
244
- Configured FastAPI application instance.
245
- """
246
- if settings is None:
247
- settings = get_settings()
248
- # Configure logging based on settings BEFORE any module uses logger
249
- # This is needed for reload mode where the app is re-imported
250
-
251
- import structlog
252
-
253
- # Only configure if not already configured or if no file handler exists
254
- # okay we have the first debug line but after uvicorn start they are not show root_logger = logging.getLogger()
255
- # for h in root_logger.handlers:
256
- # print(h)
257
- # has_file_handler = any(
258
- # isinstance(h, logging.FileHandler) for h in root_logger.handlers
259
- # )
260
-
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)
261
433
  if not structlog.is_configured():
262
- # Only setup logging if structlog is not configured at all
263
- # Always use console output, but respect file logging from settings
264
- json_logs = False
434
+ json_logs = settings.logging.format == "json"
435
+
265
436
  setup_logging(
266
437
  json_logs=json_logs,
267
- log_level_name=settings.server.log_level,
268
- log_file=settings.server.log_file,
438
+ log_level_name=settings.logging.level,
439
+ log_file=settings.logging.file,
269
440
  )
441
+ logger.trace("settings", category="lifecycle", settings=settings)
270
442
 
271
443
  app = FastAPI(
272
444
  title="CCProxy API Server",
@@ -275,92 +447,135 @@ def create_app(settings: Settings | None = None) -> FastAPI:
275
447
  lifespan=lifespan,
276
448
  )
277
449
 
278
- # Setup middleware
279
- setup_cors_middleware(app, settings)
280
- setup_error_handlers(app)
281
-
282
- # Add request content logging middleware first (will run fourth due to middleware order)
283
- 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
+ )
284
481
 
285
- # Add custom access log middleware second (will run third due to middleware order)
286
- 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
+ )
287
496
 
288
- # Add request ID middleware fourth (will run first to initialize context)
289
- 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
+ )
290
510
 
291
- # Add server header middleware (for non-proxy routes)
292
- # You can customize the server name here
293
- 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
+ )
294
521
 
295
- # Include health router (always enabled)
296
- 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
+ )
297
534
 
298
- # Include observability routers with granular controls
299
- if settings.observability.metrics_endpoint_enabled:
300
- app.include_router(prometheus_router, tags=["metrics"])
535
+ app.state.plugin_registry = plugin_registry
536
+ app.state.middleware_manager = middleware_manager
301
537
 
302
- if settings.observability.logs_endpoints_enabled:
303
- app.include_router(logs_router, prefix="/logs", tags=["logs"])
538
+ app.state.settings = settings
304
539
 
305
- if settings.observability.dashboard_enabled:
306
- app.include_router(dashboard_router, tags=["dashboard"])
540
+ setup_cors_middleware(app, settings)
541
+ setup_error_handlers(app)
307
542
 
308
- 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))
309
550
 
310
- # Codex routes for OpenAI integration
311
- app.include_router(codex_router, tags=["codex"])
551
+ setup_default_middleware(middleware_manager)
312
552
 
313
- # New /sdk/ routes for Claude SDK endpoints
314
- app.include_router(claude_router, prefix="/sdk", tags=["claude-sdk"])
553
+ middleware_manager.apply_to_app(app)
315
554
 
316
- # New /api/ routes for proxy endpoints (includes OpenAI-compatible /v1/chat/completions)
317
- 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
+ )
318
559
 
319
- # Shared models endpoints for both SDK and proxy APIs
320
- app.include_router(models_router, prefix="/sdk", tags=["claude-sdk", "models"])
321
- 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
+ )
322
565
 
323
- # Confirmation endpoints for SSE streaming and responses (conditional on builtin_permissions)
324
- if settings.claude.builtin_permissions:
566
+ if settings.enable_plugins:
325
567
  app.include_router(
326
- permissions_router, prefix="/permissions", tags=["permissions"]
568
+ plugins_router,
569
+ tags=merge_router_tags(plugins_router, default_tags=["plugins"]),
327
570
  )
328
- setup_mcp(app)
329
-
330
- # Mount static files for dashboard SPA
331
- from pathlib import Path
332
-
333
- # Get the path to the dashboard static files
334
- current_file = Path(__file__)
335
- project_root = (
336
- current_file.parent.parent.parent
337
- ) # ccproxy/api/app.py -> project root
338
- dashboard_static_path = project_root / "ccproxy" / "static" / "dashboard"
339
-
340
- # Mount dashboard static files if they exist
341
- if dashboard_static_path.exists():
342
- # Mount the _app directory for SvelteKit assets at the correct base path
343
- app_path = dashboard_static_path / "_app"
344
- if app_path.exists():
345
- app.mount(
346
- "/dashboard/_app",
347
- StaticFiles(directory=str(app_path)),
348
- name="dashboard-assets",
349
- )
350
-
351
- # Mount favicon.svg at root level
352
- favicon_path = dashboard_static_path / "favicon.svg"
353
- if favicon_path.exists():
354
- # For single files, we'll handle this in the dashboard route or add a specific route
355
- pass
356
571
 
357
572
  return app
358
573
 
359
574
 
360
575
  def get_app() -> FastAPI:
361
- """Get the FastAPI application instance.
576
+ """Get the FastAPI app instance."""
577
+ container = create_service_container()
578
+ return create_app(container)
362
579
 
363
- Returns:
364
- FastAPI application instance.
365
- """
366
- return create_app()
580
+
581
+ __all__ = ["create_app", "get_app"]