ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,967 @@
1
+ """Plugin factory implementations and registry.
2
+
3
+ This module contains all concrete factory implementations merged from
4
+ base_factory.py and factory.py to eliminate circular dependencies.
5
+ """
6
+
7
+ import inspect
8
+ from typing import TYPE_CHECKING, Any, cast
9
+
10
+ import httpx
11
+ import structlog
12
+ from fastapi import APIRouter
13
+
14
+ from ccproxy.models.provider import ProviderConfig
15
+ from ccproxy.services.adapters.base import BaseAdapter
16
+ from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
17
+ from ccproxy.services.interfaces import (
18
+ IMetricsCollector,
19
+ IRequestTracer,
20
+ NullMetricsCollector,
21
+ NullRequestTracer,
22
+ NullStreamingHandler,
23
+ StreamingMetrics,
24
+ )
25
+ from ccproxy.utils.model_mapper import ModelMapper
26
+
27
+ from .declaration import (
28
+ CliArgumentSpec,
29
+ CliCommandSpec,
30
+ FormatAdapterSpec,
31
+ FormatPair,
32
+ PluginContext,
33
+ PluginManifest,
34
+ RouterSpec,
35
+ RouteSpec,
36
+ TaskSpec,
37
+ )
38
+ from .interfaces import (
39
+ AuthProviderPluginFactory,
40
+ PluginFactory,
41
+ ProviderPluginFactory,
42
+ )
43
+
44
+
45
+ if TYPE_CHECKING:
46
+ from ccproxy.config.settings import Settings
47
+ from ccproxy.http.pool import HTTPPoolManager
48
+ from ccproxy.services.container import ServiceContainer
49
+
50
+
51
+ logger = structlog.get_logger(__name__)
52
+
53
+ # Type variable for service type checking
54
+ T = Any
55
+
56
+
57
+ class BaseProviderPluginFactory(ProviderPluginFactory):
58
+ """Base factory for provider plugins that eliminates common boilerplate.
59
+
60
+ This class uses class attributes for plugin configuration and implements
61
+ common methods that all provider factories share. Subclasses only need
62
+ to define class attributes and override methods that need custom behavior.
63
+
64
+ Required class attributes to be defined by subclasses:
65
+ - plugin_name: str
66
+ - plugin_description: str
67
+ - runtime_class: type[ProviderPluginRuntime]
68
+ - adapter_class: type[BaseAdapter]
69
+ - config_class: type[BaseSettings]
70
+
71
+ Optional class attributes with defaults:
72
+ - plugin_version: str = "1.0.0"
73
+ - detection_service_class: type | None = None
74
+ - credentials_manager_class: type | None = None
75
+ - router: APIRouter | None = None
76
+ - route_prefix: str = "/api"
77
+ - dependencies: list[str] = []
78
+ - optional_requires: list[str] = []
79
+ - tasks: list[TaskSpec] = []
80
+ """
81
+
82
+ # Required class attributes (must be overridden by subclasses)
83
+ plugin_name: str
84
+ plugin_description: str
85
+ runtime_class: Any # Should be type[ProviderPluginRuntime] subclass
86
+ adapter_class: Any # Should be type[BaseAdapter] subclass
87
+ config_class: Any # Should be type[BaseSettings] subclass
88
+
89
+ # Optional class attributes with defaults
90
+ plugin_version: str = "1.0.0"
91
+ detection_service_class: type | None = None
92
+ credentials_manager_class: type[Any] | None = None
93
+ auth_manager_name: str | None = None # String-based auth manager reference
94
+ routers: list[RouterSpec] = []
95
+ dependencies: list[str] = []
96
+ optional_requires: list[str] = []
97
+ tasks: list[TaskSpec] = []
98
+
99
+ # Format adapter declarations (populated by subclasses)
100
+ format_adapters: list[FormatAdapterSpec] = []
101
+ requires_format_adapters: list[FormatPair] = []
102
+
103
+ # CLI extension declarations (populated by subclasses)
104
+ cli_commands: list[CliCommandSpec] = []
105
+ cli_arguments: list[CliArgumentSpec] = []
106
+ tool_accumulator_class: type | None = None
107
+
108
+ def __init__(self) -> None:
109
+ """Initialize factory with manifest built from class attributes."""
110
+ # Validate required class attributes
111
+ self._validate_class_attributes()
112
+
113
+ # Validate runtime class is a proper subclass
114
+ # Import locally to avoid circular import during module import
115
+ from .runtime import ProviderPluginRuntime
116
+
117
+ if not issubclass(self.runtime_class, ProviderPluginRuntime):
118
+ raise TypeError(
119
+ f"runtime_class {self.runtime_class.__name__} must be a subclass of ProviderPluginRuntime"
120
+ )
121
+
122
+ # Build routes from routers list
123
+ routes = []
124
+ for router_spec in self.routers:
125
+ # Handle both router instances and router factory functions
126
+ router_instance = router_spec.router
127
+ if callable(router_spec.router) and not isinstance(
128
+ router_spec.router, APIRouter
129
+ ):
130
+ # Router is a factory function, call it to get the actual router
131
+ router_instance = router_spec.router()
132
+
133
+ routes.append(
134
+ RouteSpec(
135
+ router=cast(APIRouter, router_instance),
136
+ prefix=router_spec.prefix,
137
+ tags=router_spec.tags or [],
138
+ dependencies=router_spec.dependencies,
139
+ )
140
+ )
141
+
142
+ # Create manifest from class attributes
143
+ manifest = PluginManifest(
144
+ name=self.plugin_name,
145
+ version=self.plugin_version,
146
+ description=self.plugin_description,
147
+ is_provider=True,
148
+ config_class=self.config_class,
149
+ tool_accumulator_class=self.tool_accumulator_class,
150
+ dependencies=self.dependencies.copy(),
151
+ optional_requires=self.optional_requires.copy(),
152
+ routes=routes,
153
+ tasks=self.tasks.copy(),
154
+ format_adapters=self.format_adapters.copy(),
155
+ requires_format_adapters=self.requires_format_adapters.copy(),
156
+ cli_commands=self.cli_commands.copy(),
157
+ cli_arguments=self.cli_arguments.copy(),
158
+ )
159
+
160
+ # Format adapter specification validation is deferred to runtime
161
+ # when settings are available via dependency injection
162
+
163
+ # Store the manifest and runtime class directly
164
+ # We don't call parent __init__ because ProviderPluginFactory
165
+ # would override our runtime_class with ProviderPluginRuntime
166
+ self.manifest = manifest
167
+ self.runtime_class = self.__class__.runtime_class
168
+
169
+ def validate_format_adapters_with_settings(self, settings: "Settings") -> None:
170
+ """Validate format adapter specifications (feature flags removed)."""
171
+ self._validate_format_adapter_specs()
172
+
173
+ def _validate_class_attributes(self) -> None:
174
+ """Validate that required class attributes are defined."""
175
+ required_attrs = [
176
+ "plugin_name",
177
+ "plugin_description",
178
+ "runtime_class",
179
+ "adapter_class",
180
+ "config_class",
181
+ ]
182
+
183
+ for attr in required_attrs:
184
+ if (
185
+ not hasattr(self.__class__, attr)
186
+ or getattr(self.__class__, attr) is None
187
+ ):
188
+ raise ValueError(
189
+ f"Class attribute '{attr}' must be defined in {self.__class__.__name__}"
190
+ )
191
+
192
+ def _validate_format_adapter_specs(self) -> None:
193
+ """Validate format adapter specifications."""
194
+ for spec in self.format_adapters:
195
+ if not callable(spec.adapter_factory):
196
+ raise ValueError(
197
+ f"Invalid adapter factory for {spec.from_format} -> {spec.to_format}: "
198
+ f"must be callable"
199
+ ) from None
200
+
201
+ def create_runtime(self) -> Any:
202
+ """Create runtime instance using the configured runtime class."""
203
+ return cast(Any, self.runtime_class(self.manifest))
204
+
205
+ async def create_adapter(self, context: PluginContext) -> BaseAdapter:
206
+ """Create adapter instance with explicit dependencies.
207
+
208
+ This method extracts services from context and creates the adapter
209
+ with explicit dependency injection. Subclasses can override this
210
+ method if they need custom adapter creation logic.
211
+
212
+ Args:
213
+ context: Plugin context
214
+
215
+ Returns:
216
+ Adapter instance
217
+ """
218
+ # Extract services from context (one-time extraction)
219
+ http_pool_manager: HTTPPoolManager | None = cast(
220
+ "HTTPPoolManager | None", context.get("http_pool_manager")
221
+ )
222
+ request_tracer: IRequestTracer | None = context.get("request_tracer")
223
+ metrics: IMetricsCollector | None = context.get("metrics")
224
+ streaming_handler: StreamingMetrics | None = context.get("streaming_handler")
225
+ hook_manager = context.get("hook_manager")
226
+
227
+ # Get auth and detection services that may have been created by factory
228
+ auth_manager = context.get("credentials_manager")
229
+ detection_service = context.get("detection_service")
230
+
231
+ # Get config if available
232
+ config = context.get("config")
233
+
234
+ # Get all adapter dependencies from service container
235
+ service_container = context.get("service_container")
236
+ if not service_container:
237
+ raise RuntimeError("Service container is required for adapter services")
238
+
239
+ # Get standardized adapter dependencies
240
+ adapter_dependencies = service_container.get_adapter_dependencies(metrics)
241
+
242
+ # Check if this is an HTTP-based adapter
243
+ if issubclass(self.adapter_class, BaseHTTPAdapter):
244
+ # HTTP adapters require http_pool_manager
245
+ if not http_pool_manager:
246
+ raise RuntimeError(
247
+ f"HTTP pool manager required for {self.adapter_class.__name__} but not available in context"
248
+ )
249
+
250
+ # Ensure config is provided for HTTP adapters
251
+ if config is None and self.manifest.config_class:
252
+ config = self.manifest.config_class()
253
+
254
+ # Create HTTP adapter with explicit dependencies including format services
255
+ init_params = inspect.signature(self.adapter_class.__init__).parameters
256
+ adapter_kwargs: dict[str, Any] = {
257
+ "config": config,
258
+ "auth_manager": auth_manager,
259
+ "detection_service": detection_service,
260
+ "http_pool_manager": http_pool_manager,
261
+ "request_tracer": request_tracer or NullRequestTracer(),
262
+ "metrics": metrics or NullMetricsCollector(),
263
+ "streaming_handler": streaming_handler or NullStreamingHandler(),
264
+ "hook_manager": hook_manager,
265
+ "format_registry": adapter_dependencies["format_registry"],
266
+ "context": context,
267
+ "model_mapper": context.get("model_mapper")
268
+ if hasattr(context, "get")
269
+ else None,
270
+ }
271
+ if self.tool_accumulator_class:
272
+ adapter_kwargs["tool_accumulator_class"] = self.tool_accumulator_class
273
+
274
+ return cast(BaseAdapter, self.adapter_class(**adapter_kwargs))
275
+ else:
276
+ # Non-HTTP adapters (like ClaudeSDK) have different dependencies
277
+ # Build kwargs based on adapter class constructor signature
278
+ non_http_adapter_kwargs: dict[str, Any] = {}
279
+
280
+ # Get the adapter's __init__ signature
281
+ sig = inspect.signature(self.adapter_class.__init__)
282
+ params = sig.parameters
283
+
284
+ # For non-HTTP adapters, create http_client from pool manager if needed
285
+ client_for_non_http: httpx.AsyncClient | None = None
286
+ if http_pool_manager and "http_client" in params:
287
+ client_for_non_http = await http_pool_manager.get_client()
288
+
289
+ # Map available services to expected parameters
290
+ param_mapping = {
291
+ "config": config,
292
+ "http_client": client_for_non_http,
293
+ "http_pool_manager": http_pool_manager,
294
+ "auth_manager": auth_manager,
295
+ "detection_service": detection_service,
296
+ "session_manager": context.get("session_manager"),
297
+ "request_tracer": request_tracer,
298
+ "metrics": metrics,
299
+ "streaming_handler": streaming_handler,
300
+ "hook_manager": hook_manager,
301
+ "format_registry": adapter_dependencies["format_registry"],
302
+ "context": context,
303
+ "model_mapper": context.get("model_mapper")
304
+ if hasattr(context, "get")
305
+ else None,
306
+ }
307
+ if self.tool_accumulator_class:
308
+ non_http_adapter_kwargs["tool_accumulator_class"] = (
309
+ self.tool_accumulator_class
310
+ )
311
+
312
+ # Add parameters that the adapter expects
313
+ for param_name, param in params.items():
314
+ if param_name in ("self", "kwargs"):
315
+ continue
316
+ if param_name in param_mapping:
317
+ if param_mapping[param_name] is not None:
318
+ non_http_adapter_kwargs[param_name] = param_mapping[param_name]
319
+ elif (
320
+ param_name == "config"
321
+ and param.default is inspect.Parameter.empty
322
+ and self.manifest.config_class
323
+ ):
324
+ # Config is None but required, create default
325
+ default_config = self.manifest.config_class()
326
+ non_http_adapter_kwargs["config"] = default_config
327
+ elif (
328
+ param.default is inspect.Parameter.empty
329
+ and param_name not in non_http_adapter_kwargs
330
+ and param_name == "config"
331
+ and self.manifest.config_class
332
+ ):
333
+ # Config parameter is missing but required, create default
334
+ default_config = self.manifest.config_class()
335
+ non_http_adapter_kwargs["config"] = default_config
336
+
337
+ return cast(BaseAdapter, self.adapter_class(**non_http_adapter_kwargs))
338
+
339
+ def create_detection_service(self, context: PluginContext) -> Any:
340
+ """Create detection service instance if class is configured.
341
+
342
+ Args:
343
+ context: Plugin context
344
+
345
+ Returns:
346
+ Detection service instance or None if no class configured
347
+ """
348
+ if self.detection_service_class is None:
349
+ return None
350
+
351
+ settings = context.get("settings")
352
+ if settings is None:
353
+ from ccproxy.config.settings import Settings
354
+
355
+ settings = Settings()
356
+
357
+ cli_service = context.get("cli_detection_service")
358
+ return self.detection_service_class(settings, cli_service)
359
+
360
+ async def create_credentials_manager(self, context: PluginContext) -> Any:
361
+ """Resolve credentials manager via the shared auth registry."""
362
+
363
+ auth_manager_name = self.get_auth_manager_name(context)
364
+ registry = None
365
+
366
+ service_container = context.get("service_container")
367
+ if service_container and hasattr(
368
+ service_container, "get_auth_manager_registry"
369
+ ):
370
+ registry = service_container.get_auth_manager_registry()
371
+
372
+ if not auth_manager_name:
373
+ return None
374
+
375
+ if not registry:
376
+ logger.warning(
377
+ "auth_manager_registry_unavailable",
378
+ plugin=self.manifest.name,
379
+ auth_manager_name=auth_manager_name,
380
+ category="auth",
381
+ )
382
+ return None
383
+
384
+ resolved = await registry.get(auth_manager_name)
385
+ if resolved:
386
+ return resolved
387
+
388
+ # Respect explicit overrides that could not be resolved
389
+ if self.auth_manager_name and auth_manager_name != self.auth_manager_name:
390
+ logger.warning(
391
+ "auth_manager_override_not_resolved",
392
+ plugin=self.manifest.name,
393
+ auth_manager_name=auth_manager_name,
394
+ category="auth",
395
+ )
396
+ else:
397
+ logger.warning(
398
+ "auth_manager_not_registered",
399
+ plugin=self.manifest.name,
400
+ auth_manager_name=auth_manager_name,
401
+ category="auth",
402
+ )
403
+
404
+ return None
405
+
406
+ def get_auth_manager_name(self, context: PluginContext) -> str | None:
407
+ """Get auth manager name, allowing config override.
408
+
409
+ Args:
410
+ context: Plugin context containing config
411
+
412
+ Returns:
413
+ Auth manager name or None if not configured
414
+ """
415
+ # Check if plugin config overrides auth manager
416
+ if hasattr(context, "config") and context.config:
417
+ config_auth_manager = getattr(context.config, "auth_manager", None)
418
+ if config_auth_manager:
419
+ return str(config_auth_manager)
420
+
421
+ # Use plugin's default auth manager name
422
+ return self.auth_manager_name
423
+
424
+ def create_context(self, service_container: "ServiceContainer") -> PluginContext:
425
+ """Create context with provider-specific components.
426
+
427
+ This method provides a hook for subclasses to customize context creation.
428
+ The default implementation just returns the base context.
429
+
430
+ Args:
431
+ core_services: Core services container
432
+
433
+ Returns:
434
+ Plugin context
435
+ """
436
+ context = super().create_context(service_container)
437
+ config = context.get("config", None)
438
+ if isinstance(config, ProviderConfig) and config.model_mappings:
439
+ context.model_mapper = ModelMapper(config.model_mappings)
440
+ return context
441
+
442
+
443
+ class PluginRegistry:
444
+ """Registry for managing plugin factories and runtime instances."""
445
+
446
+ def __init__(self) -> None:
447
+ """Initialize plugin registry."""
448
+ self.factories: dict[str, PluginFactory] = {}
449
+ self.runtimes: dict[str, Any] = {}
450
+ self.initialization_order: list[str] = []
451
+
452
+ # Service management
453
+ self._services: dict[str, Any] = {}
454
+ self._service_providers: dict[str, str] = {} # service_name -> plugin_name
455
+
456
+ def register_service(
457
+ self, service_name: str, service_instance: Any, provider_plugin: str
458
+ ) -> None:
459
+ """Register a service provided by a plugin.
460
+
461
+ Args:
462
+ service_name: Name of the service
463
+ service_instance: Service instance
464
+ provider_plugin: Name of the plugin providing the service
465
+ """
466
+ if service_name in self._services:
467
+ logger.warning(
468
+ "service_already_registered",
469
+ service=service_name,
470
+ existing_provider=self._service_providers[service_name],
471
+ new_provider=provider_plugin,
472
+ )
473
+ self._services[service_name] = service_instance
474
+ self._service_providers[service_name] = provider_plugin
475
+
476
+ def get_service(
477
+ self, service_name: str, service_type: type[T] | None = None
478
+ ) -> T | None:
479
+ """Get a service by name with optional type checking.
480
+
481
+ Args:
482
+ service_name: Name of the service
483
+ service_type: Optional expected service type
484
+
485
+ Returns:
486
+ Service instance or None if not found
487
+ """
488
+ service = self._services.get(service_name)
489
+ if service and service_type and not isinstance(service, service_type):
490
+ logger.warning(
491
+ "service_type_mismatch",
492
+ service=service_name,
493
+ expected_type=service_type,
494
+ actual_type=type(service),
495
+ )
496
+ return None
497
+ return service
498
+
499
+ def has_service(self, service_name: str) -> bool:
500
+ """Check if a service is registered.
501
+
502
+ Args:
503
+ service_name: Name of the service
504
+
505
+ Returns:
506
+ True if service is registered
507
+ """
508
+ return service_name in self._services
509
+
510
+ def get_required_services(self, plugin_name: str) -> tuple[list[str], list[str]]:
511
+ """Get required and optional services for a plugin.
512
+
513
+ Args:
514
+ plugin_name: Name of the plugin
515
+
516
+ Returns:
517
+ Tuple of (required_services, optional_services)
518
+ """
519
+ manifest = self.factories[plugin_name].get_manifest()
520
+ return manifest.requires, manifest.optional_requires
521
+
522
+ def register_factory(self, factory: PluginFactory) -> None:
523
+ """Register a plugin factory.
524
+
525
+ Args:
526
+ factory: Plugin factory to register
527
+ """
528
+ manifest = factory.get_manifest()
529
+
530
+ if manifest.name in self.factories:
531
+ raise ValueError(f"Plugin {manifest.name} already registered")
532
+
533
+ self.factories[manifest.name] = factory
534
+
535
+ def get_factory(self, name: str) -> PluginFactory | None:
536
+ """Get a plugin factory by name.
537
+
538
+ Args:
539
+ name: Plugin name
540
+
541
+ Returns:
542
+ Plugin factory or None
543
+ """
544
+ return self.factories.get(name)
545
+
546
+ def get_all_manifests(self) -> dict[str, PluginManifest]:
547
+ """Get all registered plugin manifests.
548
+
549
+ Returns:
550
+ Dictionary mapping plugin names to manifests
551
+ """
552
+ return {
553
+ name: factory.get_manifest() for name, factory in self.factories.items()
554
+ }
555
+
556
+ def resolve_dependencies(self, settings: "Settings") -> list[str]:
557
+ """Resolve plugin dependencies and return initialization order.
558
+
559
+ Skips plugins with missing hard dependencies or required services
560
+ instead of failing the entire plugin system. Logs skipped plugins
561
+ and continues with the rest.
562
+
563
+ Args:
564
+ settings: Settings instance
565
+
566
+ Returns:
567
+ List of plugin names in initialization order
568
+ """
569
+ manifests = self.get_all_manifests()
570
+
571
+ # Start with all plugins available
572
+ available = set(manifests.keys())
573
+ skipped: dict[str, str] = {}
574
+
575
+ # Validate format adapter dependencies (latest behavior)
576
+ missing_format_adapters = self._validate_format_adapter_requirements()
577
+ if missing_format_adapters:
578
+ for plugin_name, missing in missing_format_adapters.items():
579
+ logger.error(
580
+ "plugin_missing_format_adapters",
581
+ plugin=plugin_name,
582
+ missing_adapters=missing,
583
+ category="format",
584
+ )
585
+ # Remove plugins with missing format adapter requirements
586
+ available.discard(plugin_name)
587
+ skipped[plugin_name] = f"missing format adapters: {missing}"
588
+
589
+ # Iteratively prune plugins with unsatisfied dependencies or services
590
+ while True:
591
+ removed_this_pass: set[str] = set()
592
+
593
+ # Compute services provided by currently available plugins
594
+ available_services = {
595
+ service for name in available for service in manifests[name].provides
596
+ }
597
+
598
+ for name in sorted(available):
599
+ manifest = manifests[name]
600
+
601
+ # Check plugin dependencies
602
+ missing_plugins = [
603
+ dep for dep in manifest.dependencies if dep not in available
604
+ ]
605
+ if missing_plugins:
606
+ removed_this_pass.add(name)
607
+ skipped[name] = f"missing plugin dependencies: {missing_plugins}"
608
+ continue
609
+
610
+ # Check required services
611
+ missing_services = manifest.validate_service_dependencies(
612
+ available_services
613
+ )
614
+ if missing_services:
615
+ removed_this_pass.add(name)
616
+ skipped[name] = f"missing required services: {missing_services}"
617
+
618
+ if not removed_this_pass:
619
+ break
620
+
621
+ # Remove the failing plugins and repeat until stable
622
+ available -= removed_this_pass
623
+
624
+ # Before sorting, ensure provider plugins load before consumers by
625
+ # adding provider plugins to the consumer's dependency list.
626
+ # Choose a stable provider (lexicographically first) when multiple exist.
627
+ for name in available:
628
+ manifest = manifests[name]
629
+ for required_service in manifest.requires:
630
+ provider_names = [
631
+ other_name
632
+ for other_name in available
633
+ if required_service in manifests[other_name].provides
634
+ ]
635
+ if provider_names:
636
+ provider_names.sort()
637
+ provider = provider_names[0]
638
+ if provider != name and provider not in manifest.dependencies:
639
+ manifest.dependencies.append(provider)
640
+
641
+ # Kahn's algorithm for topological sort over remaining plugins
642
+ # Build dependency graph restricted to available plugins
643
+ deps: dict[str, list[str]] = {
644
+ name: [dep for dep in manifests[name].dependencies if dep in available]
645
+ for name in available
646
+ }
647
+ in_degree: dict[str, int] = {name: len(deps[name]) for name in available}
648
+ dependents: dict[str, list[str]] = {name: [] for name in available}
649
+ for name, dlist in deps.items():
650
+ for dep in dlist:
651
+ dependents[dep].append(name)
652
+
653
+ # Initialize queue with nodes having zero in-degree
654
+ queue = [name for name, deg in in_degree.items() if deg == 0]
655
+ queue.sort()
656
+
657
+ order: list[str] = []
658
+ while queue:
659
+ node = queue.pop(0)
660
+ order.append(node)
661
+ for consumer in dependents[node]:
662
+ in_degree[consumer] -= 1
663
+ if in_degree[consumer] == 0:
664
+ queue.append(consumer)
665
+ queue.sort()
666
+
667
+ # Any nodes not in order are part of cycles; skip them
668
+ cyclic = [name for name in available if name not in order]
669
+ if cyclic:
670
+ for name in cyclic:
671
+ skipped[name] = "circular dependency"
672
+ logger.error(
673
+ "plugin_dependency_cycle_detected",
674
+ skipped=cyclic,
675
+ category="plugin",
676
+ )
677
+
678
+ # Final initialization order excludes skipped and cyclic plugins
679
+ self.initialization_order = order
680
+
681
+ if skipped:
682
+ logger.warning(
683
+ "plugins_skipped_due_to_missing_dependencies",
684
+ skipped=skipped,
685
+ category="plugin",
686
+ )
687
+
688
+ return order
689
+
690
+ def _register_auth_manager_with_registry(
691
+ self,
692
+ factory: AuthProviderPluginFactory,
693
+ context: PluginContext,
694
+ ) -> None:
695
+ """Ensure auth provider plugins publish their managers via the registry."""
696
+
697
+ service_container = context.get("service_container")
698
+ if not service_container or not hasattr(
699
+ service_container, "get_auth_manager_registry"
700
+ ):
701
+ logger.warning(
702
+ "auth_manager_registry_unavailable",
703
+ plugin=factory.get_manifest().name,
704
+ auth_manager_name=factory.get_auth_manager_registry_name(),
705
+ category="auth",
706
+ )
707
+ return
708
+
709
+ registry = service_container.get_auth_manager_registry()
710
+ manager_name = factory.get_auth_manager_registry_name()
711
+
712
+ if not manager_name:
713
+ return
714
+
715
+ if registry.has(manager_name):
716
+ registry.unregister(manager_name)
717
+
718
+ existing_manager = context.get("token_manager")
719
+ if existing_manager:
720
+ registry.register_instance(manager_name, existing_manager)
721
+ logger.debug(
722
+ "auth_manager_instance_registered",
723
+ plugin=factory.get_manifest().name,
724
+ auth_manager_name=manager_name,
725
+ category="auth",
726
+ )
727
+ return
728
+
729
+ auth_provider = context.get("auth_provider")
730
+ if auth_provider and hasattr(auth_provider, "create_token_manager"):
731
+
732
+ async def manager_factory() -> Any:
733
+ try:
734
+ candidate = auth_provider.create_token_manager()
735
+ return (
736
+ await candidate if inspect.isawaitable(candidate) else candidate
737
+ )
738
+ except Exception as exc: # pragma: no cover - defensive
739
+ logger.error(
740
+ "auth_manager_factory_failed",
741
+ plugin=factory.get_manifest().name,
742
+ auth_manager_name=manager_name,
743
+ error=str(exc),
744
+ exc_info=exc,
745
+ category="auth",
746
+ )
747
+ raise
748
+
749
+ registry.register_factory(manager_name, manager_factory)
750
+ logger.debug(
751
+ "auth_manager_factory_registered",
752
+ plugin=factory.get_manifest().name,
753
+ auth_manager_name=manager_name,
754
+ source="provider",
755
+ category="auth",
756
+ )
757
+ return
758
+
759
+ manager_class = getattr(factory, "auth_manager_class", None)
760
+ if manager_class:
761
+ registry.register_class(manager_name, manager_class)
762
+ logger.debug(
763
+ "auth_manager_class_registered_from_factory",
764
+ plugin=factory.get_manifest().name,
765
+ auth_manager_name=manager_name,
766
+ class_name=manager_class.__name__,
767
+ category="auth",
768
+ )
769
+ return
770
+
771
+ logger.warning(
772
+ "auth_manager_registration_missing",
773
+ plugin=factory.get_manifest().name,
774
+ auth_manager_name=manager_name,
775
+ category="auth",
776
+ )
777
+
778
+ def _validate_format_adapter_requirements(self) -> dict[str, list[tuple[str, str]]]:
779
+ """Self-contained helper for format adapter requirement validation.
780
+
781
+ This method is called during dependency resolution when core_services
782
+ is not yet available. In practice, format adapter validation happens
783
+ later in the initialization process when the format registry is available.
784
+ """
785
+ # During dependency resolution phase, format registry may not be available yet
786
+ # Return empty dict to allow dependency resolution to continue
787
+ # Actual format adapter validation happens during initialize_all()
788
+ logger.debug(
789
+ "format_adapter_requirements_validation_deferred",
790
+ message="Format adapter validation will happen during plugin initialization",
791
+ category="format",
792
+ )
793
+ return {}
794
+
795
+ async def create_runtime(
796
+ self, name: str, service_container: "ServiceContainer"
797
+ ) -> Any:
798
+ """Create and initialize a plugin runtime.
799
+
800
+ Args:
801
+ name: Plugin name
802
+ service_container: Service container with all available services
803
+
804
+ Returns:
805
+ Initialized plugin runtime
806
+
807
+ Raises:
808
+ ValueError: If plugin not found
809
+ """
810
+ factory = self.get_factory(name)
811
+ if not factory:
812
+ raise ValueError(f"Plugin {name} not found")
813
+
814
+ # Check if already created
815
+ if name in self.runtimes:
816
+ return self.runtimes[name]
817
+
818
+ # Create runtime instance
819
+ runtime = factory.create_runtime()
820
+
821
+ # Create context
822
+ context = factory.create_context(service_container)
823
+
824
+ # For auth provider plugins, create auth components first so registries are ready
825
+ if isinstance(factory, AuthProviderPluginFactory):
826
+ context.auth_provider = factory.create_auth_provider(context)
827
+ context.token_manager = factory.create_token_manager()
828
+ context.storage = factory.create_storage()
829
+ self._register_auth_manager_with_registry(factory, context)
830
+
831
+ # For provider plugins, create additional components (may depend on auth registry)
832
+ if isinstance(factory, ProviderPluginFactory):
833
+ # Create credentials manager and detection service first as adapter may depend on them
834
+ context.detection_service = factory.create_detection_service(context)
835
+ context.credentials_manager = await factory.create_credentials_manager(
836
+ context
837
+ )
838
+ context.adapter = await factory.create_adapter(context)
839
+
840
+ # Initialize runtime
841
+ await runtime.initialize(context)
842
+
843
+ # Store runtime
844
+ self.runtimes[name] = runtime
845
+
846
+ return runtime
847
+
848
+ async def initialize_all(self, service_container: "ServiceContainer") -> None:
849
+ """Initialize all registered plugins with format adapter support.
850
+
851
+ Args:
852
+ service_container: Service container with all available services
853
+ """
854
+
855
+ # Resolve dependencies and get initialization order
856
+ settings = service_container.settings
857
+ order = self.resolve_dependencies(settings)
858
+
859
+ # Consolidated discovery summary at INFO
860
+ logger.info(
861
+ "plugins_discovered", count=len(order), names=order, category="plugin"
862
+ )
863
+
864
+ # Register format adapters from manifests in first pass (latest behavior)
865
+ format_registry = service_container.get_format_registry()
866
+ manifests = self.get_all_manifests()
867
+ for name, manifest in manifests.items():
868
+ if manifest.format_adapters:
869
+ await format_registry.register_from_manifest(manifest, name)
870
+ logger.debug(
871
+ "plugin_format_adapters_registered_from_manifest",
872
+ plugin=name,
873
+ adapter_count=len(manifest.format_adapters),
874
+ category="format",
875
+ )
876
+
877
+ # Auth managers are registered when auth provider contexts are constructed
878
+
879
+ initialized: list[str] = []
880
+ for name in order:
881
+ try:
882
+ await self.create_runtime(name, service_container)
883
+ initialized.append(name)
884
+ except Exception as e:
885
+ logger.warning(
886
+ "plugin_initialization_failed",
887
+ plugin=name,
888
+ error=str(e),
889
+ exc_info=e,
890
+ category="plugin",
891
+ )
892
+ # Continue with other plugins
893
+
894
+ # Registry entries are available immediately; log consolidated summary
895
+ skipped = [n for n in order if n not in initialized]
896
+ logger.info(
897
+ "plugins_initialized",
898
+ count=len(initialized),
899
+ names=initialized,
900
+ skipped=skipped if skipped else [],
901
+ category="plugin",
902
+ )
903
+
904
+ # Emit a single hooks summary at the end
905
+ try:
906
+ hook_registry = service_container.get_hook_registry()
907
+ totals: dict[str, int] = {}
908
+ for event_name, hooks in hook_registry.list().items():
909
+ totals[event_name] = len(hooks)
910
+ logger.info(
911
+ "hooks_registered",
912
+ total_events=len(totals),
913
+ by_event_counts=totals,
914
+ )
915
+ except Exception:
916
+ pass
917
+
918
+ async def shutdown_all(self) -> None:
919
+ """Shutdown all plugin runtimes in reverse initialization order."""
920
+ # Shutdown in reverse order
921
+ for name in reversed(self.initialization_order):
922
+ if name in self.runtimes:
923
+ runtime = self.runtimes[name]
924
+ try:
925
+ await runtime.shutdown()
926
+ except Exception as e:
927
+ logger.error(
928
+ "plugin_shutdown_failed",
929
+ plugin=name,
930
+ error=str(e),
931
+ exc_info=e,
932
+ category="plugin",
933
+ )
934
+
935
+ # Clear runtimes
936
+ self.runtimes.clear()
937
+
938
+ def get_runtime(self, name: str) -> Any | None:
939
+ """Get a plugin runtime by name.
940
+
941
+ Args:
942
+ name: Plugin name
943
+
944
+ Returns:
945
+ Plugin runtime or None
946
+ """
947
+ return self.runtimes.get(name)
948
+
949
+ def list_plugins(self) -> list[str]:
950
+ """List all registered plugin names.
951
+
952
+ Returns:
953
+ List of plugin names
954
+ """
955
+ return list(self.factories.keys())
956
+
957
+ def list_provider_plugins(self) -> list[str]:
958
+ """List all registered provider plugin names.
959
+
960
+ Returns:
961
+ List of provider plugin names
962
+ """
963
+ return [
964
+ name
965
+ for name, factory in self.factories.items()
966
+ if factory.get_manifest().is_provider
967
+ ]