ccproxy-api 0.1.6__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 +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  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 +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  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 +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  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 +95 -342
  387. ccproxy/utils/version_checker.py +279 -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.6.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 -1231
  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 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  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.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.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.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,430 @@
1
+ """OAuth flow engines for CLI authentication."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import secrets
6
+ import sys
7
+ import webbrowser
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from ccproxy.auth.oauth.cli_errors import AuthProviderError, PortBindError
15
+ from ccproxy.auth.oauth.registry import OAuthProviderProtocol
16
+ from ccproxy.core.logging import get_logger
17
+
18
+
19
+ logger = get_logger(__name__)
20
+ console = Console()
21
+
22
+
23
+ class CLICallbackServer:
24
+ """Temporary HTTP server for handling OAuth callbacks in CLI flows."""
25
+
26
+ def __init__(self, port: int, callback_path: str = "/callback") -> None:
27
+ """Initialize the callback server.
28
+
29
+ Args:
30
+ port: Port to bind the server to
31
+ callback_path: Path to handle OAuth callbacks
32
+ """
33
+ self.port = port
34
+ self.callback_path = callback_path
35
+ self.server: Any = None
36
+ self._server_task: asyncio.Task[Any] | None = None
37
+ self.callback_received = False
38
+ self.callback_data: dict[str, Any] = {}
39
+ self.callback_future: asyncio.Future[dict[str, Any]] | None = None
40
+
41
+ async def start(self) -> None:
42
+ """Start the callback server."""
43
+ import uvicorn
44
+
45
+ # Create minimal ASGI app
46
+ async def app(scope: dict[str, Any], receive: Any, send: Any) -> None:
47
+ if scope["type"] == "http" and scope["path"] == self.callback_path:
48
+ await self._handle_callback(scope, receive, send)
49
+ else:
50
+ # 404 for other paths
51
+ await send(
52
+ {
53
+ "type": "http.response.start",
54
+ "status": 404,
55
+ "headers": [[b"content-type", b"text/plain"]],
56
+ }
57
+ )
58
+ await send(
59
+ {
60
+ "type": "http.response.body",
61
+ "body": b"Not Found",
62
+ }
63
+ )
64
+
65
+ # Create server config
66
+ config = uvicorn.Config(
67
+ app=app,
68
+ host="localhost",
69
+ port=self.port,
70
+ log_level="error", # Suppress uvicorn logs
71
+ )
72
+
73
+ # Create and start server
74
+ self.server = uvicorn.Server(config)
75
+
76
+ # Start server in background task with error handling
77
+ async def _serve_with_error_handling() -> None:
78
+ try:
79
+ await self.server.serve()
80
+ except (OSError, SystemExit) as e:
81
+ # Uvicorn calls sys.exit(1) on startup errors, convert to PortBindError
82
+ if isinstance(e, SystemExit):
83
+ raise PortBindError(
84
+ f"Failed to start callback server on port {self.port}"
85
+ ) from e
86
+ elif e.errno == 48: # Address already in use
87
+ raise PortBindError(
88
+ f"Port {self.port} is already in use. Please close other applications using this port."
89
+ ) from e
90
+ else:
91
+ raise PortBindError(
92
+ f"Failed to start callback server on port {self.port}: {e}"
93
+ ) from e
94
+
95
+ self._server_task = asyncio.create_task(_serve_with_error_handling())
96
+
97
+ # Wait briefly and check if server started successfully
98
+ await asyncio.sleep(0.1)
99
+ if self._server_task.done():
100
+ # Server failed to start, re-raise the exception
101
+ await self._server_task
102
+
103
+ logger.debug(
104
+ "cli_callback_server_started", port=self.port, path=self.callback_path
105
+ )
106
+
107
+ async def stop(self) -> None:
108
+ """Stop the callback server."""
109
+ if self.server:
110
+ self.server.should_exit = True
111
+ if hasattr(self, "_server_task") and self._server_task is not None:
112
+ try:
113
+ await asyncio.wait_for(self._server_task, timeout=2.0)
114
+ except TimeoutError:
115
+ self._server_task.cancel()
116
+ self.server = None
117
+ logger.debug("cli_callback_server_stopped", port=self.port)
118
+
119
+ async def _handle_callback(
120
+ self, scope: dict[str, Any], receive: Any, send: Any
121
+ ) -> None:
122
+ """Handle OAuth callback requests."""
123
+ from urllib.parse import parse_qs
124
+
125
+ # Extract query parameters from scope
126
+ query_string = scope.get("query_string", b"").decode()
127
+ query_params = {k: v[0] for k, v in parse_qs(query_string).items()}
128
+
129
+ # Store callback data
130
+ self.callback_data = query_params
131
+ self.callback_received = True
132
+
133
+ # Signal that callback was received
134
+ if self.callback_future and not self.callback_future.done():
135
+ self.callback_future.set_result(query_params)
136
+
137
+ logger.debug("cli_callback_received", params=list(query_params.keys()))
138
+
139
+ # Return success page
140
+ html_content = """
141
+ <!DOCTYPE html>
142
+ <html>
143
+ <head>
144
+ <title>Authentication Complete</title>
145
+ <style>
146
+ body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
147
+ .success { color: #4CAF50; }
148
+ .info { color: #2196F3; }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <h1 class="success">✓ Authentication Successful</h1>
153
+ <p class="info">You can close this window and return to the command line.</p>
154
+ </body>
155
+ </html>
156
+ """
157
+
158
+ await send(
159
+ {
160
+ "type": "http.response.start",
161
+ "status": 200,
162
+ "headers": [[b"content-type", b"text/html"]],
163
+ }
164
+ )
165
+ await send(
166
+ {
167
+ "type": "http.response.body",
168
+ "body": html_content.encode(),
169
+ }
170
+ )
171
+
172
+ async def wait_for_callback(
173
+ self, expected_state: str | None = None, timeout: float = 300
174
+ ) -> dict[str, Any]:
175
+ """Wait for OAuth callback with optional state validation.
176
+
177
+ Args:
178
+ expected_state: Expected OAuth state parameter for validation
179
+ timeout: Timeout in seconds
180
+
181
+ Returns:
182
+ Callback data dictionary
183
+
184
+ Raises:
185
+ asyncio.TimeoutError: If callback is not received within timeout
186
+ ValueError: If state validation fails
187
+ """
188
+ self.callback_future = asyncio.Future()
189
+
190
+ try:
191
+ # Wait for callback with timeout
192
+ callback_data = await asyncio.wait_for(
193
+ self.callback_future, timeout=timeout
194
+ )
195
+
196
+ # Validate state if provided
197
+ if expected_state and expected_state != "manual":
198
+ received_state = callback_data.get("state")
199
+ if received_state != expected_state:
200
+ raise ValueError(
201
+ f"OAuth state mismatch: expected {expected_state}, got {received_state}"
202
+ )
203
+
204
+ # Check for OAuth errors
205
+ if "error" in callback_data:
206
+ error = callback_data.get("error")
207
+ error_description = callback_data.get(
208
+ "error_description", "No description provided"
209
+ )
210
+ raise ValueError(f"OAuth error: {error} - {error_description}")
211
+
212
+ # Ensure we have an authorization code
213
+ if "code" not in callback_data:
214
+ raise ValueError("No authorization code received in callback")
215
+
216
+ return callback_data
217
+
218
+ except TimeoutError:
219
+ logger.error("cli_callback_timeout", timeout=timeout, port=self.port)
220
+ raise TimeoutError(f"No OAuth callback received within {timeout} seconds")
221
+
222
+
223
+ def render_qr_code(url: str) -> None:
224
+ """Render QR code for URL when TTY supports it."""
225
+ if not sys.stdout.isatty():
226
+ return
227
+
228
+ try:
229
+ import qrcode # type: ignore[import-untyped]
230
+
231
+ qr = qrcode.QRCode(border=1)
232
+ qr.add_data(url)
233
+ qr.print_ascii(invert=True)
234
+ console.print("[dim]Scan QR code with mobile device[/dim]")
235
+ except ImportError:
236
+ # QR code library not available - graceful degradation
237
+ pass
238
+
239
+
240
+ class BrowserFlow:
241
+ """Browser-based OAuth flow with callback server."""
242
+
243
+ async def run(
244
+ self,
245
+ provider: OAuthProviderProtocol,
246
+ no_browser: bool,
247
+ save_path: str | Path | None = None,
248
+ ) -> Any:
249
+ """Execute browser OAuth flow with fallback handling."""
250
+ cli_config = provider.cli
251
+
252
+ # Try provider's fixed port
253
+ try:
254
+ callback_server = CLICallbackServer(
255
+ cli_config.callback_port, cli_config.callback_path
256
+ )
257
+ await callback_server.start()
258
+ except PortBindError as e:
259
+ # Offer manual fallback for fixed-port providers
260
+ if cli_config.fixed_redirect_uri:
261
+ console.print(
262
+ f"[yellow]Port {cli_config.callback_port} unavailable. Try --manual mode.[/yellow]"
263
+ )
264
+ raise AuthProviderError(
265
+ f"Required port {cli_config.callback_port} unavailable"
266
+ ) from e
267
+ raise
268
+
269
+ try:
270
+ # Generate OAuth parameters with PKCE if supported
271
+ state = secrets.token_urlsafe(32)
272
+ code_verifier = None
273
+ if provider.supports_pkce:
274
+ code_verifier = (
275
+ base64.urlsafe_b64encode(secrets.token_bytes(32))
276
+ .decode("utf-8")
277
+ .rstrip("=")
278
+ )
279
+
280
+ # Use fixed redirect URI or construct from config
281
+ redirect_uri = (
282
+ cli_config.fixed_redirect_uri
283
+ or f"http://localhost:{cli_config.callback_port}{cli_config.callback_path}"
284
+ )
285
+
286
+ # Get authorization URL
287
+ auth_url = await provider.get_authorization_url(
288
+ state, code_verifier, redirect_uri
289
+ )
290
+
291
+ # Always show URL and QR code for fallback
292
+ console.print(f"[bold]Visit: {auth_url}[/bold]")
293
+ render_qr_code(auth_url)
294
+
295
+ # Try to open browser unless explicitly disabled
296
+ if not no_browser:
297
+ try:
298
+ webbrowser.open(auth_url)
299
+ console.print("[dim]Opening browser...[/dim]")
300
+ except Exception:
301
+ console.print(
302
+ "[yellow]Could not open browser automatically[/yellow]"
303
+ )
304
+
305
+ # Wait for callback with timeout and state validation
306
+ try:
307
+ callback_data = await callback_server.wait_for_callback(
308
+ state, timeout=300
309
+ )
310
+ credentials = await provider.handle_callback(
311
+ callback_data["code"], state, code_verifier, redirect_uri
312
+ )
313
+ return await provider.save_credentials(credentials, save_path)
314
+ except TimeoutError:
315
+ # Fallback to manual code entry if callback times out
316
+ console.print(
317
+ "[yellow]Callback timed out. You can enter the code manually.[/yellow]"
318
+ )
319
+ if cli_config.supports_manual_code:
320
+ # Use provider-specific manual redirect URI or fallback to OOB
321
+ manual_redirect_uri = (
322
+ cli_config.manual_redirect_uri or "urn:ietf:wg:oauth:2.0:oob"
323
+ )
324
+ manual_auth_url = await provider.get_authorization_url(
325
+ state, code_verifier, manual_redirect_uri
326
+ )
327
+ console.print(f"[bold]Manual URL: {manual_auth_url}[/bold]")
328
+
329
+ import typer
330
+
331
+ raw_code = typer.prompt("Enter the authorization code")
332
+
333
+ # Parse the code - some providers (like Claude) return code#state format
334
+ # Extract the code and state parts
335
+ code_parts = raw_code.split("#")
336
+ code = code_parts[0].strip()
337
+
338
+ # If there's a state in the input (Claude format), use it instead of our generated state
339
+ if len(code_parts) > 1 and code_parts[1].strip():
340
+ actual_state = code_parts[1].strip()
341
+ else:
342
+ actual_state = state
343
+
344
+ credentials = await provider.handle_callback(
345
+ code, actual_state, code_verifier, manual_redirect_uri
346
+ )
347
+ return await provider.save_credentials(credentials, save_path)
348
+ else:
349
+ raise
350
+ finally:
351
+ await callback_server.stop()
352
+
353
+
354
+ class DeviceCodeFlow:
355
+ """OAuth device code flow for headless environments."""
356
+
357
+ async def run(
358
+ self, provider: OAuthProviderProtocol, save_path: str | Path | None = None
359
+ ) -> Any:
360
+ """Execute device code flow with polling."""
361
+ (
362
+ device_code,
363
+ user_code,
364
+ verification_uri,
365
+ expires_in,
366
+ ) = await provider.start_device_flow()
367
+
368
+ console.print(f"[bold green]Visit: {verification_uri}[/bold green]")
369
+ console.print(f"[bold green]Enter code: {user_code}[/bold green]")
370
+ render_qr_code(verification_uri) # QR code for mobile
371
+
372
+ # Poll for completion with timeout
373
+ with console.status("Waiting for authorization..."):
374
+ credentials = await provider.complete_device_flow(
375
+ device_code, 5, expires_in
376
+ )
377
+
378
+ return await provider.save_credentials(credentials, save_path)
379
+
380
+
381
+ class ManualCodeFlow:
382
+ """Manual authorization code entry for restricted environments."""
383
+
384
+ async def run(
385
+ self, provider: OAuthProviderProtocol, save_path: str | Path | None = None
386
+ ) -> Any:
387
+ """Execute manual code entry flow."""
388
+ # Generate state for manual flow
389
+ state = secrets.token_urlsafe(32)
390
+ code_verifier = None
391
+ if provider.supports_pkce:
392
+ code_verifier = (
393
+ base64.urlsafe_b64encode(secrets.token_bytes(32))
394
+ .decode("utf-8")
395
+ .rstrip("=")
396
+ )
397
+
398
+ # Get provider-specific manual redirect URI or fallback to OOB
399
+ manual_redirect_uri = (
400
+ provider.cli.manual_redirect_uri or "urn:ietf:wg:oauth:2.0:oob"
401
+ )
402
+
403
+ # Get authorization URL for manual entry
404
+ auth_url = await provider.get_authorization_url(
405
+ state, code_verifier, manual_redirect_uri
406
+ )
407
+
408
+ console.print(f"[bold green]Visit: {auth_url}[/bold green]")
409
+ render_qr_code(auth_url)
410
+
411
+ # Prompt for manual code entry
412
+ raw_code = typer.prompt("[bold]Enter the authorization code[/bold]").strip()
413
+
414
+ # Parse the code - some providers (like Claude) return code#state format
415
+ # Extract the code and state parts
416
+ code_parts = raw_code.split("#")
417
+ code = code_parts[0].strip()
418
+
419
+ # If there's a state in the input (Claude format), use it instead of our generated state
420
+ if len(code_parts) > 1 and code_parts[1].strip():
421
+ actual_state = code_parts[1].strip()
422
+ else:
423
+ actual_state = state
424
+
425
+ # Use the provider's handle_callback method instead of exchange_manual_code
426
+ # to properly handle state validation
427
+ credentials = await provider.handle_callback(
428
+ code, actual_state, code_verifier, manual_redirect_uri
429
+ )
430
+ return await provider.save_credentials(credentials, save_path)