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,482 @@
1
+ """Claude API token manager implementation for the Claude API plugin."""
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import TYPE_CHECKING, Protocol, cast
5
+
6
+ import httpx
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ pass
11
+
12
+ from ccproxy.auth.managers.base_enhanced import EnhancedTokenManager
13
+ from ccproxy.auth.managers.token_snapshot import TokenSnapshot
14
+ from ccproxy.auth.storage.base import TokenStorage
15
+ from ccproxy.core.logging import get_plugin_logger
16
+
17
+ from .config import ClaudeOAuthConfig
18
+ from .models import ClaudeCredentials, ClaudeProfileInfo, ClaudeTokenWrapper
19
+ from .storage import ClaudeOAuthStorage, ClaudeProfileStorage
20
+
21
+
22
+ class TokenRefreshProvider(Protocol):
23
+ """Protocol for token refresh capability."""
24
+
25
+ async def refresh_access_token(self, refresh_token: str) -> ClaudeCredentials:
26
+ """Refresh access token using refresh token."""
27
+ ...
28
+
29
+
30
+ logger = get_plugin_logger()
31
+
32
+
33
+ class ClaudeApiTokenManager(EnhancedTokenManager[ClaudeCredentials]):
34
+ """Manager for Claude API token storage and refresh operations.
35
+
36
+ Uses the Claude-specific storage implementation with enhanced token management.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ storage: TokenStorage[ClaudeCredentials] | None = None,
42
+ http_client: "httpx.AsyncClient | None" = None,
43
+ oauth_provider: TokenRefreshProvider | None = None,
44
+ ):
45
+ """Initialize Claude API token manager.
46
+
47
+ Args:
48
+ storage: Optional custom storage, defaults to standard location
49
+ http_client: Optional HTTP client for API requests
50
+ oauth_provider: Optional OAuth provider for token refresh (protocol injection)
51
+ """
52
+ if storage is None:
53
+ storage = ClaudeOAuthStorage()
54
+ super().__init__(storage)
55
+ self._profile_cache: ClaudeProfileInfo | None = None
56
+ self.oauth_provider = oauth_provider
57
+
58
+ # Create default HTTP client if not provided; track ownership
59
+ self._owns_client = False
60
+ if http_client is None:
61
+ http_client = httpx.AsyncClient()
62
+ self._owns_client = True
63
+ self.http_client = http_client
64
+
65
+ # ==================== Internal helpers ====================
66
+
67
+ def _derive_subscription_type(self, profile: "ClaudeProfileInfo") -> str:
68
+ """Derive subscription type string from profile info.
69
+
70
+ Priority: "max" > "pro" > "free".
71
+ """
72
+ try:
73
+ if getattr(profile, "has_claude_max", None):
74
+ return "max"
75
+ if getattr(profile, "has_claude_pro", None):
76
+ return "pro"
77
+ return "free"
78
+ except Exception:
79
+ # Be defensive; default to free if unexpected structure
80
+ return "free"
81
+
82
+ async def _sync_subscription_type_with_profile(
83
+ self,
84
+ profile: "ClaudeProfileInfo",
85
+ credentials: "ClaudeCredentials | None" = None,
86
+ ) -> None:
87
+ """Update stored credentials with subscription type from profile.
88
+
89
+ Avoids unnecessary writes by only saving when the value changes.
90
+ If credentials are not provided, they will be loaded once.
91
+ """
92
+ try:
93
+ new_sub = self._derive_subscription_type(profile)
94
+
95
+ # Use provided credentials to avoid an extra read if available
96
+ creds = credentials or await self.load_credentials()
97
+ if not creds or not hasattr(creds, "claude_ai_oauth"):
98
+ return
99
+
100
+ current_sub = creds.claude_ai_oauth.subscription_type
101
+ if current_sub != new_sub:
102
+ creds.claude_ai_oauth.subscription_type = new_sub
103
+ await self.save_credentials(creds)
104
+ logger.info(
105
+ "claude_subscription_type_updated",
106
+ subscription_type=new_sub,
107
+ category="auth",
108
+ )
109
+ except Exception as e:
110
+ # Non-fatal: syncing subscription type should never break profile flow
111
+ logger.debug(
112
+ "claude_subscription_type_update_failed",
113
+ error=str(e),
114
+ category="auth",
115
+ )
116
+
117
+ @classmethod
118
+ async def create(
119
+ cls,
120
+ storage: TokenStorage["ClaudeCredentials"] | None = None,
121
+ http_client: "httpx.AsyncClient | None" = None,
122
+ oauth_provider: TokenRefreshProvider | None = None,
123
+ ) -> "ClaudeApiTokenManager":
124
+ """Async factory that constructs the manager and preloads cached profile.
125
+
126
+ This avoids creating event loops in __init__ and keeps initialization non-blocking.
127
+ """
128
+ manager = cls(
129
+ storage=storage, http_client=http_client, oauth_provider=oauth_provider
130
+ )
131
+ await manager.preload_profile_cache()
132
+ return manager
133
+
134
+ def _build_token_snapshot(self, credentials: ClaudeCredentials) -> TokenSnapshot:
135
+ """Construct a token snapshot for Claude credentials."""
136
+ wrapper = ClaudeTokenWrapper(credentials=credentials)
137
+ scopes = tuple(wrapper.scopes)
138
+ extras = {
139
+ "subscription_type": wrapper.subscription_type,
140
+ }
141
+ return TokenSnapshot(
142
+ provider="claude-api",
143
+ access_token=str(wrapper.access_token_value),
144
+ refresh_token=wrapper.refresh_token_value,
145
+ expires_at=wrapper.expires_at_datetime,
146
+ scopes=scopes,
147
+ extras=extras,
148
+ )
149
+
150
+ async def preload_profile_cache(self) -> None:
151
+ """Load profile from storage asynchronously if available."""
152
+ try:
153
+ profile_storage = ClaudeProfileStorage()
154
+
155
+ # Only attempt to read if the file exists
156
+ if profile_storage.file_path.exists():
157
+ profile = await profile_storage.load_profile()
158
+ if profile:
159
+ self._profile_cache = profile
160
+ logger.debug(
161
+ "claude_profile_loaded_from_cache",
162
+ account_id=profile.account_id,
163
+ email=profile.email,
164
+ category="auth",
165
+ )
166
+ except Exception as e:
167
+ # Don't fail if profile can't be loaded
168
+ logger.debug(
169
+ "claude_profile_cache_load_failed",
170
+ error=str(e),
171
+ category="auth",
172
+ )
173
+
174
+ # ==================== Enhanced Token Management Methods ====================
175
+
176
+ async def get_access_token(self) -> str:
177
+ """Get access token using enhanced base with automatic refresh."""
178
+ token = await self.get_access_token_with_refresh()
179
+ if not token:
180
+ from ccproxy.auth.exceptions import CredentialsInvalidError
181
+
182
+ raise CredentialsInvalidError("No valid access token available")
183
+ return token
184
+
185
+ async def refresh_token_if_needed(self) -> ClaudeCredentials | None:
186
+ """Use enhanced base's automatic refresh capability."""
187
+ if await self.ensure_valid_token():
188
+ return await self.load_credentials()
189
+ return None
190
+
191
+ # ==================== Abstract Method Implementations ====================
192
+
193
+ async def refresh_token(self) -> ClaudeCredentials | None:
194
+ """Refresh the access token using the refresh token.
195
+
196
+ Returns:
197
+ Updated credentials or None if refresh failed
198
+ """
199
+ # Load current credentials and extract refresh token
200
+ credentials = await self.load_credentials()
201
+ if not credentials:
202
+ logger.error("no_credentials_to_refresh", category="auth")
203
+ return None
204
+
205
+ wrapper = ClaudeTokenWrapper(credentials=credentials)
206
+ refresh_token = wrapper.refresh_token_value
207
+ if not refresh_token:
208
+ logger.error("no_refresh_token_available", category="auth")
209
+ return None
210
+
211
+ try:
212
+ # Use injected provider or fallback to local import
213
+ new_credentials: ClaudeCredentials
214
+ if self.oauth_provider:
215
+ new_credentials = await self.oauth_provider.refresh_access_token(
216
+ refresh_token
217
+ )
218
+ else:
219
+ # Fallback to local import if no provider injected
220
+ from .provider import ClaudeOAuthProvider
221
+
222
+ provider = ClaudeOAuthProvider(http_client=self.http_client)
223
+ new_credentials = await provider.refresh_access_token(refresh_token)
224
+
225
+ # Save updated credentials
226
+ if await self.save_credentials(new_credentials):
227
+ logger.info("token_refreshed_successfully", category="auth")
228
+ # Clear profile cache as token changed
229
+ self._profile_cache = None
230
+
231
+ return new_credentials
232
+
233
+ logger.error("failed_to_save_refreshed_credentials", category="auth")
234
+ return None
235
+
236
+ except Exception as e:
237
+ logger.error(
238
+ "Token refresh failed",
239
+ error=str(e),
240
+ exc_info=e,
241
+ category="auth",
242
+ )
243
+ return None
244
+
245
+ def is_expired(self, credentials: ClaudeCredentials) -> bool:
246
+ """Check if credentials are expired using wrapper."""
247
+ if isinstance(credentials, ClaudeCredentials):
248
+ wrapper = ClaudeTokenWrapper(credentials=credentials)
249
+ return bool(wrapper.is_expired)
250
+
251
+ expires_at = getattr(credentials, "expires_at", None)
252
+ if expires_at is None:
253
+ expires_at = getattr(credentials, "claude_ai_oauth", None)
254
+ if expires_at is not None:
255
+ expires_at = getattr(expires_at, "expires_at", None)
256
+
257
+ if expires_at is None:
258
+ return False
259
+
260
+ if isinstance(expires_at, datetime):
261
+ return expires_at <= datetime.now(UTC)
262
+ if isinstance(expires_at, int | float):
263
+ return datetime.fromtimestamp(expires_at / 1000, tz=UTC) <= datetime.now(
264
+ UTC
265
+ )
266
+
267
+ return False
268
+
269
+ # ==================== Targeted overrides ====================
270
+
271
+ async def load_credentials(self) -> ClaudeCredentials | None:
272
+ """Load credentials and backfill subscription_type from profile if missing.
273
+
274
+ Avoids network calls; uses cached profile or local ~/.claude/.account.json
275
+ and writes back only when the field actually changes.
276
+ """
277
+ creds = await super().load_credentials()
278
+ if not creds or not hasattr(creds, "claude_ai_oauth"):
279
+ return creds
280
+
281
+ sub = creds.claude_ai_oauth.subscription_type
282
+ if sub is None or str(sub).strip().lower() in {"", "unknown"}:
283
+ # Try cached profile first to avoid an extra file read
284
+ profile: ClaudeProfileInfo | None = self._profile_cache
285
+ if profile is None:
286
+ # Only read from disk if the profile file exists; no API calls here
287
+ try:
288
+ profile_storage = ClaudeProfileStorage()
289
+ if profile_storage.file_path.exists():
290
+ profile = await profile_storage.load_profile()
291
+ if profile:
292
+ self._profile_cache = profile
293
+ except Exception:
294
+ profile = None
295
+
296
+ if profile is not None:
297
+ try:
298
+ new_sub = self._derive_subscription_type(profile)
299
+ if new_sub != sub:
300
+ creds.claude_ai_oauth.subscription_type = new_sub
301
+ await self.save_credentials(creds)
302
+ logger.info(
303
+ "claude_subscription_type_backfilled_on_load",
304
+ subscription_type=new_sub,
305
+ category="auth",
306
+ )
307
+ except Exception as e:
308
+ logger.debug(
309
+ "claude_subscription_type_backfill_failed",
310
+ error=str(e),
311
+ category="auth",
312
+ )
313
+
314
+ return creds
315
+
316
+ def get_account_id(self, credentials: ClaudeCredentials) -> str | None:
317
+ """Get account ID from credentials.
318
+
319
+ Claude doesn't store account_id in tokens, would need
320
+ to fetch from profile API.
321
+ """
322
+ if self._profile_cache:
323
+ return self._profile_cache.account_id
324
+ return None
325
+
326
+ # ==================== Claude-Specific Methods ====================
327
+
328
+ def get_expiration_time(self, credentials: ClaudeCredentials) -> datetime | None:
329
+ """Get expiration time as datetime."""
330
+ wrapper = ClaudeTokenWrapper(credentials=credentials)
331
+ return wrapper.expires_at_datetime
332
+
333
+ async def get_profile_quick(self) -> ClaudeProfileInfo | None:
334
+ """Return cached profile info only, avoiding I/O or network.
335
+
336
+ Profile cache is typically preloaded from local storage by
337
+ the async factory create() via preload_profile_cache().
338
+
339
+ Returns:
340
+ Cached ClaudeProfileInfo or None
341
+ """
342
+ return self._profile_cache
343
+
344
+ async def get_access_token_value(self) -> str | None:
345
+ """Get the actual access token value.
346
+
347
+ Returns:
348
+ Access token string if available, None otherwise
349
+ """
350
+ credentials = await self.load_credentials()
351
+ if not credentials:
352
+ return None
353
+
354
+ if self.is_expired(credentials):
355
+ return None
356
+
357
+ wrapper = ClaudeTokenWrapper(credentials=credentials)
358
+ return cast(str, wrapper.access_token_value)
359
+
360
+ async def get_profile(self) -> ClaudeProfileInfo | None:
361
+ """Get user profile from cache or API.
362
+
363
+ Returns:
364
+ ClaudeProfileInfo or None if not authenticated
365
+ """
366
+ if self._profile_cache:
367
+ return self._profile_cache
368
+
369
+ # Try to load from .account.json first
370
+
371
+ profile_storage = ClaudeProfileStorage()
372
+ profile = await profile_storage.load_profile()
373
+ if profile:
374
+ self._profile_cache = profile
375
+ # Best-effort sync of subscription type from cached profile
376
+ await self._sync_subscription_type_with_profile(profile)
377
+ return profile
378
+
379
+ # If not in storage, fetch from API
380
+ credentials = await self.load_credentials()
381
+ if not credentials or self.is_expired(credentials):
382
+ return None
383
+
384
+ # Get access token
385
+ wrapper = ClaudeTokenWrapper(credentials=credentials)
386
+ access_token = cast(str, wrapper.access_token_value)
387
+ if not access_token:
388
+ return None
389
+
390
+ # Fetch profile from API and save
391
+ try:
392
+ config = ClaudeOAuthConfig()
393
+
394
+ headers = {
395
+ "Authorization": f"Bearer {access_token}",
396
+ "Content-Type": "application/json",
397
+ }
398
+ # Optionally add detection headers if client supports it
399
+ try:
400
+ # Use injected provider or fallback to local import
401
+ if self.oauth_provider and hasattr(self.oauth_provider, "client"):
402
+ if hasattr(self.oauth_provider.client, "get_custom_headers"):
403
+ headers.update(self.oauth_provider.client.get_custom_headers())
404
+ else:
405
+ # Fallback to local import if no provider injected
406
+ from .provider import ClaudeOAuthProvider
407
+
408
+ temp_provider = ClaudeOAuthProvider(http_client=self.http_client)
409
+ if hasattr(temp_provider, "client") and hasattr(
410
+ temp_provider.client, "get_custom_headers"
411
+ ):
412
+ headers.update(temp_provider.client.get_custom_headers())
413
+ except Exception:
414
+ pass
415
+
416
+ # Debug logging for HTTP client usage
417
+ logger.debug(
418
+ "claude_manager_making_http_request",
419
+ url=config.profile_url,
420
+ http_client_id=id(self.http_client),
421
+ has_hooks=hasattr(self.http_client, "hook_manager")
422
+ and self.http_client.hook_manager is not None,
423
+ hook_manager_id=id(self.http_client.hook_manager)
424
+ if hasattr(self.http_client, "hook_manager")
425
+ and self.http_client.hook_manager
426
+ else None,
427
+ )
428
+
429
+ # Use the injected HTTP client
430
+ response = await self.http_client.get(
431
+ config.profile_url,
432
+ headers=headers,
433
+ timeout=30.0,
434
+ )
435
+ response.raise_for_status()
436
+
437
+ profile_data = response.json()
438
+
439
+ # Save to .account.json
440
+ await profile_storage.save_profile(profile_data)
441
+
442
+ # Parse and cache
443
+ profile = ClaudeProfileInfo.from_api_response(profile_data)
444
+ self._profile_cache = profile
445
+
446
+ # Sync subscription type to credentials in a single write if changed
447
+ await self._sync_subscription_type_with_profile(
448
+ profile, credentials=credentials
449
+ )
450
+
451
+ logger.info(
452
+ "claude_profile_fetched_from_api",
453
+ account_id=profile.account_id,
454
+ email=profile.email,
455
+ category="auth",
456
+ )
457
+
458
+ return profile
459
+
460
+ except Exception as e:
461
+ if isinstance(e, httpx.HTTPStatusError):
462
+ logger.error(
463
+ "claude_profile_api_error",
464
+ status_code=e.response.status_code,
465
+ error=str(e),
466
+ exc_info=e,
467
+ category="auth",
468
+ )
469
+ else:
470
+ logger.error(
471
+ "claude_profile_fetch_error",
472
+ error=str(e),
473
+ error_type=type(e).__name__,
474
+ exc_info=e,
475
+ category="auth",
476
+ )
477
+ return None
478
+
479
+ async def close(self) -> None:
480
+ """Close the HTTP client if it was created internally."""
481
+ if getattr(self, "_owns_client", False) and self.http_client:
482
+ await self.http_client.aclose()