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,695 @@
1
+ """Generate scaffold files for new CCProxy plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from textwrap import dedent
10
+
11
+
12
+ class PluginTemplateType(str, Enum):
13
+ """Supported plugin template variants."""
14
+
15
+ SYSTEM = "system"
16
+ PROVIDER = "provider"
17
+ AUTH = "auth"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class TemplateContext:
22
+ """Precomputed naming helpers for template rendering."""
23
+
24
+ plugin_name: str
25
+ pascal_name: str
26
+ description: str
27
+ version: str
28
+
29
+ @property
30
+ def config_class(self) -> str:
31
+ return f"{self.pascal_name}Config"
32
+
33
+ @property
34
+ def runtime_class(self) -> str:
35
+ return f"{self.pascal_name}Runtime"
36
+
37
+ @property
38
+ def factory_class(self) -> str:
39
+ return f"{self.pascal_name}Factory"
40
+
41
+ @property
42
+ def adapter_class(self) -> str:
43
+ return f"{self.pascal_name}Adapter"
44
+
45
+ @property
46
+ def detection_class(self) -> str:
47
+ return f"{self.pascal_name}DetectionService"
48
+
49
+ @property
50
+ def oauth_provider_class(self) -> str:
51
+ return f"{self.pascal_name}OAuthProvider"
52
+
53
+
54
+ def build_plugin_scaffold(
55
+ plugin_name: str,
56
+ description: str,
57
+ version: str,
58
+ template_type: PluginTemplateType,
59
+ *,
60
+ include_tests: bool = False,
61
+ ) -> dict[str, str]:
62
+ """Return a mapping of relative file paths to scaffold contents."""
63
+
64
+ pascal_name = _to_pascal_case(plugin_name)
65
+ ctx = TemplateContext(
66
+ plugin_name=plugin_name,
67
+ pascal_name=pascal_name,
68
+ description=description,
69
+ version=version,
70
+ )
71
+
72
+ builders: dict[
73
+ PluginTemplateType, Callable[[TemplateContext, bool], dict[str, str]]
74
+ ] = {
75
+ PluginTemplateType.SYSTEM: _build_system_template,
76
+ PluginTemplateType.PROVIDER: _build_provider_template,
77
+ PluginTemplateType.AUTH: _build_auth_template,
78
+ }
79
+
80
+ builder = builders.get(template_type)
81
+ if builder is None:
82
+ raise ValueError(f"Unsupported template type: {template_type}")
83
+
84
+ return builder(ctx, include_tests)
85
+
86
+
87
+ def _build_system_template(ctx: TemplateContext, include_tests: bool) -> dict[str, str]:
88
+ """Build files for a system plugin scaffold."""
89
+
90
+ files: dict[str, str] = {
91
+ "__init__.py": dedent(
92
+ f'''
93
+ """Runtime package for {ctx.pascal_name} plugin."""
94
+
95
+ from .plugin import factory
96
+
97
+ __all__ = ["factory"]
98
+ '''
99
+ ).strip()
100
+ + "\n",
101
+ "config.py": dedent(
102
+ f'''
103
+ """Configuration for the {ctx.pascal_name} plugin."""
104
+
105
+ from pydantic import BaseModel, Field
106
+
107
+
108
+ class {ctx.config_class}(BaseModel):
109
+ """Runtime configuration toggles for {ctx.plugin_name}."""
110
+
111
+ enabled: bool = Field(
112
+ default=True,
113
+ description="Enable the plugin once configured.",
114
+ )
115
+ '''
116
+ ).strip()
117
+ + "\n",
118
+ "plugin.py": dedent(
119
+ f'''
120
+ """System plugin runtime for {ctx.plugin_name}."""
121
+
122
+ from __future__ import annotations
123
+
124
+ from ccproxy.core.logging import get_plugin_logger
125
+ from ccproxy.core.plugins import (
126
+ PluginManifest,
127
+ SystemPluginFactory,
128
+ SystemPluginRuntime,
129
+ )
130
+
131
+ from .config import {ctx.config_class}
132
+
133
+
134
+ logger = get_plugin_logger()
135
+
136
+
137
+ class {ctx.runtime_class}(SystemPluginRuntime):
138
+ """Runtime implementation for {ctx.plugin_name}."""
139
+
140
+ def __init__(self, manifest: PluginManifest) -> None:
141
+ super().__init__(manifest)
142
+ self.config: {ctx.config_class} | None = None
143
+
144
+ async def _on_initialize(self) -> None:
145
+ await super()._on_initialize()
146
+ if not self.context:
147
+ raise RuntimeError("Plugin context is not available")
148
+
149
+ try:
150
+ self.config = self.context.get({ctx.config_class})
151
+ except ValueError:
152
+ self.config = {ctx.config_class}()
153
+ logger.debug(
154
+ "plugin_using_default_config",
155
+ plugin=self.name,
156
+ )
157
+
158
+ if not self.config.enabled:
159
+ logger.info("plugin_disabled", plugin=self.name)
160
+ return
161
+
162
+ logger.info("plugin_initialized", plugin=self.name)
163
+
164
+
165
+ class {ctx.factory_class}(SystemPluginFactory):
166
+ """Factory for the {ctx.plugin_name} system plugin."""
167
+
168
+ def __init__(self) -> None:
169
+ manifest = PluginManifest(
170
+ name="{ctx.plugin_name}",
171
+ version="{ctx.version}",
172
+ description="{ctx.description}",
173
+ is_provider=False,
174
+ config_class={ctx.config_class},
175
+ )
176
+ super().__init__(manifest)
177
+
178
+ def create_runtime(self) -> {ctx.runtime_class}:
179
+ return {ctx.runtime_class}(self.manifest)
180
+
181
+
182
+ factory = {ctx.factory_class}()
183
+ '''
184
+ ).strip()
185
+ + "\n",
186
+ "README.md": dedent(
187
+ f"""
188
+ # {ctx.pascal_name} Plugin
189
+
190
+ Generated with `ccproxy plugins scaffold {ctx.plugin_name}`.
191
+
192
+ ## Next steps
193
+
194
+ - Update `config.py` with any required settings.
195
+ - Implement runtime logic in `plugin.py`.
196
+ - Add tests under `tests/` to cover the behaviour you introduce.
197
+ """
198
+ ).strip()
199
+ + "\n",
200
+ }
201
+
202
+ if include_tests:
203
+ files.update(
204
+ {
205
+ "tests/__init__.py": "\n",
206
+ f"tests/test_{ctx.plugin_name}.py": dedent(
207
+ f'''
208
+ """Smoke tests for the {ctx.plugin_name} plugin scaffold."""
209
+
210
+ from ccproxy.plugins.{ctx.plugin_name}.plugin import factory
211
+
212
+
213
+ def test_manifest_defaults() -> None:
214
+ manifest = factory.get_manifest()
215
+ assert manifest.name == "{ctx.plugin_name}"
216
+ assert manifest.version == "{ctx.version}"
217
+ '''
218
+ ).strip()
219
+ + "\n",
220
+ }
221
+ )
222
+
223
+ return files
224
+
225
+
226
+ def _build_provider_template(
227
+ ctx: TemplateContext, include_tests: bool
228
+ ) -> dict[str, str]:
229
+ """Build files for a provider plugin scaffold."""
230
+
231
+ files: dict[str, str] = {
232
+ "__init__.py": dedent(
233
+ f'''
234
+ """Provider plugin package for {ctx.pascal_name}."""
235
+
236
+ from .plugin import factory
237
+
238
+ __all__ = ["factory"]
239
+ '''
240
+ ).strip()
241
+ + "\n",
242
+ "config.py": dedent(
243
+ f'''
244
+ """Configuration for the {ctx.pascal_name} provider."""
245
+
246
+ from pydantic import BaseModel, Field, HttpUrl
247
+
248
+
249
+ class {ctx.config_class}(BaseModel):
250
+ """Runtime configuration options for {ctx.plugin_name}."""
251
+
252
+ enabled: bool = Field(
253
+ default=False,
254
+ description="Enable once adapter logic has been implemented.",
255
+ )
256
+ base_url: HttpUrl = Field(
257
+ default="https://api.example.com",
258
+ description="Upstream API base URL.",
259
+ )
260
+ supports_streaming: bool = Field(
261
+ default=False,
262
+ description="Set to true when streaming is implemented.",
263
+ )
264
+ requires_auth: bool = Field(
265
+ default=True,
266
+ description="Whether requests need authentication headers.",
267
+ )
268
+ '''
269
+ ).strip()
270
+ + "\n",
271
+ "adapter.py": dedent(
272
+ f'''
273
+ """HTTP adapter stub for {ctx.plugin_name}."""
274
+
275
+ from __future__ import annotations
276
+
277
+ from typing import Any
278
+
279
+ from fastapi import HTTPException, Request
280
+ from starlette.responses import JSONResponse
281
+
282
+ from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
283
+ from ccproxy.streaming import DeferredStreaming
284
+
285
+
286
+ class {ctx.adapter_class}(BaseHTTPAdapter):
287
+ """Minimal adapter that surfaces implementation TODOs."""
288
+
289
+ async def handle_request(
290
+ self, request: Request
291
+ ) -> JSONResponse | DeferredStreaming:
292
+ raise HTTPException(
293
+ status_code=503,
294
+ detail="{ctx.plugin_name} adapter is not implemented yet.",
295
+ )
296
+
297
+ async def handle_streaming(
298
+ self,
299
+ request: Request,
300
+ endpoint: str,
301
+ **_: Any,
302
+ ) -> DeferredStreaming:
303
+ raise HTTPException(
304
+ status_code=503,
305
+ detail=(
306
+ "Streaming for {ctx.plugin_name} is not implemented yet."
307
+ ),
308
+ )
309
+
310
+ async def cleanup(self) -> None:
311
+ """Adapter cleanup hook."""
312
+
313
+ return None
314
+ '''
315
+ ).strip()
316
+ + "\n",
317
+ "plugin.py": dedent(
318
+ f'''
319
+ """Provider plugin runtime for {ctx.plugin_name}."""
320
+
321
+ from __future__ import annotations
322
+
323
+ from ccproxy.core.logging import get_plugin_logger
324
+ from ccproxy.core.plugins import (
325
+ BaseProviderPluginFactory,
326
+ PluginContext,
327
+ PluginManifest,
328
+ ProviderPluginRuntime,
329
+ )
330
+ from ccproxy.models.provider import ProviderConfig
331
+
332
+ from .adapter import {ctx.adapter_class}
333
+ from .config import {ctx.config_class}
334
+
335
+
336
+ logger = get_plugin_logger()
337
+
338
+
339
+ class {ctx.runtime_class}(ProviderPluginRuntime):
340
+ """Runtime implementation for {ctx.plugin_name}."""
341
+
342
+ def __init__(self, manifest: PluginManifest) -> None:
343
+ super().__init__(manifest)
344
+ self.config: {ctx.config_class} | None = None
345
+
346
+ async def _on_initialize(self) -> None:
347
+ if not self.context:
348
+ raise RuntimeError("Plugin context is not available")
349
+
350
+ try:
351
+ self.config = self.context.get({ctx.config_class})
352
+ except ValueError:
353
+ self.config = {ctx.config_class}()
354
+
355
+ if not self.config.enabled:
356
+ logger.info("plugin_disabled", plugin=self.name)
357
+ return
358
+
359
+ await super()._on_initialize()
360
+ logger.info("plugin_initialized", plugin=self.name)
361
+
362
+ async def _on_validate(self) -> bool:
363
+ config = self.config or {ctx.config_class}()
364
+ if not config.enabled:
365
+ return True
366
+ return await super()._on_validate()
367
+
368
+
369
+ class {ctx.factory_class}(BaseProviderPluginFactory):
370
+ """Factory for the {ctx.plugin_name} provider plugin."""
371
+
372
+ plugin_name = "{ctx.plugin_name}"
373
+ plugin_description = "{ctx.description}"
374
+ plugin_version = "{ctx.version}"
375
+ runtime_class = {ctx.runtime_class}
376
+ adapter_class = {ctx.adapter_class}
377
+ config_class = {ctx.config_class}
378
+
379
+ async def create_adapter(
380
+ self, context: PluginContext
381
+ ) -> {ctx.adapter_class}:
382
+ config = context.get({ctx.config_class})
383
+ provider_config = ProviderConfig(
384
+ name=self.plugin_name,
385
+ base_url=str(config.base_url),
386
+ supports_streaming=config.supports_streaming,
387
+ requires_auth=config.requires_auth,
388
+ )
389
+
390
+ return {ctx.adapter_class}(
391
+ config=provider_config,
392
+ auth_manager=None,
393
+ http_pool_manager=context.http_pool_manager,
394
+ streaming_handler=context.streaming_handler,
395
+ format_registry=context.format_registry,
396
+ )
397
+
398
+ def create_detection_service(
399
+ self, context: PluginContext
400
+ ) -> None:
401
+ config = context.get({ctx.config_class})
402
+ if not config.enabled:
403
+ return None
404
+ return None
405
+
406
+ async def create_credentials_manager(
407
+ self, context: PluginContext
408
+ ) -> None:
409
+ config = context.get({ctx.config_class})
410
+ if not config.enabled:
411
+ return None
412
+ return None
413
+
414
+
415
+ factory = {ctx.factory_class}()
416
+ '''
417
+ ).strip()
418
+ + "\n",
419
+ "README.md": dedent(
420
+ f"""
421
+ # {ctx.pascal_name} Provider Plugin
422
+
423
+ Generated with `ccproxy plugins scaffold {ctx.plugin_name} --type provider`.
424
+
425
+ ## Next steps
426
+
427
+ - Fill in `adapter.py` with provider-specific HTTP logic.
428
+ - Wire authentication and detection in `plugin.py`.
429
+ - Provide plugin-specific tests before enabling the plugin.
430
+ """
431
+ ).strip()
432
+ + "\n",
433
+ }
434
+
435
+ if include_tests:
436
+ files.update(
437
+ {
438
+ "tests/__init__.py": "\n",
439
+ f"tests/test_{ctx.plugin_name}_manifest.py": dedent(
440
+ f'''
441
+ """Manifest smoke tests for the {ctx.plugin_name} provider."""
442
+
443
+ from ccproxy.plugins.{ctx.plugin_name}.plugin import factory
444
+
445
+
446
+ def test_manifest_marks_provider() -> None:
447
+ manifest = factory.get_manifest()
448
+ assert manifest.is_provider is True
449
+ '''
450
+ ).strip()
451
+ + "\n",
452
+ }
453
+ )
454
+
455
+ return files
456
+
457
+
458
+ def _build_auth_template(ctx: TemplateContext, include_tests: bool) -> dict[str, str]:
459
+ """Build files for an auth provider plugin scaffold."""
460
+
461
+ files: dict[str, str] = {
462
+ "__init__.py": dedent(
463
+ f'''
464
+ """Auth provider plugin package for {ctx.pascal_name}."""
465
+
466
+ from .plugin import factory
467
+
468
+ __all__ = ["factory"]
469
+ '''
470
+ ).strip()
471
+ + "\n",
472
+ "config.py": dedent(
473
+ f'''
474
+ """Configuration for the {ctx.pascal_name} auth provider."""
475
+
476
+ from pydantic import BaseModel, Field
477
+
478
+
479
+ class {ctx.config_class}(BaseModel):
480
+ """Runtime options for {ctx.plugin_name} OAuth."""
481
+
482
+ enabled: bool = Field(
483
+ default=False,
484
+ description="Enable once OAuth endpoints are implemented.",
485
+ )
486
+ client_id: str = Field(
487
+ default="your-client-id",
488
+ description="OAuth client identifier.",
489
+ )
490
+ client_secret: str = Field(
491
+ default="your-client-secret",
492
+ description="OAuth client secret.",
493
+ )
494
+ auth_base_url: str = Field(
495
+ default="https://auth.example.com",
496
+ description="Base URL for OAuth authorization endpoints.",
497
+ )
498
+ token_url: str = Field(
499
+ default="https://auth.example.com/token",
500
+ description="Token exchange endpoint.",
501
+ )
502
+ '''
503
+ ).strip()
504
+ + "\n",
505
+ "provider.py": dedent(
506
+ f'''
507
+ """OAuth provider stub for {ctx.plugin_name}."""
508
+
509
+ from __future__ import annotations
510
+
511
+ from typing import Any
512
+
513
+ from ccproxy.auth.oauth.registry import OAuthProviderInfo, OAuthProviderProtocol
514
+
515
+
516
+ class {ctx.oauth_provider_class}(OAuthProviderProtocol):
517
+ """Skeleton OAuth provider awaiting implementation."""
518
+
519
+ def __init__(self, *, display_name: str) -> None:
520
+ self._display_name = display_name
521
+
522
+ @property
523
+ def provider_name(self) -> str:
524
+ return "{ctx.plugin_name}"
525
+
526
+ @property
527
+ def provider_display_name(self) -> str:
528
+ return self._display_name
529
+
530
+ @property
531
+ def supports_pkce(self) -> bool:
532
+ return True
533
+
534
+ async def get_authorization_url(
535
+ self,
536
+ state: str,
537
+ code_verifier: str | None = None,
538
+ redirect_uri: str | None = None,
539
+ ) -> str:
540
+ raise NotImplementedError("Build authorization URL logic")
541
+
542
+ async def handle_callback(
543
+ self,
544
+ code: str,
545
+ state: str,
546
+ code_verifier: str | None = None,
547
+ redirect_uri: str | None = None,
548
+ ) -> Any:
549
+ raise NotImplementedError("Exchange auth code for tokens")
550
+
551
+ async def refresh_access_token(self, refresh_token: str) -> Any:
552
+ raise NotImplementedError("Refresh tokens using the provider API")
553
+
554
+ async def revoke_token(self, token: str) -> None:
555
+ raise NotImplementedError("Revoke an issued token if supported")
556
+
557
+ async def get_profile(self, access_token: str) -> Any:
558
+ raise NotImplementedError("Fetch account profile information")
559
+
560
+ def describe(self) -> OAuthProviderInfo:
561
+ return OAuthProviderInfo(
562
+ name=self.provider_name,
563
+ display_name=self.provider_display_name,
564
+ description="{ctx.description}",
565
+ plugin_name="{ctx.plugin_name}",
566
+ )
567
+ '''
568
+ ).strip()
569
+ + "\n",
570
+ "plugin.py": dedent(
571
+ f'''
572
+ """Auth provider plugin runtime for {ctx.plugin_name}."""
573
+
574
+ from __future__ import annotations
575
+
576
+ from ccproxy.core.logging import get_plugin_logger
577
+ from ccproxy.core.plugins import (
578
+ AuthProviderPluginFactory,
579
+ AuthProviderPluginRuntime,
580
+ PluginContext,
581
+ PluginManifest,
582
+ )
583
+
584
+ from .config import {ctx.config_class}
585
+ from .provider import {ctx.oauth_provider_class}
586
+
587
+
588
+ logger = get_plugin_logger()
589
+
590
+
591
+ class {ctx.runtime_class}(AuthProviderPluginRuntime):
592
+ """Runtime implementation for {ctx.plugin_name} OAuth."""
593
+
594
+ def __init__(self, manifest: PluginManifest) -> None:
595
+ super().__init__(manifest)
596
+ self.config: {ctx.config_class} | None = None
597
+
598
+ async def _on_initialize(self) -> None:
599
+ if not self.context:
600
+ raise RuntimeError("Plugin context is not available")
601
+
602
+ try:
603
+ self.config = self.context.get({ctx.config_class})
604
+ except ValueError:
605
+ self.config = {ctx.config_class}()
606
+
607
+ if not self.config.enabled:
608
+ logger.info("plugin_disabled", plugin=self.name)
609
+ return
610
+
611
+ await super()._on_initialize()
612
+ logger.info("plugin_initialized", plugin=self.name)
613
+
614
+
615
+ class {ctx.factory_class}(AuthProviderPluginFactory):
616
+ """Factory for the {ctx.plugin_name} OAuth plugin."""
617
+
618
+ cli_safe = True
619
+
620
+ def __init__(self) -> None:
621
+ manifest = PluginManifest(
622
+ name="{ctx.plugin_name}",
623
+ version="{ctx.version}",
624
+ description="{ctx.description}",
625
+ is_provider=True,
626
+ config_class={ctx.config_class},
627
+ )
628
+ super().__init__(manifest)
629
+
630
+ def create_context(self, service_container: object) -> PluginContext:
631
+ context = super().create_context(service_container)
632
+ config = context.get({ctx.config_class})
633
+
634
+ if config.enabled:
635
+ context["auth_provider"] = self.create_auth_provider(context)
636
+
637
+ return context
638
+
639
+ def create_runtime(self) -> {ctx.runtime_class}:
640
+ return {ctx.runtime_class}(self.manifest)
641
+
642
+ def create_auth_provider(
643
+ self, context: PluginContext | None = None
644
+ ) -> {ctx.oauth_provider_class}:
645
+ return {ctx.oauth_provider_class}(display_name="{ctx.pascal_name}")
646
+
647
+
648
+ factory = {ctx.factory_class}()
649
+ '''
650
+ ).strip()
651
+ + "\n",
652
+ "README.md": dedent(
653
+ f"""
654
+ # {ctx.pascal_name} OAuth Plugin
655
+
656
+ Generated with `ccproxy plugins scaffold {ctx.plugin_name} --type auth`.
657
+
658
+ ## Next steps
659
+
660
+ - Implement the OAuth flow in `provider.py`.
661
+ - Wire storage and credential handling inside `plugin.py`.
662
+ - Provide CLI guidance for user sign-in.
663
+ """
664
+ ).strip()
665
+ + "\n",
666
+ }
667
+
668
+ if include_tests:
669
+ files.update(
670
+ {
671
+ "tests/__init__.py": "\n",
672
+ f"tests/test_{ctx.plugin_name}_auth.py": dedent(
673
+ f'''
674
+ """Manifest smoke tests for the {ctx.plugin_name} auth plugin."""
675
+
676
+ from ccproxy.plugins.{ctx.plugin_name}.plugin import factory
677
+
678
+
679
+ def test_manifest_marks_provider() -> None:
680
+ manifest = factory.get_manifest()
681
+ assert manifest.is_provider is True
682
+ '''
683
+ ).strip()
684
+ + "\n",
685
+ }
686
+ )
687
+
688
+ return files
689
+
690
+
691
+ def _to_pascal_case(name: str) -> str:
692
+ """Convert snake-case or kebab-case names to PascalCase."""
693
+
694
+ parts = re.split(r"[_\-\s]+", name)
695
+ return "".join(part.capitalize() for part in parts if part)