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
@@ -1,14 +1,31 @@
1
1
  """Base scheduled task classes and task implementations."""
2
2
 
3
3
  import asyncio
4
- import contextlib
5
4
  import random
6
5
  import time
7
6
  from abc import ABC, abstractmethod
8
- from datetime import UTC
7
+ from datetime import UTC, datetime
9
8
  from typing import Any
10
9
 
11
10
  import structlog
11
+ from packaging import version as pkg_version
12
+
13
+ from ccproxy.core.async_task_manager import create_managed_task
14
+ from ccproxy.scheduler.errors import SchedulerError
15
+ from ccproxy.utils.version_checker import (
16
+ VersionCheckState,
17
+ commit_refs_match,
18
+ compare_versions,
19
+ extract_commit_from_version,
20
+ fetch_latest_branch_commit,
21
+ fetch_latest_github_version,
22
+ get_branch_override,
23
+ get_current_version,
24
+ get_version_check_state_path,
25
+ load_check_state,
26
+ resolve_branch_for_commit,
27
+ save_check_state,
28
+ )
12
29
 
13
30
 
14
31
  logger = structlog.get_logger(__name__)
@@ -50,6 +67,7 @@ class BaseScheduledTask(ABC):
50
67
  self._last_run_time: float = 0
51
68
  self._running = False
52
69
  self._task: asyncio.Task[Any] | None = None
70
+ self._stop_complete: asyncio.Event | None = None
53
71
 
54
72
  @abstractmethod
55
73
  async def run(self) -> bool:
@@ -111,12 +129,27 @@ class BaseScheduledTask(ABC):
111
129
  return
112
130
 
113
131
  self._running = True
132
+ self._stop_complete = asyncio.Event()
114
133
  logger.debug("task_starting", task_name=self.name)
115
134
 
116
135
  try:
117
136
  await self.setup()
118
- self._task = asyncio.create_task(self._run_loop())
137
+ self._task = await create_managed_task(
138
+ self._run_loop(),
139
+ name=f"scheduled_task_{self.name}",
140
+ creator="BaseScheduledTask",
141
+ )
119
142
  logger.debug("task_started", task_name=self.name)
143
+ except SchedulerError as e:
144
+ self._running = False
145
+ logger.error(
146
+ "task_start_scheduler_error",
147
+ task_name=self.name,
148
+ error=str(e),
149
+ error_type=type(e).__name__,
150
+ exc_info=e,
151
+ )
152
+ raise
120
153
  except Exception as e:
121
154
  self._running = False
122
155
  logger.error(
@@ -124,6 +157,7 @@ class BaseScheduledTask(ABC):
124
157
  task_name=self.name,
125
158
  error=str(e),
126
159
  error_type=type(e).__name__,
160
+ exc_info=e,
127
161
  )
128
162
  raise
129
163
 
@@ -135,21 +169,55 @@ class BaseScheduledTask(ABC):
135
169
  self._running = False
136
170
  logger.debug("task_stopping", task_name=self.name)
137
171
 
138
- # Cancel the running task
172
+ # Cancel the running task and wait for it to complete
139
173
  if self._task and not self._task.done():
140
174
  self._task.cancel()
141
- with contextlib.suppress(asyncio.CancelledError):
175
+ try:
176
+ # Wait for the task to complete cancellation
142
177
  await self._task
178
+ except asyncio.CancelledError:
179
+ # Expected when task is cancelled
180
+ pass
181
+ except Exception as e:
182
+ logger.warning(
183
+ "task_stop_unexpected_error",
184
+ task_name=self.name,
185
+ error=str(e),
186
+ error_type=type(e).__name__,
187
+ )
188
+
189
+ # Ensure the task reference is cleared
190
+ self._task = None
191
+
192
+ # Wait for the completion event to be signaled
193
+ if self._stop_complete is not None:
194
+ try:
195
+ await asyncio.wait_for(self._stop_complete.wait(), timeout=1.0)
196
+ except TimeoutError:
197
+ logger.warning(
198
+ "task_stop_completion_timeout",
199
+ task_name=self.name,
200
+ message="Task stop completion event not signaled within timeout",
201
+ )
143
202
 
144
203
  try:
145
204
  await self.cleanup()
146
205
  logger.debug("task_stopped", task_name=self.name)
206
+ except SchedulerError as e:
207
+ logger.error(
208
+ "task_cleanup_scheduler_error",
209
+ task_name=self.name,
210
+ error=str(e),
211
+ error_type=type(e).__name__,
212
+ exc_info=e,
213
+ )
147
214
  except Exception as e:
148
215
  logger.error(
149
216
  "task_cleanup_failed",
150
217
  task_name=self.name,
151
218
  error=str(e),
152
219
  error_type=type(e).__name__,
220
+ exc_info=e,
153
221
  )
154
222
 
155
223
  async def _run_loop(self) -> None:
@@ -199,6 +267,32 @@ class BaseScheduledTask(ABC):
199
267
  except asyncio.CancelledError:
200
268
  logger.debug("task_cancelled", task_name=self.name)
201
269
  break
270
+ except TimeoutError as e:
271
+ self._consecutive_failures += 1
272
+ logger.error(
273
+ "task_execution_timeout_error",
274
+ task_name=self.name,
275
+ error=str(e),
276
+ error_type=type(e).__name__,
277
+ consecutive_failures=self._consecutive_failures,
278
+ exc_info=e,
279
+ )
280
+ # Use backoff delay for exceptions too
281
+ backoff_delay = self.calculate_next_delay()
282
+ await asyncio.sleep(backoff_delay)
283
+ except SchedulerError as e:
284
+ self._consecutive_failures += 1
285
+ logger.error(
286
+ "task_execution_scheduler_error",
287
+ task_name=self.name,
288
+ error=str(e),
289
+ error_type=type(e).__name__,
290
+ consecutive_failures=self._consecutive_failures,
291
+ exc_info=e,
292
+ )
293
+ # Use backoff delay for exceptions too
294
+ backoff_delay = self.calculate_next_delay()
295
+ await asyncio.sleep(backoff_delay)
202
296
  except Exception as e:
203
297
  self._consecutive_failures += 1
204
298
  logger.error(
@@ -207,12 +301,16 @@ class BaseScheduledTask(ABC):
207
301
  error=str(e),
208
302
  error_type=type(e).__name__,
209
303
  consecutive_failures=self._consecutive_failures,
304
+ exc_info=e,
210
305
  )
211
-
212
306
  # Use backoff delay for exceptions too
213
307
  backoff_delay = self.calculate_next_delay()
214
308
  await asyncio.sleep(backoff_delay)
215
309
 
310
+ # Signal that the task has completed
311
+ if self._stop_complete is not None:
312
+ self._stop_complete.set()
313
+
216
314
  @property
217
315
  def is_running(self) -> bool:
218
316
  """Check if the task is currently running."""
@@ -246,243 +344,6 @@ class BaseScheduledTask(ABC):
246
344
  }
247
345
 
248
346
 
249
- class PushgatewayTask(BaseScheduledTask):
250
- """Task for pushing metrics to Pushgateway periodically."""
251
-
252
- def __init__(
253
- self,
254
- name: str,
255
- interval_seconds: float,
256
- enabled: bool = True,
257
- max_backoff_seconds: float = 300.0,
258
- ):
259
- """
260
- Initialize pushgateway task.
261
-
262
- Args:
263
- name: Task name
264
- interval_seconds: Interval between pushgateway operations
265
- enabled: Whether task is enabled
266
- max_backoff_seconds: Maximum backoff delay for failures
267
- """
268
- super().__init__(
269
- name=name,
270
- interval_seconds=interval_seconds,
271
- enabled=enabled,
272
- max_backoff_seconds=max_backoff_seconds,
273
- )
274
- self._metrics_instance: Any | None = None
275
-
276
- async def setup(self) -> None:
277
- """Initialize metrics instance for pushgateway operations."""
278
- try:
279
- from ccproxy.observability.metrics import get_metrics
280
-
281
- self._metrics_instance = get_metrics()
282
- logger.debug("pushgateway_task_setup_complete", task_name=self.name)
283
- except Exception as e:
284
- logger.error(
285
- "pushgateway_task_setup_failed",
286
- task_name=self.name,
287
- error=str(e),
288
- error_type=type(e).__name__,
289
- )
290
- raise
291
-
292
- async def run(self) -> bool:
293
- """Execute pushgateway metrics push."""
294
- try:
295
- if not self._metrics_instance:
296
- logger.warning("pushgateway_no_metrics_instance", task_name=self.name)
297
- return False
298
-
299
- if not self._metrics_instance.is_pushgateway_enabled():
300
- logger.debug("pushgateway_disabled", task_name=self.name)
301
- return True # Not an error, just disabled
302
-
303
- success = bool(self._metrics_instance.push_to_gateway())
304
-
305
- if success:
306
- logger.debug("pushgateway_push_success", task_name=self.name)
307
- else:
308
- logger.warning("pushgateway_push_failed", task_name=self.name)
309
-
310
- return success
311
-
312
- except Exception as e:
313
- logger.error(
314
- "pushgateway_task_error",
315
- task_name=self.name,
316
- error=str(e),
317
- error_type=type(e).__name__,
318
- )
319
- return False
320
-
321
-
322
- class StatsPrintingTask(BaseScheduledTask):
323
- """Task for printing stats summary periodically."""
324
-
325
- def __init__(
326
- self,
327
- name: str,
328
- interval_seconds: float,
329
- enabled: bool = True,
330
- ):
331
- """
332
- Initialize stats printing task.
333
-
334
- Args:
335
- name: Task name
336
- interval_seconds: Interval between stats printing
337
- enabled: Whether task is enabled
338
- """
339
- super().__init__(
340
- name=name,
341
- interval_seconds=interval_seconds,
342
- enabled=enabled,
343
- )
344
- self._stats_collector_instance: Any | None = None
345
- self._metrics_instance: Any | None = None
346
-
347
- async def setup(self) -> None:
348
- """Initialize stats collector and metrics instances."""
349
- try:
350
- from ccproxy.config.settings import get_settings
351
- from ccproxy.observability.metrics import get_metrics
352
- from ccproxy.observability.stats_printer import get_stats_collector
353
-
354
- self._metrics_instance = get_metrics()
355
- settings = get_settings()
356
- self._stats_collector_instance = get_stats_collector(
357
- settings=settings.observability,
358
- metrics_instance=self._metrics_instance,
359
- )
360
- logger.debug("stats_printing_task_setup_complete", task_name=self.name)
361
- except Exception as e:
362
- logger.error(
363
- "stats_printing_task_setup_failed",
364
- task_name=self.name,
365
- error=str(e),
366
- error_type=type(e).__name__,
367
- )
368
- raise
369
-
370
- async def run(self) -> bool:
371
- """Execute stats printing."""
372
- try:
373
- if not self._stats_collector_instance:
374
- logger.warning("stats_printing_no_collector", task_name=self.name)
375
- return False
376
-
377
- await self._stats_collector_instance.print_stats()
378
- logger.debug("stats_printing_success", task_name=self.name)
379
- return True
380
-
381
- except Exception as e:
382
- logger.error(
383
- "stats_printing_task_error",
384
- task_name=self.name,
385
- error=str(e),
386
- error_type=type(e).__name__,
387
- )
388
- return False
389
-
390
-
391
- class PricingCacheUpdateTask(BaseScheduledTask):
392
- """Task for updating pricing cache periodically."""
393
-
394
- def __init__(
395
- self,
396
- name: str,
397
- interval_seconds: float,
398
- enabled: bool = True,
399
- force_refresh_on_startup: bool = False,
400
- pricing_updater: Any | None = None,
401
- ):
402
- """
403
- Initialize pricing cache update task.
404
-
405
- Args:
406
- name: Task name
407
- interval_seconds: Interval between pricing updates
408
- enabled: Whether task is enabled
409
- force_refresh_on_startup: Whether to force refresh on first run
410
- pricing_updater: Injected pricing updater instance
411
- """
412
- super().__init__(
413
- name=name,
414
- interval_seconds=interval_seconds,
415
- enabled=enabled,
416
- )
417
- self.force_refresh_on_startup = force_refresh_on_startup
418
- self._pricing_updater = pricing_updater
419
- self._first_run = True
420
-
421
- async def setup(self) -> None:
422
- """Initialize pricing updater instance if not injected."""
423
- if self._pricing_updater is None:
424
- try:
425
- from ccproxy.config.pricing import PricingSettings
426
- from ccproxy.pricing.cache import PricingCache
427
- from ccproxy.pricing.updater import PricingUpdater
428
-
429
- # Create pricing components with dependency injection
430
- settings = PricingSettings()
431
- cache = PricingCache(settings)
432
- self._pricing_updater = PricingUpdater(cache, settings)
433
- logger.debug("pricing_update_task_setup_complete", task_name=self.name)
434
- except Exception as e:
435
- logger.error(
436
- "pricing_update_task_setup_failed",
437
- task_name=self.name,
438
- error=str(e),
439
- error_type=type(e).__name__,
440
- )
441
- raise
442
- else:
443
- logger.debug(
444
- "pricing_update_task_using_injected_updater", task_name=self.name
445
- )
446
-
447
- async def run(self) -> bool:
448
- """Execute pricing cache update."""
449
- try:
450
- if not self._pricing_updater:
451
- logger.warning("pricing_update_no_updater", task_name=self.name)
452
- return False
453
-
454
- # Force refresh on first run if configured
455
- force_refresh = self._first_run and self.force_refresh_on_startup
456
- self._first_run = False
457
-
458
- if force_refresh:
459
- logger.info("pricing_update_force_refresh_startup", task_name=self.name)
460
- refresh_result = await self._pricing_updater.force_refresh()
461
- success = bool(refresh_result)
462
- else:
463
- # Regular update check
464
- pricing_data = await self._pricing_updater.get_current_pricing(
465
- force_refresh=False
466
- )
467
- success = pricing_data is not None
468
-
469
- if success:
470
- logger.debug("pricing_update_success", task_name=self.name)
471
- else:
472
- logger.warning("pricing_update_failed", task_name=self.name)
473
-
474
- return success
475
-
476
- except Exception as e:
477
- logger.error(
478
- "pricing_update_task_error",
479
- task_name=self.name,
480
- error=str(e),
481
- error_type=type(e).__name__,
482
- )
483
- return False
484
-
485
-
486
347
  class PoolStatsTask(BaseScheduledTask):
487
348
  """Task for displaying pool statistics periodically."""
488
349
 
@@ -601,6 +462,7 @@ class PoolStatsTask(BaseScheduledTask):
601
462
  task_name=self.name,
602
463
  error=str(e),
603
464
  error_type=type(e).__name__,
465
+ exc_info=e,
604
466
  )
605
467
  return False
606
468
 
@@ -647,8 +509,6 @@ class VersionUpdateCheckTask(BaseScheduledTask):
647
509
  current_version: Current version string
648
510
  latest_version: Latest version string
649
511
  """
650
- from ccproxy.utils.version_checker import compare_versions
651
-
652
512
  if compare_versions(current_version, latest_version):
653
513
  logger.warning(
654
514
  "version_update_available",
@@ -656,7 +516,7 @@ class VersionUpdateCheckTask(BaseScheduledTask):
656
516
  current_version=current_version,
657
517
  latest_version=latest_version,
658
518
  source=source,
659
- message=(f"New version available: {latest_version}"),
519
+ description=(f"New version available: {latest_version}"),
660
520
  )
661
521
  else:
662
522
  logger.debug(
@@ -665,7 +525,7 @@ class VersionUpdateCheckTask(BaseScheduledTask):
665
525
  current_version=current_version,
666
526
  latest_version=latest_version,
667
527
  source=source,
668
- message=(
528
+ description=(
669
529
  f"No update: latest_version={latest_version} "
670
530
  f"current_version={current_version}"
671
531
  ),
@@ -679,17 +539,6 @@ class VersionUpdateCheckTask(BaseScheduledTask):
679
539
  task_name=self.name,
680
540
  first_run=self._first_run,
681
541
  )
682
- from datetime import datetime
683
-
684
- from ccproxy.utils.version_checker import (
685
- VersionCheckState,
686
- fetch_latest_github_version,
687
- get_current_version,
688
- get_version_check_state_path,
689
- load_check_state,
690
- save_check_state,
691
- )
692
-
693
542
  state_path = get_version_check_state_path()
694
543
  current_time = datetime.now(UTC)
695
544
 
@@ -709,7 +558,38 @@ class VersionUpdateCheckTask(BaseScheduledTask):
709
558
 
710
559
  # Load previous state if available
711
560
  prev_state: VersionCheckState | None = await load_check_state(state_path)
561
+
562
+ current_version = get_current_version()
563
+ current_commit = extract_commit_from_version(current_version)
564
+
565
+ if prev_state is not None:
566
+ invalidation_reason: str | None = None
567
+ if (
568
+ prev_state.running_version is not None
569
+ and prev_state.running_version != current_version
570
+ ):
571
+ invalidation_reason = "version"
572
+ elif (
573
+ prev_state.running_commit is not None
574
+ and current_commit is not None
575
+ and not commit_refs_match(prev_state.running_commit, current_commit)
576
+ ):
577
+ invalidation_reason = "commit"
578
+
579
+ if invalidation_reason is not None:
580
+ logger.debug(
581
+ "version_check_cache_invalidated",
582
+ task_name=self.name,
583
+ reason=invalidation_reason,
584
+ cached_running_version=prev_state.running_version,
585
+ cached_running_commit=prev_state.running_commit,
586
+ current_version=current_version,
587
+ current_commit=current_commit,
588
+ )
589
+ prev_state = None
590
+
712
591
  latest_version: str | None = None
592
+ latest_branch_commit: str | None = None
713
593
  source: str | None = None
714
594
 
715
595
  # If we have a recent state within the freshness window, avoid network call
@@ -725,6 +605,7 @@ class VersionUpdateCheckTask(BaseScheduledTask):
725
605
  max_age_hours=max_age_hours,
726
606
  )
727
607
  latest_version = prev_state.latest_version_found
608
+ latest_branch_commit = prev_state.latest_branch_commit
728
609
  source = "cache"
729
610
  else:
730
611
  logger.debug(
@@ -734,26 +615,118 @@ class VersionUpdateCheckTask(BaseScheduledTask):
734
615
  max_age_hours=max_age_hours,
735
616
  )
736
617
 
737
- # Fetch only if we don't have a fresh cached version
738
- if latest_version is None:
739
- latest_version = await fetch_latest_github_version()
618
+ current_version_parsed = pkg_version.parse(current_version)
619
+ branch_name: str | None = None
620
+
621
+ if current_version_parsed.is_devrelease and current_commit is not None:
622
+ branch_name = get_branch_override()
623
+ if branch_name is None and prev_state is not None:
624
+ branch_name = prev_state.latest_branch_name
625
+ if branch_name is None:
626
+ branch_name = await resolve_branch_for_commit(current_commit)
627
+
628
+ if branch_name is not None:
629
+ if source == "cache" and (
630
+ prev_state is None
631
+ or prev_state.latest_branch_name != branch_name
632
+ or not prev_state.latest_branch_commit
633
+ ):
634
+ latest_branch_commit = None
635
+ source = None
636
+
637
+ if latest_branch_commit is None:
638
+ latest_branch_commit = await fetch_latest_branch_commit(branch_name)
639
+ if latest_branch_commit is None:
640
+ logger.warning(
641
+ "version_check_branch_fetch_failed",
642
+ task_name=self.name,
643
+ branch=branch_name,
644
+ )
645
+ return False
646
+
647
+ await save_check_state(
648
+ state_path,
649
+ VersionCheckState(
650
+ last_check_at=current_time,
651
+ latest_version_found=(
652
+ latest_version
653
+ or (
654
+ prev_state.latest_version_found
655
+ if prev_state is not None
656
+ else None
657
+ )
658
+ ),
659
+ latest_branch_name=branch_name,
660
+ latest_branch_commit=latest_branch_commit,
661
+ running_version=current_version,
662
+ running_commit=current_commit,
663
+ ),
664
+ )
665
+ source = "network"
666
+
667
+ if current_commit is None:
668
+ logger.debug(
669
+ "branch_revision_no_commit_to_compare",
670
+ task_name=self.name,
671
+ branch=branch_name,
672
+ source=source,
673
+ )
674
+ else:
675
+ update_available = not commit_refs_match(
676
+ current_commit, latest_branch_commit
677
+ )
678
+ if update_available:
679
+ logger.warning(
680
+ "branch_revision_update_available",
681
+ task_name=self.name,
682
+ branch=branch_name,
683
+ current_commit=current_commit,
684
+ latest_commit=latest_branch_commit,
685
+ source=source,
686
+ description=(
687
+ "New commits available for branch "
688
+ f"{branch_name}: {latest_branch_commit}"
689
+ ),
690
+ )
691
+ else:
692
+ logger.debug(
693
+ "branch_revision_up_to_date",
694
+ task_name=self.name,
695
+ branch=branch_name,
696
+ current_commit=current_commit,
697
+ source=source,
698
+ )
699
+ else:
740
700
  if latest_version is None:
741
- logger.warning("version_check_fetch_failed", task_name=self.name)
742
- return False
743
- # Persist refreshed state
744
- new_state = VersionCheckState(
745
- last_check_at=current_time,
746
- latest_version_found=latest_version,
701
+ latest_version = await fetch_latest_github_version()
702
+ if latest_version is None:
703
+ logger.warning(
704
+ "version_check_fetch_failed", task_name=self.name
705
+ )
706
+ return False
707
+ await save_check_state(
708
+ state_path,
709
+ VersionCheckState(
710
+ last_check_at=current_time,
711
+ latest_version_found=latest_version,
712
+ latest_branch_name=(
713
+ prev_state.latest_branch_name
714
+ if prev_state is not None
715
+ else None
716
+ ),
717
+ latest_branch_commit=(
718
+ prev_state.latest_branch_commit
719
+ if prev_state is not None
720
+ else None
721
+ ),
722
+ running_version=current_version,
723
+ running_commit=current_commit,
724
+ ),
725
+ )
726
+ source = "network"
727
+ self._log_version_comparison(
728
+ current_version, latest_version, source=source
747
729
  )
748
- await save_check_state(state_path, new_state)
749
- source = "network"
750
- else:
751
- # Ensure state file at least exists; if it didn't, we wouldn't be here
752
- pass
753
-
754
- # Compare versions and log result
755
- current_version = get_current_version()
756
- self._log_version_comparison(current_version, latest_version, source=source)
757
730
 
758
731
  # Mark first run as complete
759
732
  if self._first_run:
@@ -761,11 +734,30 @@ class VersionUpdateCheckTask(BaseScheduledTask):
761
734
 
762
735
  return True
763
736
 
737
+ except ImportError as e:
738
+ logger.error(
739
+ "version_check_task_import_error",
740
+ task_name=self.name,
741
+ error=str(e),
742
+ error_type=type(e).__name__,
743
+ exc_info=e,
744
+ )
745
+ return False
746
+
764
747
  except Exception as e:
765
748
  logger.error(
766
749
  "version_check_task_error",
767
750
  task_name=self.name,
768
751
  error=str(e),
769
752
  error_type=type(e).__name__,
753
+ exc_info=e,
770
754
  )
771
755
  return False
756
+
757
+
758
+ # Test helper task exposed for tests that import from this module
759
+ class MockScheduledTask(BaseScheduledTask):
760
+ """Minimal mock task used by tests for registration and lifecycle checks."""
761
+
762
+ async def run(self) -> bool:
763
+ return True
@@ -6,5 +6,4 @@
6
6
  __all__ = [
7
7
  "ClaudeSDKService",
8
8
  "MetricsService",
9
- "ProxyService",
10
9
  ]