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
@@ -1,561 +0,0 @@
1
- """Credentials manager for coordinating storage and OAuth operations."""
2
-
3
- import asyncio
4
- import json
5
- from datetime import UTC, datetime, timedelta
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- import httpx
10
- from structlog import get_logger
11
-
12
- from ccproxy.auth.exceptions import (
13
- CredentialsExpiredError,
14
- CredentialsNotFoundError,
15
- )
16
- from ccproxy.auth.models import (
17
- ClaudeCredentials,
18
- OAuthToken,
19
- UserProfile,
20
- ValidationResult,
21
- )
22
- from ccproxy.auth.storage import JsonFileTokenStorage as JsonFileStorage
23
- from ccproxy.auth.storage import TokenStorage as CredentialsStorageBackend
24
- from ccproxy.config.auth import AuthSettings
25
- from ccproxy.services.credentials.oauth_client import OAuthClient
26
-
27
-
28
- logger = get_logger(__name__)
29
-
30
-
31
- class CredentialsManager:
32
- """Manager for Claude credentials with storage and OAuth support."""
33
-
34
- # ==================== Initialization ====================
35
-
36
- def __init__(
37
- self,
38
- config: AuthSettings | None = None,
39
- storage: CredentialsStorageBackend | None = None,
40
- oauth_client: OAuthClient | None = None,
41
- http_client: httpx.AsyncClient | None = None,
42
- ):
43
- """Initialize credentials manager.
44
-
45
- Args:
46
- config: Credentials configuration (uses defaults if not provided)
47
- storage: Storage backend (uses JSON file storage if not provided)
48
- oauth_client: OAuth client (creates one if not provided)
49
- http_client: HTTP client for OAuth operations
50
- """
51
- self.config = config or AuthSettings()
52
- self._storage = storage
53
- self._oauth_client = oauth_client
54
- self._http_client = http_client
55
- self._owns_http_client = http_client is None
56
- self._refresh_lock = asyncio.Lock()
57
-
58
- # Initialize OAuth client if not provided
59
- if self._oauth_client is None:
60
- self._oauth_client = OAuthClient(
61
- config=self.config.oauth,
62
- )
63
-
64
- async def __aenter__(self) -> "CredentialsManager":
65
- """Async context manager entry."""
66
- if self._http_client is None:
67
- self._http_client = httpx.AsyncClient()
68
- return self
69
-
70
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
71
- """Async context manager exit."""
72
- if self._owns_http_client and self._http_client:
73
- await self._http_client.aclose()
74
-
75
- # ==================== Storage Operations ====================
76
-
77
- @property
78
- def storage(self) -> CredentialsStorageBackend:
79
- """Get the storage backend, creating default if needed."""
80
- if self._storage is None:
81
- # Find first existing credentials file or use first path
82
- existing_path = self._find_existing_path()
83
- if existing_path:
84
- self._storage = JsonFileStorage(existing_path)
85
- else:
86
- # Use first path as default
87
- self._storage = JsonFileStorage(
88
- Path(self.config.storage.storage_paths[0]).expanduser()
89
- )
90
- return self._storage
91
-
92
- async def find_credentials_file(self) -> Path | None:
93
- """Find existing credentials file in configured paths.
94
-
95
- Returns:
96
- Path to credentials file if found, None otherwise
97
- """
98
- for path_str in self.config.storage.storage_paths:
99
- path = Path(path_str).expanduser()
100
- logger.debug("checking_credentials_path", path=str(path))
101
- if path.exists() and path.is_file():
102
- logger.info("credentials_file_found", path=str(path))
103
- return path
104
- else:
105
- logger.debug("credentials_path_not_found", path=str(path))
106
-
107
- logger.warning(
108
- "no_credentials_files_found",
109
- searched_paths=self.config.storage.storage_paths,
110
- )
111
- return None
112
-
113
- async def load(self) -> ClaudeCredentials | None:
114
- """Load credentials from storage.
115
-
116
- Returns:
117
- Credentials if found and valid, None otherwise
118
- """
119
- try:
120
- return await self.storage.load()
121
- except Exception as e:
122
- logger.error("credentials_load_failed", error=str(e))
123
- return None
124
-
125
- async def save(self, credentials: ClaudeCredentials) -> bool:
126
- """Save credentials to storage.
127
-
128
- Args:
129
- credentials: Credentials to save
130
-
131
- Returns:
132
- True if saved successfully, False otherwise
133
- """
134
- try:
135
- return await self.storage.save(credentials)
136
- except Exception as e:
137
- logger.error("credentials_save_failed", error=str(e))
138
- return False
139
-
140
- # ==================== OAuth Operations ====================
141
-
142
- async def login(self) -> ClaudeCredentials:
143
- """Perform OAuth login and save credentials.
144
-
145
- Returns:
146
- New credentials from login
147
-
148
- Raises:
149
- OAuthLoginError: If login fails
150
- """
151
- if self._oauth_client is None:
152
- raise RuntimeError("OAuth client not initialized")
153
- credentials = await self._oauth_client.login()
154
-
155
- # Fetch and save user profile after successful login
156
- try:
157
- profile = await self._oauth_client.fetch_user_profile(
158
- credentials.claude_ai_oauth.access_token
159
- )
160
- if profile:
161
- # Save profile data
162
- await self._save_account_profile(profile)
163
-
164
- # Update subscription type based on profile
165
- determined_subscription = self._determine_subscription_type(profile)
166
- credentials.claude_ai_oauth.subscription_type = determined_subscription
167
-
168
- logger.debug(
169
- "subscription_type_set", subscription_type=determined_subscription
170
- )
171
- else:
172
- logger.debug(
173
- "profile_fetch_skipped", context="login", reason="no_profile_data"
174
- )
175
- except Exception as e:
176
- logger.warning("profile_fetch_failed", context="login", error=str(e))
177
- # Continue with login even if profile fetch fails
178
-
179
- await self.save(credentials)
180
- return credentials
181
-
182
- async def get_valid_credentials(self) -> ClaudeCredentials:
183
- """Get valid credentials, refreshing if necessary.
184
-
185
- Returns:
186
- Valid credentials
187
-
188
- Raises:
189
- CredentialsNotFoundError: If no credentials found
190
- CredentialsExpiredError: If credentials expired and refresh fails
191
- """
192
- credentials = await self.load()
193
- if not credentials:
194
- raise CredentialsNotFoundError("No credentials found. Please login first.")
195
-
196
- # Check if token needs refresh
197
- oauth_token = credentials.claude_ai_oauth
198
- should_refresh = self._should_refresh_token(oauth_token)
199
-
200
- if should_refresh:
201
- async with self._refresh_lock:
202
- # Re-check if refresh is still needed after acquiring lock
203
- # Another request might have already refreshed the token
204
- credentials = await self.load()
205
- if not credentials:
206
- raise CredentialsNotFoundError(
207
- "No credentials found. Please login first."
208
- )
209
-
210
- oauth_token = credentials.claude_ai_oauth
211
- should_refresh = self._should_refresh_token(oauth_token)
212
-
213
- if should_refresh:
214
- logger.info(
215
- "token_refresh_start", reason="expired_or_expiring_soon"
216
- )
217
- try:
218
- credentials = await self._refresh_token_with_profile(
219
- credentials
220
- )
221
- except Exception as e:
222
- logger.error(
223
- "token_refresh_failed", error=str(e), exc_info=True
224
- )
225
- if oauth_token.is_expired:
226
- raise CredentialsExpiredError(
227
- "Token expired and refresh failed. Please login again."
228
- ) from e
229
- # If not expired yet but refresh failed, return existing token
230
- logger.warning(
231
- "token_refresh_fallback",
232
- reason="refresh_failed_but_token_not_expired",
233
- )
234
-
235
- return credentials
236
-
237
- async def get_access_token(self) -> str:
238
- """Get valid access token, refreshing if necessary.
239
-
240
- Returns:
241
- Access token string
242
-
243
- Raises:
244
- CredentialsNotFoundError: If no credentials found
245
- CredentialsExpiredError: If credentials expired and refresh fails
246
- """
247
- credentials = await self.get_valid_credentials()
248
- return credentials.claude_ai_oauth.access_token
249
-
250
- async def refresh_token(self) -> ClaudeCredentials:
251
- """Refresh the access token without checking expiration.
252
-
253
- This method directly refreshes the token regardless of whether it's expired.
254
- Useful for force-refreshing tokens or testing.
255
-
256
- Returns:
257
- Updated credentials with new token
258
-
259
- Raises:
260
- CredentialsNotFoundError: If no credentials found
261
- RuntimeError: If OAuth client not initialized
262
- ValueError: If no refresh token available
263
- Exception: If token refresh fails
264
- """
265
- credentials = await self.load()
266
- if not credentials:
267
- raise CredentialsNotFoundError("No credentials found. Please login first.")
268
-
269
- logger.info("token_refresh_start", reason="forced")
270
- return await self._refresh_token_with_profile(credentials)
271
-
272
- async def fetch_user_profile(self) -> UserProfile | None:
273
- """Fetch user profile information.
274
-
275
- Returns:
276
- UserProfile if successful, None otherwise
277
- """
278
- try:
279
- credentials = await self.get_valid_credentials()
280
- if self._oauth_client is None:
281
- raise RuntimeError("OAuth client not initialized")
282
- profile = await self._oauth_client.fetch_user_profile(
283
- credentials.claude_ai_oauth.access_token,
284
- )
285
- return profile
286
- except Exception as e:
287
- logger.error(
288
- "user_profile_fetch_failed",
289
- error=str(e),
290
- exc_info=True,
291
- )
292
- return None
293
-
294
- async def get_account_profile(self) -> UserProfile | None:
295
- """Get saved account profile information.
296
-
297
- Returns:
298
- UserProfile if available, None otherwise
299
- """
300
- return await self._load_account_profile()
301
-
302
- # ==================== Validation and Management ====================
303
-
304
- async def validate(self) -> ValidationResult:
305
- """Validate current credentials.
306
-
307
- Returns:
308
- ValidationResult with credentials status and details
309
- """
310
- credentials = await self.load()
311
- if not credentials:
312
- raise CredentialsNotFoundError()
313
-
314
- return ValidationResult(
315
- valid=True,
316
- expired=credentials.claude_ai_oauth.is_expired,
317
- credentials=credentials,
318
- path=self.storage.get_location(),
319
- )
320
-
321
- async def logout(self) -> bool:
322
- """Delete stored credentials.
323
-
324
- Returns:
325
- True if deleted successfully, False otherwise
326
- """
327
- try:
328
- # Delete both credentials and account profile
329
- success = await self.storage.delete()
330
- await self._delete_account_profile()
331
- return success
332
- except Exception as e:
333
- logger.error("credentials_delete_failed", error=str(e), exc_info=True)
334
- return False
335
-
336
- # ==================== Private Helper Methods ====================
337
-
338
- async def _get_account_profile_path(self) -> Path:
339
- """Get the path for account profile storage.
340
-
341
- Returns:
342
- Path to account.json file alongside credentials
343
- """
344
- # Use the same directory as credentials file but with account.json name
345
- credentials_path = self._find_existing_path()
346
- if credentials_path is None:
347
- # Use first path as default
348
- credentials_path = Path(self.config.storage.storage_paths[0]).expanduser()
349
-
350
- # Replace filename with account.json
351
- return credentials_path.parent / "account.json"
352
-
353
- async def _save_account_profile(self, profile: UserProfile) -> bool:
354
- """Save account profile to account.json.
355
-
356
- Args:
357
- profile: User profile to save
358
-
359
- Returns:
360
- True if saved successfully
361
- """
362
- try:
363
- account_path = await self._get_account_profile_path()
364
- account_path.parent.mkdir(parents=True, exist_ok=True)
365
-
366
- # Convert to dict and save as JSON
367
- profile_data = profile.model_dump()
368
-
369
- with account_path.open("w", encoding="utf-8") as f:
370
- json.dump(profile_data, f, indent=2, ensure_ascii=False)
371
-
372
- logger.debug("account_profile_saved", path=str(account_path))
373
- return True
374
-
375
- except Exception as e:
376
- logger.error("account_profile_save_failed", error=str(e), exc_info=True)
377
- return False
378
-
379
- async def _load_account_profile(self) -> UserProfile | None:
380
- """Load account profile from account.json.
381
-
382
- Returns:
383
- User profile if found, None otherwise
384
- """
385
- try:
386
- account_path = await self._get_account_profile_path()
387
-
388
- if not account_path.exists():
389
- logger.debug("account_profile_not_found", path=str(account_path))
390
- return None
391
-
392
- with account_path.open("r", encoding="utf-8") as f:
393
- profile_data = json.load(f)
394
-
395
- return UserProfile.model_validate(profile_data)
396
-
397
- except Exception as e:
398
- logger.debug("account_profile_load_failed", error=str(e))
399
- return None
400
-
401
- async def _delete_account_profile(self) -> bool:
402
- """Delete account profile file.
403
-
404
- Returns:
405
- True if deleted successfully
406
- """
407
- try:
408
- account_path = await self._get_account_profile_path()
409
- if account_path.exists():
410
- account_path.unlink()
411
- logger.debug("account_profile_deleted", path=str(account_path))
412
- return True
413
- except Exception as e:
414
- logger.debug("account_profile_delete_failed", error=str(e))
415
- return False
416
-
417
- def _determine_subscription_type(self, profile: UserProfile) -> str:
418
- """Determine subscription type from profile information.
419
-
420
- Args:
421
- profile: User profile with account information
422
-
423
- Returns:
424
- Subscription type string
425
- """
426
- if not profile.account:
427
- return "unknown"
428
-
429
- # Check account flags first
430
- if profile.account.has_claude_max:
431
- return "max"
432
- elif profile.account.has_claude_pro:
433
- return "pro"
434
-
435
- # Fallback to organization type
436
- if profile.organization and profile.organization.organization_type:
437
- org_type = profile.organization.organization_type.lower()
438
- if "max" in org_type:
439
- return "max"
440
- elif "pro" in org_type:
441
- return "pro"
442
-
443
- return "free"
444
-
445
- def _find_existing_path(self) -> Path | None:
446
- """Find first existing path from configured storage paths.
447
-
448
- Returns:
449
- Path if found, None otherwise
450
- """
451
- for path_str in self.config.storage.storage_paths:
452
- path = Path(path_str).expanduser()
453
- if path.exists():
454
- return path
455
- return None
456
-
457
- def _should_refresh_token(self, oauth_token: OAuthToken) -> bool:
458
- """Check if token should be refreshed based on configuration.
459
-
460
- Args:
461
- oauth_token: Token to check
462
-
463
- Returns:
464
- True if token should be refreshed
465
- """
466
- if self.config.storage.auto_refresh:
467
- buffer = timedelta(seconds=self.config.storage.refresh_buffer_seconds)
468
- return datetime.now(UTC) + buffer >= oauth_token.expires_at_datetime
469
- else:
470
- return oauth_token.is_expired
471
-
472
- async def _refresh_token_with_profile(
473
- self, credentials: ClaudeCredentials
474
- ) -> ClaudeCredentials:
475
- """Refresh token and update profile information.
476
-
477
- Args:
478
- credentials: Current credentials with token to refresh
479
-
480
- Returns:
481
- Updated credentials with new token and profile info
482
-
483
- Raises:
484
- RuntimeError: If OAuth client not initialized
485
- ValueError: If no refresh token available
486
- Exception: If token refresh fails
487
- """
488
- if self._oauth_client is None:
489
- raise RuntimeError("OAuth client not initialized")
490
-
491
- oauth_token = credentials.claude_ai_oauth
492
-
493
- # Refresh the token
494
- token_response = await self._oauth_client.refresh_access_token(
495
- oauth_token.refresh_token
496
- )
497
-
498
- # Calculate expires_at from expires_in if provided
499
- expires_at = oauth_token.expires_at # Start with existing value
500
- if token_response.expires_in:
501
- expires_at = int(
502
- (datetime.now(UTC).timestamp() + token_response.expires_in) * 1000
503
- )
504
-
505
- # Parse scopes from server response
506
- new_scopes = oauth_token.scopes # Start with existing scopes
507
- if token_response.scope:
508
- new_scopes = token_response.scope.split()
509
-
510
- # Create new token preserving all server fields when available
511
- # Ensure we have valid refresh token
512
- if not token_response.refresh_token and not oauth_token.refresh_token:
513
- raise ValueError("No refresh token available")
514
-
515
- # Convert OAuthTokenResponse to OAuthToken format
516
- new_token = OAuthToken(
517
- accessToken=token_response.access_token,
518
- refreshToken=token_response.refresh_token or oauth_token.refresh_token,
519
- expiresAt=expires_at,
520
- scopes=new_scopes,
521
- subscriptionType=token_response.subscription_type
522
- or oauth_token.subscription_type,
523
- tokenType=token_response.token_type or oauth_token.token_type,
524
- )
525
-
526
- # Update credentials with new token
527
- credentials.claude_ai_oauth = new_token
528
-
529
- # Fetch user profile to update subscription type
530
- try:
531
- profile = await self._oauth_client.fetch_user_profile(
532
- new_token.access_token
533
- )
534
- if profile:
535
- # Save profile data
536
- await self._save_account_profile(profile)
537
-
538
- # Update subscription type based on profile
539
- determined_subscription = self._determine_subscription_type(profile)
540
- new_token.subscription_type = determined_subscription
541
- credentials.claude_ai_oauth = new_token
542
-
543
- logger.debug(
544
- "subscription_type_updated",
545
- subscription_type=determined_subscription,
546
- )
547
- else:
548
- logger.debug(
549
- "profile_fetch_skipped", reason="no_profile_data_available"
550
- )
551
- except Exception as e:
552
- logger.warning(
553
- "profile_fetch_failed", context="token_refresh", error=str(e)
554
- )
555
- # Continue with token refresh even if profile fetch fails
556
-
557
- # Save updated credentials
558
- await self.save(credentials)
559
-
560
- logger.info("token_refresh_completed")
561
- return credentials