ccproxy-api 0.1.7__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 +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.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.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 -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.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,604 @@
1
+ """Plugin discovery system for finding and loading plugins.
2
+
3
+ This module provides mechanisms to discover plugins from the filesystem
4
+ and dynamically load their factories.
5
+ """
6
+
7
+ import importlib
8
+ import importlib.machinery
9
+ import importlib.util
10
+ import logging
11
+ from collections.abc import Iterable
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+ from typing import Any, cast
15
+
16
+ import structlog
17
+
18
+ from ccproxy.config import Settings
19
+
20
+
21
+ try:
22
+ # Python 3.10+
23
+ from importlib.metadata import EntryPoint, entry_points
24
+ except ImportError: # pragma: no cover
25
+ entry_points = None # type: ignore[assignment]
26
+ EntryPoint = Any # type: ignore[misc,assignment]
27
+
28
+ from .interfaces import PluginFactory
29
+
30
+
31
+ logger = structlog.get_logger(__name__)
32
+
33
+
34
+ def _get_logger(context: str, plugin_name: str | None = None) -> Any:
35
+ """Return a structlog logger bound with shared plugin metadata."""
36
+
37
+ bound = logger.bind(type=context, category="plugin")
38
+ if plugin_name:
39
+ bound = bound.bind(name=plugin_name)
40
+ return bound
41
+
42
+
43
+ def _log_missing_dependency(
44
+ *, plugin_name: str, error: ModuleNotFoundError, context: str
45
+ ) -> None:
46
+ """Log a structured warning for a missing plugin dependency."""
47
+
48
+ missing_dependency = getattr(error, "name", None)
49
+ if not missing_dependency:
50
+ missing_dependency = str(error).removeprefix("No module named ").strip("'\"")
51
+
52
+ event_name = "plugin_dependency_missing"
53
+ log_payload = {"dependency": missing_dependency, "details": context}
54
+
55
+ _get_logger(context=context, plugin_name=plugin_name).warning(
56
+ event_name,
57
+ **log_payload,
58
+ )
59
+
60
+ logging.warning("%s %s", event_name, log_payload)
61
+
62
+
63
+ def build_combined_plugin_denylist(
64
+ disabled_plugins: Iterable[str] | None,
65
+ plugin_configs: dict[str, Any] | None,
66
+ ) -> set[str]:
67
+ """Merge explicit and per-plugin disabled settings into a single deny list."""
68
+
69
+ combined = set(disabled_plugins or [])
70
+
71
+ if not plugin_configs:
72
+ return combined
73
+
74
+ for plugin_name, config in plugin_configs.items():
75
+ if not isinstance(config, dict):
76
+ continue
77
+
78
+ enabled_flag = config.get("enabled")
79
+ if enabled_flag is False:
80
+ combined.add(plugin_name)
81
+
82
+ return combined
83
+
84
+
85
+ class PluginDiscovery:
86
+ """Discovers and loads plugins from the filesystem."""
87
+
88
+ def __init__(self, plugins_dirs: Iterable[Path]):
89
+ """Initialize plugin discovery.
90
+
91
+ Args:
92
+ plugins_dirs: Ordered directories containing plugin packages
93
+ """
94
+ seen: set[Path] = set()
95
+ ordered: list[Path] = []
96
+ for directory in plugins_dirs:
97
+ path = Path(directory)
98
+ resolved = path.resolve()
99
+ if resolved in seen:
100
+ continue
101
+ seen.add(resolved)
102
+ ordered.append(path)
103
+ self.plugin_dirs = ordered
104
+ self.discovered_plugins: dict[str, Path] = {}
105
+
106
+ def discover_plugins(self) -> dict[str, Path]:
107
+ """Discover all plugins in the plugins directory.
108
+
109
+ Returns:
110
+ Dictionary mapping plugin names to their paths
111
+ """
112
+ self.discovered_plugins.clear()
113
+
114
+ logger_fs = _get_logger("filesystem")
115
+ discovered: list[str] = []
116
+ missing_dirs: list[str] = []
117
+
118
+ for base_dir in self.plugin_dirs:
119
+ if not base_dir.exists():
120
+ missing_dirs.append(str(base_dir))
121
+ continue
122
+
123
+ for item in sorted(base_dir.iterdir()):
124
+ if not item.is_dir() or item.name.startswith("_"):
125
+ continue
126
+
127
+ plugin_file = item / "plugin.py"
128
+ if not plugin_file.exists():
129
+ continue
130
+
131
+ if item.name in self.discovered_plugins:
132
+ _get_logger("filesystem", item.name).debug(
133
+ "plugin_duplicate_ignored",
134
+ original=str(self.discovered_plugins[item.name]),
135
+ ignored=str(plugin_file),
136
+ )
137
+ continue
138
+
139
+ self.discovered_plugins[item.name] = plugin_file
140
+ discovered.append(item.name)
141
+
142
+ plugin_logger = _get_logger("filesystem", item.name)
143
+ plugin_trace = getattr(plugin_logger, "trace", plugin_logger.debug)
144
+ plugin_trace(
145
+ "plugin_found",
146
+ path=str(plugin_file),
147
+ )
148
+
149
+ if missing_dirs:
150
+ logger_fs.warning(
151
+ "plugins_directories_missing",
152
+ paths=missing_dirs,
153
+ )
154
+
155
+ # Single consolidated log for all discoveries
156
+ logger_fs.info(
157
+ "plugins_discovered",
158
+ count=len(discovered),
159
+ names=discovered if discovered else [],
160
+ directories=[str(path) for path in self.plugin_dirs],
161
+ )
162
+ return self.discovered_plugins
163
+
164
+ def load_plugin_factory(self, name: str) -> PluginFactory | None:
165
+ """Load a plugin factory by name.
166
+
167
+ Args:
168
+ name: Plugin name
169
+
170
+ Returns:
171
+ Plugin factory or None if not found or failed to load
172
+ """
173
+ logger_fs = _get_logger("filesystem", name)
174
+ if name not in self.discovered_plugins:
175
+ logger_fs.warning("plugin_not_discovered")
176
+ return None
177
+
178
+ plugin_path = self.discovered_plugins[name]
179
+
180
+ try:
181
+ plugin_dir = plugin_path.parent
182
+
183
+ # Ensure the namespace package includes this plugin directory so
184
+ # relative imports like 'from .config import ...' resolve when the
185
+ # plugin lives outside the main repository (e.g., ~/.config/ccproxy/plugins).
186
+ try:
187
+ import ccproxy.plugins as builtin_plugins
188
+
189
+ if hasattr(builtin_plugins, "__path__"):
190
+ location = str(plugin_dir)
191
+ if location not in builtin_plugins.__path__:
192
+ builtin_plugins.__path__.append(location)
193
+ except ModuleNotFoundError: # pragma: no cover - defensive
194
+ pass
195
+
196
+ module_name = f"ccproxy.plugins.{name}.plugin"
197
+ package_name = f"ccproxy.plugins.{name}"
198
+
199
+ # Reload package/module to pick up filesystem changes
200
+ import sys
201
+
202
+ package_module: None | ModuleType = None
203
+ for candidate in (module_name, package_name):
204
+ if candidate in sys.modules:
205
+ sys.modules.pop(candidate)
206
+
207
+ init_file = plugin_dir / "__init__.py"
208
+ if init_file.exists():
209
+ package_spec = importlib.util.spec_from_file_location(
210
+ package_name,
211
+ init_file,
212
+ submodule_search_locations=[str(plugin_dir)],
213
+ )
214
+ if package_spec and package_spec.loader:
215
+ package_module = importlib.util.module_from_spec(package_spec)
216
+ sys.modules[package_name] = package_module
217
+ package_spec.loader.exec_module(package_module)
218
+
219
+ package_module = sys.modules.get(package_name)
220
+ if package_module is None:
221
+ if init_file.exists():
222
+ package_spec = importlib.util.spec_from_file_location(
223
+ package_name,
224
+ init_file,
225
+ submodule_search_locations=[str(plugin_dir)],
226
+ )
227
+ if package_spec and package_spec.loader:
228
+ package_module = importlib.util.module_from_spec(package_spec)
229
+ package_module.__path__ = [str(plugin_dir)]
230
+ sys.modules[package_name] = package_module
231
+ package_spec.loader.exec_module(package_module)
232
+ else: # pragma: no cover - defensive
233
+ package_module = importlib.util.module_from_spec(
234
+ importlib.machinery.ModuleSpec(
235
+ package_name, loader=None, is_package=True
236
+ )
237
+ )
238
+ package_module.__path__ = [str(plugin_dir)]
239
+ sys.modules[package_name] = package_module
240
+ else:
241
+ package_module = importlib.util.module_from_spec(
242
+ importlib.machinery.ModuleSpec(
243
+ package_name, loader=None, is_package=True
244
+ )
245
+ )
246
+ package_module.__path__ = [str(plugin_dir)]
247
+ sys.modules[package_name] = package_module
248
+ else:
249
+ package_module.__file__ = (
250
+ str(init_file) if init_file.exists() else package_module.__file__
251
+ )
252
+ package_module.__path__ = [str(plugin_dir)]
253
+
254
+ if package_name in sys.modules:
255
+ package_module = sys.modules[package_name]
256
+ package_module.__file__ = (
257
+ str(init_file)
258
+ if init_file.exists()
259
+ else getattr(package_module, "__file__", None)
260
+ )
261
+ package_module.__path__ = [str(plugin_dir)]
262
+
263
+ spec = importlib.util.spec_from_file_location(
264
+ module_name,
265
+ plugin_path,
266
+ )
267
+
268
+ if not spec or not spec.loader:
269
+ logger_fs.error("plugin_spec_creation_failed")
270
+ return None
271
+
272
+ module = importlib.util.module_from_spec(spec)
273
+ sys.modules[module_name] = module
274
+ spec.loader.exec_module(module)
275
+
276
+ # Get the factory from the module
277
+ if not hasattr(module, "factory"):
278
+ logger_fs.error(
279
+ "plugin_factory_not_found",
280
+ msg="Module must export 'factory' variable",
281
+ )
282
+ return None
283
+
284
+ factory = module.factory
285
+
286
+ if not isinstance(factory, PluginFactory):
287
+ logger_fs.error(
288
+ "plugin_factory_invalid_type",
289
+ type=type(factory).__name__,
290
+ )
291
+ return None
292
+
293
+ logger_fs.debug(
294
+ "plugin_factory_loaded",
295
+ version=factory.get_manifest().version,
296
+ )
297
+
298
+ return factory
299
+
300
+ except ModuleNotFoundError as exc:
301
+ _log_missing_dependency(
302
+ plugin_name=name,
303
+ error=exc,
304
+ context="filesystem",
305
+ )
306
+ return None
307
+ except Exception as e:
308
+ logger_fs.error(
309
+ "plugin_load_failed",
310
+ error=str(e),
311
+ exc_info=e,
312
+ )
313
+ return None
314
+
315
+ def load_all_factories(
316
+ self, plugin_filter: "PluginFilter | None" = None
317
+ ) -> dict[str, PluginFactory]:
318
+ """Load all discovered plugin factories.
319
+
320
+ Returns:
321
+ Dictionary mapping plugin names to their factories
322
+ """
323
+ logger_fs = _get_logger("filesystem")
324
+ factories: dict[str, PluginFactory] = {}
325
+
326
+ skipped_names: list[str] = []
327
+
328
+ for name in self.discovered_plugins:
329
+ if plugin_filter and not plugin_filter.is_enabled(name):
330
+ skipped_names.append(name)
331
+ continue
332
+ factory = self.load_plugin_factory(name)
333
+ if factory:
334
+ factories[name] = factory
335
+
336
+ if skipped_names:
337
+ logger_fs.debug("plugin_skipped_before_load", names=skipped_names)
338
+
339
+ logger_fs.debug(
340
+ "plugin_factories_loaded",
341
+ count=len(factories),
342
+ names=list(factories.keys()),
343
+ )
344
+
345
+ return factories
346
+
347
+ def load_entry_point_factories(
348
+ self,
349
+ skip_names: set[str] | None = None,
350
+ plugin_filter: "PluginFilter | None" = None,
351
+ ) -> dict[str, PluginFactory]:
352
+ """Load plugin factories from installed entry points.
353
+
354
+ Returns:
355
+ Dictionary mapping plugin names to their factories
356
+ """
357
+ factories: dict[str, PluginFactory] = {}
358
+ logger_ep = _get_logger("entrypoint")
359
+ if entry_points is None:
360
+ logger_ep.debug("entry_points_not_available")
361
+ return factories
362
+
363
+ try:
364
+ groups = entry_points()
365
+ eps = []
366
+ # importlib.metadata API differences across Python versions
367
+ if hasattr(groups, "select"):
368
+ eps = list(groups.select(group="ccproxy.plugins"))
369
+ else: # pragma: no cover
370
+ eps = list(groups.get("ccproxy.plugins", []))
371
+
372
+ skip_logged: set[str] = set()
373
+ filtered_skipped: list[str] = []
374
+ for ep in eps:
375
+ name = ep.name
376
+ # Skip entry points that collide with existing filesystem plugins
377
+ if skip_names and name in skip_names:
378
+ if name not in skip_logged:
379
+ _get_logger("entrypoint", name).debug(
380
+ "entry_point_skipped_preexisting_filesystem"
381
+ )
382
+ skip_logged.add(name)
383
+ continue
384
+ # Skip duplicates within entry points themselves
385
+ if name in factories:
386
+ if name not in skip_logged:
387
+ _get_logger("entrypoint", name).debug(
388
+ "entry_point_duplicate_ignored"
389
+ )
390
+ skip_logged.add(name)
391
+ continue
392
+ if plugin_filter and not plugin_filter.is_enabled(name):
393
+ filtered_skipped.append(name)
394
+ continue
395
+ try:
396
+ # Primary load
397
+ obj = ep.load()
398
+ except ModuleNotFoundError as exc:
399
+ _log_missing_dependency(
400
+ plugin_name=name,
401
+ error=exc,
402
+ context="entrypoint",
403
+ )
404
+ continue
405
+ except Exception as e:
406
+ # Fallback: import module and get 'factory'
407
+ try:
408
+ module_name = getattr(ep, "module", None)
409
+ if not module_name:
410
+ value = getattr(ep, "value", "")
411
+ module_name = value.split(":")[0] if ":" in value else None
412
+ if not module_name:
413
+ raise e
414
+ mod = importlib.import_module(module_name)
415
+ if hasattr(mod, "factory"):
416
+ obj = mod.factory
417
+ else:
418
+ raise e
419
+ except ModuleNotFoundError as exc2:
420
+ _log_missing_dependency(
421
+ plugin_name=name,
422
+ error=exc2,
423
+ context="entrypoint_fallback",
424
+ )
425
+ continue
426
+ except Exception as e2:
427
+ _get_logger("entrypoint", name).error(
428
+ "entry_point_load_failed", error=str(e2), exc_info=e2
429
+ )
430
+ continue
431
+
432
+ factory: PluginFactory | None = None
433
+
434
+ # If the object already looks like a factory (duck typing)
435
+ if hasattr(obj, "get_manifest") and hasattr(obj, "create_runtime"):
436
+ factory = cast(PluginFactory, obj)
437
+ # If it's callable, try to call to get a factory
438
+ elif callable(obj):
439
+ try:
440
+ maybe = obj()
441
+ if hasattr(maybe, "get_manifest") and hasattr(
442
+ maybe, "create_runtime"
443
+ ):
444
+ factory = cast(PluginFactory, maybe)
445
+ except Exception:
446
+ factory = None
447
+
448
+ if not factory:
449
+ _get_logger("entrypoint", name).warning(
450
+ "entry_point_not_factory", obj_type=type(obj).__name__
451
+ )
452
+ continue
453
+
454
+ factories[name] = factory
455
+ # logger.debug(
456
+ # "entry_point_factory_loaded",
457
+ # name=name,
458
+ # version=factory.get_manifest().version,
459
+ # category="plugin",
460
+ # )
461
+
462
+ if filtered_skipped:
463
+ logger_ep.info("plugin_skipped_before_load", names=filtered_skipped)
464
+ except Exception as e: # pragma: no cover
465
+ logger_ep.error("entry_points_enumeration_failed", error=str(e), exc_info=e)
466
+ return factories
467
+
468
+
469
+ class PluginFilter:
470
+ """Filter plugins based on configuration."""
471
+
472
+ def __init__(
473
+ self,
474
+ enabled_plugins: list[str] | None = None,
475
+ disabled_plugins: Iterable[str] | None = None,
476
+ ):
477
+ """Initialize plugin filter.
478
+
479
+ Args:
480
+ enabled_plugins: List of explicitly enabled plugins (None = all)
481
+ disabled_plugins: Precomputed deny list of disabled plugins
482
+ """
483
+ self.enabled_plugins = set(enabled_plugins) if enabled_plugins else None
484
+ self.disabled_plugins = set(disabled_plugins or [])
485
+
486
+ def is_enabled(self, plugin_name: str) -> bool:
487
+ """Check if a plugin is enabled using allow/deny-list precedence."""
488
+
489
+ # 1. If enabled_plugins is specified, ONLY those are allowed
490
+ if self.enabled_plugins is not None:
491
+ return plugin_name in self.enabled_plugins
492
+
493
+ # 2. Check disabled_plugins blacklist
494
+ return plugin_name not in self.disabled_plugins
495
+
496
+ def filter_factories(
497
+ self, factories: dict[str, PluginFactory]
498
+ ) -> dict[str, PluginFactory]:
499
+ """Filter plugin factories based on configuration.
500
+
501
+ Args:
502
+ factories: All discovered factories
503
+
504
+ Returns:
505
+ Filtered factories
506
+ """
507
+ logger_filter = _get_logger("filter")
508
+ filtered = {}
509
+ enabled_plugins = []
510
+ disabled_plugins = []
511
+
512
+ for name, factory in factories.items():
513
+ if self.is_enabled(name):
514
+ filtered[name] = factory
515
+ enabled_plugins.append(name)
516
+ else:
517
+ disabled_plugins.append(name)
518
+ _get_logger("filter", name).info("plugin_disabled")
519
+
520
+ # Debug logging for enabled and disabled plugins
521
+ logger_filter.debug(
522
+ "plugin_filter_summary",
523
+ enabled_plugins=sorted(enabled_plugins),
524
+ disabled_plugins=sorted(disabled_plugins),
525
+ enabled_count=len(enabled_plugins),
526
+ disabled_count=len(disabled_plugins),
527
+ )
528
+
529
+ return filtered
530
+
531
+
532
+ def discover_and_load_plugins(settings: Settings) -> dict[str, PluginFactory]:
533
+ """Discover and load all configured plugins.
534
+
535
+ Args:
536
+ settings: Application settings
537
+
538
+ Returns:
539
+ Dictionary of loaded plugin factories
540
+ """
541
+ plugin_dirs: list[Path]
542
+ # if len(settings.plugin_discovery.directories) > 0:
543
+ plugin_dirs = [Path(path) for path in settings.plugin_discovery.directories]
544
+ # else:
545
+ # plugin_dirs = [Path(__file__).parent.parent.parent / "plugins"]
546
+
547
+ logger_mgr = _get_logger("manager")
548
+
549
+ logger_mgr.debug(
550
+ "plugin_filesystem_directories",
551
+ directories=[str(path) for path in plugin_dirs],
552
+ )
553
+
554
+ # Discover plugins
555
+ discovery = PluginDiscovery(plugin_dirs)
556
+
557
+ combined_denylist = build_combined_plugin_denylist(
558
+ getattr(settings, "disabled_plugins", None),
559
+ getattr(settings, "plugins", None),
560
+ )
561
+
562
+ filter_config = PluginFilter(
563
+ enabled_plugins=getattr(settings, "enabled_plugins", None),
564
+ disabled_plugins=combined_denylist,
565
+ )
566
+
567
+ # Determine whether to use local filesystem discovery
568
+ if settings.plugins_disable_local_discovery:
569
+ logger_mgr.info(
570
+ "plugins_local_discovery_disabled",
571
+ reason="settings.plugins_disable_local_discovery",
572
+ )
573
+
574
+ all_factories: dict[str, PluginFactory] = {}
575
+
576
+ filesystem_factories: dict[str, PluginFactory] = {}
577
+ filesystem_names: set[str] = set()
578
+
579
+ if not settings.plugins_disable_local_discovery:
580
+ discovery.discover_plugins()
581
+ filesystem_factories = discovery.load_all_factories(plugin_filter=filter_config)
582
+ filesystem_names = set(filesystem_factories.keys())
583
+ all_factories.update(filesystem_factories)
584
+
585
+ entry_point_factories = discovery.load_entry_point_factories(
586
+ skip_names=filesystem_names,
587
+ plugin_filter=filter_config,
588
+ )
589
+
590
+ for name, factory in entry_point_factories.items():
591
+ if name in all_factories:
592
+ _get_logger("manager", name).debug("plugin_filesystem_override")
593
+ all_factories.setdefault(name, factory)
594
+
595
+ filtered_factories = filter_config.filter_factories(all_factories)
596
+
597
+ logger_mgr.info(
598
+ "plugins_ready",
599
+ discovered=len(all_factories),
600
+ enabled=len(filtered_factories),
601
+ names=list(filtered_factories.keys()),
602
+ )
603
+
604
+ return filtered_factories