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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,523 @@
1
+ """Base token manager for all authentication providers."""
2
+
3
+ import json
4
+ import os
5
+ from abc import ABC, abstractmethod
6
+ from datetime import UTC, datetime
7
+ from typing import Any, Generic, TypeVar
8
+
9
+ from pydantic import ValidationError
10
+
11
+ from ccproxy.auth.exceptions import (
12
+ CredentialsInvalidError,
13
+ CredentialsStorageError,
14
+ )
15
+ from ccproxy.auth.managers.token_snapshot import TokenSnapshot
16
+ from ccproxy.auth.models.credentials import BaseCredentials
17
+ from ccproxy.auth.storage.base import TokenStorage
18
+ from ccproxy.core.logging import get_logger
19
+ from ccproxy.utils.caching import AuthStatusCache, async_ttl_cache
20
+
21
+
22
+ logger = get_logger(__name__)
23
+
24
+ # Type variable for credentials
25
+ CredentialsT = TypeVar("CredentialsT", bound=BaseCredentials)
26
+
27
+
28
+ class BaseTokenManager(ABC, Generic[CredentialsT]):
29
+ """Base manager for token storage and refresh operations.
30
+
31
+ This generic base class provides common functionality for managing
32
+ authentication tokens across different providers (OpenAI, Claude, etc.).
33
+
34
+ Type Parameters:
35
+ CredentialsT: The specific credential type (e.g., OpenAICredentials, ClaudeCredentials)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ storage: TokenStorage[CredentialsT],
41
+ credentials_ttl: float | None = None,
42
+ refresh_grace_seconds: float | None = None,
43
+ ):
44
+ """Initialize token manager.
45
+
46
+ Args:
47
+ storage: Token storage backend that matches the credential type
48
+ """
49
+ self.storage = storage
50
+ self._auth_cache = AuthStatusCache(ttl=60.0) # 1 minute TTL for auth status
51
+ self._profile_cache: Any = None # For subclasses that cache profiles
52
+ # In-memory credentials cache to reduce file checks
53
+ self._credentials_cache: CredentialsT | None = None
54
+ self._credentials_loaded_at: float | None = None
55
+ # TTL for rechecking credentials from storage (config-driven)
56
+ # Prefer explicit parameter; fallback to environment; then default.
57
+ if credentials_ttl is not None:
58
+ try:
59
+ ttl_val = float(credentials_ttl)
60
+ self._credentials_ttl = ttl_val if ttl_val >= 0 else 30.0
61
+ except Exception:
62
+ self._credentials_ttl = 30.0
63
+ else:
64
+ env_val = os.getenv("AUTH__CREDENTIALS_TTL_SECONDS")
65
+ try:
66
+ self._credentials_ttl = float(env_val) if env_val is not None else 30.0
67
+ if self._credentials_ttl < 0:
68
+ self._credentials_ttl = 30.0
69
+ except Exception:
70
+ self._credentials_ttl = 30.0
71
+
72
+ # Grace period before expiry to trigger proactive refresh
73
+ if refresh_grace_seconds is not None:
74
+ try:
75
+ grace_val = float(refresh_grace_seconds)
76
+ self._refresh_grace_seconds = grace_val if grace_val >= 0 else 0.0
77
+ except Exception:
78
+ self._refresh_grace_seconds = 120.0
79
+ else:
80
+ env_grace = os.getenv("AUTH__REFRESH_GRACE_SECONDS")
81
+ try:
82
+ grace_val = float(env_grace) if env_grace is not None else 120.0
83
+ if grace_val < 0:
84
+ grace_val = 0.0
85
+ self._refresh_grace_seconds = grace_val
86
+ except Exception:
87
+ self._refresh_grace_seconds = 120.0
88
+
89
+ # ==================== Core Operations ====================
90
+
91
+ async def load_credentials(self) -> CredentialsT | None:
92
+ """Load credentials from storage.
93
+
94
+ Returns:
95
+ Credentials if found and valid, None otherwise
96
+ """
97
+ try:
98
+ # Serve from cache when fresh and not expired
99
+ if self._credentials_cache is not None and self._credentials_loaded_at:
100
+ from time import time as _now
101
+
102
+ age = _now() - self._credentials_loaded_at
103
+ if age < self._credentials_ttl and not self.is_expired(
104
+ self._credentials_cache
105
+ ):
106
+ logger.debug(
107
+ "credentials_cache_hit",
108
+ age_seconds=round(age, 2),
109
+ ttl_seconds=self._credentials_ttl,
110
+ )
111
+ return self._credentials_cache
112
+
113
+ # Otherwise, reload from storage (also triggers on expired or stale cache)
114
+ creds = await self.storage.load()
115
+ # Update cache regardless of result (None clears cache)
116
+ self._credentials_cache = creds
117
+ from time import time as _now
118
+
119
+ self._credentials_loaded_at = _now()
120
+ logger.debug(
121
+ "credentials_cache_refreshed",
122
+ has_credentials=bool(creds),
123
+ ttl_seconds=self._credentials_ttl,
124
+ )
125
+ return creds
126
+ except (OSError, PermissionError) as e:
127
+ logger.error("storage_access_failed", error=str(e), exc_info=e)
128
+ return None
129
+ except (CredentialsStorageError, CredentialsInvalidError) as e:
130
+ logger.error("credentials_load_failed", error=str(e), exc_info=e)
131
+ return None
132
+ except json.JSONDecodeError as e:
133
+ logger.error("credentials_json_decode_error", error=str(e), exc_info=e)
134
+ return None
135
+ except ValidationError as e:
136
+ logger.error("credentials_validation_error", error=str(e), exc_info=e)
137
+ return None
138
+ except Exception as e:
139
+ logger.error("unexpected_load_error", error=str(e), exc_info=e)
140
+ return None
141
+
142
+ async def save_credentials(self, credentials: CredentialsT) -> bool:
143
+ """Save credentials to storage.
144
+
145
+ Args:
146
+ credentials: Credentials to save
147
+
148
+ Returns:
149
+ True if saved successfully, False otherwise
150
+ """
151
+ try:
152
+ ok = await self.storage.save(credentials)
153
+ if ok:
154
+ # Update cache immediately
155
+ self._credentials_cache = credentials
156
+ from time import time as _now
157
+
158
+ self._credentials_loaded_at = _now()
159
+ return ok
160
+ except (OSError, PermissionError) as e:
161
+ logger.error("storage_access_failed", error=str(e), exc_info=e)
162
+ return False
163
+ except CredentialsStorageError as e:
164
+ logger.error("credentials_save_failed", error=str(e), exc_info=e)
165
+ return False
166
+ except json.JSONDecodeError as e:
167
+ logger.error("credentials_json_encode_error", error=str(e), exc_info=e)
168
+ return False
169
+ except ValidationError as e:
170
+ logger.error("credentials_validation_error", error=str(e), exc_info=e)
171
+ return False
172
+ except Exception as e:
173
+ logger.error("unexpected_save_error", error=str(e), exc_info=e)
174
+ return False
175
+
176
+ async def clear_credentials(self) -> bool:
177
+ """Clear stored credentials.
178
+
179
+ Returns:
180
+ True if cleared successfully, False otherwise
181
+ """
182
+ try:
183
+ # Clear the caches
184
+ self._auth_cache.clear()
185
+ self._credentials_cache = None
186
+ self._credentials_loaded_at = None
187
+
188
+ # Delete from storage
189
+ return await self.storage.delete()
190
+ except Exception as e:
191
+ logger.error("failed_to_clear_credentials", error=str(e), exc_info=e)
192
+ return False
193
+
194
+ def get_storage_location(self) -> str:
195
+ """Get the storage location for credentials.
196
+
197
+ Returns:
198
+ Storage location description
199
+ """
200
+ return self.storage.get_location()
201
+
202
+ @abstractmethod
203
+ def _build_token_snapshot(self, credentials: CredentialsT) -> TokenSnapshot:
204
+ """Construct a token snapshot for the given credentials."""
205
+
206
+ def _safe_token_snapshot(self, credentials: CredentialsT) -> TokenSnapshot | None:
207
+ """Safely build a token snapshot with defensive logging."""
208
+ try:
209
+ return self._build_token_snapshot(credentials)
210
+ except NotImplementedError:
211
+ raise
212
+ except Exception as exc: # pragma: no cover - defensive logging
213
+ logger.debug(
214
+ "token_snapshot_failed",
215
+ error=str(exc),
216
+ credentials_type=type(credentials).__name__,
217
+ category="auth",
218
+ )
219
+ return None
220
+
221
+ async def get_token_snapshot(self) -> TokenSnapshot | None:
222
+ """Return a lightweight snapshot of stored token metadata."""
223
+ credentials = await self.load_credentials()
224
+ if not credentials:
225
+ return None
226
+ try:
227
+ return self._build_token_snapshot(credentials)
228
+ except NotImplementedError:
229
+ raise
230
+ except Exception as exc: # pragma: no cover - defensive logging
231
+ logger.debug(
232
+ "token_snapshot_failed",
233
+ error=str(exc),
234
+ credentials_type=type(credentials).__name__,
235
+ category="auth",
236
+ )
237
+ return None
238
+
239
+ # ==================== Common Implementations ====================
240
+
241
+ @property
242
+ def refresh_grace_seconds(self) -> float:
243
+ """Seconds before expiry when tokens should be proactively refreshed."""
244
+
245
+ return self._refresh_grace_seconds
246
+
247
+ def seconds_until_expiration(self, credentials: CredentialsT) -> float | None:
248
+ """Return seconds until the access token expires, if available."""
249
+
250
+ expires_at = self.get_expiration_time(credentials)
251
+ if not isinstance(expires_at, datetime):
252
+ return None
253
+
254
+ # Normalise naive datetimes to UTC to avoid comparison issues
255
+ if expires_at.tzinfo is None:
256
+ expires_at = expires_at.replace(tzinfo=UTC)
257
+
258
+ delta = expires_at - datetime.now(UTC)
259
+ return delta.total_seconds()
260
+
261
+ def should_refresh(
262
+ self, credentials: CredentialsT, grace_seconds: float | None = None
263
+ ) -> bool:
264
+ """Determine whether credentials should be refreshed."""
265
+
266
+ seconds_remaining = self.seconds_until_expiration(credentials)
267
+ if seconds_remaining is None:
268
+ return False
269
+
270
+ grace = (
271
+ self.refresh_grace_seconds
272
+ if grace_seconds is None
273
+ else max(grace_seconds, 0.0)
274
+ )
275
+
276
+ return seconds_remaining <= grace
277
+
278
+ async def validate_token(self) -> bool:
279
+ """Check if stored token is valid and not expired.
280
+
281
+ Returns:
282
+ True if valid, False otherwise
283
+ """
284
+ credentials = await self.load_credentials()
285
+ if not credentials:
286
+ return False
287
+
288
+ if self.is_expired(credentials):
289
+ logger.info("token_expired")
290
+ return False
291
+
292
+ return True
293
+
294
+ # Subclasses should implement protocol methods
295
+
296
+ @abstractmethod
297
+ async def refresh_token(self) -> CredentialsT | None:
298
+ """Refresh the access token using the refresh token.
299
+
300
+ Returns:
301
+ Updated credentials or None if refresh failed
302
+ """
303
+ pass
304
+
305
+ async def get_auth_status(self) -> dict[str, Any]:
306
+ """Get current authentication status.
307
+
308
+ Returns:
309
+ Dictionary with authentication status information
310
+ """
311
+ credentials = await self.load_credentials()
312
+
313
+ if not credentials:
314
+ return {
315
+ "authenticated": False,
316
+ "reason": "No credentials found",
317
+ }
318
+
319
+ if self.is_expired(credentials):
320
+ status = {
321
+ "authenticated": False,
322
+ "reason": "Token expired",
323
+ }
324
+
325
+ # Add expiration info if available
326
+ expires_at = self.get_expiration_time(credentials)
327
+ if expires_at:
328
+ status["expires_at"] = expires_at.isoformat()
329
+
330
+ # Add account ID if available
331
+ account_id = self.get_account_id(credentials)
332
+ if account_id:
333
+ status["account_id"] = account_id
334
+
335
+ return status
336
+
337
+ # Token is valid
338
+ status = {"authenticated": True}
339
+
340
+ # Add expiration info if available
341
+ expires_at = self.get_expiration_time(credentials)
342
+ if expires_at:
343
+ status["expires_at"] = expires_at.isoformat()
344
+ seconds_remaining = self.seconds_until_expiration(credentials)
345
+ if seconds_remaining is not None:
346
+ status["expires_in"] = max(0, int(seconds_remaining))
347
+
348
+ # Add account ID if available
349
+ account_id = self.get_account_id(credentials)
350
+ if account_id:
351
+ status["account_id"] = account_id
352
+
353
+ return status
354
+
355
+ @abstractmethod
356
+ def is_expired(self, credentials: CredentialsT) -> bool:
357
+ """Check if credentials are expired.
358
+
359
+ Args:
360
+ credentials: Credentials to check
361
+
362
+ Returns:
363
+ True if expired, False otherwise
364
+ """
365
+ pass
366
+
367
+ @abstractmethod
368
+ def get_account_id(self, credentials: CredentialsT) -> str | None:
369
+ """Get account ID from credentials.
370
+
371
+ Args:
372
+ credentials: Credentials to extract account ID from
373
+
374
+ Returns:
375
+ Account ID if available, None otherwise
376
+ """
377
+ pass
378
+
379
+ def get_expiration_time(self, credentials: CredentialsT) -> Any:
380
+ """Get expiration time from credentials.
381
+
382
+ Args:
383
+ credentials: Credentials to extract expiration time from
384
+
385
+ Returns:
386
+ Expiration datetime if available, None otherwise
387
+ """
388
+ snapshot = self._safe_token_snapshot(credentials)
389
+ if snapshot:
390
+ return snapshot.expires_at
391
+ return None
392
+
393
+ # ==================== Unified Profile Support ====================
394
+
395
+ async def get_profile(self) -> Any:
396
+ """Get profile information.
397
+
398
+ To be implemented by provider-specific managers.
399
+ Returns provider-specific profile model.
400
+ """
401
+ return None
402
+
403
+ async def get_profile_quick(self) -> Any:
404
+ """Get profile information without performing I/O or network when possible.
405
+
406
+ Default behavior returns any cached profile stored on the manager.
407
+ Provider implementations may override to derive lightweight profiles
408
+ directly from credentials (e.g., JWT claims) without remote calls.
409
+
410
+ Returns:
411
+ Provider-specific profile model or None if unavailable
412
+ """
413
+ # Return cached profile if a subclass maintains one
414
+ return getattr(self, "_profile_cache", None)
415
+
416
+ async def get_unified_profile(self) -> dict[str, Any]:
417
+ """Get profile in a unified format across all providers.
418
+
419
+ Returns:
420
+ Dictionary with standardized fields plus provider-specific extras
421
+ """
422
+ profile = await self.get_profile()
423
+ if not profile:
424
+ return {}
425
+
426
+ extras = getattr(profile, "extras", None)
427
+ if extras is None:
428
+ extras = getattr(profile, "features", {}) or {}
429
+
430
+ return {
431
+ "account_id": profile.account_id,
432
+ "email": profile.email,
433
+ "display_name": profile.display_name,
434
+ "provider": profile.provider_type,
435
+ "extras": extras,
436
+ }
437
+
438
+ async def get_unified_profile_quick(self) -> dict[str, Any]:
439
+ """Get a lightweight unified profile across providers.
440
+
441
+ Uses cached or locally derivable data only. Implementations can
442
+ override get_profile_quick() to provide provider-specific logic.
443
+
444
+ Returns:
445
+ Dictionary with standardized fields or empty dict if unavailable
446
+ """
447
+ profile = await self.get_profile_quick()
448
+ if not profile:
449
+ return {}
450
+
451
+ extras = getattr(profile, "extras", None)
452
+ if extras is None:
453
+ extras = getattr(profile, "features", {}) or {}
454
+
455
+ return {
456
+ "account_id": getattr(profile, "account_id", ""),
457
+ "email": getattr(profile, "email", ""),
458
+ "display_name": getattr(profile, "display_name", None),
459
+ "provider": getattr(profile, "provider_type", "unknown"),
460
+ "extras": extras,
461
+ }
462
+
463
+ async def clear_cache(self) -> None:
464
+ """Clear any cached data (profiles, etc.).
465
+
466
+ Should be called after token refresh or logout.
467
+ """
468
+ # Clear auth status cache
469
+ if hasattr(self, "_auth_cache"):
470
+ self._auth_cache.clear()
471
+
472
+ # Clear profile cache if exists
473
+ if hasattr(self, "_profile_cache"):
474
+ self._profile_cache = None
475
+
476
+ # Clear credentials cache so next access rechecks storage
477
+ self._credentials_cache = None
478
+ self._credentials_loaded_at = None
479
+
480
+ # ==================== Common Utility Methods ====================
481
+
482
+ async def is_authenticated(self) -> bool:
483
+ """Check if current authentication is valid.
484
+
485
+ Returns:
486
+ True if authenticated, False otherwise
487
+ """
488
+ credentials = await self.load_credentials()
489
+ if not credentials:
490
+ return False
491
+
492
+ return not self.is_expired(credentials)
493
+
494
+ async def get_access_token(self) -> str | None:
495
+ """Get valid access token from credentials.
496
+
497
+ Returns:
498
+ Access token if available and valid, None otherwise
499
+ """
500
+ credentials = await self.load_credentials()
501
+ if not credentials:
502
+ return None
503
+
504
+ if self.is_expired(credentials):
505
+ logger.info("token_expired")
506
+ return None
507
+
508
+ snapshot = self._safe_token_snapshot(credentials)
509
+ if snapshot and snapshot.access_token:
510
+ return snapshot.access_token
511
+
512
+ return None
513
+
514
+ @async_ttl_cache(ttl=60.0) # Cache auth status for 1 minute
515
+ async def get_cached_auth_status(self) -> dict[str, Any]:
516
+ """Get current authentication status with caching.
517
+
518
+ This is a convenience method that wraps get_auth_status() with caching.
519
+
520
+ Returns:
521
+ Dictionary with authentication status information
522
+ """
523
+ return await self.get_auth_status()
@@ -0,0 +1,63 @@
1
+ """Enhanced base token manager with automatic token refresh."""
2
+
3
+ from ccproxy.auth.exceptions import OAuthTokenRefreshError
4
+ from ccproxy.auth.managers.base import BaseTokenManager, CredentialsT
5
+ from ccproxy.core.logging import get_logger
6
+
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class EnhancedTokenManager(BaseTokenManager[CredentialsT]):
12
+ """Enhanced token manager with automatic refresh capability."""
13
+
14
+ async def get_access_token_with_refresh(self) -> str | None:
15
+ """Get valid access token, automatically refreshing if expired.
16
+
17
+ Returns:
18
+ Access token if available and valid, None otherwise
19
+ """
20
+ credentials = await self.load_credentials()
21
+ if not credentials:
22
+ logger.debug("no_credentials_found")
23
+ return None
24
+
25
+ # Check if token is expired
26
+ if self.should_refresh(credentials):
27
+ expires_in = self.seconds_until_expiration(credentials)
28
+ reason = "expired" if self.is_expired(credentials) else "expiring_soon"
29
+ logger.info(
30
+ "token_refresh_needed",
31
+ reason=reason,
32
+ expires_in=expires_in,
33
+ )
34
+
35
+ try:
36
+ refreshed = await self.refresh_token()
37
+ except Exception as exc: # pragma: no cover - defensive
38
+ logger.warning(
39
+ "token_refresh_exception", error=str(exc), category="auth"
40
+ )
41
+ raise OAuthTokenRefreshError("Token refresh failed") from exc
42
+
43
+ if refreshed:
44
+ logger.info("token_refreshed_successfully")
45
+ credentials = refreshed
46
+ else:
47
+ logger.warning("token_refresh_failed")
48
+ raise OAuthTokenRefreshError("Token refresh failed")
49
+
50
+ snapshot = self._safe_token_snapshot(credentials)
51
+ if snapshot and snapshot.access_token:
52
+ return snapshot.access_token
53
+
54
+ return None
55
+
56
+ async def ensure_valid_token(self) -> bool:
57
+ """Ensure we have a valid (non-expired) token, refreshing if needed.
58
+
59
+ Returns:
60
+ True if we have a valid token (after refresh if needed), False otherwise
61
+ """
62
+ token = await self.get_access_token_with_refresh()
63
+ return token is not None
@@ -0,0 +1,77 @@
1
+ """Shared token snapshot model for credential managers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass, field
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class TokenSnapshot:
13
+ """Immutable view over sensitive token metadata.
14
+
15
+ Token managers return this lightweight structure to share
16
+ credential state without exposing implementation details.
17
+ Secrets should only appear in the access/refresh token fields
18
+ and remain masked when rendered via the helper methods.
19
+ """
20
+
21
+ provider: str | None = None
22
+ account_id: str | None = None
23
+ access_token: str | None = None
24
+ refresh_token: str | None = None
25
+ expires_at: datetime | None = None
26
+ scopes: tuple[str, ...] = ()
27
+ extras: dict[str, Any] = field(default_factory=dict)
28
+
29
+ def has_access_token(self) -> bool:
30
+ """Whether an access token is present."""
31
+ return bool(self.access_token)
32
+
33
+ def has_refresh_token(self) -> bool:
34
+ """Whether a refresh token is present."""
35
+ return bool(self.refresh_token)
36
+
37
+ def access_token_preview(self, visible: int = 8) -> str | None:
38
+ """Return a masked preview of the access token."""
39
+ token = self.access_token
40
+ if not token:
41
+ return None
42
+ if visible <= 0 or len(token) <= visible * 2:
43
+ return "*" * len(token)
44
+ return f"{token[:visible]}...{token[-visible:]}"
45
+
46
+ def refresh_token_preview(self, visible: int = 4) -> str | None:
47
+ """Return a masked preview of the refresh token."""
48
+ token = self.refresh_token
49
+ if not token:
50
+ return None
51
+ if visible <= 0 or len(token) <= visible * 2:
52
+ return "*" * len(token)
53
+ return f"{token[:visible]}...{token[-visible:]}"
54
+
55
+ def expires_in_seconds(self) -> int | None:
56
+ """Return seconds until expiration when available."""
57
+ if not self.expires_at:
58
+ return None
59
+ now = datetime.now(UTC)
60
+ delta = self.expires_at - now
61
+ return max(0, int(delta.total_seconds()))
62
+
63
+ def with_scopes(self, scopes: Iterable[str]) -> TokenSnapshot:
64
+ """Return a copy with the provided scopes tuple."""
65
+ scope_tuple = tuple(scope for scope in scopes if scope)
66
+ return TokenSnapshot(
67
+ provider=self.provider,
68
+ account_id=self.account_id,
69
+ access_token=self.access_token,
70
+ refresh_token=self.refresh_token,
71
+ expires_at=self.expires_at,
72
+ scopes=scope_tuple,
73
+ extras=dict(self.extras),
74
+ )
75
+
76
+
77
+ __all__ = ["TokenSnapshot"]