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,494 @@
1
+ """OAuth client implementation for GitHub Copilot with Device Code Flow."""
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import httpx
8
+ from pydantic import SecretStr
9
+
10
+ from ccproxy.auth.oauth.protocol import StandardProfileFields
11
+ from ccproxy.core.logging import get_plugin_logger
12
+
13
+ from ..config import CopilotOAuthConfig
14
+ from .models import (
15
+ CopilotCredentials,
16
+ CopilotOAuthToken,
17
+ CopilotProfileInfo,
18
+ CopilotTokenResponse,
19
+ DeviceCodeResponse,
20
+ DeviceTokenPollResponse,
21
+ )
22
+ from .storage import CopilotOAuthStorage
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from ccproxy.services.cli_detection import CLIDetectionService
27
+
28
+
29
+ logger = get_plugin_logger()
30
+
31
+
32
+ class CopilotOAuthClient:
33
+ """OAuth client for GitHub Copilot using Device Code Flow."""
34
+
35
+ def __init__(
36
+ self,
37
+ config: CopilotOAuthConfig,
38
+ storage: CopilotOAuthStorage,
39
+ http_client: httpx.AsyncClient | None = None,
40
+ hook_manager: Any | None = None,
41
+ detection_service: "CLIDetectionService | None" = None,
42
+ ):
43
+ """Initialize the OAuth client.
44
+
45
+ Args:
46
+ config: OAuth configuration
47
+ storage: Token storage
48
+ http_client: Optional HTTP client for request tracing
49
+ hook_manager: Optional hook manager for events
50
+ detection_service: Optional CLI detection service
51
+ """
52
+ self.config = config
53
+ self.storage = storage
54
+ self.hook_manager = hook_manager
55
+ self.detection_service = detection_service
56
+ self._http_client = http_client
57
+ self._owns_client = http_client is None
58
+
59
+ async def _get_http_client(self) -> httpx.AsyncClient:
60
+ """Get HTTP client for making requests."""
61
+ if self._http_client is None:
62
+ self._http_client = httpx.AsyncClient(
63
+ timeout=httpx.Timeout(self.config.request_timeout),
64
+ headers={
65
+ "Accept": "application/json",
66
+ "User-Agent": "CCProxy-Copilot/1.0.0",
67
+ },
68
+ )
69
+ return self._http_client
70
+
71
+ async def close(self) -> None:
72
+ """Close HTTP client if we own it."""
73
+ if self._owns_client and self._http_client:
74
+ await self._http_client.aclose()
75
+ self._http_client = None
76
+
77
+ async def start_device_flow(self) -> DeviceCodeResponse:
78
+ """Start the GitHub device code authorization flow.
79
+
80
+ Returns:
81
+ Device code response with verification details
82
+ """
83
+ client = await self._get_http_client()
84
+
85
+ # Request device code from GitHub
86
+ data = {
87
+ "client_id": self.config.client_id,
88
+ "scope": " ".join(self.config.scopes),
89
+ }
90
+
91
+ logger.debug(
92
+ "requesting_device_code",
93
+ client_id=self.config.client_id[:8] + "...",
94
+ scopes=self.config.scopes,
95
+ )
96
+
97
+ try:
98
+ response = await client.post(
99
+ self.config.authorize_url,
100
+ data=data,
101
+ headers={
102
+ "Accept": "application/json",
103
+ },
104
+ )
105
+ response.raise_for_status()
106
+
107
+ device_code_data = response.json()
108
+ device_code_response = DeviceCodeResponse.model_validate(device_code_data)
109
+
110
+ logger.debug(
111
+ "device_code_received",
112
+ user_code=device_code_response.user_code,
113
+ verification_uri=device_code_response.verification_uri,
114
+ expires_in=device_code_response.expires_in,
115
+ )
116
+
117
+ return device_code_response
118
+
119
+ except httpx.HTTPError as e:
120
+ logger.error(
121
+ "device_code_request_failed",
122
+ error=str(e),
123
+ status_code=getattr(e.response, "status_code", None)
124
+ if hasattr(e, "response")
125
+ else None,
126
+ exc_info=e,
127
+ )
128
+ raise
129
+
130
+ async def poll_for_token(
131
+ self, device_code: str, interval: int, expires_in: int
132
+ ) -> CopilotOAuthToken:
133
+ """Poll GitHub for OAuth token after user authorization.
134
+
135
+ Args:
136
+ device_code: Device code from device flow
137
+ interval: Polling interval in seconds
138
+ expires_in: Code expiration time in seconds
139
+
140
+ Returns:
141
+ OAuth token once authorized
142
+
143
+ Raises:
144
+ TimeoutError: If device code expires
145
+ ValueError: If user denies authorization
146
+ """
147
+ client = await self._get_http_client()
148
+
149
+ start_time = time.time()
150
+ current_interval = interval
151
+
152
+ logger.debug(
153
+ "polling_for_token",
154
+ interval=interval,
155
+ expires_in=expires_in,
156
+ )
157
+
158
+ while True:
159
+ # Check if we've exceeded the expiration time
160
+ if time.time() - start_time > expires_in:
161
+ raise TimeoutError("Device code has expired")
162
+
163
+ await asyncio.sleep(current_interval)
164
+
165
+ data = {
166
+ "client_id": self.config.client_id,
167
+ "device_code": device_code,
168
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
169
+ }
170
+
171
+ try:
172
+ response = await client.post(
173
+ self.config.token_url,
174
+ data=data,
175
+ headers={
176
+ "Accept": "application/json",
177
+ },
178
+ )
179
+
180
+ poll_response = DeviceTokenPollResponse.model_validate(response.json())
181
+
182
+ if poll_response.is_success:
183
+ # Success! Create OAuth token
184
+ oauth_token = CopilotOAuthToken(
185
+ access_token=SecretStr(poll_response.access_token or ""),
186
+ token_type=poll_response.token_type or "bearer",
187
+ scope=poll_response.scope or " ".join(self.config.scopes),
188
+ created_at=int(time.time()),
189
+ expires_in=None, # GitHub tokens don't typically expire
190
+ )
191
+
192
+ logger.debug(
193
+ "oauth_token_received",
194
+ token_type=oauth_token.token_type,
195
+ scope=oauth_token.scope,
196
+ )
197
+
198
+ return oauth_token
199
+
200
+ elif poll_response.is_pending:
201
+ # Still waiting for user authorization
202
+ logger.debug("authorization_pending")
203
+ continue
204
+
205
+ elif poll_response.is_slow_down:
206
+ # Need to slow down polling
207
+ current_interval += 5
208
+ logger.debug("slowing_down_poll", new_interval=current_interval)
209
+ continue
210
+
211
+ elif poll_response.is_expired:
212
+ raise TimeoutError("Device code has expired")
213
+
214
+ elif poll_response.is_denied:
215
+ raise ValueError("User denied authorization")
216
+
217
+ else:
218
+ # Unknown error
219
+ logger.error(
220
+ "unknown_oauth_error",
221
+ error=poll_response.error,
222
+ error_description=poll_response.error_description,
223
+ )
224
+ raise ValueError(f"OAuth error: {poll_response.error}")
225
+
226
+ except httpx.HTTPError as e:
227
+ logger.error(
228
+ "token_poll_request_failed",
229
+ error=str(e),
230
+ status_code=getattr(e.response, "status_code", None)
231
+ if hasattr(e, "response")
232
+ else None,
233
+ exc_info=e,
234
+ )
235
+ # Continue polling on HTTP errors
236
+ await asyncio.sleep(current_interval)
237
+ continue
238
+
239
+ async def exchange_for_copilot_token(
240
+ self, oauth_token: CopilotOAuthToken
241
+ ) -> CopilotTokenResponse:
242
+ """Exchange GitHub OAuth token for Copilot service token.
243
+
244
+ Args:
245
+ oauth_token: GitHub OAuth token
246
+
247
+ Returns:
248
+ Copilot service token response
249
+ """
250
+ client = await self._get_http_client()
251
+
252
+ logger.debug(
253
+ "exchanging_for_copilot_token",
254
+ copilot_token_url=self.config.copilot_token_url,
255
+ )
256
+
257
+ try:
258
+ response = await client.get(
259
+ self.config.copilot_token_url,
260
+ headers={
261
+ "Authorization": f"Bearer {oauth_token.access_token.get_secret_value()}",
262
+ "Accept": "application/json",
263
+ },
264
+ )
265
+ response.raise_for_status()
266
+
267
+ copilot_data = response.json()
268
+ copilot_token = CopilotTokenResponse.model_validate(copilot_data)
269
+
270
+ logger.debug(
271
+ "copilot_token_received",
272
+ expires_at=copilot_token.expires_at,
273
+ refresh_in=copilot_token.refresh_in,
274
+ )
275
+
276
+ return copilot_token
277
+
278
+ except httpx.HTTPError as e:
279
+ logger.error(
280
+ "copilot_token_exchange_failed",
281
+ error=str(e),
282
+ status_code=getattr(e.response, "status_code", None)
283
+ if hasattr(e, "response")
284
+ else None,
285
+ exc_info=e,
286
+ )
287
+ raise
288
+
289
+ async def get_user_profile(
290
+ self, oauth_token: CopilotOAuthToken
291
+ ) -> CopilotProfileInfo:
292
+ """Get user profile information from GitHub API.
293
+
294
+ Args:
295
+ oauth_token: GitHub OAuth token
296
+
297
+ Returns:
298
+ User profile information
299
+ """
300
+ client = await self._get_http_client()
301
+
302
+ try:
303
+ # Get basic user info
304
+ response = await client.get(
305
+ "https://api.github.com/user",
306
+ headers={
307
+ "Authorization": f"Bearer {oauth_token.access_token.get_secret_value()}",
308
+ "Accept": "application/vnd.github.v3+json",
309
+ },
310
+ )
311
+ response.raise_for_status()
312
+ user_data = response.json()
313
+
314
+ # Check Copilot access
315
+ copilot_access = False
316
+ copilot_plan = None
317
+
318
+ try:
319
+ copilot_response = await client.get(
320
+ "https://api.github.com/user/copilot_business_accounts",
321
+ headers={
322
+ "Authorization": f"Bearer {oauth_token.access_token.get_secret_value()}",
323
+ "Accept": "application/vnd.github.v3+json",
324
+ },
325
+ )
326
+ if copilot_response.status_code == 200:
327
+ copilot_data = copilot_response.json()
328
+ copilot_access = (
329
+ len(copilot_data.get("copilot_business_accounts", [])) > 0
330
+ )
331
+ copilot_plan = "business" if copilot_access else None
332
+ elif copilot_response.status_code == 404:
333
+ # Try individual plan
334
+ individual_response = await client.get(
335
+ "https://api.github.com/copilot_internal/user",
336
+ headers={
337
+ "Authorization": f"Bearer {oauth_token.access_token.get_secret_value()}",
338
+ "Accept": "application/vnd.github.v3+json",
339
+ },
340
+ )
341
+ if individual_response.status_code == 200:
342
+ copilot_access = True
343
+ copilot_plan = "individual"
344
+ except httpx.HTTPError:
345
+ # Ignore Copilot access check errors
346
+ logger.debug("copilot_access_check_failed")
347
+
348
+ profile = CopilotProfileInfo(
349
+ account_id=str(user_data.get("id", user_data["login"])),
350
+ login=user_data["login"],
351
+ name=user_data.get("name"),
352
+ email=user_data.get("email") or "",
353
+ avatar_url=user_data.get("avatar_url"),
354
+ html_url=user_data.get("html_url"),
355
+ copilot_plan=copilot_plan,
356
+ copilot_access=copilot_access,
357
+ )
358
+
359
+ logger.debug(
360
+ "profile_retrieved",
361
+ login=profile.login,
362
+ user_name=profile.name,
363
+ copilot_access=copilot_access,
364
+ copilot_plan=copilot_plan,
365
+ )
366
+
367
+ return profile
368
+
369
+ except httpx.HTTPError as e:
370
+ logger.error(
371
+ "profile_request_failed",
372
+ error=str(e),
373
+ status_code=getattr(e.response, "status_code", None)
374
+ if hasattr(e, "response")
375
+ else None,
376
+ exc_info=e,
377
+ )
378
+ raise
379
+
380
+ def to_standard_profile(self, profile: CopilotProfileInfo) -> StandardProfileFields:
381
+ """Convert Copilot profile info into `StandardProfileFields`."""
382
+
383
+ display_name = getattr(profile, "computed_display_name", None) or (
384
+ profile.display_name or profile.name or profile.login
385
+ )
386
+
387
+ features: dict[str, Any] = {
388
+ "copilot_access": profile.copilot_access,
389
+ "login": profile.login,
390
+ }
391
+ if profile.copilot_plan:
392
+ features["copilot_plan"] = profile.copilot_plan
393
+
394
+ raw_profile = {"copilot_profile": profile.model_dump()}
395
+
396
+ return StandardProfileFields(
397
+ account_id=profile.account_id,
398
+ provider_type="copilot",
399
+ email=profile.email or None,
400
+ display_name=display_name,
401
+ subscription_type=profile.copilot_plan,
402
+ features=features,
403
+ raw_profile_data=raw_profile,
404
+ )
405
+
406
+ async def get_standard_profile(
407
+ self, oauth_token: CopilotOAuthToken
408
+ ) -> StandardProfileFields:
409
+ """Fetch profile info and normalize it for generic consumers."""
410
+
411
+ profile = await self.get_user_profile(oauth_token)
412
+ return self.to_standard_profile(profile)
413
+
414
+ async def complete_authorization(
415
+ self, device_code: str, interval: int, expires_in: int
416
+ ) -> CopilotCredentials:
417
+ """Complete the full authorization flow.
418
+
419
+ Args:
420
+ device_code: Device code from device flow
421
+ interval: Polling interval
422
+ expires_in: Code expiration time
423
+
424
+ Returns:
425
+ Complete Copilot credentials
426
+ """
427
+ # Get OAuth token
428
+ oauth_token = await self.poll_for_token(device_code, interval, expires_in)
429
+
430
+ # Exchange for Copilot token
431
+ copilot_token = await self.exchange_for_copilot_token(oauth_token)
432
+
433
+ # Get user profile
434
+ profile = await self.get_user_profile(oauth_token)
435
+
436
+ # Determine account type from profile
437
+ account_type = "individual"
438
+ if profile.copilot_plan == "business":
439
+ account_type = "business"
440
+ elif profile.copilot_plan and "enterprise" in profile.copilot_plan:
441
+ account_type = "enterprise"
442
+
443
+ # Create credentials
444
+ credentials = CopilotCredentials(
445
+ oauth_token=oauth_token,
446
+ copilot_token=copilot_token,
447
+ account_type=account_type,
448
+ )
449
+
450
+ # Store credentials
451
+ await self.storage.store_credentials(credentials)
452
+
453
+ logger.debug(
454
+ "authorization_completed",
455
+ login=profile.login,
456
+ account_type=account_type,
457
+ copilot_access=profile.copilot_access,
458
+ )
459
+
460
+ return credentials
461
+
462
+ async def refresh_copilot_token(
463
+ self, credentials: CopilotCredentials
464
+ ) -> CopilotCredentials:
465
+ """Refresh the Copilot service token using stored OAuth token.
466
+
467
+ Args:
468
+ credentials: Current credentials
469
+
470
+ Returns:
471
+ Updated credentials with new Copilot token
472
+ """
473
+ if credentials.oauth_token.is_expired:
474
+ logger.warning("oauth_token_expired_cannot_refresh")
475
+ raise ValueError("OAuth token is expired, re-authorization required")
476
+
477
+ # Exchange OAuth token for new Copilot token
478
+ new_copilot_token = await self.exchange_for_copilot_token(
479
+ credentials.oauth_token
480
+ )
481
+
482
+ # Update credentials
483
+ credentials.copilot_token = new_copilot_token
484
+ credentials.refresh_updated_at()
485
+
486
+ # Store updated credentials
487
+ await self.storage.store_credentials(credentials)
488
+
489
+ logger.debug(
490
+ "copilot_token_refreshed",
491
+ account_type=credentials.account_type,
492
+ )
493
+
494
+ return credentials