ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -1,550 +0,0 @@
1
- """Session-aware connection pool for persistent Claude SDK connections."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import contextlib
7
- from typing import TYPE_CHECKING, Any
8
-
9
- import structlog
10
- from claude_code_sdk import ClaudeCodeOptions
11
-
12
- from ccproxy.claude_sdk.session_client import SessionClient, SessionStatus
13
- from ccproxy.config.claude import SessionPoolSettings
14
- from ccproxy.core.errors import ClaudeProxyError, ServiceUnavailableError
15
-
16
-
17
- if TYPE_CHECKING:
18
- pass
19
-
20
-
21
- logger = structlog.get_logger(__name__)
22
-
23
-
24
- class SessionPool:
25
- """Manages persistent Claude SDK connections by session."""
26
-
27
- def __init__(self, config: SessionPoolSettings | None = None):
28
- self.config = config or SessionPoolSettings()
29
- self.sessions: dict[str, SessionClient] = {}
30
- self.cleanup_task: asyncio.Task[None] | None = None
31
- self._shutdown = False
32
- self._lock = asyncio.Lock()
33
-
34
- async def start(self) -> None:
35
- """Start the session pool and cleanup task."""
36
- if not self.config.enabled:
37
- return
38
-
39
- logger.debug(
40
- "session_pool_starting",
41
- max_sessions=self.config.max_sessions,
42
- ttl=self.config.session_ttl,
43
- cleanup_interval=self.config.cleanup_interval,
44
- )
45
-
46
- self.cleanup_task = asyncio.create_task(self._cleanup_loop())
47
-
48
- async def stop(self) -> None:
49
- """Stop the session pool and cleanup all sessions."""
50
- self._shutdown = True
51
-
52
- if self.cleanup_task:
53
- self.cleanup_task.cancel()
54
- with contextlib.suppress(asyncio.CancelledError):
55
- await self.cleanup_task
56
-
57
- # Disconnect all active sessions
58
- async with self._lock:
59
- disconnect_tasks = [
60
- session_client.disconnect() for session_client in self.sessions.values()
61
- ]
62
-
63
- if disconnect_tasks:
64
- await asyncio.gather(*disconnect_tasks, return_exceptions=True)
65
-
66
- self.sessions.clear()
67
-
68
- logger.debug("session_pool_stopped")
69
-
70
- async def get_session_client(
71
- self, session_id: str, options: ClaudeCodeOptions
72
- ) -> SessionClient:
73
- """Get or create a session context for the given session_id."""
74
- logger.debug(
75
- "session_pool_get_client_start",
76
- session_id=session_id,
77
- pool_enabled=self.config.enabled,
78
- current_sessions=len(self.sessions),
79
- max_sessions=self.config.max_sessions,
80
- session_exists=session_id in self.sessions,
81
- )
82
-
83
- if not self.config.enabled:
84
- logger.error("session_pool_disabled", session_id=session_id)
85
- raise ClaudeProxyError(
86
- message="Session pool is disabled",
87
- error_type="configuration_error",
88
- status_code=500,
89
- )
90
-
91
- # Check session limit and get/create session
92
- async with self._lock:
93
- if (
94
- session_id not in self.sessions
95
- and len(self.sessions) >= self.config.max_sessions
96
- ):
97
- logger.error(
98
- "session_pool_at_capacity",
99
- session_id=session_id,
100
- current_sessions=len(self.sessions),
101
- max_sessions=self.config.max_sessions,
102
- )
103
- raise ServiceUnavailableError(
104
- f"Session pool at capacity: {self.config.max_sessions}"
105
- )
106
- options.continue_conversation = True
107
- # Get existing session or create new one
108
- if session_id in self.sessions:
109
- session_client = self.sessions[session_id]
110
- logger.debug(
111
- "session_pool_existing_session_found",
112
- session_id=session_id,
113
- client_id=session_client.client_id,
114
- session_status=session_client.status.value,
115
- )
116
-
117
- # Check if session is currently being interrupted
118
- if session_client.status.value == "interrupting":
119
- logger.warning(
120
- "session_pool_interrupting_session",
121
- session_id=session_id,
122
- client_id=session_client.client_id,
123
- message="Session is currently being interrupted, waiting for completion then creating new session",
124
- )
125
- # Wait for the interrupt process to complete properly
126
- interrupt_completed = (
127
- await session_client.wait_for_interrupt_complete(timeout=5.0)
128
- )
129
- if interrupt_completed:
130
- logger.debug(
131
- "session_pool_interrupt_completed",
132
- session_id=session_id,
133
- client_id=session_client.client_id,
134
- message="Interrupt completed successfully, proceeding with session replacement",
135
- )
136
- else:
137
- logger.warning(
138
- "session_pool_interrupt_timeout",
139
- session_id=session_id,
140
- client_id=session_client.client_id,
141
- message="Interrupt did not complete within 5 seconds, proceeding anyway",
142
- )
143
- # Don't try to reuse a session that was being interrupted
144
- await self._remove_session_unlocked(session_id)
145
- session_client = await self._create_session_unlocked(
146
- session_id, options
147
- )
148
- # Check if session has an active stream that needs cleanup
149
- elif (
150
- session_client.has_active_stream
151
- or session_client.active_stream_handle
152
- ):
153
- logger.debug(
154
- "session_pool_active_stream_detected",
155
- session_id=session_id,
156
- client_id=session_client.client_id,
157
- has_stream=session_client.has_active_stream,
158
- has_handle=bool(session_client.active_stream_handle),
159
- idle_seconds=session_client.metrics.idle_seconds,
160
- message="Session has active stream/handle, checking if cleanup needed",
161
- )
162
-
163
- # Check timeout types based on proper message lifecycle timing
164
- # - No SystemMessage received within configured timeout (first chunk timeout) -> terminate session
165
- # - SystemMessage received but no activity for configured timeout (ongoing timeout) -> interrupt stream
166
- # - Never check for completed streams (ResultMessage received)
167
- handle = session_client.active_stream_handle
168
- if handle is not None:
169
- is_first_chunk_timeout = handle.is_first_chunk_timeout()
170
- is_ongoing_timeout = handle.is_ongoing_timeout()
171
- else:
172
- # Handle was cleared by another thread, no timeout checks needed
173
- is_first_chunk_timeout = False
174
- is_ongoing_timeout = False
175
-
176
- if session_client.active_stream_handle and (
177
- is_first_chunk_timeout or is_ongoing_timeout
178
- ):
179
- old_handle_id = session_client.active_stream_handle.handle_id
180
-
181
- if is_first_chunk_timeout:
182
- # First chunk timeout indicates connection issue - terminate session client
183
- logger.warning(
184
- "session_pool_first_chunk_timeout",
185
- session_id=session_id,
186
- old_handle_id=old_handle_id,
187
- idle_seconds=session_client.active_stream_handle.idle_seconds,
188
- message=f"No first chunk received within {self.config.stream_first_chunk_timeout} seconds, terminating session client",
189
- )
190
-
191
- # Remove the entire session - connection is likely broken
192
- await self._remove_session_unlocked(session_id)
193
- session_client = await self._create_session_unlocked(
194
- session_id, options
195
- )
196
-
197
- elif is_ongoing_timeout:
198
- # Ongoing timeout - interrupt the stream but keep session
199
- logger.info(
200
- "session_pool_interrupting_ongoing_timeout",
201
- session_id=session_id,
202
- old_handle_id=old_handle_id,
203
- idle_seconds=session_client.active_stream_handle.idle_seconds,
204
- has_first_chunk=session_client.active_stream_handle.has_first_chunk,
205
- is_completed=session_client.active_stream_handle.is_completed,
206
- message=f"Stream idle for {self.config.stream_ongoing_timeout}+ seconds, interrupting stream but keeping session",
207
- )
208
-
209
- try:
210
- # Interrupt the old stream handle to stop its worker
211
- interrupted = await session_client.active_stream_handle.interrupt()
212
- if interrupted:
213
- logger.info(
214
- "session_pool_interrupted_ongoing_timeout",
215
- session_id=session_id,
216
- old_handle_id=old_handle_id,
217
- message="Successfully interrupted ongoing timeout stream",
218
- )
219
- else:
220
- logger.debug(
221
- "session_pool_interrupt_ongoing_not_needed",
222
- session_id=session_id,
223
- old_handle_id=old_handle_id,
224
- message="Ongoing timeout stream was already completed",
225
- )
226
- except Exception as e:
227
- logger.warning(
228
- "session_pool_interrupt_ongoing_failed",
229
- session_id=session_id,
230
- old_handle_id=old_handle_id,
231
- error=str(e),
232
- error_type=type(e).__name__,
233
- message="Failed to interrupt ongoing timeout stream, clearing anyway",
234
- )
235
- finally:
236
- # Always clear the handle after interrupt attempt
237
- session_client.active_stream_handle = None
238
- session_client.has_active_stream = False
239
- elif session_client.active_stream_handle and not (
240
- is_first_chunk_timeout or is_ongoing_timeout
241
- ):
242
- # Stream is recent, likely from a previous request that just finished
243
- # Just clear the handle without interrupting to allow immediate reuse
244
- logger.debug(
245
- "session_pool_clearing_recent_stream",
246
- session_id=session_id,
247
- old_handle_id=session_client.active_stream_handle.handle_id,
248
- idle_seconds=session_client.active_stream_handle.idle_seconds,
249
- has_first_chunk=session_client.active_stream_handle.has_first_chunk,
250
- is_completed=session_client.active_stream_handle.is_completed,
251
- message="Clearing recent stream handle for immediate reuse",
252
- )
253
- session_client.active_stream_handle = None
254
- session_client.has_active_stream = False
255
- else:
256
- # No handle but has_active_stream flag is set, just clear the flag
257
- session_client.has_active_stream = False
258
-
259
- logger.debug(
260
- "session_pool_stream_cleared",
261
- session_id=session_id,
262
- client_id=session_client.client_id,
263
- was_interrupted=(is_first_chunk_timeout or is_ongoing_timeout),
264
- was_recent=not (is_first_chunk_timeout or is_ongoing_timeout),
265
- was_first_chunk_timeout=is_first_chunk_timeout,
266
- was_ongoing_timeout=is_ongoing_timeout,
267
- message="Stream state cleared, session ready for reuse",
268
- )
269
- # Check if session is still valid
270
- elif session_client.is_expired():
271
- logger.debug("session_expired", session_id=session_id)
272
- await self._remove_session_unlocked(session_id)
273
- session_client = await self._create_session_unlocked(
274
- session_id, options
275
- )
276
- elif (
277
- not await session_client.is_healthy()
278
- and self.config.connection_recovery
279
- ):
280
- logger.debug("session_unhealthy_recovering", session_id=session_id)
281
- await session_client.connect()
282
- # Mark session as reused since we're recovering an existing session
283
- session_client.mark_as_reused()
284
- else:
285
- logger.debug(
286
- "session_pool_reusing_healthy_session",
287
- session_id=session_id,
288
- client_id=session_client.client_id,
289
- )
290
- # Mark session as reused
291
- session_client.mark_as_reused()
292
- else:
293
- logger.debug("session_pool_creating_new_session", session_id=session_id)
294
- session_client = await self._create_session_unlocked(
295
- session_id, options
296
- )
297
-
298
- # Ensure session is connected before returning (inside lock to prevent race conditions)
299
- if not await session_client.ensure_connected():
300
- logger.error(
301
- "session_pool_connection_failed",
302
- session_id=session_id,
303
- )
304
- raise ServiceUnavailableError(
305
- f"Failed to establish session connection: {session_id}"
306
- )
307
-
308
- logger.debug(
309
- "session_pool_get_client_complete",
310
- session_id=session_id,
311
- client_id=session_client.client_id,
312
- session_status=session_client.status,
313
- session_age_seconds=session_client.metrics.age_seconds,
314
- session_message_count=session_client.metrics.message_count,
315
- )
316
- return session_client
317
-
318
- async def _create_session(
319
- self, session_id: str, options: ClaudeCodeOptions
320
- ) -> SessionClient:
321
- """Create a new session context (acquires lock)."""
322
- async with self._lock:
323
- return await self._create_session_unlocked(session_id, options)
324
-
325
- async def _create_session_unlocked(
326
- self, session_id: str, options: ClaudeCodeOptions
327
- ) -> SessionClient:
328
- """Create a new session context (requires lock to be held)."""
329
- session_client = SessionClient(
330
- session_id=session_id, options=options, ttl_seconds=self.config.session_ttl
331
- )
332
-
333
- # Start connection in background
334
- connection_task = session_client.connect_background()
335
-
336
- # Add to sessions immediately (will connect in background)
337
- self.sessions[session_id] = session_client
338
-
339
- # Optionally wait for connection to verify it works
340
- # For now, we'll let it connect in background and check on first use
341
- logger.debug(
342
- "session_connecting_background",
343
- session_id=session_id,
344
- client_id=session_client.client_id,
345
- )
346
-
347
- logger.debug(
348
- "session_created",
349
- session_id=session_id,
350
- client_id=session_client.client_id,
351
- total_sessions=len(self.sessions),
352
- )
353
-
354
- return session_client
355
-
356
- async def _remove_session(self, session_id: str) -> None:
357
- """Remove and cleanup a session (acquires lock)."""
358
- async with self._lock:
359
- await self._remove_session_unlocked(session_id)
360
-
361
- async def _remove_session_unlocked(self, session_id: str) -> None:
362
- """Remove and cleanup a session (requires lock to be held)."""
363
- if session_id not in self.sessions:
364
- return
365
-
366
- session_client = self.sessions.pop(session_id)
367
- await session_client.disconnect()
368
-
369
- logger.debug(
370
- "session_removed",
371
- session_id=session_id,
372
- total_sessions=len(self.sessions),
373
- age_seconds=session_client.metrics.age_seconds,
374
- message_count=session_client.metrics.message_count,
375
- )
376
-
377
- async def _cleanup_loop(self) -> None:
378
- """Background task to cleanup expired sessions."""
379
- while not self._shutdown:
380
- try:
381
- await asyncio.sleep(self.config.cleanup_interval)
382
- await self._cleanup_sessions()
383
- except asyncio.CancelledError:
384
- break
385
- except Exception as e:
386
- logger.error("session_cleanup_error", error=str(e), exc_info=True)
387
-
388
- async def _cleanup_sessions(self) -> None:
389
- """Remove expired, idle, and stuck sessions."""
390
- sessions_to_remove = []
391
- stuck_sessions = []
392
-
393
- # Get a snapshot of sessions to check
394
- async with self._lock:
395
- sessions_snapshot = list(self.sessions.items())
396
-
397
- # Check sessions outside the lock to avoid holding it too long
398
- for session_id, session_client in sessions_snapshot:
399
- # Check if session is potentially stuck (active too long)
400
- is_stuck = (
401
- session_client.status.value == "active"
402
- and session_client.metrics.idle_seconds < 10
403
- and session_client.metrics.age_seconds > 900 # 15 minutes
404
- )
405
-
406
- if is_stuck:
407
- stuck_sessions.append(session_id)
408
- logger.warning(
409
- "session_stuck_detected",
410
- session_id=session_id,
411
- age_seconds=session_client.metrics.age_seconds,
412
- idle_seconds=session_client.metrics.idle_seconds,
413
- message_count=session_client.metrics.message_count,
414
- message="Session appears stuck, will interrupt and cleanup",
415
- )
416
-
417
- # Try to interrupt stuck session before cleanup
418
- try:
419
- await session_client.interrupt()
420
- except Exception as e:
421
- logger.warning(
422
- "session_stuck_interrupt_failed",
423
- session_id=session_id,
424
- error=str(e),
425
- )
426
-
427
- # Check normal cleanup criteria (including stuck sessions)
428
- if session_client.should_cleanup(
429
- self.config.idle_threshold, stuck_threshold=900
430
- ):
431
- sessions_to_remove.append(session_id)
432
-
433
- if sessions_to_remove:
434
- logger.debug(
435
- "session_cleanup_starting",
436
- sessions_to_remove=len(sessions_to_remove),
437
- stuck_sessions=len(stuck_sessions),
438
- total_sessions=len(self.sessions),
439
- )
440
-
441
- for session_id in sessions_to_remove:
442
- await self._remove_session(session_id)
443
-
444
- async def interrupt_session(self, session_id: str) -> bool:
445
- """Interrupt a specific session due to client disconnection.
446
-
447
- Args:
448
- session_id: The session ID to interrupt
449
-
450
- Returns:
451
- True if session was found and interrupted, False otherwise
452
- """
453
- async with self._lock:
454
- if session_id not in self.sessions:
455
- logger.warning("session_not_found", session_id=session_id)
456
- return False
457
-
458
- session_client = self.sessions[session_id]
459
-
460
- try:
461
- # Interrupt the session with 30-second timeout (allows for longer SDK response times)
462
- await asyncio.wait_for(session_client.interrupt(), timeout=30.0)
463
- logger.debug("session_interrupted", session_id=session_id)
464
-
465
- # Remove the session to prevent reuse
466
- await self._remove_session(session_id)
467
- return True
468
-
469
- except (TimeoutError, Exception) as e:
470
- logger.error(
471
- "session_interrupt_failed",
472
- session_id=session_id,
473
- error=str(e)
474
- if not isinstance(e, TimeoutError)
475
- else "Timeout after 30s",
476
- )
477
- # Always remove the session on failure
478
- with contextlib.suppress(Exception):
479
- await self._remove_session(session_id)
480
- return False
481
-
482
- async def interrupt_all_sessions(self) -> int:
483
- """Interrupt all active sessions (stops ongoing operations).
484
-
485
- Returns:
486
- Number of sessions that were interrupted
487
- """
488
- # Get snapshot of all sessions
489
- async with self._lock:
490
- session_items = list(self.sessions.items())
491
-
492
- interrupted_count = 0
493
-
494
- logger.debug(
495
- "session_interrupt_all_requested",
496
- total_sessions=len(session_items),
497
- )
498
-
499
- for session_id, session_client in session_items:
500
- try:
501
- await session_client.interrupt()
502
- interrupted_count += 1
503
- except Exception as e:
504
- logger.error(
505
- "session_interrupt_failed_during_all",
506
- session_id=session_id,
507
- error=str(e),
508
- )
509
-
510
- logger.debug(
511
- "session_interrupt_all_completed",
512
- interrupted_count=interrupted_count,
513
- total_requested=len(session_items),
514
- )
515
-
516
- return interrupted_count
517
-
518
- async def has_session(self, session_id: str) -> bool:
519
- """Check if a session exists in the pool.
520
-
521
- Args:
522
- session_id: The session ID to check
523
-
524
- Returns:
525
- True if session exists, False otherwise
526
- """
527
- async with self._lock:
528
- return session_id in self.sessions
529
-
530
- async def get_stats(self) -> dict[str, Any]:
531
- """Get session pool statistics."""
532
- async with self._lock:
533
- sessions_list = list(self.sessions.values())
534
- total_sessions = len(self.sessions)
535
-
536
- active_sessions = sum(
537
- 1 for s in sessions_list if s.status == SessionStatus.ACTIVE
538
- )
539
-
540
- total_messages = sum(s.metrics.message_count for s in sessions_list)
541
-
542
- return {
543
- "enabled": self.config.enabled,
544
- "total_sessions": total_sessions,
545
- "active_sessions": active_sessions,
546
- "max_sessions": self.config.max_sessions,
547
- "total_messages": total_messages,
548
- "session_ttl": self.config.session_ttl,
549
- "cleanup_interval": self.config.cleanup_interval,
550
- }
@@ -1,34 +0,0 @@
1
- """Docker-related CLI utilities for Claude Code Proxy."""
2
-
3
- from ccproxy.cli.docker.adapter_factory import (
4
- _create_docker_adapter_from_settings,
5
- )
6
- from ccproxy.cli.docker.params import (
7
- DockerOptions,
8
- docker_arg_option,
9
- docker_env_option,
10
- docker_home_option,
11
- docker_image_option,
12
- docker_volume_option,
13
- docker_workspace_option,
14
- user_gid_option,
15
- user_mapping_option,
16
- user_uid_option,
17
- )
18
-
19
-
20
- __all__ = [
21
- # Factory functions
22
- "_create_docker_adapter_from_settings",
23
- # Docker options
24
- "DockerOptions",
25
- "docker_image_option",
26
- "docker_env_option",
27
- "docker_volume_option",
28
- "docker_arg_option",
29
- "docker_home_option",
30
- "docker_workspace_option",
31
- "user_mapping_option",
32
- "user_uid_option",
33
- "user_gid_option",
34
- ]