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
@@ -0,0 +1,52 @@
1
+ """Claude API plugin configuration."""
2
+
3
+ from pydantic import Field
4
+
5
+ from ccproxy.models.provider import ModelCard, ModelMappingRule, ProviderConfig
6
+ from ccproxy.plugins.claude_shared.model_defaults import (
7
+ DEFAULT_CLAUDE_MODEL_CARDS,
8
+ DEFAULT_CLAUDE_MODEL_MAPPINGS,
9
+ )
10
+
11
+
12
+ class ClaudeAPISettings(ProviderConfig):
13
+ """Claude API specific configuration.
14
+
15
+ This configuration extends the base ProviderConfig to include
16
+ Claude API specific settings like API endpoint and model support.
17
+ """
18
+
19
+ # Base configuration from ProviderConfig
20
+ name: str = "claude-api"
21
+ base_url: str = "https://api.anthropic.com"
22
+ supports_streaming: bool = True
23
+ requires_auth: bool = True
24
+ auth_type: str = "oauth"
25
+
26
+ # Claude API specific settings
27
+ enabled: bool = True
28
+ priority: int = 5 # Higher priority than SDK-based approach
29
+ default_max_tokens: int = 4096
30
+
31
+ model_mappings: list[ModelMappingRule] = Field(
32
+ default_factory=lambda: [
33
+ rule.model_copy(deep=True) for rule in DEFAULT_CLAUDE_MODEL_MAPPINGS
34
+ ]
35
+ )
36
+ models_endpoint: list[ModelCard] = Field(
37
+ default_factory=lambda: [
38
+ card.model_copy(deep=True) for card in DEFAULT_CLAUDE_MODEL_CARDS
39
+ ]
40
+ )
41
+
42
+ # Feature flags
43
+ include_sdk_content_as_xml: bool = False
44
+ support_openai_format: bool = True # Support both Anthropic and OpenAI formats
45
+
46
+ # System prompt injection mode
47
+ system_prompt_injection_mode: str = "minimal" # "none", "minimal", or "full"
48
+
49
+ # NEW: Auth manager override support
50
+ auth_manager: str | None = (
51
+ None # Override auth manager name (e.g., 'oauth_claude_lb' for load balancing)
52
+ )
@@ -0,0 +1,461 @@
1
+ """Claude API plugin detection service using centralized detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import socket
9
+ from contextlib import suppress
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from fastapi import FastAPI, Request, Response
14
+
15
+ from ccproxy.config.settings import Settings
16
+ from ccproxy.config.utils import get_ccproxy_cache_dir
17
+ from ccproxy.core.logging import get_plugin_logger
18
+ from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
19
+ from ccproxy.services.cli_detection import CLIDetectionService
20
+ from ccproxy.utils.caching import async_ttl_cache
21
+ from ccproxy.utils.headers import extract_request_headers
22
+
23
+ from .models import ClaudeCacheData
24
+
25
+
26
+ logger = get_plugin_logger()
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from .models import ClaudeCliInfo
31
+
32
+
33
+ class ClaudeAPIDetectionService:
34
+ """Claude API plugin detection service for automatically detecting Claude CLI headers."""
35
+
36
+ # Headers to ignore at injection time (lowercase). Cache keeps keys (possibly empty) to preserve order.
37
+ ignores_header: list[str] = [
38
+ # Common excludes
39
+ "host",
40
+ "content-length",
41
+ "authorization",
42
+ "x-api-key",
43
+ ]
44
+
45
+ redact_headers: list[str] = [
46
+ "x-api-key",
47
+ "authorization",
48
+ ]
49
+
50
+ def __init__(
51
+ self,
52
+ settings: Settings,
53
+ cli_service: CLIDetectionService | None = None,
54
+ redact_sensitive_cache: bool = True,
55
+ ) -> None:
56
+ """Initialize Claude detection service.
57
+
58
+ Args:
59
+ settings: Application settings
60
+ cli_service: Optional CLIDetectionService instance for dependency injection.
61
+ If None, creates a new instance for backward compatibility.
62
+ """
63
+ self.settings = settings
64
+ self.cache_dir = get_ccproxy_cache_dir()
65
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
66
+ self._cached_data: ClaudeCacheData | None = None
67
+ self._cli_service = cli_service or CLIDetectionService(settings)
68
+ self._cli_info: ClaudeCliInfo | None = None
69
+ self._redact_sensitive_cache = redact_sensitive_cache
70
+
71
+ async def initialize_detection(self) -> ClaudeCacheData:
72
+ """Initialize Claude detection at startup."""
73
+ try:
74
+ # Get current Claude version
75
+ current_version = await self._get_claude_version()
76
+
77
+ # Try to load from cache first
78
+ cached = False
79
+ try:
80
+ detected_data = self._load_from_cache(current_version)
81
+ cached = detected_data is not None
82
+
83
+ except Exception as e:
84
+ logger.warning(
85
+ "invalid_cache_file",
86
+ error=str(e),
87
+ category="plugin",
88
+ exc_info=e,
89
+ )
90
+
91
+ if not cached:
92
+ # No cache or version changed - detect fresh
93
+ detected_data = await self._detect_claude_headers(current_version)
94
+ # Cache the results
95
+ self._save_to_cache(detected_data)
96
+
97
+ self._cached_data = detected_data
98
+
99
+ logger.trace(
100
+ "detection_headers_completed",
101
+ version=current_version,
102
+ cached=cached,
103
+ )
104
+
105
+ if detected_data is None:
106
+ raise ValueError("Claude detection failed")
107
+ return detected_data
108
+
109
+ except Exception as e:
110
+ logger.warning(
111
+ "detection_claude_headers_failed",
112
+ fallback=True,
113
+ error=e,
114
+ category="plugin",
115
+ )
116
+ # Return fallback data
117
+ fallback_data = self._get_fallback_data()
118
+ self._cached_data = fallback_data
119
+ return fallback_data
120
+
121
+ def get_cached_data(self) -> ClaudeCacheData | None:
122
+ """Get currently cached detection data."""
123
+ return self._cached_data
124
+
125
+ def get_detected_headers(self) -> DetectedHeaders:
126
+ """Return cached headers as structured data."""
127
+
128
+ data = self.get_cached_data()
129
+ if not data:
130
+ return DetectedHeaders()
131
+ return data.headers
132
+
133
+ def get_detected_prompts(self) -> DetectedPrompts:
134
+ """Return cached prompt metadata as structured data."""
135
+
136
+ data = self.get_cached_data()
137
+ if not data:
138
+ return DetectedPrompts()
139
+ return data.prompts
140
+
141
+ def get_ignored_headers(self) -> list[str]:
142
+ """Headers that should be ignored when injecting CLI values."""
143
+
144
+ return list(self.ignores_header)
145
+
146
+ def get_redacted_headers(self) -> list[str]:
147
+ """Headers that must never be forwarded from detection cache."""
148
+
149
+ return list(self.redact_headers)
150
+
151
+ def get_cli_health_info(self) -> ClaudeCliInfo:
152
+ """Get lightweight CLI health info using centralized detection, cached locally.
153
+
154
+ Returns:
155
+ ClaudeCliInfo with availability, version, and binary path
156
+ """
157
+ from .models import ClaudeCliInfo, ClaudeCliStatus
158
+
159
+ if self._cli_info is not None:
160
+ return self._cli_info
161
+
162
+ info = self._cli_service.get_cli_info("claude")
163
+ status = (
164
+ ClaudeCliStatus.AVAILABLE
165
+ if info["is_available"]
166
+ else ClaudeCliStatus.NOT_INSTALLED
167
+ )
168
+ cli_info = ClaudeCliInfo(
169
+ status=status,
170
+ version=info.get("version"),
171
+ binary_path=info.get("path"),
172
+ )
173
+ self._cli_info = cli_info
174
+ return cli_info
175
+
176
+ def get_version(self) -> str | None:
177
+ """Get the detected Claude CLI version."""
178
+ if self._cached_data:
179
+ return self._cached_data.claude_version
180
+ return None
181
+
182
+ def get_cli_path(self) -> list[str] | None:
183
+ """Get the Claude CLI command with caching.
184
+
185
+ Returns:
186
+ Command list to execute Claude CLI if found, None otherwise
187
+ """
188
+ info = self._cli_service.get_cli_info("claude")
189
+ return info["command"] if info["is_available"] else None
190
+
191
+ def get_binary_path(self) -> list[str] | None:
192
+ """Alias for get_cli_path for consistency with Codex."""
193
+ return self.get_cli_path()
194
+
195
+ @async_ttl_cache(maxsize=16, ttl=900.0) # 15 minute cache for version
196
+ async def _get_claude_version(self) -> str:
197
+ """Get Claude CLI version with caching."""
198
+ try:
199
+ # Use centralized CLI detection
200
+ result = await self._cli_service.detect_cli(
201
+ binary_name="claude",
202
+ package_name="@anthropic-ai/claude-code",
203
+ version_flag="--version",
204
+ cache_key="claude_api_version",
205
+ )
206
+
207
+ if result.is_available and result.version:
208
+ return result.version
209
+ else:
210
+ raise FileNotFoundError("Claude CLI not found")
211
+
212
+ except Exception as e:
213
+ logger.warning(
214
+ "claude_version_detection_failed", error=str(e), category="plugin"
215
+ )
216
+ return "unknown"
217
+
218
+ async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
219
+ """Execute Claude CLI with proxy to capture headers and system prompt."""
220
+ # Data captured from the request
221
+ captured_data: dict[str, Any] = {}
222
+
223
+ async def capture_handler(request: Request) -> Response:
224
+ """Capture the Claude CLI request."""
225
+ # Capture request details
226
+ headers = extract_request_headers(request)
227
+ captured_data["headers"] = headers
228
+ captured_data["method"] = request.method
229
+ captured_data["url"] = str(request.url)
230
+ captured_data["path"] = request.url.path
231
+ captured_data["query_params"] = (
232
+ dict(request.query_params) if request.query_params else {}
233
+ )
234
+
235
+ raw_body = await request.body()
236
+ captured_data["body"] = raw_body
237
+ # Try to parse to JSON for body_json
238
+ try:
239
+ captured_data["body_json"] = (
240
+ json.loads(raw_body.decode("utf-8")) if raw_body else None
241
+ )
242
+ except Exception:
243
+ captured_data["body_json"] = None
244
+ # Return a mock response to satisfy Claude CLI
245
+ return Response(
246
+ content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
247
+ media_type="application/json",
248
+ status_code=200,
249
+ )
250
+
251
+ # Create temporary FastAPI app
252
+ temp_app = FastAPI()
253
+ temp_app.post("/v1/messages")(capture_handler)
254
+
255
+ # Find available port
256
+ sock = socket.socket()
257
+ sock.bind(("", 0))
258
+ port = sock.getsockname()[1]
259
+ sock.close()
260
+
261
+ # Start server in background
262
+ from uvicorn import Config, Server
263
+
264
+ config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
265
+ server = Server(config)
266
+
267
+ server_ready = asyncio.Event()
268
+
269
+ @temp_app.on_event("startup")
270
+ async def signal_server_ready() -> None:
271
+ """Signal when the temporary detection server starts."""
272
+
273
+ server_ready.set()
274
+
275
+ server_task = asyncio.create_task(server.serve())
276
+ ready_task = asyncio.create_task(server_ready.wait())
277
+
278
+ try:
279
+ done, _pending = await asyncio.wait(
280
+ {ready_task, server_task},
281
+ timeout=5,
282
+ return_when=asyncio.FIRST_COMPLETED,
283
+ )
284
+ if ready_task in done:
285
+ await ready_task
286
+ elif server_task in done:
287
+ await server_task
288
+ raise RuntimeError(
289
+ "Claude detection server exited before signalling readiness"
290
+ )
291
+ else:
292
+ raise TimeoutError(
293
+ "Timed out waiting for Claude detection server startup"
294
+ )
295
+
296
+ stdout, stderr = b"", b""
297
+
298
+ env: dict[str, str] = dict(os.environ)
299
+ env["ANTHROPIC_BASE_URL"] = f"http://127.0.0.1:{port}"
300
+
301
+ home_path = os.environ.get("HOME")
302
+ cwd_path = Path(home_path) if home_path else Path.cwd()
303
+
304
+ logger.debug(
305
+ "detection_service_using",
306
+ home_dir=home_path,
307
+ cwd=cwd_path,
308
+ category="plugin",
309
+ )
310
+
311
+ if home_path is not None:
312
+ env["HOME"] = home_path
313
+
314
+ cli_info = self._cli_service.get_cli_info("claude")
315
+ if not cli_info["is_available"] or not cli_info["command"]:
316
+ raise FileNotFoundError("Claude CLI not found for header detection")
317
+
318
+ cmd = cli_info["command"] + ["test"]
319
+
320
+ process = await asyncio.create_subprocess_exec(
321
+ *cmd,
322
+ env=env,
323
+ stdout=asyncio.subprocess.PIPE,
324
+ stderr=asyncio.subprocess.PIPE,
325
+ cwd=str(cwd_path),
326
+ )
327
+
328
+ try:
329
+ await asyncio.wait_for(process.wait(), timeout=30)
330
+ except TimeoutError:
331
+ process.kill()
332
+ await process.wait()
333
+
334
+ stdout = await process.stdout.read() if process.stdout else b""
335
+ stderr = await process.stderr.read() if process.stderr else b""
336
+ finally:
337
+ if not ready_task.done():
338
+ ready_task.cancel()
339
+ with suppress(asyncio.CancelledError):
340
+ await ready_task
341
+
342
+ server.should_exit = True
343
+ await server_task
344
+
345
+ if not captured_data:
346
+ logger.error(
347
+ "failed_to_capture_claude_cli_request",
348
+ stdout=stdout.decode(errors="ignore"),
349
+ stderr=stderr.decode(errors="ignore"),
350
+ category="plugin",
351
+ )
352
+ raise RuntimeError("Failed to capture Claude CLI request")
353
+
354
+ headers_dict = (
355
+ self._sanitize_headers_for_cache(captured_data["headers"])
356
+ if self._redact_sensitive_cache
357
+ else captured_data["headers"]
358
+ )
359
+ body_json = (
360
+ self._sanitize_body_json_for_cache(captured_data.get("body_json"))
361
+ if self._redact_sensitive_cache
362
+ else captured_data.get("body_json")
363
+ )
364
+
365
+ prompts = DetectedPrompts.from_body(body_json)
366
+
367
+ return ClaudeCacheData(
368
+ claude_version=version,
369
+ headers=DetectedHeaders(headers_dict),
370
+ prompts=prompts,
371
+ body_json=body_json,
372
+ method=captured_data.get("method"),
373
+ url=captured_data.get("url"),
374
+ path=captured_data.get("path"),
375
+ query_params=captured_data.get("query_params"),
376
+ )
377
+
378
+ def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
379
+ """Load cached data for specific Claude version."""
380
+ cache_file = self.cache_dir / f"claude_headers_{version}.json"
381
+
382
+ if not cache_file.exists():
383
+ return None
384
+
385
+ with cache_file.open("r") as f:
386
+ data = json.load(f)
387
+ return ClaudeCacheData.model_validate(data)
388
+
389
+ def _save_to_cache(self, data: ClaudeCacheData) -> None:
390
+ """Save detection data to cache."""
391
+ cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
392
+
393
+ try:
394
+ with cache_file.open("w") as f:
395
+ json.dump(data.model_dump(), f, indent=2, default=str)
396
+ logger.debug(
397
+ "cache_saved",
398
+ file=str(cache_file),
399
+ version=data.claude_version,
400
+ category="plugin",
401
+ )
402
+ except Exception as e:
403
+ logger.warning(
404
+ "cache_save_failed",
405
+ file=str(cache_file),
406
+ error=str(e),
407
+ category="plugin",
408
+ )
409
+
410
+ def _get_fallback_data(self) -> ClaudeCacheData:
411
+ """Get fallback data when detection fails."""
412
+ logger.warning("using_fallback_claude_data", category="plugin")
413
+
414
+ # Load fallback data from package data file
415
+ package_data_file = (
416
+ Path(__file__).resolve().parents[2]
417
+ / "data"
418
+ / "claude_headers_fallback.json"
419
+ )
420
+ with package_data_file.open("r") as f:
421
+ fallback_data_dict = json.load(f)
422
+ return ClaudeCacheData.model_validate(fallback_data_dict)
423
+
424
+ def invalidate_cache(self) -> None:
425
+ """Clear all cached detection data."""
426
+ # Clear the async cache for _get_claude_version
427
+ if hasattr(self._get_claude_version, "cache_clear"):
428
+ self._get_claude_version.cache_clear()
429
+ # Clear CLI info cache
430
+ self._cli_info = None
431
+ logger.debug("detection_cache_cleared", category="plugin")
432
+
433
+ # --- Helpers ---
434
+ def _sanitize_headers_for_cache(self, headers: dict[str, str]) -> dict[str, str]:
435
+ """Redact sensitive headers for cache while preserving keys and order."""
436
+ # Build ordered dict copy
437
+ sanitized: dict[str, str] = {}
438
+ for k, v in headers.items():
439
+ lk = k.lower()
440
+ if lk in {"authorization", "host"}:
441
+ sanitized[lk] = ""
442
+ else:
443
+ sanitized[lk] = v
444
+ return sanitized
445
+
446
+ def _sanitize_body_json_for_cache(
447
+ self, body: dict[str, Any] | None
448
+ ) -> dict[str, Any] | None:
449
+ if body is None:
450
+ return None
451
+ # For Claude, no specific fields to redact currently; return as-is
452
+ return body
453
+
454
+ def get_system_prompt(self, mode: str | None = "minimal") -> dict[str, Any]:
455
+ """Return a system prompt dict for injection based on cached prompts.
456
+
457
+ mode: "none", "minimal", or "full"
458
+ """
459
+ prompts = self.get_detected_prompts()
460
+ mode_value = "full" if mode is None else mode
461
+ return prompts.system_payload(mode=mode_value)
@@ -0,0 +1,175 @@
1
+ """Claude API plugin health check implementation."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins.protocol import HealthCheckResult
7
+ from ccproxy.plugins.oauth_claude.manager import ClaudeApiTokenManager
8
+
9
+ from .config import ClaudeAPISettings
10
+ from .detection_service import ClaudeAPIDetectionService
11
+
12
+
13
+ logger = get_plugin_logger()
14
+
15
+
16
+ async def claude_api_health_check(
17
+ config: ClaudeAPISettings | None,
18
+ detection_service: ClaudeAPIDetectionService | None = None,
19
+ credentials_manager: ClaudeApiTokenManager | None = None,
20
+ *,
21
+ version: str,
22
+ ) -> HealthCheckResult:
23
+ """Perform health check for Claude API plugin.
24
+
25
+ Args:
26
+ config: Plugin configuration
27
+ credentials_manager: Token manager for OAuth token status
28
+
29
+ Returns:
30
+ HealthCheckResult with plugin status including OAuth token details
31
+ """
32
+ try:
33
+ if not config:
34
+ return HealthCheckResult(
35
+ status="fail",
36
+ componentId="plugin-claude-api",
37
+ componentType="provider_plugin",
38
+ output="Claude API plugin configuration not available",
39
+ version=version,
40
+ )
41
+
42
+ # Check if plugin is enabled
43
+ if not config.enabled:
44
+ return HealthCheckResult(
45
+ status="warn",
46
+ componentId="plugin-claude-api",
47
+ componentType="provider_plugin",
48
+ output="Claude API plugin is disabled",
49
+ version=version,
50
+ details={"enabled": False},
51
+ )
52
+
53
+ # Check basic configuration
54
+ if not config.base_url:
55
+ return HealthCheckResult(
56
+ status="fail",
57
+ componentId="plugin-claude-api",
58
+ componentType="provider_plugin",
59
+ output="Claude API base URL not configured",
60
+ version=version,
61
+ )
62
+
63
+ # Standardized details
64
+ from ccproxy.core.plugins.models import (
65
+ AuthHealth,
66
+ CLIHealth,
67
+ ConfigHealth,
68
+ ProviderHealthDetails,
69
+ )
70
+
71
+ cli_info = (
72
+ detection_service.get_cli_health_info() if detection_service else None
73
+ )
74
+ cli_health = (
75
+ CLIHealth(
76
+ available=bool(
77
+ cli_info
78
+ and getattr(cli_info, "status", None)
79
+ == getattr(cli_info.__class__, "__members__", {}).get("AVAILABLE")
80
+ ),
81
+ status=(cli_info.status.value if cli_info else "unknown"),
82
+ version=(cli_info.version if cli_info else None),
83
+ path=(cli_info.binary_path if cli_info else None),
84
+ )
85
+ if cli_info
86
+ else None
87
+ )
88
+
89
+ auth_raw: dict[str, Any] = {}
90
+ if credentials_manager:
91
+ try:
92
+ auth_raw = await credentials_manager.get_auth_status()
93
+ except Exception as e:
94
+ logger.debug("auth_status_failed", error=str(e), category="auth")
95
+ auth_raw = {"authenticated": False, "reason": str(e)}
96
+
97
+ auth_health = (
98
+ AuthHealth(
99
+ configured=bool(credentials_manager),
100
+ token_available=auth_raw.get("authenticated"),
101
+ token_expired=(
102
+ not auth_raw.get("authenticated")
103
+ and auth_raw.get("reason") == "Token expired"
104
+ ),
105
+ account_id=auth_raw.get("account_id"),
106
+ expires_at=auth_raw.get("expires_at"),
107
+ error=(
108
+ None if auth_raw.get("authenticated") else auth_raw.get("reason")
109
+ ),
110
+ )
111
+ if credentials_manager
112
+ else AuthHealth(configured=False)
113
+ )
114
+
115
+ config_health = ConfigHealth(
116
+ model_count=len(config.models_endpoint) if config.models_endpoint else 0,
117
+ supports_openai_format=config.support_openai_format,
118
+ extra=None,
119
+ )
120
+
121
+ # Compose output message
122
+ status: Literal["pass", "warn", "fail"]
123
+ output_parts: list[str] = []
124
+ if auth_health.token_available and not auth_health.token_expired:
125
+ output_parts.append("Authenticated")
126
+ status = "pass"
127
+ elif auth_health.token_expired:
128
+ output_parts.append("Token expired")
129
+ status = "warn"
130
+ elif auth_health.configured:
131
+ output_parts.append("Auth configured but token unavailable")
132
+ status = "warn"
133
+ else:
134
+ output_parts.append("Authentication not configured")
135
+ status = "warn"
136
+
137
+ if cli_health and cli_health.available:
138
+ output_parts.append(
139
+ f"CLI v{cli_health.version}" if cli_health.version else "CLI available"
140
+ )
141
+ else:
142
+ output_parts.append("CLI not found")
143
+
144
+ if config.models_endpoint:
145
+ output_parts.append(f"{len(config.models_endpoint)} models available")
146
+
147
+ output = "Claude API: " + ", ".join(output_parts)
148
+
149
+ details_model = ProviderHealthDetails(
150
+ provider="claude_api",
151
+ enabled=config.enabled,
152
+ base_url=config.base_url,
153
+ cli=cli_health,
154
+ auth=auth_health,
155
+ config=config_health,
156
+ )
157
+
158
+ return HealthCheckResult(
159
+ status=status,
160
+ componentId="plugin-claude-api",
161
+ componentType="provider_plugin",
162
+ output=output,
163
+ version=version,
164
+ details=details_model.model_dump(),
165
+ )
166
+
167
+ except Exception as e:
168
+ logger.error("health_check_failed", error=str(e))
169
+ return HealthCheckResult(
170
+ status="fail",
171
+ componentId="plugin-claude-api",
172
+ componentType="provider_plugin",
173
+ output=f"Claude API health check failed: {str(e)}",
174
+ version=version,
175
+ )