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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,516 @@
1
+ """Centralized async task management for lifecycle control and resource cleanup.
2
+
3
+ This module provides a centralized task manager that tracks all spawned async tasks,
4
+ handles proper cancellation on shutdown, and provides exception handling for
5
+ background tasks to prevent resource leaks and unhandled exceptions.
6
+ """
7
+
8
+ import asyncio
9
+ import contextlib
10
+ import time
11
+ import uuid
12
+ from collections.abc import Awaitable, Callable
13
+ from typing import TYPE_CHECKING, Any, Optional, TypeVar
14
+
15
+ from ccproxy.core.logging import TraceBoundLogger, get_logger
16
+
17
+
18
+ if TYPE_CHECKING: # pragma: no cover - import for type checking only
19
+ from ccproxy.services.container import ServiceContainer
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+ logger: TraceBoundLogger = get_logger(__name__)
25
+
26
+
27
+ class TaskInfo:
28
+ """Information about a managed task."""
29
+
30
+ def __init__(
31
+ self,
32
+ task: asyncio.Task[Any],
33
+ name: str,
34
+ created_at: float,
35
+ creator: str | None = None,
36
+ cleanup_callback: Callable[[], None] | None = None,
37
+ ):
38
+ self.task = task
39
+ self.name = name
40
+ self.created_at = created_at
41
+ self.creator = creator
42
+ self.cleanup_callback = cleanup_callback
43
+ self.task_id = str(uuid.uuid4())
44
+
45
+ @property
46
+ def age_seconds(self) -> float:
47
+ """Get the age of the task in seconds."""
48
+ return time.time() - self.created_at
49
+
50
+ @property
51
+ def is_done(self) -> bool:
52
+ """Check if the task is done."""
53
+ return self.task.done()
54
+
55
+ @property
56
+ def is_cancelled(self) -> bool:
57
+ """Check if the task was cancelled."""
58
+ return self.task.cancelled()
59
+
60
+ def get_exception(self) -> BaseException | None:
61
+ """Get the exception if the task failed."""
62
+ if self.task.done() and not self.task.cancelled():
63
+ try:
64
+ return self.task.exception()
65
+ except asyncio.InvalidStateError:
66
+ return None
67
+ return None
68
+
69
+
70
+ class AsyncTaskManager:
71
+ """Centralized manager for async tasks with lifecycle control.
72
+
73
+ This class provides:
74
+ - Task registration and tracking
75
+ - Automatic cleanup of completed tasks
76
+ - Graceful shutdown with cancellation
77
+ - Exception handling for background tasks
78
+ - Task monitoring and statistics
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ cleanup_interval: float = 30.0,
84
+ shutdown_timeout: float = 30.0,
85
+ max_tasks: int = 1000,
86
+ ):
87
+ """Initialize the task manager.
88
+
89
+ Args:
90
+ cleanup_interval: Interval for cleaning up completed tasks (seconds)
91
+ shutdown_timeout: Timeout for graceful shutdown (seconds)
92
+ max_tasks: Maximum number of tasks to track (prevents memory leaks)
93
+ """
94
+ self.cleanup_interval = cleanup_interval
95
+ self.shutdown_timeout = shutdown_timeout
96
+ self.max_tasks = max_tasks
97
+
98
+ self._tasks: dict[str, TaskInfo] = {}
99
+ self._lock = asyncio.Lock()
100
+ self._shutdown_event = asyncio.Event()
101
+ self._cleanup_task: asyncio.Task[None] | None = None
102
+ self._started = False
103
+
104
+ async def start(self) -> None:
105
+ """Start the task manager and its cleanup task."""
106
+ if self._started:
107
+ logger.warning("task_manager_already_started")
108
+ return
109
+
110
+ self._started = True
111
+ logger.debug("task_manager_starting", cleanup_interval=self.cleanup_interval)
112
+
113
+ # Start cleanup task
114
+ self._cleanup_task = asyncio.create_task(
115
+ self._cleanup_loop(), name="task_manager_cleanup"
116
+ )
117
+
118
+ logger.debug("task_manager_started")
119
+
120
+ async def stop(self) -> None:
121
+ """Stop the task manager and cancel all managed tasks."""
122
+ if not self._started:
123
+ return
124
+
125
+ logger.debug("task_manager_stopping", active_tasks=len(self._tasks))
126
+ self._shutdown_event.set()
127
+
128
+ # Stop cleanup task first
129
+ if self._cleanup_task and not self._cleanup_task.done():
130
+ self._cleanup_task.cancel()
131
+ with contextlib.suppress(asyncio.CancelledError):
132
+ await self._cleanup_task
133
+
134
+ # Cancel all managed tasks
135
+ await self._cancel_all_tasks()
136
+
137
+ # Clear task registry
138
+ async with self._lock:
139
+ self._tasks.clear()
140
+
141
+ self._started = False
142
+ logger.debug("task_manager_stopped")
143
+
144
+ async def create_task(
145
+ self,
146
+ coro: Awaitable[T],
147
+ *,
148
+ name: str | None = None,
149
+ creator: str | None = None,
150
+ cleanup_callback: Callable[[], None] | None = None,
151
+ ) -> asyncio.Task[T]:
152
+ """Create a managed task.
153
+
154
+ Args:
155
+ coro: Coroutine to execute
156
+ name: Optional name for the task (auto-generated if None)
157
+ creator: Optional creator identifier for debugging
158
+ cleanup_callback: Optional callback to run when task completes
159
+
160
+ Returns:
161
+ The created task
162
+
163
+ Raises:
164
+ RuntimeError: If task manager is not started or has too many tasks
165
+ """
166
+ if not self._started:
167
+ raise RuntimeError("Task manager is not started")
168
+
169
+ # Check task limit
170
+ if len(self._tasks) >= self.max_tasks:
171
+ logger.warning(
172
+ "task_manager_at_capacity",
173
+ current_tasks=len(self._tasks),
174
+ max_tasks=self.max_tasks,
175
+ )
176
+ # Clean up completed tasks to make room
177
+ await self._cleanup_completed_tasks()
178
+
179
+ if len(self._tasks) >= self.max_tasks:
180
+ raise RuntimeError(f"Task manager at capacity ({self.max_tasks} tasks)")
181
+
182
+ # Generate name if not provided
183
+ if name is None:
184
+ name = f"managed_task_{len(self._tasks)}"
185
+
186
+ # Create the task with exception handling
187
+ task = asyncio.create_task(
188
+ self._wrap_with_exception_handling(coro, name),
189
+ name=name,
190
+ )
191
+
192
+ # Register the task
193
+ task_info = TaskInfo(
194
+ task=task,
195
+ name=name,
196
+ created_at=time.time(),
197
+ creator=creator,
198
+ cleanup_callback=cleanup_callback,
199
+ )
200
+
201
+ async with self._lock:
202
+ self._tasks[task_info.task_id] = task_info
203
+
204
+ # Add done callback for automatic cleanup
205
+ task.add_done_callback(lambda t: self._schedule_cleanup_callback(task_info))
206
+
207
+ logger.debug(
208
+ "task_created",
209
+ task_id=task_info.task_id,
210
+ task_name=name,
211
+ creator=creator,
212
+ total_tasks=len(self._tasks),
213
+ )
214
+
215
+ return task
216
+
217
+ async def _wrap_with_exception_handling(
218
+ self, coro: Awaitable[T], task_name: str
219
+ ) -> T:
220
+ """Wrap coroutine with exception handling."""
221
+ try:
222
+ return await coro
223
+ except asyncio.CancelledError:
224
+ logger.debug("task_cancelled", task_name=task_name)
225
+ raise
226
+ except Exception as e:
227
+ logger.error(
228
+ "task_exception",
229
+ task_name=task_name,
230
+ error=str(e),
231
+ error_type=type(e).__name__,
232
+ exc_info=True,
233
+ )
234
+ raise
235
+
236
+ def _schedule_cleanup_callback(self, task_info: TaskInfo) -> None:
237
+ """Schedule cleanup callback for completed task."""
238
+ try:
239
+ # Run cleanup callback if provided
240
+ if task_info.cleanup_callback:
241
+ task_info.cleanup_callback()
242
+ except Exception as e:
243
+ logger.warning(
244
+ "task_cleanup_callback_failed",
245
+ task_id=task_info.task_id,
246
+ task_name=task_info.name,
247
+ error=str(e),
248
+ exc_info=True,
249
+ )
250
+
251
+ async def _cleanup_loop(self) -> None:
252
+ """Background loop for cleaning up completed tasks."""
253
+ logger.debug("task_cleanup_loop_started")
254
+
255
+ while not self._shutdown_event.is_set():
256
+ try:
257
+ await asyncio.wait_for(
258
+ self._shutdown_event.wait(), timeout=self.cleanup_interval
259
+ )
260
+ break # Shutdown event set
261
+ except TimeoutError:
262
+ pass # Continue with cleanup
263
+
264
+ await self._cleanup_completed_tasks()
265
+
266
+ logger.debug("task_cleanup_loop_stopped")
267
+
268
+ async def _cleanup_completed_tasks(self) -> None:
269
+ """Clean up completed tasks from the registry."""
270
+ completed_tasks = []
271
+
272
+ async with self._lock:
273
+ for task_id, task_info in list(self._tasks.items()):
274
+ if task_info.is_done:
275
+ completed_tasks.append((task_id, task_info))
276
+ del self._tasks[task_id]
277
+
278
+ if completed_tasks:
279
+ logger.debug(
280
+ "tasks_cleaned_up",
281
+ completed_count=len(completed_tasks),
282
+ remaining_tasks=len(self._tasks),
283
+ )
284
+
285
+ # Log any task exceptions
286
+ for task_id, task_info in completed_tasks:
287
+ if task_info.get_exception():
288
+ logger.warning(
289
+ "completed_task_had_exception",
290
+ task_id=task_id,
291
+ task_name=task_info.name,
292
+ exception=str(task_info.get_exception()),
293
+ )
294
+
295
+ async def _cancel_all_tasks(self) -> None:
296
+ """Cancel all managed tasks with timeout."""
297
+ if not self._tasks:
298
+ return
299
+
300
+ logger.debug("cancelling_all_tasks", task_count=len(self._tasks))
301
+
302
+ # Cancel all tasks
303
+ tasks_to_cancel = []
304
+ async with self._lock:
305
+ for task_info in self._tasks.values():
306
+ if not task_info.is_done:
307
+ task_info.task.cancel()
308
+ tasks_to_cancel.append(task_info.task)
309
+
310
+ if not tasks_to_cancel:
311
+ return
312
+
313
+ # Wait for cancellation with timeout
314
+ try:
315
+ await asyncio.wait_for(
316
+ asyncio.gather(*tasks_to_cancel, return_exceptions=True),
317
+ timeout=self.shutdown_timeout,
318
+ )
319
+ logger.debug("all_tasks_cancelled_gracefully")
320
+ except TimeoutError:
321
+ logger.warning(
322
+ "task_cancellation_timeout",
323
+ timeout=self.shutdown_timeout,
324
+ remaining_tasks=sum(1 for t in tasks_to_cancel if not t.done()),
325
+ )
326
+
327
+ async def get_task_stats(self) -> dict[str, Any]:
328
+ """Get statistics about managed tasks."""
329
+ async with self._lock:
330
+ active_tasks = sum(1 for t in self._tasks.values() if not t.is_done)
331
+ cancelled_tasks = sum(1 for t in self._tasks.values() if t.is_cancelled)
332
+ failed_tasks = sum(
333
+ 1
334
+ for t in self._tasks.values()
335
+ if t.is_done and not t.is_cancelled and t.get_exception()
336
+ )
337
+
338
+ return {
339
+ "total_tasks": len(self._tasks),
340
+ "active_tasks": active_tasks,
341
+ "cancelled_tasks": cancelled_tasks,
342
+ "failed_tasks": failed_tasks,
343
+ "completed_tasks": len(self._tasks) - active_tasks,
344
+ "started": self._started,
345
+ "max_tasks": self.max_tasks,
346
+ }
347
+
348
+ async def list_active_tasks(self) -> list[dict[str, Any]]:
349
+ """Get list of active tasks with details."""
350
+ active_tasks = []
351
+
352
+ async with self._lock:
353
+ for task_info in self._tasks.values():
354
+ if not task_info.is_done:
355
+ active_tasks.append(
356
+ {
357
+ "task_id": task_info.task_id,
358
+ "name": task_info.name,
359
+ "creator": task_info.creator,
360
+ "age_seconds": task_info.age_seconds,
361
+ "created_at": task_info.created_at,
362
+ }
363
+ )
364
+
365
+ return active_tasks
366
+
367
+ @property
368
+ def is_started(self) -> bool:
369
+ """Check if the task manager is started."""
370
+ return self._started
371
+
372
+
373
+ # Dependency-injected access helpers
374
+
375
+
376
+ def _resolve_task_manager(
377
+ *,
378
+ container: Optional["ServiceContainer"] = None,
379
+ task_manager: Optional["AsyncTaskManager"] = None,
380
+ ) -> "AsyncTaskManager":
381
+ """Resolve the async task manager instance using dependency injection.
382
+
383
+ Args:
384
+ container: Optional service container to resolve the manager from
385
+ task_manager: Optional explicit manager instance (takes precedence)
386
+
387
+ Returns:
388
+ AsyncTaskManager instance
389
+
390
+ Raises:
391
+ RuntimeError: If the manager cannot be resolved
392
+ """
393
+
394
+ if task_manager is not None:
395
+ return task_manager
396
+
397
+ from ccproxy.services.container import ServiceContainer as _ServiceContainer
398
+
399
+ if container is not None:
400
+ resolved_container: _ServiceContainer = container
401
+ else:
402
+ resolved_container_maybe = _ServiceContainer.get_current(strict=False)
403
+ if resolved_container_maybe is None:
404
+ raise RuntimeError(
405
+ "ServiceContainer is not available; provide a container or task manager"
406
+ )
407
+ resolved_container = resolved_container_maybe
408
+
409
+ try:
410
+ return resolved_container.get_async_task_manager()
411
+ except Exception as exc:
412
+ raise RuntimeError(
413
+ "AsyncTaskManager is not registered in the provided ServiceContainer"
414
+ ) from exc
415
+
416
+
417
+ async def create_managed_task(
418
+ coro: Awaitable[T],
419
+ *,
420
+ name: str | None = None,
421
+ creator: str | None = None,
422
+ cleanup_callback: Callable[[], None] | None = None,
423
+ container: Optional["ServiceContainer"] = None,
424
+ task_manager: Optional["AsyncTaskManager"] = None,
425
+ ) -> asyncio.Task[T]:
426
+ """Create a managed task using the dependency-injected task manager.
427
+
428
+ Args:
429
+ coro: Coroutine to execute
430
+ name: Optional name for the task
431
+ creator: Optional creator identifier
432
+ cleanup_callback: Optional cleanup callback
433
+ container: Optional service container for resolving the task manager
434
+ task_manager: Optional explicit task manager instance
435
+
436
+ Returns:
437
+ The created managed task
438
+ """
439
+
440
+ manager = _resolve_task_manager(container=container, task_manager=task_manager)
441
+ return await manager.create_task(
442
+ coro, name=name, creator=creator, cleanup_callback=cleanup_callback
443
+ )
444
+
445
+
446
+ async def start_task_manager(
447
+ *,
448
+ container: Optional["ServiceContainer"] = None,
449
+ task_manager: Optional["AsyncTaskManager"] = None,
450
+ ) -> None:
451
+ """Start the dependency-injected task manager."""
452
+
453
+ manager = _resolve_task_manager(container=container, task_manager=task_manager)
454
+ await manager.start()
455
+
456
+
457
+ async def stop_task_manager(
458
+ *,
459
+ container: Optional["ServiceContainer"] = None,
460
+ task_manager: Optional["AsyncTaskManager"] = None,
461
+ ) -> None:
462
+ """Stop the dependency-injected task manager."""
463
+
464
+ manager = _resolve_task_manager(container=container, task_manager=task_manager)
465
+ await manager.stop()
466
+
467
+
468
+ def create_fire_and_forget_task(
469
+ coro: Awaitable[T],
470
+ *,
471
+ name: str | None = None,
472
+ creator: str | None = None,
473
+ container: Optional["ServiceContainer"] = None,
474
+ task_manager: Optional["AsyncTaskManager"] = None,
475
+ ) -> None:
476
+ """Create a fire-and-forget managed task from a synchronous context.
477
+
478
+ This function schedules a coroutine to run as a managed task without
479
+ needing to await it. Useful for calling from synchronous functions
480
+ that need to schedule background work.
481
+
482
+ Args:
483
+ coro: Coroutine to execute
484
+ name: Optional name for the task
485
+ creator: Optional creator identifier
486
+ container: Optional service container to resolve the task manager
487
+ task_manager: Optional explicit task manager instance
488
+ """
489
+
490
+ manager = _resolve_task_manager(container=container, task_manager=task_manager)
491
+
492
+ if not manager.is_started:
493
+ # If task manager isn't started, fall back to regular asyncio.create_task
494
+ logger.warning(
495
+ "task_manager_not_started_fire_and_forget",
496
+ name=name,
497
+ creator=creator,
498
+ )
499
+ asyncio.create_task(coro, name=name) # type: ignore[arg-type]
500
+ return
501
+
502
+ # Schedule the task creation as a fire-and-forget operation
503
+ async def _create_managed_task() -> None:
504
+ try:
505
+ await manager.create_task(coro, name=name, creator=creator)
506
+ except Exception as e:
507
+ logger.error(
508
+ "fire_and_forget_task_creation_failed",
509
+ name=name,
510
+ creator=creator,
511
+ error=str(e),
512
+ exc_info=True,
513
+ )
514
+
515
+ # Use asyncio.create_task to schedule the managed task creation
516
+ asyncio.create_task(_create_managed_task(), name=f"create_{name or 'unnamed'}")
@@ -5,7 +5,9 @@ import re
5
5
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
6
6
  from contextlib import asynccontextmanager, contextmanager
7
7
  from pathlib import Path
8
- from typing import Any, TypeVar
8
+ from typing import Any, TypeVar, cast
9
+
10
+ from ccproxy.core.logging import get_logger
9
11
 
10
12
 
11
13
  T = TypeVar("T")
@@ -45,7 +47,13 @@ def get_package_dir() -> Path:
45
47
  package_dir = Path(spec.origin).parent.parent.resolve()
46
48
  else:
47
49
  package_dir = Path(__file__).parent.parent.parent.resolve()
48
- except Exception:
50
+ except (AttributeError, ImportError, ModuleNotFoundError) as e:
51
+ logger = get_logger(__name__)
52
+ logger.debug("package_dir_fallback", error=str(e), exc_info=e)
53
+ package_dir = Path(__file__).parent.parent.parent.resolve()
54
+ except Exception as e:
55
+ logger = get_logger(__name__)
56
+ logger.debug("package_dir_unexpected_error", error=str(e), exc_info=e)
49
57
  package_dir = Path(__file__).parent.parent.parent.resolve()
50
58
 
51
59
  return package_dir
@@ -100,7 +108,11 @@ async def safe_await(awaitable: Awaitable[T], timeout: float | None = None) -> T
100
108
  return await awaitable
101
109
  except TimeoutError:
102
110
  return None
103
- except Exception:
111
+ except asyncio.CancelledError:
112
+ return None
113
+ except Exception as e:
114
+ logger = get_logger(__name__)
115
+ logger.debug("awaitable_silent_error", error=str(e), exc_info=e)
104
116
  return None
105
117
 
106
118
 
@@ -215,7 +227,11 @@ async def wait_for_condition(
215
227
  result = await result
216
228
  if result:
217
229
  return True
218
- except Exception:
230
+ except (asyncio.CancelledError, KeyboardInterrupt):
231
+ return False
232
+ except Exception as e:
233
+ logger = get_logger(__name__)
234
+ logger.debug("condition_check_error", error=str(e), exc_info=e)
219
235
  pass
220
236
 
221
237
  if asyncio.get_event_loop().time() - start_time > timeout:
@@ -254,7 +270,7 @@ async def async_cache_result(
254
270
  if cache_key in _cache:
255
271
  cached_time, cached_result = _cache[cache_key]
256
272
  if current_time - cached_time < cache_duration:
257
- return cached_result # type: ignore[no-any-return]
273
+ return cast(T, cached_result)
258
274
 
259
275
  # Compute and cache the result
260
276
  result = await func(*args, **kwargs)
@@ -467,10 +483,14 @@ def validate_config_with_schema(
467
483
  import tempfile
468
484
 
469
485
  # Import tomllib for Python 3.11+ or fallback to tomli
486
+ # Avoid name redefinition warnings by selecting a loader function.
470
487
  try:
471
- import tomllib
488
+ import tomllib as _tomllib
489
+
490
+ toml_load = _tomllib.load
472
491
  except ImportError:
473
- import tomli as tomllib # type: ignore[no-redef]
492
+ _tomli = __import__("tomli")
493
+ toml_load = _tomli.load
474
494
 
475
495
  config_path = Path()
476
496
 
@@ -483,7 +503,7 @@ def validate_config_with_schema(
483
503
  if suffix == ".toml":
484
504
  # Read and parse TOML - let TOML parse errors bubble up
485
505
  with config_path.open("rb") as f:
486
- toml_data = tomllib.load(f)
506
+ toml_data = toml_load(f)
487
507
 
488
508
  # Get or generate schema
489
509
  if schema_path:
@@ -528,6 +548,16 @@ def validate_config_with_schema(
528
548
  "check-jsonschema command not found. "
529
549
  "Install with: pip install check-jsonschema"
530
550
  ) from e
551
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
552
+ # Clean up temporary files in case of error
553
+ Path(temp_schema_path).unlink(missing_ok=True)
554
+ Path(temp_json_path).unlink(missing_ok=True)
555
+ raise ValueError(f"Schema validation subprocess error: {e}") from e
556
+ except (OSError, PermissionError) as e:
557
+ # Clean up temporary files in case of error
558
+ Path(temp_schema_path).unlink(missing_ok=True)
559
+ Path(temp_json_path).unlink(missing_ok=True)
560
+ raise ValueError(f"File operation error during validation: {e}") from e
531
561
  except Exception as e:
532
562
  # Clean up temporary files in case of error
533
563
  Path(temp_schema_path).unlink(missing_ok=True)
@@ -577,6 +607,14 @@ def validate_config_with_schema(
577
607
  "check-jsonschema command not found. "
578
608
  "Install with: pip install check-jsonschema"
579
609
  ) from e
610
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
611
+ if cleanup_schema:
612
+ Path(temp_schema_path).unlink(missing_ok=True)
613
+ raise ValueError(f"Schema validation subprocess error: {e}") from e
614
+ except (OSError, PermissionError) as e:
615
+ if cleanup_schema:
616
+ Path(temp_schema_path).unlink(missing_ok=True)
617
+ raise ValueError(f"File operation error during validation: {e}") from e
580
618
  except Exception as e:
581
619
  if cleanup_schema:
582
620
  Path(temp_schema_path).unlink(missing_ok=True)
@@ -595,13 +633,8 @@ def generate_json_schema() -> dict[str, Any]:
595
633
  Returns:
596
634
  JSON Schema dictionary
597
635
 
598
- Raises:
599
- ImportError: If required dependencies are not available
600
636
  """
601
- try:
602
- from ccproxy.config.settings import Settings
603
- except ImportError as e:
604
- raise ImportError(f"Required dependencies not available: {e}") from e
637
+ from ccproxy.config.settings import Settings
605
638
 
606
639
  schema = Settings.model_json_schema()
607
640
 
@@ -0,0 +1,6 @@
1
+ """Core auth module."""
2
+
3
+ # Note: AuthManagerRegistry has been moved to ccproxy.services.auth_registry
4
+ # to avoid circular imports
5
+
6
+ __all__: list[str] = []