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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1231
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,18 +3,18 @@
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
+ import httpx
6
7
  from fastapi import APIRouter, Query, Request
7
8
  from fastapi.responses import HTMLResponse
8
- from structlog import get_logger
9
+ from pydantic import ValidationError
9
10
 
10
- from ccproxy.auth.models import (
11
- ClaudeCredentials,
12
- OAuthToken,
11
+ from ccproxy.auth.exceptions import (
12
+ CredentialsStorageError,
13
+ OAuthError,
14
+ OAuthTokenRefreshError,
13
15
  )
14
- from ccproxy.auth.storage import JsonFileTokenStorage as JsonFileStorage
15
-
16
- # Import CredentialsManager locally to avoid circular import
17
- from ccproxy.services.credentials.config import OAuthConfig
16
+ from ccproxy.auth.oauth.registry import OAuthRegistry
17
+ from ccproxy.core.logging import get_logger
18
18
 
19
19
 
20
20
  logger = get_logger(__name__)
@@ -36,7 +36,12 @@ def register_oauth_flow(
36
36
  "success": False,
37
37
  "error": None,
38
38
  }
39
- logger.debug("Registered OAuth flow", state=state, operation="register_oauth_flow")
39
+ logger.debug(
40
+ "Registered OAuth flow",
41
+ state=state,
42
+ operation="register_oauth_flow",
43
+ category="auth",
44
+ )
40
45
 
41
46
 
42
47
  def get_oauth_flow_result(state: str) -> dict[str, Any] | None:
@@ -68,6 +73,7 @@ async def oauth_callback(
68
73
  oauth_error_description=error_description,
69
74
  state=state,
70
75
  operation="oauth_callback",
76
+ category="auth",
71
77
  )
72
78
 
73
79
  # Update pending flow if state is provided
@@ -102,6 +108,7 @@ async def oauth_callback(
102
108
  error_message=error_msg,
103
109
  state=state,
104
110
  operation="oauth_callback",
111
+ category="auth",
105
112
  )
106
113
 
107
114
  if state and state in _pending_flows:
@@ -134,6 +141,7 @@ async def oauth_callback(
134
141
  error_type="missing_state",
135
142
  error_message=error_msg,
136
143
  operation="oauth_callback",
144
+ category="auth",
137
145
  )
138
146
  return HTMLResponse(
139
147
  content=f"""
@@ -158,6 +166,7 @@ async def oauth_callback(
158
166
  error_message="Invalid or expired state parameter",
159
167
  state=state,
160
168
  operation="oauth_callback",
169
+ category="auth",
161
170
  )
162
171
  return HTMLResponse(
163
172
  content=f"""
@@ -178,8 +187,13 @@ async def oauth_callback(
178
187
  code_verifier = flow["code_verifier"]
179
188
  custom_paths = flow["custom_paths"]
180
189
 
181
- # Exchange authorization code for tokens
182
- success = await _exchange_code_for_tokens(code, code_verifier, custom_paths)
190
+ # Exchange authorization code for tokens using app-scoped registry
191
+ registry: OAuthRegistry | None = getattr(
192
+ request.app.state, "oauth_registry", None
193
+ )
194
+ success = await _exchange_code_for_tokens(
195
+ code, code_verifier, state, custom_paths, registry
196
+ )
183
197
 
184
198
  # Update flow result
185
199
  _pending_flows[state].update(
@@ -192,7 +206,10 @@ async def oauth_callback(
192
206
 
193
207
  if success:
194
208
  logger.info(
195
- "OAuth login successful", state=state, operation="oauth_callback"
209
+ "OAuth login successful",
210
+ state=state,
211
+ operation="oauth_callback",
212
+ category="auth",
196
213
  )
197
214
  return HTMLResponse(
198
215
  content="""
@@ -220,6 +237,7 @@ async def oauth_callback(
220
237
  error_message=error_msg,
221
238
  state=state,
222
239
  operation="oauth_callback",
240
+ category="auth",
223
241
  )
224
242
  return HTMLResponse(
225
243
  content=f"""
@@ -235,14 +253,108 @@ async def oauth_callback(
235
253
  status_code=500,
236
254
  )
237
255
 
256
+ except (OAuthError, OAuthTokenRefreshError, CredentialsStorageError) as e:
257
+ logger.error(
258
+ "oauth_callback_error",
259
+ error_type="auth_error",
260
+ error=str(e),
261
+ state=state,
262
+ operation="oauth_callback",
263
+ exc_info=e,
264
+ )
265
+
266
+ if state and state in _pending_flows:
267
+ _pending_flows[state].update(
268
+ {
269
+ "completed": True,
270
+ "success": False,
271
+ "error": str(e),
272
+ }
273
+ )
274
+
275
+ return HTMLResponse(
276
+ content=f"""
277
+ <html>
278
+ <head><title>Login Error</title></head>
279
+ <body>
280
+ <h1>Login Error</h1>
281
+ <p>Authentication error: {str(e)}</p>
282
+ <p>You can close this window and try again.</p>
283
+ </body>
284
+ </html>
285
+ """,
286
+ status_code=500,
287
+ )
288
+ except httpx.HTTPError as e:
289
+ logger.error(
290
+ "oauth_callback_http_error",
291
+ error=str(e),
292
+ status=e.response.status_code if hasattr(e, "response") else None,
293
+ state=state,
294
+ operation="oauth_callback",
295
+ exc_info=e,
296
+ )
297
+
298
+ if state and state in _pending_flows:
299
+ _pending_flows[state].update(
300
+ {
301
+ "completed": True,
302
+ "success": False,
303
+ "error": f"HTTP error: {str(e)}",
304
+ }
305
+ )
306
+
307
+ return HTMLResponse(
308
+ content=f"""
309
+ <html>
310
+ <head><title>Login Error</title></head>
311
+ <body>
312
+ <h1>Login Error</h1>
313
+ <p>Network error occurred: {str(e)}</p>
314
+ <p>You can close this window and try again.</p>
315
+ </body>
316
+ </html>
317
+ """,
318
+ status_code=500,
319
+ )
320
+ except ValidationError as e:
321
+ logger.error(
322
+ "oauth_callback_validation_error",
323
+ error=str(e),
324
+ state=state,
325
+ operation="oauth_callback",
326
+ exc_info=e,
327
+ )
328
+
329
+ if state and state in _pending_flows:
330
+ _pending_flows[state].update(
331
+ {
332
+ "completed": True,
333
+ "success": False,
334
+ "error": f"Validation error: {str(e)}",
335
+ }
336
+ )
337
+
338
+ return HTMLResponse(
339
+ content="""
340
+ <html>
341
+ <head><title>Login Error</title></head>
342
+ <body>
343
+ <h1>Login Error</h1>
344
+ <p>Data validation error occurred</p>
345
+ <p>You can close this window and try again.</p>
346
+ </body>
347
+ </html>
348
+ """,
349
+ status_code=500,
350
+ )
238
351
  except Exception as e:
239
352
  logger.error(
240
- "Unexpected error in OAuth callback",
241
- error_type="unexpected_error",
242
- error_message=str(e),
353
+ "oauth_callback_unexpected_error",
354
+ error=str(e),
243
355
  state=state,
244
356
  operation="oauth_callback",
245
- exc_info=True,
357
+ exc_info=e,
246
358
  )
247
359
 
248
360
  if state and state in _pending_flows:
@@ -270,125 +382,86 @@ async def oauth_callback(
270
382
 
271
383
 
272
384
  async def _exchange_code_for_tokens(
273
- authorization_code: str, code_verifier: str, custom_paths: list[Path] | None = None
385
+ authorization_code: str,
386
+ code_verifier: str,
387
+ state: str,
388
+ custom_paths: list[Path] | None = None,
389
+ registry: OAuthRegistry | None = None,
274
390
  ) -> bool:
275
391
  """Exchange authorization code for access tokens."""
276
392
  try:
277
- from datetime import UTC, datetime
278
-
279
- import httpx
280
-
281
- # Create OAuth config with default values
282
- oauth_config = OAuthConfig()
283
-
284
- # Exchange authorization code for tokens
285
- token_data = {
286
- "grant_type": "authorization_code",
287
- "code": authorization_code,
288
- "redirect_uri": oauth_config.redirect_uri,
289
- "client_id": oauth_config.client_id,
290
- "code_verifier": code_verifier,
291
- }
292
-
293
- headers = {
294
- "Content-Type": "application/json",
295
- "anthropic-beta": oauth_config.beta_version,
296
- "User-Agent": oauth_config.user_agent,
297
- }
298
-
299
- async with httpx.AsyncClient() as client:
300
- response = await client.post(
301
- oauth_config.token_url,
302
- headers=headers,
303
- json=token_data,
304
- timeout=30.0,
393
+ # Get OAuth provider from provided registry
394
+ if registry is None:
395
+ logger.error(
396
+ "oauth_registry_not_available", operation="exchange_code_for_tokens"
397
+ )
398
+ return False
399
+ oauth_provider = registry.get("claude-api")
400
+ if not oauth_provider:
401
+ logger.error("claude_oauth_provider_not_found", category="auth")
402
+ return False
403
+
404
+ # Use OAuth provider to handle the callback
405
+ try:
406
+ credentials = await oauth_provider.handle_callback(
407
+ authorization_code, state, code_verifier
305
408
  )
306
409
 
307
- if response.status_code == 200:
308
- result = response.json()
309
-
310
- # Calculate expires_at from expires_in
311
- expires_in = result.get("expires_in")
312
- expires_at = None
313
- if expires_in:
314
- expires_at = int(
315
- (datetime.now(UTC).timestamp() + expires_in) * 1000
410
+ # Save credentials using provider's storage mechanism
411
+ if custom_paths:
412
+ # Let the provider handle storage with custom path
413
+ success = await oauth_provider.save_credentials(
414
+ credentials, custom_path=custom_paths[0] if custom_paths else None
415
+ )
416
+ if success:
417
+ logger.info(
418
+ "Successfully saved OAuth credentials to custom path",
419
+ operation="exchange_code_for_tokens",
420
+ path=str(custom_paths[0]),
316
421
  )
317
-
318
- # Create credentials object
319
- oauth_data = {
320
- "accessToken": result.get("access_token"),
321
- "refreshToken": result.get("refresh_token"),
322
- "expiresAt": expires_at,
323
- "scopes": result.get("scope", "").split()
324
- if result.get("scope")
325
- else oauth_config.scopes,
326
- "subscriptionType": result.get("subscription_type", "unknown"),
327
- }
328
-
329
- credentials = ClaudeCredentials(claudeAiOauth=OAuthToken(**oauth_data))
330
-
331
- # Save credentials using CredentialsManager (lazy import to avoid circular import)
332
- from ccproxy.services.credentials.manager import CredentialsManager
333
-
334
- if custom_paths:
335
- # Use the first custom path for storage
336
- storage = JsonFileStorage(custom_paths[0])
337
- manager = CredentialsManager(storage=storage)
338
422
  else:
339
- manager = CredentialsManager()
340
-
341
- if await manager.save(credentials):
423
+ logger.error(
424
+ "Failed to save OAuth credentials to custom path",
425
+ error_type="save_credentials_failed",
426
+ operation="exchange_code_for_tokens",
427
+ path=str(custom_paths[0]),
428
+ )
429
+ else:
430
+ # Save using provider's default storage
431
+ success = await oauth_provider.save_credentials(credentials)
432
+ if success:
342
433
  logger.info(
343
434
  "Successfully saved OAuth credentials",
344
- subscription_type=oauth_data["subscriptionType"],
345
- scopes=oauth_data["scopes"],
346
435
  operation="exchange_code_for_tokens",
347
436
  )
348
- return True
349
437
  else:
350
438
  logger.error(
351
439
  "Failed to save OAuth credentials",
352
440
  error_type="save_credentials_failed",
353
441
  operation="exchange_code_for_tokens",
354
442
  )
355
- return False
356
443
 
357
- else:
358
- # Use compact logging for the error message
359
- import os
360
-
361
- verbose_api = (
362
- os.environ.get("CCPROXY_VERBOSE_API", "false").lower() == "true"
363
- )
444
+ logger.info(
445
+ "OAuth flow completed successfully",
446
+ operation="exchange_code_for_tokens",
447
+ )
448
+ return True
364
449
 
365
- if verbose_api:
366
- error_detail = response.text
367
- else:
368
- response_text = response.text
369
- if len(response_text) > 200:
370
- error_detail = f"{response_text[:100]}...{response_text[-50:]}"
371
- elif len(response_text) > 100:
372
- error_detail = f"{response_text[:100]}..."
373
- else:
374
- error_detail = response_text
375
-
376
- logger.error(
377
- "Token exchange failed",
378
- error_type="token_exchange_failed",
379
- status_code=response.status_code,
380
- error_detail=error_detail,
381
- verbose_api_enabled=verbose_api,
382
- operation="exchange_code_for_tokens",
383
- )
384
- return False
450
+ except Exception as e:
451
+ logger.error(
452
+ "oauth_provider_callback_error",
453
+ error=str(e),
454
+ error_type=type(e).__name__,
455
+ operation="exchange_code_for_tokens",
456
+ exc_info=e,
457
+ )
458
+ return False
385
459
 
386
460
  except Exception as e:
387
461
  logger.error(
388
- "Error during token exchange",
389
- error_type="token_exchange_exception",
390
- error_message=str(e),
462
+ "oauth_exchange_error",
463
+ error=str(e),
391
464
  operation="exchange_code_for_tokens",
392
- exc_info=True,
465
+ exc_info=e,
393
466
  )
394
467
  return False
@@ -0,0 +1,151 @@
1
+ """OAuth session management for handling OAuth state and PKCE.
2
+
3
+ This module provides session management for OAuth flows, storing
4
+ state, PKCE verifiers, and other session data during the OAuth process.
5
+ """
6
+
7
+ import time
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+
13
+ logger = structlog.get_logger(__name__)
14
+
15
+
16
+ class OAuthSessionManager:
17
+ """Manages OAuth session data during authentication flows.
18
+
19
+ This is a simple in-memory implementation. In production,
20
+ consider using Redis or another persistent store.
21
+ """
22
+
23
+ def __init__(self, ttl_seconds: int = 600) -> None:
24
+ """Initialize the session manager.
25
+
26
+ Args:
27
+ ttl_seconds: Time-to-live for sessions in seconds (default: 10 minutes)
28
+ """
29
+ self._sessions: dict[str, dict[str, Any]] = {}
30
+ self._ttl_seconds = ttl_seconds
31
+ logger.info(
32
+ "oauth_session_manager_initialized",
33
+ ttl_seconds=ttl_seconds,
34
+ category="auth",
35
+ )
36
+
37
+ async def create_session(self, state: str, data: dict[str, Any]) -> None:
38
+ """Create a new OAuth session.
39
+
40
+ Args:
41
+ state: OAuth state parameter (session key)
42
+ data: Session data to store
43
+ """
44
+ self._sessions[state] = {
45
+ **data,
46
+ "created_at": time.time(),
47
+ }
48
+ logger.debug(
49
+ "oauth_session_created",
50
+ state=state,
51
+ provider=data.get("provider"),
52
+ has_pkce=bool(data.get("code_verifier")),
53
+ category="auth",
54
+ )
55
+
56
+ # Clean up expired sessions
57
+ await self._cleanup_expired()
58
+
59
+ async def get_session(self, state: str) -> dict[str, Any] | None:
60
+ """Retrieve session data by state.
61
+
62
+ Args:
63
+ state: OAuth state parameter
64
+
65
+ Returns:
66
+ Session data or None if not found/expired
67
+ """
68
+ session = self._sessions.get(state)
69
+
70
+ if not session:
71
+ logger.debug("oauth_session_not_found", state=state, category="auth")
72
+ return None
73
+
74
+ # Check if session expired
75
+ created_at = session.get("created_at", 0)
76
+ if time.time() - created_at > self._ttl_seconds:
77
+ logger.debug("oauth_session_expired", state=state, category="auth")
78
+ await self.delete_session(state)
79
+ return None
80
+
81
+ logger.debug(
82
+ "oauth_session_retrieved",
83
+ state=state,
84
+ provider=session.get("provider"),
85
+ category="auth",
86
+ )
87
+ return session
88
+
89
+ async def delete_session(self, state: str) -> None:
90
+ """Delete a session.
91
+
92
+ Args:
93
+ state: OAuth state parameter
94
+ """
95
+ if state in self._sessions:
96
+ provider = self._sessions[state].get("provider")
97
+ del self._sessions[state]
98
+ logger.debug(
99
+ "oauth_session_deleted", state=state, provider=provider, category="auth"
100
+ )
101
+
102
+ async def _cleanup_expired(self) -> None:
103
+ """Remove expired sessions."""
104
+ current_time = time.time()
105
+ expired_states = [
106
+ state
107
+ for state, session in self._sessions.items()
108
+ if current_time - session.get("created_at", 0) > self._ttl_seconds
109
+ ]
110
+
111
+ for state in expired_states:
112
+ await self.delete_session(state)
113
+
114
+ if expired_states:
115
+ logger.debug(
116
+ "oauth_sessions_cleaned", count=len(expired_states), category="auth"
117
+ )
118
+
119
+ def clear_all(self) -> None:
120
+ """Clear all sessions (mainly for testing)."""
121
+ count = len(self._sessions)
122
+ self._sessions.clear()
123
+ logger.info("oauth_sessions_cleared", count=count, category="auth")
124
+
125
+
126
+ # Global session manager instance
127
+ _session_manager: OAuthSessionManager | None = None
128
+
129
+
130
+ def get_oauth_session_manager() -> OAuthSessionManager:
131
+ """Get the global OAuth session manager instance.
132
+
133
+ Returns:
134
+ Global OAuth session manager
135
+ """
136
+ global _session_manager
137
+ if _session_manager is None:
138
+ _session_manager = OAuthSessionManager()
139
+ return _session_manager
140
+
141
+
142
+ def reset_oauth_session_manager() -> None:
143
+ """Reset the global OAuth session manager.
144
+
145
+ This clears all sessions and creates a new manager.
146
+ Mainly useful for testing.
147
+ """
148
+ global _session_manager
149
+ if _session_manager:
150
+ _session_manager.clear_all()
151
+ _session_manager = OAuthSessionManager()