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,476 @@
1
+ """Binary resolution with package manager fallback support."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, NamedTuple
7
+
8
+ from typing_extensions import TypedDict
9
+
10
+ from ccproxy.core.logging import TraceBoundLogger, get_logger
11
+ from ccproxy.utils.caching import ttl_cache
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from ccproxy.config.settings import Settings
16
+
17
+ logger: TraceBoundLogger = get_logger()
18
+
19
+
20
+ class BinaryCommand(NamedTuple):
21
+ """Represents a resolved binary command."""
22
+
23
+ command: list[str]
24
+ is_direct: bool
25
+ is_in_path: bool
26
+ package_manager: str | None = None
27
+
28
+
29
+ class PackageManagerConfig(TypedDict, total=False):
30
+ """Configuration for a package manager."""
31
+
32
+ check_cmd: list[str]
33
+ priority: int
34
+ exec_cmd: str # Optional field
35
+
36
+
37
+ class CLIInfo(TypedDict):
38
+ """Common structure for CLI information."""
39
+
40
+ name: str # CLI name (e.g., "claude", "codex")
41
+ version: str | None # Version string
42
+ source: str # "path" | "package_manager" | "unknown"
43
+ path: str | None # Direct path if available
44
+ command: list[str] # Full command to execute
45
+ package_manager: str | None # Package manager used (if applicable)
46
+ is_available: bool # Whether the CLI is accessible
47
+
48
+
49
+ class BinaryResolver:
50
+ """Resolves binaries with fallback to package managers."""
51
+
52
+ PACKAGE_MANAGERS: dict[str, PackageManagerConfig] = {
53
+ "bunx": {"check_cmd": ["bun", "--version"], "priority": 1},
54
+ "pnpm": {"check_cmd": ["pnpm", "--version"], "exec_cmd": "dlx", "priority": 2},
55
+ "npx": {"check_cmd": ["npx", "--version"], "priority": 3},
56
+ }
57
+
58
+ KNOWN_PACKAGES = {
59
+ "claude": "@anthropic-ai/claude-code",
60
+ "codex": "@openai/codex",
61
+ "gemini": "@google/gemini-cli",
62
+ }
63
+
64
+ def __init__(
65
+ self,
66
+ fallback_enabled: bool = True,
67
+ package_manager_only: bool = False,
68
+ preferred_package_manager: str | None = None,
69
+ package_manager_priority: list[str] | None = None,
70
+ ):
71
+ """Initialize the binary resolver.
72
+
73
+ Args:
74
+ fallback_enabled: Whether to use package manager fallback
75
+ package_manager_only: Skip direct binary lookup and use package managers exclusively
76
+ preferred_package_manager: Preferred package manager (bunx, pnpm, npx)
77
+ package_manager_priority: Custom priority order for package managers
78
+ """
79
+ self.fallback_enabled = fallback_enabled
80
+ self.package_manager_only = package_manager_only
81
+ self.preferred_package_manager = preferred_package_manager
82
+ self.package_manager_priority = package_manager_priority or [
83
+ "bunx",
84
+ "pnpm",
85
+ "npx",
86
+ ]
87
+ self._available_managers: dict[str, bool] | None = None
88
+
89
+ @ttl_cache(maxsize=32, ttl=300.0)
90
+ def find_binary(
91
+ self,
92
+ binary_name: str,
93
+ package_name: str | None = None,
94
+ package_manager_only: bool | None = None,
95
+ fallback_enabled: bool | None = None,
96
+ ) -> BinaryCommand | None:
97
+ """Find a binary with optional package manager fallback.
98
+
99
+ Args:
100
+ binary_name: Name of the binary to find. Can be:
101
+ - Simple binary name (e.g., "claude")
102
+ - Full package name (e.g., "@anthropic-ai/claude-code")
103
+ package_name: NPM package name if different from binary name
104
+
105
+ Returns:
106
+ BinaryCommand with resolved command or None if not found
107
+ """
108
+ if package_manager_only is None:
109
+ package_manager_only = self.package_manager_only
110
+ if fallback_enabled is None:
111
+ fallback_enabled = self.fallback_enabled
112
+
113
+ # Determine if binary_name is a full package name (contains @ or /)
114
+ is_full_package = "@" in binary_name or "/" in binary_name
115
+
116
+ if is_full_package and package_name is None:
117
+ # If binary_name is a full package name, use it as the package
118
+ # and extract the binary name from it
119
+ package_name = binary_name
120
+ # Extract binary name from package (last part after /)
121
+ binary_name = binary_name.split("/")[-1]
122
+
123
+ # If package_manager_only mode, skip direct binary lookup
124
+ if package_manager_only:
125
+ package_name = package_name or self.KNOWN_PACKAGES.get(
126
+ binary_name, binary_name
127
+ )
128
+ result = self._find_via_package_manager(binary_name, package_name)
129
+ if result:
130
+ logger.trace(
131
+ "binary_resolved",
132
+ binary=binary_name,
133
+ manager=result.package_manager,
134
+ command=result.command,
135
+ source="package_manager",
136
+ )
137
+ else:
138
+ logger.trace(
139
+ "binary_resolution_failed",
140
+ binary=binary_name,
141
+ source="package_manager",
142
+ )
143
+ return result
144
+
145
+ # First, try direct binary lookup in PATH
146
+ direct_path = shutil.which(binary_name)
147
+ if direct_path:
148
+ return BinaryCommand(command=[direct_path], is_direct=True, is_in_path=True)
149
+
150
+ # Check common installation locations
151
+ common_paths = self._get_common_paths(binary_name)
152
+ for path in common_paths:
153
+ if path.exists() and path.is_file():
154
+ logger.debug(
155
+ "binary_found_in_common_path", binary=binary_name, path=str(path)
156
+ )
157
+ return BinaryCommand(
158
+ command=[str(path)], is_direct=True, is_in_path=False
159
+ )
160
+
161
+ # If fallback is disabled, stop here
162
+ if not fallback_enabled:
163
+ logger.debug("binary_fallback_disabled", binary=binary_name)
164
+ return None
165
+
166
+ # Try package manager fallback
167
+ package_name = package_name or self.KNOWN_PACKAGES.get(binary_name, binary_name)
168
+ return self._find_via_package_manager(binary_name, package_name)
169
+
170
+ def _find_via_package_manager(
171
+ self, binary_name: str, package_name: str
172
+ ) -> BinaryCommand | None:
173
+ """Find binary via package manager execution.
174
+
175
+ Args:
176
+ binary_name: Name of the binary
177
+ package_name: NPM package name
178
+
179
+ Returns:
180
+ BinaryCommand with package manager command or None
181
+ """
182
+ # Get available package managers
183
+ available = self._get_available_managers()
184
+
185
+ # If preferred manager is set and available, try it first
186
+ if (
187
+ self.preferred_package_manager
188
+ and self.preferred_package_manager in available
189
+ ):
190
+ cmd = self._build_package_manager_command(
191
+ self.preferred_package_manager, package_name
192
+ )
193
+ if cmd:
194
+ logger.debug(
195
+ "binary_using_preferred_manager",
196
+ binary=binary_name,
197
+ manager=self.preferred_package_manager,
198
+ command=cmd,
199
+ )
200
+ return BinaryCommand(
201
+ command=cmd,
202
+ is_direct=False,
203
+ is_in_path=False,
204
+ package_manager=self.preferred_package_manager,
205
+ )
206
+
207
+ # Try package managers in priority order
208
+ for manager_name in self.package_manager_priority:
209
+ if manager_name not in available or not available[manager_name]:
210
+ continue
211
+
212
+ cmd = self._build_package_manager_command(manager_name, package_name)
213
+ if cmd:
214
+ return BinaryCommand(
215
+ command=cmd,
216
+ is_direct=False,
217
+ is_in_path=False,
218
+ package_manager=manager_name,
219
+ )
220
+
221
+ logger.debug(
222
+ "binary_not_found_with_fallback",
223
+ binary=binary_name,
224
+ package=package_name,
225
+ available_managers=list(available.keys()),
226
+ )
227
+ return None
228
+
229
+ def _build_package_manager_command(
230
+ self, manager_name: str, package_name: str
231
+ ) -> list[str] | None:
232
+ """Build command for executing via package manager.
233
+
234
+ Args:
235
+ manager_name: Name of the package manager
236
+ package_name: Package to execute
237
+
238
+ Returns:
239
+ Command list or None if manager not configured
240
+ """
241
+ commands = {
242
+ "bunx": ["bunx", package_name],
243
+ "pnpm": ["pnpm", "dlx", package_name],
244
+ "npx": ["npx", "--yes", package_name],
245
+ }
246
+ return commands.get(manager_name)
247
+
248
+ def _get_common_paths(self, binary_name: str) -> list[Path]:
249
+ """Get common installation paths for a binary.
250
+
251
+ Args:
252
+ binary_name: Name of the binary
253
+
254
+ Returns:
255
+ List of paths to check
256
+ """
257
+ paths = [
258
+ # User-specific locations
259
+ Path.home() / ".cache" / ".bun" / "bin" / binary_name,
260
+ Path.home() / ".local" / "bin" / binary_name,
261
+ Path.home() / ".local" / "share" / "nvim" / "mason" / "bin" / binary_name,
262
+ Path.home() / ".npm-global" / "bin" / binary_name,
263
+ Path.home() / "bin" / binary_name,
264
+ # System locations
265
+ Path("/usr/local/bin") / binary_name,
266
+ Path("/usr/bin") / binary_name,
267
+ Path("/opt/homebrew/bin") / binary_name, # macOS ARM
268
+ # Node/npm locations
269
+ Path.home()
270
+ / ".nvm"
271
+ / "versions"
272
+ / "node"
273
+ / "default"
274
+ / "bin"
275
+ / binary_name,
276
+ Path.home() / ".volta" / "bin" / binary_name,
277
+ ]
278
+ return paths
279
+
280
+ def _get_available_managers(self) -> dict[str, bool]:
281
+ """Get available package managers on the system.
282
+
283
+ Returns:
284
+ Dictionary of manager names to availability status
285
+ """
286
+ if self._available_managers is not None:
287
+ return self._available_managers
288
+
289
+ self._available_managers = {}
290
+ manager_info = {}
291
+
292
+ for manager_name, config in self.PACKAGE_MANAGERS.items():
293
+ check_cmd = config["check_cmd"]
294
+ try:
295
+ # Use subprocess.run with capture to check availability
296
+ result = subprocess.run(
297
+ check_cmd,
298
+ capture_output=True,
299
+ text=True,
300
+ timeout=2,
301
+ check=False,
302
+ )
303
+ available = result.returncode == 0
304
+ self._available_managers[manager_name] = available
305
+ if available:
306
+ version = result.stdout.strip() if result.stdout else "unknown"
307
+ manager_info[manager_name] = version
308
+ except (subprocess.TimeoutExpired, FileNotFoundError):
309
+ self._available_managers[manager_name] = False
310
+
311
+ # Log all available managers in one consolidated message
312
+ if manager_info:
313
+ logger.debug(
314
+ "package_managers_detected",
315
+ managers=manager_info,
316
+ count=len(manager_info),
317
+ )
318
+
319
+ return self._available_managers
320
+
321
+ def get_available_package_managers(self) -> list[str]:
322
+ """Get list of available package managers on the system.
323
+
324
+ Returns:
325
+ List of package manager names that are available (e.g., ['bunx', 'pnpm'])
326
+ """
327
+ available = self._get_available_managers()
328
+ return [name for name, is_available in available.items() if is_available]
329
+
330
+ def get_package_manager_info(self) -> dict[str, dict[str, str | bool | int]]:
331
+ """Get detailed information about package managers.
332
+
333
+ Returns:
334
+ Dictionary with package manager info including availability and priority
335
+ """
336
+ available = self._get_available_managers()
337
+ info: dict[str, dict[str, str | bool | int]] = {}
338
+
339
+ for name, config in self.PACKAGE_MANAGERS.items():
340
+ exec_cmd = config.get("exec_cmd", name)
341
+ info[name] = {
342
+ "available": bool(available.get(name, False)),
343
+ "priority": int(config["priority"]),
344
+ "check_command": str(" ".join(config["check_cmd"])),
345
+ "exec_command": str(exec_cmd if exec_cmd is not None else name),
346
+ }
347
+
348
+ return info
349
+
350
+ def get_cli_info(
351
+ self,
352
+ binary_name: str,
353
+ package_name: str | None = None,
354
+ version: str | None = None,
355
+ ) -> CLIInfo:
356
+ """Get comprehensive CLI information in common format.
357
+
358
+ Args:
359
+ binary_name: Name of the binary to find
360
+ package_name: NPM package name if different from binary name
361
+ version: Optional version string (if known)
362
+
363
+ Returns:
364
+ CLIInfo dictionary with structured information
365
+ """
366
+ result = self.find_binary(binary_name, package_name)
367
+
368
+ if not result:
369
+ return CLIInfo(
370
+ name=binary_name,
371
+ version=version,
372
+ source="unknown",
373
+ path=None,
374
+ command=[],
375
+ package_manager=None,
376
+ is_available=False,
377
+ )
378
+
379
+ # Determine source and path
380
+ if result.is_direct:
381
+ source = "path"
382
+ path = result.command[0] if result.command else None
383
+ else:
384
+ source = "package_manager"
385
+ path = None
386
+
387
+ return CLIInfo(
388
+ name=binary_name,
389
+ version=version,
390
+ source=source,
391
+ path=path,
392
+ command=result.command,
393
+ package_manager=result.package_manager,
394
+ is_available=True,
395
+ )
396
+
397
+ def clear_cache(self) -> None:
398
+ """Clear all caches."""
399
+ # Reset the available managers cache
400
+ self._available_managers = None
401
+
402
+ @classmethod
403
+ def from_settings(cls, settings: "Settings") -> "BinaryResolver":
404
+ """Create a BinaryResolver from application settings.
405
+
406
+ Args:
407
+ settings: Application settings
408
+
409
+ Returns:
410
+ Configured BinaryResolver instance
411
+ """
412
+ return cls(
413
+ fallback_enabled=settings.binary.fallback_enabled,
414
+ package_manager_only=settings.binary.package_manager_only,
415
+ preferred_package_manager=settings.binary.preferred_package_manager,
416
+ package_manager_priority=settings.binary.package_manager_priority,
417
+ )
418
+
419
+
420
+ # Global instance for convenience
421
+ _default_resolver = BinaryResolver()
422
+
423
+
424
+ def find_binary_with_fallback(
425
+ binary_name: str,
426
+ package_name: str | None = None,
427
+ fallback_enabled: bool = True,
428
+ ) -> list[str] | None:
429
+ """Convenience function to find a binary with package manager fallback.
430
+
431
+ Args:
432
+ binary_name: Name of the binary to find. Can be:
433
+ - Simple binary name (e.g., "claude")
434
+ - Full package name (e.g., "@anthropic-ai/claude-code")
435
+ package_name: NPM package name if different from binary name
436
+ fallback_enabled: Whether to use package manager fallback
437
+
438
+ Returns:
439
+ Command list to execute the binary, or None if not found
440
+ """
441
+ resolver = BinaryResolver(fallback_enabled=fallback_enabled)
442
+ result = resolver.find_binary(binary_name, package_name)
443
+ return result.command if result else None
444
+
445
+
446
+ def is_package_manager_command(command: list[str]) -> bool:
447
+ """Check if a command uses a package manager.
448
+
449
+ Args:
450
+ command: Command list to check
451
+
452
+ Returns:
453
+ True if command uses a package manager
454
+ """
455
+ if not command:
456
+ return False
457
+ first_cmd = Path(command[0]).name
458
+ return first_cmd in ["npx", "bunx", "pnpm"]
459
+
460
+
461
+ def get_available_package_managers() -> list[str]:
462
+ """Convenience function to get available package managers using default resolver.
463
+
464
+ Returns:
465
+ List of package manager names that are available
466
+ """
467
+ return _default_resolver.get_available_package_managers()
468
+
469
+
470
+ def get_package_manager_info() -> dict[str, dict[str, str | bool | int]]:
471
+ """Convenience function to get package manager info using default resolver.
472
+
473
+ Returns:
474
+ Dictionary with package manager info including availability and priority
475
+ """
476
+ return _default_resolver.get_package_manager_info()