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
@@ -0,0 +1,571 @@
1
+ """Claude OAuth provider for plugin registration."""
2
+
3
+ import hashlib
4
+ from base64 import urlsafe_b64encode
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+ from urllib.parse import urlencode
8
+
9
+ import httpx
10
+
11
+ from ccproxy.auth.oauth.protocol import ProfileLoggingMixin, StandardProfileFields
12
+ from ccproxy.auth.oauth.registry import CliAuthConfig, FlowType, OAuthProviderInfo
13
+ from ccproxy.auth.storage.generic import GenericJsonStorage
14
+ from ccproxy.config.settings import Settings
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from ccproxy.services.cli_detection import CLIDetectionService
19
+
20
+ from .manager import ClaudeApiTokenManager
21
+
22
+ from ccproxy.core.logging import get_plugin_logger
23
+
24
+ from .client import ClaudeOAuthClient
25
+ from .config import ClaudeOAuthConfig
26
+ from .models import ClaudeCredentials, ClaudeProfileInfo
27
+ from .storage import ClaudeOAuthStorage
28
+
29
+
30
+ logger = get_plugin_logger()
31
+
32
+
33
+ class ClaudeOAuthProvider(ProfileLoggingMixin):
34
+ """Claude OAuth provider implementation for registry."""
35
+
36
+ def __init__(
37
+ self,
38
+ config: ClaudeOAuthConfig | None = None,
39
+ storage: ClaudeOAuthStorage | None = None,
40
+ http_client: httpx.AsyncClient | None = None,
41
+ hook_manager: Any | None = None,
42
+ detection_service: "CLIDetectionService | None" = None,
43
+ settings: Settings | None = None,
44
+ ):
45
+ """Initialize Claude OAuth provider.
46
+
47
+ Args:
48
+ config: OAuth configuration
49
+ storage: Token storage
50
+ http_client: Optional HTTP client (for request tracing support)
51
+ hook_manager: Optional hook manager for emitting events
52
+ detection_service: Optional CLI detection service for headers
53
+ settings: Optional settings for HTTP client configuration
54
+ """
55
+ self.config = config or ClaudeOAuthConfig()
56
+ self.storage = storage or ClaudeOAuthStorage()
57
+ self.hook_manager = hook_manager
58
+ self.detection_service = detection_service
59
+ self.http_client = http_client
60
+ self.settings = settings
61
+ self._cached_profile: ClaudeProfileInfo | None = (
62
+ None # Cache enhanced profile data for UI display
63
+ )
64
+
65
+ self.client = ClaudeOAuthClient(
66
+ self.config,
67
+ self.storage,
68
+ http_client,
69
+ hook_manager=hook_manager,
70
+ detection_service=detection_service,
71
+ settings=settings,
72
+ )
73
+
74
+ @property
75
+ def provider_name(self) -> str:
76
+ """Internal provider name."""
77
+ return "claude-api"
78
+
79
+ @property
80
+ def provider_display_name(self) -> str:
81
+ """Display name for UI."""
82
+ return "Claude API"
83
+
84
+ @property
85
+ def supports_pkce(self) -> bool:
86
+ """Whether this provider supports PKCE."""
87
+ return self.config.use_pkce
88
+
89
+ @property
90
+ def supports_refresh(self) -> bool:
91
+ """Whether this provider supports token refresh."""
92
+ return True
93
+
94
+ @property
95
+ def requires_client_secret(self) -> bool:
96
+ """Whether this provider requires a client secret."""
97
+ return False # Claude uses PKCE-like flow without client secret
98
+
99
+ async def get_authorization_url(
100
+ self,
101
+ state: str,
102
+ code_verifier: str | None = None,
103
+ redirect_uri: str | None = None,
104
+ ) -> str:
105
+ """Get the authorization URL for OAuth flow.
106
+
107
+ Args:
108
+ state: OAuth state parameter for CSRF protection
109
+ code_verifier: PKCE code verifier (if PKCE is supported)
110
+
111
+ Returns:
112
+ Authorization URL to redirect user to
113
+ """
114
+ # Use provided redirect URI or fall back to config default
115
+ if redirect_uri is None:
116
+ redirect_uri = self.config.get_redirect_uri()
117
+
118
+ params = {
119
+ "code": "true", # Required by Claude OAuth
120
+ "client_id": self.config.client_id,
121
+ "redirect_uri": redirect_uri,
122
+ "response_type": "code",
123
+ "scope": " ".join(self.config.scopes),
124
+ "state": state,
125
+ }
126
+
127
+ # Add PKCE challenge if supported and verifier provided
128
+ if self.config.use_pkce and code_verifier:
129
+ code_challenge = (
130
+ urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
131
+ .decode()
132
+ .rstrip("=")
133
+ )
134
+ params["code_challenge"] = code_challenge
135
+ params["code_challenge_method"] = "S256"
136
+
137
+ auth_url = f"{self.config.authorize_url}?{urlencode(params)}"
138
+
139
+ logger.info(
140
+ "claude_oauth_auth_url_generated",
141
+ state=state,
142
+ has_pkce=bool(code_verifier and self.config.use_pkce),
143
+ category="auth",
144
+ )
145
+
146
+ return auth_url
147
+
148
+ async def handle_callback(
149
+ self,
150
+ code: str,
151
+ state: str,
152
+ code_verifier: str | None = None,
153
+ redirect_uri: str | None = None,
154
+ ) -> Any:
155
+ """Handle OAuth callback and exchange code for tokens.
156
+
157
+ Args:
158
+ code: Authorization code from OAuth callback
159
+ state: State parameter for validation
160
+ code_verifier: PKCE code verifier (if PKCE is used)
161
+ redirect_uri: Redirect URI used in authorization (optional)
162
+
163
+ Returns:
164
+ Claude credentials object
165
+ """
166
+ # Use the client's handle_callback method which includes code exchange
167
+ # If a specific redirect_uri was provided, create a temporary client with that URI
168
+ if redirect_uri and redirect_uri != self.client.redirect_uri:
169
+ # Create temporary config with the specific redirect URI
170
+ temp_config = ClaudeOAuthConfig(
171
+ client_id=self.config.client_id,
172
+ redirect_uri=redirect_uri,
173
+ scopes=self.config.scopes,
174
+ base_url=self.config.base_url,
175
+ authorize_url=self.config.authorize_url,
176
+ token_url=self.config.token_url,
177
+ use_pkce=self.config.use_pkce,
178
+ )
179
+
180
+ # Create temporary client with the correct redirect URI
181
+ temp_client = ClaudeOAuthClient(
182
+ temp_config,
183
+ self.storage,
184
+ self.http_client,
185
+ hook_manager=self.hook_manager,
186
+ detection_service=self.detection_service,
187
+ settings=self.settings,
188
+ )
189
+
190
+ credentials = await temp_client.handle_callback(
191
+ code, state, code_verifier or ""
192
+ )
193
+ else:
194
+ # Use the regular client
195
+ credentials = await self.client.handle_callback(
196
+ code, state, code_verifier or ""
197
+ )
198
+
199
+ # The client already saves to storage if available, but we can save again
200
+ # to our specific storage if needed
201
+ if self.storage:
202
+ await self.storage.save(credentials)
203
+
204
+ logger.info(
205
+ "claude_oauth_callback_handled",
206
+ state=state,
207
+ has_credentials=bool(credentials),
208
+ category="auth",
209
+ )
210
+
211
+ return credentials
212
+
213
+ async def refresh_access_token(self, refresh_token: str) -> Any:
214
+ """Refresh access token using refresh token.
215
+
216
+ Args:
217
+ refresh_token: Refresh token from previous auth
218
+
219
+ Returns:
220
+ New token response
221
+ """
222
+ credentials = await self.client.refresh_token(refresh_token)
223
+
224
+ # Store updated credentials
225
+ if self.storage:
226
+ await self.storage.save(credentials)
227
+
228
+ logger.info("claude_oauth_token_refreshed", category="auth")
229
+
230
+ return credentials
231
+
232
+ async def revoke_token(self, token: str) -> None:
233
+ """Revoke an access or refresh token.
234
+
235
+ Args:
236
+ token: Token to revoke
237
+ """
238
+ # Claude doesn't have a revoke endpoint, so we just delete stored credentials
239
+ if self.storage:
240
+ await self.storage.delete()
241
+
242
+ logger.info("claude_oauth_token_revoked_locally", category="auth")
243
+
244
+ def get_provider_info(self) -> OAuthProviderInfo:
245
+ """Get provider information for discovery.
246
+
247
+ Returns:
248
+ Provider information
249
+ """
250
+ return OAuthProviderInfo(
251
+ name=self.provider_name,
252
+ display_name=self.provider_display_name,
253
+ description="OAuth authentication for Claude AI",
254
+ supports_pkce=self.supports_pkce,
255
+ scopes=self.config.scopes,
256
+ is_available=True,
257
+ plugin_name="oauth_claude",
258
+ )
259
+
260
+ async def validate_token(self, access_token: str) -> bool:
261
+ """Validate an access token.
262
+
263
+ Args:
264
+ access_token: Token to validate
265
+
266
+ Returns:
267
+ True if token is valid
268
+ """
269
+ # Claude doesn't have a validation endpoint, so we check if stored token matches
270
+ if self.storage:
271
+ credentials = await self.storage.load()
272
+ if credentials and credentials.claude_ai_oauth:
273
+ stored_token = (
274
+ credentials.claude_ai_oauth.access_token.get_secret_value()
275
+ )
276
+ return stored_token == access_token
277
+ return False
278
+
279
+ async def get_user_info(self, access_token: str) -> dict[str, Any] | None:
280
+ """Get user information using access token.
281
+
282
+ Args:
283
+ access_token: Valid access token
284
+
285
+ Returns:
286
+ User information or None
287
+ """
288
+ # Load stored credentials which contain user info
289
+ if self.storage:
290
+ credentials = await self.storage.load()
291
+ if credentials and credentials.claude_ai_oauth:
292
+ return {
293
+ "subscription_type": credentials.claude_ai_oauth.subscription_type,
294
+ "scopes": credentials.claude_ai_oauth.scopes,
295
+ }
296
+ return None
297
+
298
+ def get_storage(self) -> Any:
299
+ """Get storage implementation for this provider.
300
+
301
+ Returns:
302
+ Storage implementation
303
+ """
304
+ return self.storage
305
+
306
+ def get_config(self) -> Any:
307
+ """Get configuration for this provider.
308
+
309
+ Returns:
310
+ Configuration implementation
311
+ """
312
+ return self.config
313
+
314
+ async def save_credentials(
315
+ self, credentials: Any, custom_path: Any | None = None
316
+ ) -> bool:
317
+ """Save credentials using provider's storage mechanism.
318
+
319
+ Args:
320
+ credentials: Claude credentials object
321
+ custom_path: Optional custom storage path (Path object)
322
+
323
+ Returns:
324
+ True if saved successfully, False otherwise
325
+ """
326
+ try:
327
+ if custom_path:
328
+ # Use custom path for storage
329
+ storage = GenericJsonStorage(Path(custom_path), ClaudeCredentials)
330
+ manager = await self.create_token_manager(storage=storage)
331
+ else:
332
+ # Use default storage
333
+ manager = await self.create_token_manager()
334
+
335
+ return await manager.save_credentials(credentials)
336
+ except Exception as e:
337
+ logger.error(
338
+ "Failed to save Claude credentials",
339
+ error=str(e),
340
+ exc_info=e,
341
+ has_custom_path=bool(custom_path),
342
+ )
343
+ return False
344
+
345
+ async def load_credentials(self, custom_path: Any | None = None) -> Any | None:
346
+ """Load credentials from provider's storage.
347
+
348
+ Args:
349
+ custom_path: Optional custom storage path (Path object)
350
+
351
+ Returns:
352
+ Credentials if found, None otherwise
353
+ """
354
+ try:
355
+ if custom_path:
356
+ # Load from custom path
357
+ storage = GenericJsonStorage(Path(custom_path), ClaudeCredentials)
358
+ manager = await self.create_token_manager(storage=storage)
359
+ else:
360
+ # Load from default storage
361
+ manager = await self.create_token_manager()
362
+
363
+ credentials = await manager.load_credentials()
364
+
365
+ # Use standardized profile logging with rich Claude profile data
366
+ if credentials:
367
+ profile = await manager.get_profile()
368
+ if profile:
369
+ # Cache profile for UI display
370
+ self._cached_profile = profile
371
+ # Create enhanced standardized profile with rich Claude data
372
+ standard_profile = self._create_enhanced_profile(
373
+ credentials, profile
374
+ )
375
+ self._log_profile_dump("claude", standard_profile)
376
+
377
+ return credentials
378
+ except Exception as e:
379
+ logger.error(
380
+ "Failed to load Claude credentials",
381
+ error=str(e),
382
+ exc_info=e,
383
+ has_custom_path=bool(custom_path),
384
+ )
385
+ return None
386
+
387
+ async def create_token_manager(
388
+ self, storage: Any | None = None
389
+ ) -> "ClaudeApiTokenManager":
390
+ """Create token manager with proper dependency injection.
391
+
392
+ Provided to allow core/CLI code to obtain a manager without
393
+ importing plugin classes directly.
394
+ """
395
+ from .manager import ClaudeApiTokenManager
396
+
397
+ return await ClaudeApiTokenManager.create(
398
+ storage=storage,
399
+ http_client=self.http_client,
400
+ oauth_provider=self, # Inject self as protocol
401
+ )
402
+
403
+ def _extract_standard_profile(
404
+ self, credentials: ClaudeCredentials
405
+ ) -> StandardProfileFields:
406
+ """Extract standardized profile fields from Claude credentials for UI display.
407
+
408
+ Args:
409
+ credentials: Claude credentials with profile information
410
+
411
+ Returns:
412
+ StandardProfileFields with clean, UI-friendly data
413
+ """
414
+ # Use cached enhanced profile data if available
415
+ if self._cached_profile:
416
+ return self._create_enhanced_profile(credentials, self._cached_profile)
417
+
418
+ # Fallback to basic credential info
419
+ from typing import Any
420
+
421
+ profile_data: dict[str, Any] = {
422
+ "account_id": getattr(credentials, "account_id", "unknown"),
423
+ "provider_type": "claude-api",
424
+ "active": getattr(credentials, "active", True),
425
+ "expired": False, # Claude handles expiration internally
426
+ "has_refresh_token": bool(getattr(credentials, "refresh_token", None)),
427
+ }
428
+
429
+ # Store raw credential data for debugging
430
+ raw_data = {}
431
+ if hasattr(credentials, "model_dump"):
432
+ raw_data["credentials"] = credentials.model_dump()
433
+
434
+ profile_data["raw_profile_data"] = raw_data
435
+
436
+ return StandardProfileFields(**profile_data)
437
+
438
+ def _create_enhanced_profile(
439
+ self, credentials: ClaudeCredentials, profile: Any
440
+ ) -> StandardProfileFields:
441
+ """Create enhanced standardized profile with rich Claude profile data.
442
+
443
+ Args:
444
+ credentials: Claude credentials
445
+ profile: Rich profile data from manager
446
+
447
+ Returns:
448
+ StandardProfileFields with full Claude profile information
449
+ """
450
+ # Create basic profile data without recursion
451
+ basic_profile_data: dict[str, Any] = {
452
+ "account_id": getattr(credentials, "account_id", "unknown"),
453
+ "provider_type": "claude-api",
454
+ "active": getattr(credentials, "active", True),
455
+ "expired": False, # Claude handles expiration internally
456
+ "has_refresh_token": bool(getattr(credentials, "refresh_token", None)),
457
+ "raw_profile_data": {},
458
+ }
459
+
460
+ # Extract profile data
461
+ profile_dict = (
462
+ profile.model_dump()
463
+ if hasattr(profile, "model_dump")
464
+ else {"profile": str(profile)}
465
+ )
466
+
467
+ # Map Claude profile fields to standard fields
468
+ updates = {}
469
+
470
+ if profile_dict.get("account_id"):
471
+ updates["account_id"] = profile_dict["account_id"]
472
+
473
+ if profile_dict.get("email"):
474
+ updates["email"] = profile_dict["email"]
475
+
476
+ if profile_dict.get("display_name"):
477
+ updates["display_name"] = profile_dict["display_name"]
478
+
479
+ # Extract subscription information from extras
480
+ extras = profile_dict.get("extras", {})
481
+ if isinstance(extras, dict):
482
+ account = extras.get("account", {})
483
+ if isinstance(account, dict):
484
+ # Map Claude subscription types
485
+ if account.get("has_claude_max"):
486
+ updates.update(
487
+ {
488
+ "subscription_type": "max",
489
+ "subscription_status": "active",
490
+ }
491
+ )
492
+ elif account.get("has_claude_pro"):
493
+ updates.update(
494
+ {
495
+ "subscription_type": "pro",
496
+ "subscription_status": "active",
497
+ }
498
+ )
499
+
500
+ # Features
501
+ updates["features"] = {
502
+ "claude_max": account.get("has_claude_max", False),
503
+ "claude_pro": account.get("has_claude_pro", False),
504
+ }
505
+
506
+ # Organization info
507
+ org = extras.get("organization", {})
508
+ if isinstance(org, dict):
509
+ updates.update(
510
+ {
511
+ "organization_name": org.get("name"),
512
+ "organization_role": "member", # Claude doesn't provide role details
513
+ }
514
+ )
515
+
516
+ # Store full profile data in raw data (start from basic profile data)
517
+ from typing import cast
518
+
519
+ base_raw = cast(dict[str, Any], basic_profile_data.get("raw_profile_data", {}))
520
+ raw_data = dict(base_raw)
521
+ raw_data["full_profile"] = profile_dict
522
+ updates["raw_profile_data"] = raw_data
523
+
524
+ # Create new profile with updates starting from basic profile data
525
+ profile_data = dict(basic_profile_data)
526
+ profile_data.update(updates)
527
+
528
+ return StandardProfileFields(**profile_data)
529
+
530
+ async def exchange_manual_code(self, code: str) -> Any:
531
+ """Exchange manual authorization code for tokens.
532
+
533
+ Args:
534
+ code: Authorization code from manual entry
535
+
536
+ Returns:
537
+ Claude credentials object
538
+ """
539
+ # For manual code flow, use OOB redirect URI and no state validation
540
+ credentials: ClaudeCredentials = await self.client.handle_callback(
541
+ code, "manual", ""
542
+ )
543
+
544
+ if self.storage:
545
+ await self.storage.save(credentials)
546
+
547
+ logger.info(
548
+ "claude_oauth_manual_code_exchanged",
549
+ has_credentials=bool(credentials),
550
+ category="auth",
551
+ )
552
+
553
+ return credentials
554
+
555
+ @property
556
+ def cli(self) -> CliAuthConfig:
557
+ """Get CLI authentication configuration for this provider."""
558
+ return CliAuthConfig(
559
+ preferred_flow=FlowType.browser,
560
+ callback_port=54545,
561
+ callback_path="/callback",
562
+ supports_manual_code=True,
563
+ supports_device_flow=False,
564
+ fixed_redirect_uri=None,
565
+ manual_redirect_uri="https://console.anthropic.com/oauth/code/callback",
566
+ )
567
+
568
+ async def cleanup(self) -> None:
569
+ """Cleanup resources."""
570
+ if self.client:
571
+ await self.client.close()