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,211 @@
1
+ """Lightweight CLI discovery for plugin command registration.
2
+
3
+ This module provides minimal plugin discovery specifically for CLI command
4
+ registration, loading only plugin manifests without full initialization.
5
+ """
6
+
7
+ import importlib.util
8
+ import sys
9
+ from importlib.metadata import entry_points
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import structlog
14
+
15
+ from ccproxy.core.plugins.declaration import PluginManifest
16
+ from ccproxy.core.plugins.discovery import (
17
+ PluginFilter,
18
+ build_combined_plugin_denylist,
19
+ )
20
+ from ccproxy.core.plugins.interfaces import PluginFactory
21
+
22
+
23
+ logger = structlog.get_logger(__name__)
24
+
25
+
26
+ def discover_plugin_cli_extensions(
27
+ settings: Any | None = None,
28
+ ) -> list[tuple[str, PluginManifest]]:
29
+ """Lightweight discovery of plugin CLI extensions.
30
+
31
+ Only loads plugin factories and manifests, no runtime initialization.
32
+ Used during CLI app creation to register plugin commands/arguments.
33
+
34
+ Args:
35
+ settings: Optional settings object to filter plugins
36
+
37
+ Returns:
38
+ List of (plugin_name, manifest) tuples for plugins with CLI extensions.
39
+ """
40
+ plugin_manifests = []
41
+
42
+ # Discover from filesystem (plugins/ directory)
43
+ try:
44
+ filesystem_manifests = _discover_filesystem_cli_extensions()
45
+ plugin_manifests.extend(filesystem_manifests)
46
+ except Exception as e:
47
+ logger.debug("filesystem_cli_discovery_failed", error=str(e))
48
+
49
+ # Discover from entry points
50
+ try:
51
+ entry_point_manifests = _discover_entry_point_cli_extensions()
52
+ plugin_manifests.extend(entry_point_manifests)
53
+ except Exception as e:
54
+ logger.debug("entry_point_cli_discovery_failed", error=str(e))
55
+
56
+ # Remove duplicates (filesystem takes precedence)
57
+ seen_names = set()
58
+ unique_manifests = []
59
+ for name, manifest in plugin_manifests:
60
+ if name not in seen_names:
61
+ unique_manifests.append((name, manifest))
62
+ seen_names.add(name)
63
+
64
+ # Apply plugin filtering if settings provided
65
+ if settings is not None:
66
+ combined_denylist = build_combined_plugin_denylist(
67
+ getattr(settings, "disabled_plugins", None),
68
+ getattr(settings, "plugins", None),
69
+ )
70
+
71
+ plugin_filter = PluginFilter(
72
+ enabled_plugins=getattr(settings, "enabled_plugins", None),
73
+ disabled_plugins=combined_denylist,
74
+ )
75
+
76
+ filtered_manifests = []
77
+ for name, manifest in unique_manifests:
78
+ if plugin_filter.is_enabled(name):
79
+ filtered_manifests.append((name, manifest))
80
+ else:
81
+ logger.debug(
82
+ "plugin_cli_extension_disabled", plugin=name, category="plugin"
83
+ )
84
+
85
+ return filtered_manifests
86
+
87
+ return unique_manifests
88
+
89
+
90
+ def _discover_filesystem_cli_extensions() -> list[tuple[str, PluginManifest]]:
91
+ """Discover CLI extensions from filesystem plugins/ directories."""
92
+ manifests: list[tuple[str, PluginManifest]] = []
93
+
94
+ # Check local plugins/
95
+ plugins_dirs = [
96
+ Path("plugins"),
97
+ ]
98
+
99
+ for plugins_dir in plugins_dirs:
100
+ if not plugins_dir.exists():
101
+ continue
102
+
103
+ manifests.extend(_discover_plugins_in_directory(plugins_dir))
104
+
105
+ return manifests
106
+
107
+
108
+ def _discover_plugins_in_directory(
109
+ plugins_dir: Path,
110
+ ) -> list[tuple[str, PluginManifest]]:
111
+ """Discover CLI extensions from a specific plugins directory."""
112
+ manifests: list[tuple[str, PluginManifest]] = []
113
+
114
+ for plugin_path in plugins_dir.iterdir():
115
+ if not plugin_path.is_dir() or plugin_path.name.startswith("_"):
116
+ continue
117
+
118
+ plugin_file = plugin_path / "plugin.py"
119
+ if not plugin_file.exists():
120
+ continue
121
+
122
+ try:
123
+ factory = _load_plugin_factory_from_file(plugin_file)
124
+ if factory:
125
+ manifest = factory.get_manifest()
126
+ if manifest.cli_commands or manifest.cli_arguments:
127
+ manifests.append((manifest.name, manifest))
128
+ except Exception as e:
129
+ logger.debug(
130
+ "filesystem_plugin_cli_discovery_failed",
131
+ plugin=plugin_path.name,
132
+ error=str(e),
133
+ )
134
+
135
+ return manifests
136
+
137
+
138
+ def _discover_entry_point_cli_extensions() -> list[tuple[str, PluginManifest]]:
139
+ """Discover CLI extensions from installed entry points."""
140
+ manifests: list[tuple[str, PluginManifest]] = []
141
+
142
+ try:
143
+ plugin_entries = entry_points(group="ccproxy.plugins")
144
+ except Exception:
145
+ return manifests
146
+
147
+ for entry_point in plugin_entries:
148
+ try:
149
+ factory_or_callable = entry_point.load()
150
+
151
+ # Handle both factory instances and factory callables
152
+ if callable(factory_or_callable) and not isinstance(
153
+ factory_or_callable, PluginFactory
154
+ ):
155
+ factory = factory_or_callable()
156
+ else:
157
+ factory = factory_or_callable
158
+
159
+ if isinstance(factory, PluginFactory):
160
+ manifest = factory.get_manifest()
161
+ if manifest.cli_commands or manifest.cli_arguments:
162
+ manifests.append((manifest.name, manifest))
163
+ except Exception as e:
164
+ logger.debug(
165
+ "entry_point_plugin_cli_discovery_failed",
166
+ entry_point=entry_point.name,
167
+ error=str(e),
168
+ )
169
+
170
+ return manifests
171
+
172
+
173
+ def _load_plugin_factory_from_file(plugin_file: Path) -> PluginFactory | None:
174
+ """Load plugin factory from a plugin.py file."""
175
+ try:
176
+ # Use proper package naming for ccproxy plugins
177
+ plugin_name = plugin_file.parent.name
178
+
179
+ # Check if it's in ccproxy/plugins/ structure
180
+ if "ccproxy/plugins" in str(plugin_file):
181
+ module_name = f"ccproxy.plugins.{plugin_name}.plugin"
182
+ else:
183
+ module_name = f"plugin_{plugin_name}"
184
+
185
+ spec = importlib.util.spec_from_file_location(module_name, plugin_file)
186
+ if not spec or not spec.loader:
187
+ return None
188
+
189
+ module = importlib.util.module_from_spec(spec)
190
+
191
+ # Temporarily add to sys.modules for relative imports
192
+ old_module = sys.modules.get(spec.name)
193
+ sys.modules[spec.name] = module
194
+
195
+ try:
196
+ spec.loader.exec_module(module)
197
+ factory = getattr(module, "factory", None)
198
+
199
+ if isinstance(factory, PluginFactory):
200
+ return factory
201
+ finally:
202
+ # Restore original module or remove
203
+ if old_module is not None:
204
+ sys.modules[spec.name] = old_module
205
+ else:
206
+ sys.modules.pop(spec.name, None)
207
+
208
+ except Exception:
209
+ pass
210
+
211
+ return None
@@ -0,0 +1,455 @@
1
+ """Plugin declaration system for static plugin specification.
2
+
3
+ This module provides the declaration layer of the plugin system, allowing plugins
4
+ to specify their requirements and capabilities at declaration time (app creation)
5
+ rather than runtime (lifespan).
6
+ """
7
+
8
+ from collections.abc import Awaitable, Callable
9
+ from dataclasses import dataclass, field
10
+ from enum import IntEnum
11
+ from typing import TYPE_CHECKING, Any, Protocol, TypeVar
12
+
13
+ import httpx
14
+ import structlog
15
+ from fastapi import APIRouter, FastAPI
16
+ from pydantic import BaseModel
17
+ from starlette.middleware.base import BaseHTTPMiddleware
18
+
19
+ from ccproxy.services.adapters.format_adapter import FormatAdapterProtocol
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from ccproxy.auth.oauth.registry import OAuthRegistry
24
+ from ccproxy.config.settings import Settings
25
+ from ccproxy.core.plugins import PluginRegistry
26
+ from ccproxy.core.plugins.hooks.base import Hook
27
+ from ccproxy.core.plugins.hooks.manager import HookManager
28
+ from ccproxy.core.plugins.hooks.registry import HookRegistry
29
+ from ccproxy.core.plugins.protocol import OAuthClientProtocol
30
+ from ccproxy.scheduler.core import Scheduler
31
+ from ccproxy.scheduler.tasks import BaseScheduledTask
32
+ from ccproxy.services.adapters.base import BaseAdapter
33
+ from ccproxy.services.cli_detection import CLIDetectionService
34
+ from ccproxy.services.interfaces import (
35
+ IMetricsCollector,
36
+ IRequestTracer,
37
+ StreamingMetrics,
38
+ )
39
+
40
+ T = TypeVar("T")
41
+
42
+ # Type aliases for format adapter system
43
+ FormatPair = tuple[str, str]
44
+
45
+
46
+ @dataclass
47
+ class FormatAdapterSpec:
48
+ """Specification for format adapter registration."""
49
+
50
+ from_format: str
51
+ to_format: str
52
+ adapter_factory: Callable[
53
+ [], FormatAdapterProtocol | Awaitable[FormatAdapterProtocol]
54
+ ]
55
+ priority: int = 100 # Lower = higher priority for conflict resolution
56
+ description: str = ""
57
+
58
+ def __post_init__(self) -> None:
59
+ """Validate specification."""
60
+ if not self.from_format or not self.to_format:
61
+ raise ValueError("Format names cannot be empty") from None
62
+ if self.from_format == self.to_format:
63
+ raise ValueError("from_format and to_format cannot be the same") from None
64
+
65
+ @property
66
+ def format_pair(self) -> FormatPair:
67
+ """Get the format pair tuple."""
68
+ return (self.from_format, self.to_format)
69
+
70
+
71
+ class MiddlewareLayer(IntEnum):
72
+ """Middleware layers for ordering."""
73
+
74
+ SECURITY = 100 # Authentication, rate limiting
75
+ OBSERVABILITY = 200 # Logging, metrics
76
+ TRANSFORMATION = 300 # Compression, encoding
77
+ ROUTING = 400 # Path rewriting, proxy
78
+ APPLICATION = 500 # Business logic
79
+
80
+
81
+ @dataclass
82
+ class MiddlewareSpec:
83
+ """Specification for plugin middleware."""
84
+
85
+ middleware_class: type[BaseHTTPMiddleware]
86
+ priority: int = MiddlewareLayer.APPLICATION
87
+ kwargs: dict[str, Any] = field(default_factory=dict)
88
+
89
+ def __lt__(self, other: "MiddlewareSpec") -> bool:
90
+ """Sort by priority (lower values first)."""
91
+ return self.priority < other.priority
92
+
93
+
94
+ @dataclass
95
+ class RouterSpec:
96
+ """Specification for individual routers in a plugin."""
97
+
98
+ router: APIRouter | Callable[[], APIRouter]
99
+ prefix: str
100
+ tags: list[str] = field(default_factory=list)
101
+ dependencies: list[Any] = field(default_factory=list)
102
+
103
+
104
+ @dataclass
105
+ class RouteSpec:
106
+ """Specification for plugin routes."""
107
+
108
+ router: APIRouter
109
+ prefix: str
110
+ tags: list[str] = field(default_factory=list)
111
+ dependencies: list[Any] = field(default_factory=list)
112
+
113
+
114
+ @dataclass
115
+ class TaskSpec:
116
+ """Specification for scheduled tasks."""
117
+
118
+ task_name: str
119
+ task_type: str
120
+ task_class: type["BaseScheduledTask"] # BaseScheduledTask type from scheduler.tasks
121
+ interval_seconds: float
122
+ enabled: bool = True
123
+ kwargs: dict[str, Any] = field(default_factory=dict)
124
+
125
+
126
+ @dataclass
127
+ class HookSpec:
128
+ """Specification for plugin hooks."""
129
+
130
+ hook_class: type["Hook"] # Hook type from hooks.base
131
+ kwargs: dict[str, Any] = field(default_factory=dict)
132
+
133
+
134
+ @dataclass
135
+ class AuthCommandSpec:
136
+ """Specification for auth commands."""
137
+
138
+ command_name: str
139
+ description: str
140
+ handler: Callable[..., Any]
141
+ options: dict[str, Any] = field(default_factory=dict)
142
+
143
+
144
+ @dataclass
145
+ class CliCommandSpec:
146
+ """Specification for plugin CLI commands."""
147
+
148
+ command_name: str
149
+ command_function: Callable[..., Any]
150
+ help_text: str = ""
151
+ parent_command: str | None = None # For subcommands like "auth login-myservice"
152
+
153
+ def __post_init__(self) -> None:
154
+ """Validate CLI command specification."""
155
+ if not self.command_name:
156
+ raise ValueError("command_name cannot be empty") from None
157
+ if not callable(self.command_function):
158
+ raise ValueError("command_function must be callable") from None
159
+
160
+
161
+ @dataclass
162
+ class CliArgumentSpec:
163
+ """Specification for adding arguments to existing commands."""
164
+
165
+ target_command: str # e.g., "serve", "auth"
166
+ argument_name: str
167
+ argument_type: type = str
168
+ help_text: str = ""
169
+ default: Any = None
170
+ required: bool = False
171
+ typer_kwargs: dict[str, Any] = field(default_factory=dict)
172
+
173
+ def __post_init__(self) -> None:
174
+ """Validate CLI argument specification."""
175
+ if not self.target_command:
176
+ raise ValueError("target_command cannot be empty") from None
177
+ if not self.argument_name:
178
+ raise ValueError("argument_name cannot be empty") from None
179
+
180
+
181
+ @dataclass
182
+ class PluginManifest:
183
+ """Complete static declaration of a plugin's capabilities.
184
+
185
+ This manifest is created at module import time and contains all
186
+ static information needed to integrate the plugin into the application.
187
+ """
188
+
189
+ # Basic metadata
190
+ name: str
191
+ version: str
192
+ description: str = ""
193
+ dependencies: list[str] = field(default_factory=list)
194
+
195
+ # Plugin type
196
+ is_provider: bool = False # True for provider plugins, False for system plugins
197
+
198
+ # Service declarations
199
+ provides: list[str] = field(default_factory=list) # Services this plugin provides
200
+ requires: list[str] = field(default_factory=list) # Required service dependencies
201
+ optional_requires: list[str] = field(
202
+ default_factory=list
203
+ ) # Optional service dependencies
204
+
205
+ # Static specifications
206
+ middleware: list[MiddlewareSpec] = field(default_factory=list)
207
+ routes: list[RouteSpec] = field(default_factory=list)
208
+ tasks: list[TaskSpec] = field(default_factory=list)
209
+ hooks: list[HookSpec] = field(default_factory=list)
210
+ auth_commands: list[AuthCommandSpec] = field(default_factory=list)
211
+
212
+ # Configuration
213
+ config_class: type[BaseModel] | None = None
214
+ tool_accumulator_class: type | None = None
215
+
216
+ # OAuth support (for provider plugins)
217
+ oauth_client_factory: Callable[[], "OAuthClientProtocol"] | None = (
218
+ None # Returns OAuthClientProtocol
219
+ )
220
+ oauth_provider_factory: Callable[[], Any] | None = (
221
+ None # Returns OAuthProviderProtocol
222
+ )
223
+ token_manager_factory: Callable[[], Any] | None = (
224
+ None # Returns TokenManager for the provider
225
+ )
226
+ oauth_config_class: type[BaseModel] | None = None # OAuth configuration model
227
+ oauth_routes: list[RouteSpec] = field(
228
+ default_factory=list
229
+ ) # Plugin-specific OAuth routes
230
+
231
+ # Format adapter declarations
232
+ format_adapters: list[FormatAdapterSpec] = field(default_factory=list)
233
+ requires_format_adapters: list[FormatPair] = field(default_factory=list)
234
+
235
+ # CLI extensions
236
+ cli_commands: list[CliCommandSpec] = field(default_factory=list)
237
+ cli_arguments: list[CliArgumentSpec] = field(default_factory=list)
238
+
239
+ def validate_dependencies(self, available_plugins: set[str]) -> list[str]:
240
+ """Validate that all dependencies are available.
241
+
242
+ Args:
243
+ available_plugins: Set of available plugin names
244
+
245
+ Returns:
246
+ List of missing dependencies
247
+ """
248
+ return [dep for dep in self.dependencies if dep not in available_plugins]
249
+
250
+ def validate_service_dependencies(self, available_services: set[str]) -> list[str]:
251
+ """Validate that required services are available.
252
+
253
+ Args:
254
+ available_services: Set of available service names
255
+
256
+ Returns:
257
+ List of missing required services
258
+ """
259
+ missing = []
260
+ for required in self.requires:
261
+ if required not in available_services:
262
+ missing.append(required)
263
+ return missing
264
+
265
+ def get_sorted_middleware(self) -> list[MiddlewareSpec]:
266
+ """Get middleware sorted by priority."""
267
+ return sorted(self.middleware)
268
+
269
+ def validate_format_adapter_requirements(
270
+ self, available_adapters: set[FormatPair]
271
+ ) -> list[FormatPair]:
272
+ """Validate that required format adapters are available."""
273
+ return [
274
+ req
275
+ for req in self.requires_format_adapters
276
+ if req not in available_adapters
277
+ ]
278
+
279
+
280
+ class PluginContext:
281
+ """Context provided to plugin runtime during initialization."""
282
+
283
+ def __init__(self) -> None:
284
+ """Initialize plugin context."""
285
+ # Application settings
286
+ self.settings: Settings | None = None
287
+ self.http_client: httpx.AsyncClient | None = None
288
+ self.logger: structlog.BoundLogger | None = None
289
+ self.scheduler: Scheduler | None = None
290
+ self.config: BaseModel | None = None
291
+ self.cli_detection_service: CLIDetectionService | None = None
292
+ self.plugin_registry: PluginRegistry | None = None
293
+
294
+ # Core app and hook system
295
+ self.app: FastAPI | None = None
296
+ self.hook_registry: HookRegistry | None = None
297
+ self.hook_manager: HookManager | None = None
298
+
299
+ # Observability and streaming
300
+ self.request_tracer: IRequestTracer | None = None
301
+ self.streaming_handler: StreamingMetrics | None = None
302
+ self.metrics: IMetricsCollector | None = None
303
+
304
+ # Provider-specific
305
+ self.adapter: BaseAdapter | None = None
306
+ self.detection_service: Any = None
307
+ self.credentials_manager: Any = None
308
+ self.oauth_registry: OAuthRegistry | None = None
309
+ self.http_pool_manager: Any = None
310
+ self.service_container: Any = None
311
+ self.auth_provider: Any = None
312
+ self.token_manager: Any = None
313
+ self.storage: Any = None
314
+
315
+ self.format_registry: Any = None
316
+ self.model_mapper: Any = None
317
+
318
+ # Testing/utilities
319
+ self.proxy_service: Any = None
320
+
321
+ # Internal service mapping for type-safe access
322
+ self._service_map: dict[type[Any], str] = {}
323
+ self._initialize_service_map()
324
+
325
+ def _initialize_service_map(self) -> None:
326
+ """Initialize the service type mapping."""
327
+ if TYPE_CHECKING:
328
+ pass
329
+
330
+ # Map service types to their attribute names
331
+ self._service_map = {
332
+ # Core services - using Any to avoid circular imports at runtime
333
+ **(
334
+ {}
335
+ if TYPE_CHECKING
336
+ else {
337
+ type(None): "settings", # Placeholder, will be populated at runtime
338
+ }
339
+ ),
340
+ httpx.AsyncClient: "http_client",
341
+ structlog.BoundLogger: "logger",
342
+ BaseModel: "config",
343
+ }
344
+
345
+ def get_service(self, service_type: type[T]) -> T:
346
+ """Get a service instance by type with proper type safety.
347
+
348
+ Args:
349
+ service_type: The type of service to retrieve
350
+
351
+ Returns:
352
+ The service instance
353
+
354
+ Raises:
355
+ ValueError: If the service is not available
356
+ """
357
+ # Create service mappings dynamically to access current values
358
+ service_mappings: dict[type[Any], Any] = {}
359
+
360
+ # Common concrete types
361
+ if self.settings is not None:
362
+ service_mappings[type(self.settings)] = self.settings
363
+ if self.http_client is not None:
364
+ service_mappings[httpx.AsyncClient] = self.http_client
365
+ if self.logger is not None:
366
+ service_mappings[structlog.BoundLogger] = self.logger
367
+ if self.config is not None:
368
+ service_mappings[type(self.config)] = self.config
369
+ service_mappings[BaseModel] = self.config
370
+
371
+ # Check if service type directly matches a known service
372
+ if service_type in service_mappings:
373
+ return service_mappings[service_type] # type: ignore[no-any-return]
374
+
375
+ # Check all attributes for an instance of the requested type
376
+ for attr_name in dir(self):
377
+ if not attr_name.startswith("_"): # Skip private attributes
378
+ attr_value = getattr(self, attr_name)
379
+ if attr_value is not None and isinstance(attr_value, service_type):
380
+ return attr_value # type: ignore[no-any-return]
381
+
382
+ # Service not found
383
+ type_name = getattr(service_type, "__name__", str(service_type))
384
+ raise ValueError(f"Service {type_name} not available in plugin context")
385
+
386
+ def get(self, key_or_type: type[T] | str, default: Any = None) -> T | Any:
387
+ """Get service by type (new) or by string key (backward compatibility).
388
+
389
+ Args:
390
+ key_or_type: Service type for type-safe access or string key for compatibility
391
+ default: Default value for string-based access (ignored for type-safe access)
392
+
393
+ Returns:
394
+ Service instance for type-safe access, or attribute value for string access
395
+ """
396
+ if isinstance(key_or_type, str):
397
+ # Backward compatibility: string-based access
398
+ return getattr(self, key_or_type, default)
399
+ else:
400
+ # Type-safe access
401
+ return self.get_service(key_or_type)
402
+
403
+ def get_attr(self, key: str, default: Any = None) -> Any:
404
+ """Get attribute by string name - for backward compatibility.
405
+
406
+ Args:
407
+ key: String attribute name
408
+ default: Default value if attribute not found
409
+
410
+ Returns:
411
+ Attribute value or default
412
+ """
413
+ return getattr(self, key, default)
414
+
415
+ def __getitem__(self, key: str) -> Any:
416
+ """Backward compatibility: Allow dictionary-style access."""
417
+ return getattr(self, key, None)
418
+
419
+ def __setitem__(self, key: str, value: Any) -> None:
420
+ """Backward compatibility: Allow dictionary-style assignment."""
421
+ setattr(self, key, value)
422
+
423
+ def __contains__(self, key: str) -> bool:
424
+ """Backward compatibility: Support 'key in context' checks."""
425
+ return hasattr(self, key) and getattr(self, key) is not None
426
+
427
+ def keys(self) -> list[str]:
428
+ """Backward compatibility: Return list of available service keys."""
429
+ return [
430
+ attr
431
+ for attr in dir(self)
432
+ if not attr.startswith("_")
433
+ and not callable(getattr(self, attr))
434
+ and getattr(self, attr) is not None
435
+ ]
436
+
437
+
438
+ class PluginRuntimeProtocol(Protocol):
439
+ """Protocol for plugin runtime instances."""
440
+
441
+ async def initialize(self, context: PluginContext) -> None:
442
+ """Initialize the plugin with runtime context."""
443
+ ...
444
+
445
+ async def shutdown(self) -> None:
446
+ """Cleanup on shutdown."""
447
+ ...
448
+
449
+ async def validate(self) -> bool:
450
+ """Validate plugin is ready."""
451
+ ...
452
+
453
+ async def health_check(self) -> dict[str, Any]:
454
+ """Perform health check."""
455
+ ...