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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,169 @@
1
+ """Pricing plugin implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins import (
7
+ PluginManifest,
8
+ SystemPluginFactory,
9
+ SystemPluginRuntime,
10
+ )
11
+
12
+ from .config import PricingConfig
13
+ from .service import PricingService
14
+ from .tasks import PricingCacheUpdateTask
15
+
16
+
17
+ logger = get_plugin_logger()
18
+
19
+
20
+ class PricingRuntime(SystemPluginRuntime):
21
+ """Runtime for pricing plugin."""
22
+
23
+ def __init__(self, manifest: PluginManifest):
24
+ """Initialize runtime."""
25
+ super().__init__(manifest)
26
+ self.config: PricingConfig | None = None
27
+ self.service: PricingService | None = None
28
+ self.update_task: PricingCacheUpdateTask | None = None
29
+
30
+ async def _on_initialize(self) -> None:
31
+ """Initialize the pricing plugin."""
32
+ if not self.context:
33
+ raise RuntimeError("Context not set")
34
+
35
+ # Get configuration
36
+ config = self.context.get("config")
37
+ if not isinstance(config, PricingConfig):
38
+ logger.debug("plugin_no_config_using_defaults", category="plugin")
39
+ # Use default config if none provided
40
+ self.config = PricingConfig()
41
+ else:
42
+ self.config = config
43
+
44
+ logger.debug("initializing_pricing_plugin", enabled=self.config.enabled)
45
+
46
+ # Create pricing service
47
+ self.service = PricingService(self.config)
48
+
49
+ if self.config.enabled:
50
+ # Initialize the service
51
+ await self.service.initialize()
52
+
53
+ # Register service with plugin registry
54
+ plugin_registry = self.context.get("plugin_registry")
55
+ if plugin_registry:
56
+ plugin_registry.register_service(
57
+ "pricing", self.service, self.manifest.name
58
+ )
59
+ logger.debug("pricing_service_registered")
60
+
61
+ # Create and start pricing update task
62
+ interval_seconds = self.config.update_interval_hours * 3600
63
+ self.update_task = PricingCacheUpdateTask(
64
+ name="pricing_cache_update",
65
+ interval_seconds=interval_seconds,
66
+ pricing_service=self.service,
67
+ enabled=self.config.auto_update,
68
+ force_refresh_on_startup=self.config.force_refresh_on_startup,
69
+ )
70
+
71
+ await self.update_task.start()
72
+ logger.debug(
73
+ "pricing_plugin_initialized",
74
+ update_interval_hours=self.config.update_interval_hours,
75
+ auto_update=self.config.auto_update,
76
+ force_refresh_on_startup=self.config.force_refresh_on_startup,
77
+ )
78
+ else:
79
+ logger.debug("pricing_plugin_disabled")
80
+
81
+ async def _on_shutdown(self) -> None:
82
+ """Shutdown the plugin and cleanup resources."""
83
+ logger.debug("shutting_down_pricing_plugin")
84
+
85
+ # Stop the update task
86
+ if self.update_task:
87
+ await self.update_task.stop()
88
+
89
+ logger.debug("pricing_plugin_shutdown_complete")
90
+
91
+ async def _get_health_details(self) -> dict[str, Any]:
92
+ """Get health check details."""
93
+ try:
94
+ base_health = {
95
+ "type": "system",
96
+ "initialized": self.initialized,
97
+ "enabled": self.config.enabled if self.config else False,
98
+ }
99
+
100
+ if not self.config or not self.config.enabled:
101
+ return base_health
102
+
103
+ # Add service-specific health info
104
+ health_details = base_health.copy()
105
+
106
+ if self.service:
107
+ cache_info = self.service.get_cache_info()
108
+ health_details.update(
109
+ {
110
+ "cache_valid": cache_info.get("valid", False),
111
+ "cache_age_hours": cache_info.get("age_hours"),
112
+ "cache_exists": cache_info.get("exists", False),
113
+ }
114
+ )
115
+
116
+ if self.update_task:
117
+ task_status = self.update_task.get_status()
118
+ health_details.update(
119
+ {
120
+ "update_task_running": task_status["running"],
121
+ "consecutive_failures": task_status["consecutive_failures"],
122
+ "last_success_ago_seconds": task_status[
123
+ "last_success_ago_seconds"
124
+ ],
125
+ "next_run_in_seconds": task_status["next_run_in_seconds"],
126
+ }
127
+ )
128
+
129
+ return health_details
130
+
131
+ except Exception as e:
132
+ logger.error("health_check_failed", error=str(e))
133
+ return {
134
+ "type": "system",
135
+ "initialized": self.initialized,
136
+ "enabled": self.config.enabled if self.config else False,
137
+ "error": str(e),
138
+ }
139
+
140
+ def get_pricing_service(self) -> PricingService | None:
141
+ """Get the pricing service instance."""
142
+ return self.service
143
+
144
+
145
+ class PricingFactory(SystemPluginFactory):
146
+ """Factory for pricing plugin."""
147
+
148
+ def __init__(self) -> None:
149
+ """Initialize factory with manifest."""
150
+ # Create manifest with static declarations
151
+ manifest = PluginManifest(
152
+ name="pricing",
153
+ version="0.1.0",
154
+ description="Dynamic pricing plugin for AI model cost calculation",
155
+ is_provider=False,
156
+ config_class=PricingConfig,
157
+ provides=["pricing"], # This plugin provides the pricing service
158
+ )
159
+
160
+ # Initialize with manifest
161
+ super().__init__(manifest)
162
+
163
+ def create_runtime(self) -> PricingRuntime:
164
+ """Create runtime instance."""
165
+ return PricingRuntime(self.manifest)
166
+
167
+
168
+ # Export the factory instance
169
+ factory = PricingFactory()
@@ -0,0 +1,191 @@
1
+ """Pricing service providing unified interface for pricing functionality."""
2
+
3
+ from decimal import Decimal
4
+ from typing import Any
5
+
6
+ from ccproxy.core.logging import get_plugin_logger
7
+
8
+ from .cache import PricingCache
9
+ from .config import PricingConfig
10
+ from .exceptions import (
11
+ ModelPricingNotFoundError,
12
+ PricingDataNotLoadedError,
13
+ PricingServiceDisabledError,
14
+ )
15
+ from .loader import PricingLoader
16
+ from .models import ModelPricing, PricingData
17
+ from .updater import PricingUpdater
18
+
19
+
20
+ logger = get_plugin_logger(__name__)
21
+
22
+
23
+ class PricingService:
24
+ """Main service interface for pricing functionality."""
25
+
26
+ def __init__(self, config: PricingConfig):
27
+ """Initialize pricing service with configuration."""
28
+ self.config = config
29
+ self.cache = PricingCache(config)
30
+ self.loader = PricingLoader()
31
+ self.updater = PricingUpdater(self.cache, config)
32
+ self._current_pricing: PricingData | None = None
33
+
34
+ async def initialize(self) -> None:
35
+ """Initialize the pricing service."""
36
+ if not self.config.enabled:
37
+ logger.info("pricing_service_disabled")
38
+ return
39
+
40
+ logger.debug("pricing_service_initializing")
41
+
42
+ # Force refresh on startup if configured
43
+ if self.config.force_refresh_on_startup:
44
+ await self.force_refresh_pricing()
45
+ else:
46
+ # Load current pricing data
47
+ await self.get_current_pricing()
48
+
49
+ async def get_current_pricing(
50
+ self, force_refresh: bool = False
51
+ ) -> PricingData | None:
52
+ """Get current pricing data."""
53
+ if not self.config.enabled:
54
+ return None
55
+
56
+ if force_refresh or self._current_pricing is None:
57
+ self._current_pricing = await self.updater.get_current_pricing(
58
+ force_refresh
59
+ )
60
+
61
+ return self._current_pricing
62
+
63
+ async def get_model_pricing(self, model_name: str) -> ModelPricing | None:
64
+ """Get pricing for specific model."""
65
+ pricing_data = await self.get_current_pricing()
66
+ if pricing_data is None:
67
+ return None
68
+
69
+ return pricing_data.get(model_name)
70
+
71
+ async def calculate_cost(
72
+ self,
73
+ model_name: str,
74
+ input_tokens: int = 0,
75
+ output_tokens: int = 0,
76
+ cache_read_tokens: int = 0,
77
+ cache_write_tokens: int = 0,
78
+ ) -> Decimal:
79
+ """Calculate cost for token usage.
80
+
81
+ Raises:
82
+ PricingServiceDisabledError: If pricing service is disabled
83
+ ModelPricingNotFoundError: If model pricing is not found
84
+ """
85
+ if not self.config.enabled:
86
+ raise PricingServiceDisabledError()
87
+
88
+ model_pricing = await self.get_model_pricing(model_name)
89
+ if model_pricing is None:
90
+ raise ModelPricingNotFoundError(model_name)
91
+
92
+ # Calculate cost per million tokens, then scale to actual tokens
93
+ total_cost = Decimal("0")
94
+
95
+ if input_tokens > 0:
96
+ total_cost += (model_pricing.input * input_tokens) / Decimal("1000000")
97
+
98
+ if output_tokens > 0:
99
+ total_cost += (model_pricing.output * output_tokens) / Decimal("1000000")
100
+
101
+ if cache_read_tokens > 0:
102
+ total_cost += (model_pricing.cache_read * cache_read_tokens) / Decimal(
103
+ "1000000"
104
+ )
105
+
106
+ if cache_write_tokens > 0:
107
+ total_cost += (model_pricing.cache_write * cache_write_tokens) / Decimal(
108
+ "1000000"
109
+ )
110
+
111
+ return total_cost
112
+
113
+ def calculate_cost_sync(
114
+ self,
115
+ model_name: str,
116
+ input_tokens: int = 0,
117
+ output_tokens: int = 0,
118
+ cache_read_tokens: int = 0,
119
+ cache_write_tokens: int = 0,
120
+ ) -> Decimal:
121
+ """Calculate cost synchronously using cached pricing data.
122
+
123
+ This method uses the cached pricing data and doesn't make any async calls,
124
+ making it safe to use in streaming contexts where we can't await.
125
+
126
+ Raises:
127
+ PricingServiceDisabledError: If pricing service is disabled
128
+ PricingDataNotLoadedError: If pricing data is not loaded yet
129
+ ModelPricingNotFoundError: If model pricing is not found
130
+ """
131
+ if not self.config.enabled:
132
+ raise PricingServiceDisabledError()
133
+
134
+ if self._current_pricing is None:
135
+ raise PricingDataNotLoadedError()
136
+
137
+ model_pricing = self._current_pricing.get(model_name)
138
+ if model_pricing is None:
139
+ raise ModelPricingNotFoundError(model_name)
140
+
141
+ # Calculate cost per million tokens, then scale to actual tokens
142
+ total_cost = Decimal("0")
143
+
144
+ if input_tokens > 0:
145
+ total_cost += (model_pricing.input * input_tokens) / Decimal("1000000")
146
+
147
+ if output_tokens > 0:
148
+ total_cost += (model_pricing.output * output_tokens) / Decimal("1000000")
149
+
150
+ if cache_read_tokens > 0:
151
+ total_cost += (model_pricing.cache_read * cache_read_tokens) / Decimal(
152
+ "1000000"
153
+ )
154
+
155
+ if cache_write_tokens > 0:
156
+ total_cost += (model_pricing.cache_write * cache_write_tokens) / Decimal(
157
+ "1000000"
158
+ )
159
+
160
+ return total_cost
161
+
162
+ async def force_refresh_pricing(self) -> bool:
163
+ """Force refresh of pricing data."""
164
+ if not self.config.enabled:
165
+ return False
166
+
167
+ success = await self.updater.force_refresh()
168
+ if success:
169
+ # Reload the current pricing data after successful refresh
170
+ self._current_pricing = await self.updater.get_current_pricing(
171
+ force_refresh=True
172
+ )
173
+ return True
174
+ return False
175
+
176
+ async def get_available_models(self) -> list[str]:
177
+ """Get list of available models with pricing."""
178
+ pricing_data = await self.get_current_pricing()
179
+ if pricing_data is None:
180
+ return []
181
+
182
+ return pricing_data.model_names()
183
+
184
+ def get_cache_info(self) -> dict[str, Any]:
185
+ """Get cache status information."""
186
+ return self.cache.get_cache_info()
187
+
188
+ async def clear_cache(self) -> bool:
189
+ """Clear pricing cache."""
190
+ self._current_pricing = None
191
+ return self.cache.clear_cache()
@@ -0,0 +1,300 @@
1
+ """Pricing plugin scheduled tasks."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import random
6
+ import time
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any
9
+
10
+ from ccproxy.core.async_task_manager import create_managed_task
11
+ from ccproxy.core.logging import get_plugin_logger
12
+
13
+ from .service import PricingService
14
+
15
+
16
+ logger = get_plugin_logger(__name__)
17
+
18
+
19
+ class BaseScheduledTask(ABC):
20
+ """
21
+ Abstract base class for all scheduled tasks.
22
+
23
+ Provides common functionality for task lifecycle management, error handling,
24
+ and exponential backoff for failed executions.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ name: str,
30
+ interval_seconds: float,
31
+ enabled: bool = True,
32
+ max_backoff_seconds: float = 300.0,
33
+ jitter_factor: float = 0.25,
34
+ ):
35
+ """
36
+ Initialize scheduled task.
37
+
38
+ Args:
39
+ name: Human-readable task name
40
+ interval_seconds: Interval between task executions in seconds
41
+ enabled: Whether the task is enabled
42
+ max_backoff_seconds: Maximum backoff delay for failed tasks
43
+ jitter_factor: Jitter factor for backoff randomization (0.0-1.0)
44
+ """
45
+ self.name = name
46
+ self.interval_seconds = max(1.0, interval_seconds)
47
+ self.enabled = enabled
48
+ self.max_backoff_seconds = max_backoff_seconds
49
+ self.jitter_factor = min(1.0, max(0.0, jitter_factor))
50
+
51
+ # Task state
52
+ self._task: asyncio.Task[None] | None = None
53
+ self._stop_event = asyncio.Event()
54
+ self._consecutive_failures = 0
55
+ self._last_success_time: float | None = None
56
+ self._next_run_time: float | None = None
57
+
58
+ @abstractmethod
59
+ async def run(self) -> bool:
60
+ """
61
+ Execute the task logic.
62
+
63
+ Returns:
64
+ True if task completed successfully, False otherwise
65
+ """
66
+
67
+ async def setup(self) -> None: # noqa: B027
68
+ """
69
+ Optional setup hook called before the task starts running.
70
+
71
+ Override this method to perform any initialization required by the task.
72
+ """
73
+ pass
74
+
75
+ async def teardown(self) -> None: # noqa: B027
76
+ """
77
+ Optional teardown hook called when the task stops.
78
+
79
+ Override this method to perform any cleanup required by the task.
80
+ """
81
+ pass
82
+
83
+ def _calculate_next_run_delay(self, failed: bool = False) -> float:
84
+ """Calculate delay until next task execution with exponential backoff."""
85
+ if not failed:
86
+ # Normal interval with jitter
87
+ base_delay = self.interval_seconds
88
+ jitter = random.uniform(-self.jitter_factor, self.jitter_factor)
89
+ return float(base_delay * (1 + jitter))
90
+
91
+ # Exponential backoff for failures
92
+ backoff_factor = min(2**self._consecutive_failures, 32)
93
+ backoff_delay = min(
94
+ self.interval_seconds * backoff_factor, self.max_backoff_seconds
95
+ )
96
+
97
+ # Add jitter to prevent thundering herd
98
+ jitter = random.uniform(-self.jitter_factor, self.jitter_factor)
99
+ return float(backoff_delay * (1 + jitter))
100
+
101
+ async def _run_with_error_handling(self) -> bool:
102
+ """Execute task with error handling and metrics."""
103
+ start_time = time.time()
104
+
105
+ try:
106
+ success = await self.run()
107
+
108
+ if success:
109
+ self._consecutive_failures = 0
110
+ self._last_success_time = start_time
111
+ logger.debug(
112
+ "scheduled_task_success",
113
+ task_name=self.name,
114
+ duration=time.time() - start_time,
115
+ )
116
+ else:
117
+ self._consecutive_failures += 1
118
+ logger.warning(
119
+ "scheduled_task_failed",
120
+ task_name=self.name,
121
+ consecutive_failures=self._consecutive_failures,
122
+ duration=time.time() - start_time,
123
+ )
124
+
125
+ return success
126
+
127
+ except Exception as e:
128
+ self._consecutive_failures += 1
129
+ logger.error(
130
+ "scheduled_task_error",
131
+ task_name=self.name,
132
+ error=str(e),
133
+ error_type=type(e).__name__,
134
+ consecutive_failures=self._consecutive_failures,
135
+ duration=time.time() - start_time,
136
+ exc_info=e,
137
+ )
138
+ return False
139
+
140
+ async def _task_loop(self) -> None:
141
+ """Main task execution loop."""
142
+ logger.info("scheduled_task_starting", task_name=self.name)
143
+
144
+ try:
145
+ # Run setup
146
+ with contextlib.suppress(Exception):
147
+ await self.setup()
148
+
149
+ while not self._stop_event.is_set():
150
+ # Execute task
151
+ success = await self._run_with_error_handling()
152
+
153
+ # Calculate next run delay
154
+ delay = self._calculate_next_run_delay(failed=not success)
155
+ self._next_run_time = time.time() + delay
156
+
157
+ # Wait for next execution or stop event
158
+ try:
159
+ await asyncio.wait_for(self._stop_event.wait(), timeout=delay)
160
+ break # Stop event was set
161
+ except TimeoutError:
162
+ continue # Time to run again
163
+
164
+ finally:
165
+ # Run teardown
166
+ with contextlib.suppress(Exception):
167
+ await self.teardown()
168
+
169
+ logger.info("scheduled_task_stopped", task_name=self.name)
170
+
171
+ async def start(self) -> None:
172
+ """Start the scheduled task."""
173
+ if not self.enabled:
174
+ logger.info("scheduled_task_disabled", task_name=self.name)
175
+ return
176
+
177
+ if self._task and not self._task.done():
178
+ logger.warning("scheduled_task_already_running", task_name=self.name)
179
+ return
180
+
181
+ self._stop_event.clear()
182
+ self._task = await create_managed_task(
183
+ self._task_loop(), name=f"scheduled_task_{self.name}"
184
+ )
185
+
186
+ async def stop(self, timeout: float = 10.0) -> None:
187
+ """Stop the scheduled task."""
188
+ if not self._task:
189
+ return
190
+
191
+ logger.info("scheduled_task_stopping", task_name=self.name)
192
+
193
+ # Signal stop
194
+ self._stop_event.set()
195
+
196
+ # Wait for task to complete
197
+ try:
198
+ await asyncio.wait_for(self._task, timeout=timeout)
199
+ except TimeoutError:
200
+ logger.warning(
201
+ "scheduled_task_stop_timeout", task_name=self.name, timeout=timeout
202
+ )
203
+ if not self._task.done():
204
+ self._task.cancel()
205
+ with contextlib.suppress(asyncio.CancelledError):
206
+ await self._task
207
+
208
+ self._task = None
209
+
210
+ def is_running(self) -> bool:
211
+ """Check if task is currently running."""
212
+ return self._task is not None and not self._task.done()
213
+
214
+ def get_status(self) -> dict[str, Any]:
215
+ """Get current task status information."""
216
+ now = time.time()
217
+ return {
218
+ "name": self.name,
219
+ "enabled": self.enabled,
220
+ "running": self.is_running(),
221
+ "consecutive_failures": self._consecutive_failures,
222
+ "last_success_time": self._last_success_time,
223
+ "last_success_ago_seconds": (
224
+ now - self._last_success_time if self._last_success_time else None
225
+ ),
226
+ "next_run_time": self._next_run_time,
227
+ "next_run_in_seconds": (
228
+ self._next_run_time - now if self._next_run_time else None
229
+ ),
230
+ "interval_seconds": self.interval_seconds,
231
+ }
232
+
233
+
234
+ class PricingCacheUpdateTask(BaseScheduledTask):
235
+ """Task for updating pricing cache periodically."""
236
+
237
+ def __init__(
238
+ self,
239
+ name: str,
240
+ interval_seconds: float,
241
+ pricing_service: PricingService,
242
+ enabled: bool = True,
243
+ force_refresh_on_startup: bool = False,
244
+ ):
245
+ """
246
+ Initialize pricing cache update task.
247
+
248
+ Args:
249
+ name: Task name
250
+ interval_seconds: Interval between pricing updates
251
+ pricing_service: Pricing service instance
252
+ enabled: Whether task is enabled
253
+ force_refresh_on_startup: Whether to force refresh on first run
254
+ """
255
+ super().__init__(
256
+ name=name,
257
+ interval_seconds=interval_seconds,
258
+ enabled=enabled,
259
+ )
260
+ self.pricing_service = pricing_service
261
+ self.force_refresh_on_startup = force_refresh_on_startup
262
+ self._first_run = True
263
+
264
+ async def run(self) -> bool:
265
+ """Execute pricing cache update."""
266
+ try:
267
+ if not self.pricing_service.config.enabled:
268
+ logger.debug("pricing_service_disabled", task_name=self.name)
269
+ return True # Not a failure, just disabled
270
+
271
+ # Force refresh on first run if configured
272
+ force_refresh = self._first_run and self.force_refresh_on_startup
273
+ self._first_run = False
274
+
275
+ if force_refresh:
276
+ logger.info("pricing_update_force_refresh_startup", task_name=self.name)
277
+ success = await self.pricing_service.force_refresh_pricing()
278
+ else:
279
+ # Regular update check
280
+ pricing_data = await self.pricing_service.get_current_pricing(
281
+ force_refresh=False
282
+ )
283
+ success = pricing_data is not None
284
+
285
+ if success:
286
+ logger.debug("pricing_update_success", task_name=self.name)
287
+ else:
288
+ logger.warning("pricing_update_failed", task_name=self.name)
289
+
290
+ return success
291
+
292
+ except Exception as e:
293
+ logger.error(
294
+ "pricing_update_task_error",
295
+ task_name=self.name,
296
+ error=str(e),
297
+ error_type=type(e).__name__,
298
+ exc_info=e,
299
+ )
300
+ return False