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
@@ -6,535 +6,42 @@ Implements modern health check patterns following 2024 best practices:
6
6
  - /health: Detailed diagnostics (comprehensive status)
7
7
 
8
8
  Follows IETF Health Check Response Format draft standard.
9
- TODO: health endpoint Content-Type header to only return application/health+json per IETF spec
10
9
  """
11
10
 
12
- import asyncio
13
- import functools
14
- import shutil
15
- import time
16
11
  from datetime import UTC, datetime
17
- from enum import Enum
18
12
  from typing import Any
19
13
 
20
14
  from fastapi import APIRouter, Response, status
21
- from pydantic import BaseModel
22
- from structlog import get_logger
15
+ from fastapi.responses import JSONResponse
23
16
 
24
- from ccproxy import __version__
25
- from ccproxy.auth.exceptions import CredentialsExpiredError, CredentialsNotFoundError
26
- from ccproxy.core.async_utils import patched_typing
27
- from ccproxy.services.credentials import CredentialsManager
17
+ from ccproxy.core import __version__
18
+ from ccproxy.core.logging import get_logger
28
19
 
29
20
 
30
- router = APIRouter()
31
- logger = get_logger(__name__)
32
-
33
-
34
- class ClaudeCliStatus(str, Enum):
35
- """Claude CLI status enumeration."""
36
-
37
- AVAILABLE = "available"
38
- NOT_INSTALLED = "not_installed"
39
- BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
40
- TIMEOUT = "timeout"
41
- ERROR = "error"
42
-
43
-
44
- class CodexCliStatus(str, Enum):
45
- """Codex CLI status enumeration."""
46
-
47
- AVAILABLE = "available"
48
- NOT_INSTALLED = "not_installed"
49
- BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
50
- TIMEOUT = "timeout"
51
- ERROR = "error"
52
-
53
-
54
- class ClaudeCliInfo(BaseModel):
55
- """Claude CLI information with structured data."""
56
-
57
- status: ClaudeCliStatus
58
- version: str | None = None
59
- binary_path: str | None = None
60
- version_output: str | None = None
61
- error: str | None = None
62
- return_code: str | None = None
21
+ class HealthJSONResponse(JSONResponse):
22
+ media_type = "application/health+json"
63
23
 
64
24
 
65
- class CodexCliInfo(BaseModel):
66
- """Codex CLI information with structured data."""
67
-
68
- status: CodexCliStatus
69
- version: str | None = None
70
- binary_path: str | None = None
71
- version_output: str | None = None
72
- error: str | None = None
73
- return_code: str | None = None
74
-
75
-
76
- # Cache for Claude CLI check results
77
- _claude_cli_cache: tuple[float, tuple[str, dict[str, Any]]] | None = None
78
- # Cache for Codex CLI check results
79
- _codex_cli_cache: tuple[float, tuple[str, dict[str, Any]]] | None = None
80
- _cache_ttl_seconds = 300 # Cache for 5 minutes
81
-
82
-
83
- async def _check_oauth2_credentials() -> tuple[str, dict[str, Any]]:
84
- """Check OAuth2 credentials health status.
85
-
86
- Returns:
87
- Tuple of (status, details) where status is 'pass'/'fail'/'warn'
88
- Details include token metadata without exposing sensitive data
89
- """
90
- try:
91
- manager = CredentialsManager()
92
- validation = await manager.validate()
93
-
94
- if validation.valid and not validation.expired:
95
- # Get token metadata without exposing sensitive information
96
- credentials = validation.credentials
97
- oauth_token = credentials.claude_ai_oauth if credentials else None
98
-
99
- details = {
100
- "auth_status": "valid",
101
- "credentials_path": str(validation.path) if validation.path else None,
102
- }
103
-
104
- if oauth_token:
105
- details.update(
106
- {
107
- "expiration": oauth_token.expires_at_datetime.isoformat()
108
- if oauth_token.expires_at_datetime
109
- else None,
110
- "subscription_type": oauth_token.subscription_type,
111
- "expires_in_hours": str(
112
- int(
113
- (
114
- oauth_token.expires_at_datetime - datetime.now(UTC)
115
- ).total_seconds()
116
- / 3600
117
- )
118
- )
119
- if oauth_token.expires_at_datetime
120
- else None,
121
- }
122
- )
123
-
124
- return "pass", details
125
- else:
126
- # Handle expired credentials
127
- credentials = validation.credentials
128
- oauth_token = credentials.claude_ai_oauth if credentials else None
129
-
130
- details = {
131
- "auth_status": "expired" if validation.expired else "invalid",
132
- "credentials_path": str(validation.path) if validation.path else None,
133
- }
134
-
135
- if oauth_token and oauth_token.expires_at_datetime:
136
- details.update(
137
- {
138
- "expiration": oauth_token.expires_at_datetime.isoformat(),
139
- "subscription_type": oauth_token.subscription_type,
140
- "expired_hours_ago": str(
141
- int(
142
- (
143
- datetime.now(UTC) - oauth_token.expires_at_datetime
144
- ).total_seconds()
145
- / 3600
146
- )
147
- )
148
- if validation.expired
149
- else None,
150
- }
151
- )
152
-
153
- return "warn", details
154
-
155
- except CredentialsNotFoundError:
156
- return "warn", {
157
- "auth_status": "not_configured",
158
- "error": "Claude credentials file not found",
159
- "credentials_path": None,
160
- }
161
- except CredentialsExpiredError:
162
- return "warn", {
163
- "auth_status": "expired",
164
- "error": "Claude credentials have expired",
165
- }
166
- except Exception as e:
167
- return "fail", {
168
- "auth_status": "error",
169
- "error": f"Unexpected error: {str(e)}",
25
+ def _health_responses(description: str) -> dict[int | str, dict[str, Any]]:
26
+ return {
27
+ 200: {
28
+ "description": description,
29
+ "content": {"application/health+json": {"schema": {"type": "object"}}},
170
30
  }
31
+ }
171
32
 
172
33
 
173
- @functools.lru_cache(maxsize=1)
174
- def _get_claude_cli_path() -> str | None:
175
- """Get Claude CLI path with caching. Returns None if not found."""
176
- return shutil.which("claude")
177
-
178
-
179
- def _get_codex_cli_path() -> str | None:
180
- """Get Codex CLI path with caching. Returns None if not found."""
181
- return shutil.which("codex")
182
-
183
-
184
- async def check_claude_code() -> tuple[str, dict[str, Any]]:
185
- """Check Claude Code CLI installation and version by running 'claude --version'.
186
-
187
- Results are cached for 5 minutes to avoid repeated subprocess calls.
188
-
189
- Returns:
190
- Tuple of (status, details) where status is 'pass'/'fail'/'warn'
191
- Details include CLI version and binary path
192
- """
193
- global _claude_cli_cache
194
-
195
- # Check if we have a valid cached result
196
- current_time = time.time()
197
- if _claude_cli_cache is not None:
198
- cache_time, cached_result = _claude_cli_cache
199
- if current_time - cache_time < _cache_ttl_seconds:
200
- logger.debug("claude_cli_check_cache_hit")
201
- return cached_result
202
-
203
- logger.debug("claude_cli_check_cache_miss")
204
-
205
- # First check if claude binary exists in PATH (cached)
206
- claude_path = _get_claude_cli_path()
207
-
208
- if not claude_path:
209
- result = (
210
- "warn",
211
- {
212
- "installation_status": "not_found",
213
- "cli_status": "not_installed",
214
- "error": "Claude CLI binary not found in PATH",
215
- "version": None,
216
- "binary_path": None,
217
- },
218
- )
219
- # Cache the result
220
- _claude_cli_cache = (current_time, result)
221
- return result
222
-
223
- try:
224
- # Run 'claude --version' to get actual version
225
- process = await asyncio.create_subprocess_exec(
226
- "claude",
227
- "--version",
228
- stdout=asyncio.subprocess.PIPE,
229
- stderr=asyncio.subprocess.PIPE,
230
- )
231
-
232
- stdout, stderr = await process.communicate()
233
-
234
- if process.returncode == 0:
235
- version_output = stdout.decode().strip()
236
- # Extract version from output (e.g., "1.0.48 (Claude Code)" -> "1.0.48")
237
- if version_output:
238
- import re
239
-
240
- # Try to find a version pattern (e.g., "1.0.48", "v2.1.0")
241
- version_match = re.search(
242
- r"\b(?:v)?(\d+\.\d+(?:\.\d+)?)\b", version_output
243
- )
244
- if version_match:
245
- version = version_match.group(1)
246
- else:
247
- # Fallback: take the first part if no version pattern found
248
- parts = version_output.split()
249
- version = parts[0] if parts else "unknown"
250
- else:
251
- version = "unknown"
252
-
253
- result = (
254
- "pass",
255
- {
256
- "installation_status": "found",
257
- "cli_status": "available",
258
- "version": version,
259
- "binary_path": claude_path,
260
- "version_output": version_output,
261
- },
262
- )
263
- # Cache the result
264
- _claude_cli_cache = (current_time, result)
265
- return result
266
- else:
267
- # Binary exists but --version failed
268
- error_output = stderr.decode().strip() if stderr else "Unknown error"
269
- result = (
270
- "warn",
271
- {
272
- "installation_status": "found_with_issues",
273
- "cli_status": "binary_found_but_errors",
274
- "error": f"'claude --version' failed: {error_output}",
275
- "version": None,
276
- "binary_path": claude_path,
277
- "return_code": str(process.returncode),
278
- },
279
- )
280
- # Cache the result
281
- _claude_cli_cache = (current_time, result)
282
- return result
283
-
284
- except TimeoutError:
285
- result = (
286
- "warn",
287
- {
288
- "installation_status": "found_with_issues",
289
- "cli_status": "timeout",
290
- "error": "Timeout running 'claude --version'",
291
- "version": None,
292
- "binary_path": claude_path,
293
- },
294
- )
295
- # Cache the result
296
- _claude_cli_cache = (current_time, result)
297
- return result
298
- except Exception as e:
299
- result = (
300
- "fail",
301
- {
302
- "installation_status": "error",
303
- "cli_status": "error",
304
- "error": f"Unexpected error running 'claude --version': {str(e)}",
305
- "version": None,
306
- "binary_path": claude_path,
307
- },
308
- )
309
- # Cache the result
310
- _claude_cli_cache = (current_time, result)
311
- return result
312
-
313
-
314
- async def get_claude_cli_info() -> ClaudeCliInfo:
315
- """Get Claude CLI information as a structured Pydantic model.
316
-
317
- Returns:
318
- ClaudeCliInfo: Structured information about Claude CLI installation and status
319
- """
320
- cli_status, cli_details = await check_claude_code()
321
-
322
- # Map the status to our enum values
323
- if cli_status == "pass":
324
- status_value = ClaudeCliStatus.AVAILABLE
325
- elif cli_details.get("cli_status") == "not_installed":
326
- status_value = ClaudeCliStatus.NOT_INSTALLED
327
- elif cli_details.get("cli_status") == "binary_found_but_errors":
328
- status_value = ClaudeCliStatus.BINARY_FOUND_BUT_ERRORS
329
- elif cli_details.get("cli_status") == "timeout":
330
- status_value = ClaudeCliStatus.TIMEOUT
331
- else:
332
- status_value = ClaudeCliStatus.ERROR
333
-
334
- return ClaudeCliInfo(
335
- status=status_value,
336
- version=cli_details.get("version"),
337
- binary_path=cli_details.get("binary_path"),
338
- version_output=cli_details.get("version_output"),
339
- error=cli_details.get("error"),
340
- return_code=cli_details.get("return_code"),
341
- )
342
-
343
-
344
- async def check_codex_cli() -> tuple[str, dict[str, Any]]:
345
- """Check Codex CLI installation and version by running 'codex --version'.
346
- Results are cached for 5 minutes to avoid repeated subprocess calls.
347
- Returns:
348
- Tuple of (status, details) where status is 'pass'/'fail'/'warn'
349
- Details include CLI version and binary path
350
- """
351
- global _codex_cli_cache
352
- # Check if we have a valid cached result
353
- current_time = time.time()
354
- if _codex_cli_cache is not None:
355
- cache_time, cached_result = _codex_cli_cache
356
- if current_time - cache_time < _cache_ttl_seconds:
357
- logger.debug("codex_cli_check_cache_hit")
358
- return cached_result
359
-
360
- logger.debug("codex_cli_check_cache_miss")
361
-
362
- # First check if codex binary exists in PATH (cached)
363
- codex_path = _get_codex_cli_path()
364
- if not codex_path:
365
- result = (
366
- "warn",
367
- {
368
- "installation_status": "not_found",
369
- "cli_status": "not_installed",
370
- "error": "Codex CLI binary not found in PATH",
371
- "version": None,
372
- "binary_path": None,
373
- },
374
- )
375
- # Cache the result
376
- _codex_cli_cache = (current_time, result)
377
- return result
378
-
379
- try:
380
- # Run 'codex --version' to get actual version
381
- process = await asyncio.create_subprocess_exec(
382
- "codex",
383
- "--version",
384
- stdout=asyncio.subprocess.PIPE,
385
- stderr=asyncio.subprocess.PIPE,
386
- )
387
-
388
- stdout, stderr = await process.communicate()
389
-
390
- if process.returncode == 0:
391
- version_output = stdout.decode().strip()
392
- # Extract version from output (e.g., "codex 0.21.0" -> "0.21.0")
393
- if version_output:
394
- import re
395
-
396
- # Try to find a version pattern (e.g., "0.21.0", "v1.0.0")
397
- version_match = re.search(
398
- r"\b(?:v)?(\d+\.\d+(?:\.\d+)?)\b", version_output
399
- )
400
- if version_match:
401
- version = version_match.group(1)
402
- else:
403
- # Fallback: take the last part if no version pattern found
404
- parts = version_output.split()
405
- version = parts[-1] if parts else "unknown"
406
- else:
407
- version = "unknown"
408
-
409
- result = (
410
- "pass",
411
- {
412
- "installation_status": "found",
413
- "cli_status": "available",
414
- "version": version,
415
- "binary_path": codex_path,
416
- "version_output": version_output,
417
- },
418
- )
419
- # Cache the result
420
- _codex_cli_cache = (current_time, result)
421
- return result
422
- else:
423
- # Binary exists but --version failed
424
- error_output = stderr.decode().strip() if stderr else "Unknown error"
425
- result = (
426
- "warn",
427
- {
428
- "installation_status": "found_with_issues",
429
- "cli_status": "binary_found_but_errors",
430
- "error": f"'codex --version' failed: {error_output}",
431
- "version": None,
432
- "binary_path": codex_path,
433
- "return_code": str(process.returncode),
434
- },
435
- )
436
- # Cache the result
437
- _codex_cli_cache = (current_time, result)
438
- return result
439
-
440
- except TimeoutError:
441
- result = (
442
- "warn",
443
- {
444
- "installation_status": "found_with_issues",
445
- "cli_status": "timeout",
446
- "error": "Timeout running 'codex --version'",
447
- "version": None,
448
- "binary_path": codex_path,
449
- },
450
- )
451
- # Cache the result
452
- _codex_cli_cache = (current_time, result)
453
- return result
454
-
455
- except Exception as e:
456
- result = (
457
- "fail",
458
- {
459
- "installation_status": "error",
460
- "cli_status": "error",
461
- "error": f"Unexpected error running 'codex --version': {str(e)}",
462
- "version": None,
463
- "binary_path": codex_path,
464
- },
465
- )
466
- # Cache the result
467
- _codex_cli_cache = (current_time, result)
468
- return result
469
-
470
-
471
- async def get_codex_cli_info() -> CodexCliInfo:
472
- """Get Codex CLI information as a structured Pydantic model.
473
- Returns:
474
- CodexCliInfo: Structured information about Codex CLI installation and status
475
- """
476
- cli_status, cli_details = await check_codex_cli()
477
-
478
- # Map the status to our enum values
479
- if cli_status == "pass":
480
- status_value = CodexCliStatus.AVAILABLE
481
- elif cli_details.get("cli_status") == "not_installed":
482
- status_value = CodexCliStatus.NOT_INSTALLED
483
- elif cli_details.get("cli_status") == "binary_found_but_errors":
484
- status_value = CodexCliStatus.BINARY_FOUND_BUT_ERRORS
485
- elif cli_details.get("cli_status") == "timeout":
486
- status_value = CodexCliStatus.TIMEOUT
487
- else:
488
- status_value = CodexCliStatus.ERROR
489
-
490
- return CodexCliInfo(
491
- status=status_value,
492
- version=cli_details.get("version"),
493
- binary_path=cli_details.get("binary_path"),
494
- version_output=cli_details.get("version_output"),
495
- error=cli_details.get("error"),
496
- return_code=cli_details.get("return_code"),
497
- )
498
-
499
-
500
- async def _check_claude_sdk() -> tuple[str, dict[str, Any]]:
501
- """Check Claude SDK installation and version.
502
-
503
- Returns:
504
- Tuple of (status, details) where status is 'pass'/'fail'/'warn'
505
- Details include SDK version and availability
506
- """
507
- try:
508
- # Try to import Claude Code SDK
509
- with patched_typing():
510
- from claude_code_sdk import __version__ as sdk_version
511
-
512
- return "pass", {
513
- "installation_status": "found",
514
- "sdk_status": "available",
515
- "version": sdk_version,
516
- "import_successful": True,
517
- }
34
+ router = APIRouter(default_response_class=HealthJSONResponse)
35
+ logger = get_logger(__name__)
518
36
 
519
- except ImportError as e:
520
- return "warn", {
521
- "installation_status": "not_found",
522
- "sdk_status": "not_installed",
523
- "error": f"Claude SDK not available: {str(e)}",
524
- "version": None,
525
- "import_successful": False,
526
- }
527
- except Exception as e:
528
- return "fail", {
529
- "installation_status": "error",
530
- "sdk_status": "error",
531
- "error": f"Unexpected error checking SDK: {str(e)}",
532
- "version": None,
533
- "import_successful": False,
534
- }
37
+ # Authentication and CLI health are managed by provider plugins; no core CLI checks
535
38
 
536
39
 
537
- @router.get("/health/live")
40
+ @router.get(
41
+ "/health/live",
42
+ response_class=HealthJSONResponse,
43
+ responses=_health_responses("Liveness probe result"),
44
+ )
538
45
  async def liveness_probe(response: Response) -> dict[str, Any]:
539
46
  """Liveness probe for Kubernetes.
540
47
 
@@ -544,11 +51,10 @@ async def liveness_probe(response: Response) -> dict[str, Any]:
544
51
  Returns:
545
52
  Simple health status following IETF health check format
546
53
  """
547
- # Add cache control headers as per best practices
548
54
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
549
55
  response.headers["Content-Type"] = "application/health+json"
550
56
 
551
- logger.debug("Liveness probe request")
57
+ logger.debug("liveness_probe_request")
552
58
 
553
59
  return {
554
60
  "status": "pass",
@@ -557,7 +63,11 @@ async def liveness_probe(response: Response) -> dict[str, Any]:
557
63
  }
558
64
 
559
65
 
560
- @router.get("/health/ready")
66
+ @router.get(
67
+ "/health/ready",
68
+ response_class=HealthJSONResponse,
69
+ responses=_health_responses("Readiness probe result"),
70
+ )
561
71
  async def readiness_probe(response: Response) -> dict[str, Any]:
562
72
  """Readiness probe for Kubernetes.
563
73
 
@@ -567,143 +77,40 @@ async def readiness_probe(response: Response) -> dict[str, Any]:
567
77
  Returns:
568
78
  Readiness status with critical dependency checks
569
79
  """
570
- # Add cache control headers
571
80
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
572
81
  response.headers["Content-Type"] = "application/health+json"
573
82
 
574
- logger.debug("Readiness probe request")
575
-
576
- # Check OAuth credentials, CLI, and SDK separately
577
- oauth_status, oauth_details = await _check_oauth2_credentials()
578
- cli_status, cli_details = await check_claude_code()
579
- codex_cli_status, codex_cli_details = await check_codex_cli()
580
- sdk_status, sdk_details = await _check_claude_sdk()
581
-
582
- # Service is ready if no check returns "fail"
583
- # "warn" statuses (missing credentials/CLI/SDK) don't prevent readiness
584
- if (
585
- oauth_status == "fail"
586
- or cli_status == "fail"
587
- or codex_cli_status == "fail"
588
- or sdk_status == "fail"
589
- ):
590
- response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
591
- failed_components = []
592
-
593
- if oauth_status == "fail":
594
- failed_components.append("oauth2_credentials")
595
- if cli_status == "fail":
596
- failed_components.append("claude_cli")
597
- if codex_cli_status == "fail":
598
- failed_components.append("codex_cli")
599
- if sdk_status == "fail":
600
- failed_components.append("claude_sdk")
601
-
602
- return {
603
- "status": "fail",
604
- "version": __version__,
605
- "output": f"Critical dependency error: {', '.join(failed_components)}",
606
- "checks": {
607
- "oauth2_credentials": [
608
- {
609
- "status": oauth_status,
610
- "output": oauth_details.get("error", "OAuth credentials error"),
611
- }
612
- ],
613
- "claude_cli": [
614
- {
615
- "status": cli_status,
616
- "output": cli_details.get("error", "Claude CLI error"),
617
- }
618
- ],
619
- "codex_cli": [
620
- {
621
- "status": codex_cli_status,
622
- "output": codex_cli_details.get("error", "Codex CLI error"),
623
- }
624
- ],
625
- "claude_sdk": [
626
- {
627
- "status": sdk_status,
628
- "output": sdk_details.get("error", "Claude SDK error"),
629
- }
630
- ],
631
- },
632
- }
83
+ logger.debug("readiness_probe_request")
633
84
 
85
+ # Core readiness only checks application availability; plugins provide their own health
634
86
  return {
635
87
  "status": "pass",
636
88
  "version": __version__,
637
89
  "output": "Service is ready to accept traffic",
638
- "checks": {
639
- "oauth2_credentials": [
640
- {
641
- "status": oauth_status,
642
- "output": f"OAuth credentials: {oauth_details.get('auth_status', 'unknown')}",
643
- }
644
- ],
645
- "claude_cli": [
646
- {
647
- "status": cli_status,
648
- "output": f"Claude CLI: {cli_details.get('cli_status', 'unknown')}",
649
- }
650
- ],
651
- "codex_cli": [
652
- {
653
- "status": codex_cli_status,
654
- "output": f"Codex CLI: {codex_cli_details.get('cli_status', 'unknown')}",
655
- }
656
- ],
657
- "claude_sdk": [
658
- {
659
- "status": sdk_status,
660
- "output": f"Claude SDK: {sdk_details.get('sdk_status', 'unknown')}",
661
- }
662
- ],
663
- },
664
90
  }
665
91
 
666
92
 
667
- @router.get("/health")
93
+ @router.get(
94
+ "/health",
95
+ response_class=HealthJSONResponse,
96
+ responses=_health_responses("Detailed health diagnostics"),
97
+ )
668
98
  async def detailed_health_check(response: Response) -> dict[str, Any]:
669
99
  """Comprehensive health check for diagnostics and monitoring.
670
100
 
671
- Provides detailed status of all services and dependencies.
672
- Used by monitoring dashboards, debugging, and operations teams.
101
+ Provides detailed status of core service only. Provider/plugin-specific
102
+ health, including CLI availability, is reported by each plugin's health endpoint.
673
103
 
674
104
  Returns:
675
105
  Detailed health status following IETF health check format
676
106
  """
677
- # Add cache control headers
678
107
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
679
108
  response.headers["Content-Type"] = "application/health+json"
680
109
 
681
- logger.debug("Detailed health check request")
110
+ logger.debug("detailed_health_check_request")
682
111
 
683
- # Perform all health checks
684
- oauth_status, oauth_details = await _check_oauth2_credentials()
685
- cli_status, cli_details = await check_claude_code()
686
- codex_cli_status, codex_cli_details = await check_codex_cli()
687
- sdk_status, sdk_details = await _check_claude_sdk()
688
-
689
- # Determine overall status - prioritize failures, then warnings
690
112
  overall_status = "pass"
691
- if (
692
- oauth_status == "fail"
693
- or cli_status == "fail"
694
- or codex_cli_status == "fail"
695
- or sdk_status == "fail"
696
- ):
697
- overall_status = "fail"
698
- response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
699
- elif (
700
- oauth_status == "warn"
701
- or cli_status == "warn"
702
- or codex_cli_status == "warn"
703
- or sdk_status == "warn"
704
- ):
705
- overall_status = "warn"
706
- response.status_code = status.HTTP_200_OK
113
+ response.status_code = status.HTTP_200_OK
707
114
 
708
115
  current_time = datetime.now(UTC).isoformat()
709
116
 
@@ -714,53 +121,13 @@ async def detailed_health_check(response: Response) -> dict[str, Any]:
714
121
  "description": "CCProxy API Server",
715
122
  "time": current_time,
716
123
  "checks": {
717
- "oauth2_credentials": [
718
- {
719
- "componentId": "oauth2-credentials",
720
- "componentType": "authentication",
721
- "status": oauth_status,
722
- "time": current_time,
723
- "output": f"OAuth2 credentials: {oauth_details.get('auth_status', 'unknown')}",
724
- **oauth_details,
725
- }
726
- ],
727
- "claude_cli": [
728
- {
729
- "componentId": "claude-cli",
730
- "componentType": "external_dependency",
731
- "status": cli_status,
732
- "time": current_time,
733
- "output": f"Claude CLI: {cli_details.get('cli_status', 'unknown')}",
734
- **cli_details,
735
- }
736
- ],
737
- "codex_cli": [
738
- {
739
- "componentId": "codex-cli",
740
- "componentType": "external_dependency",
741
- "status": codex_cli_status,
742
- "time": current_time,
743
- "output": f"Codex CLI: {codex_cli_details.get('cli_status', 'unknown')}",
744
- **codex_cli_details,
745
- }
746
- ],
747
- "claude_sdk": [
748
- {
749
- "componentId": "claude-sdk",
750
- "componentType": "python_package",
751
- "status": sdk_status,
752
- "time": current_time,
753
- "output": f"Claude SDK: {sdk_details.get('sdk_status', 'unknown')}",
754
- **sdk_details,
755
- }
756
- ],
757
- "proxy_service": [
124
+ "service_container": [
758
125
  {
759
- "componentId": "proxy-service",
126
+ "componentId": "service-container",
760
127
  "componentType": "service",
761
128
  "status": "pass",
762
129
  "time": current_time,
763
- "output": "Proxy service operational",
130
+ "output": "Service container operational",
764
131
  "version": __version__,
765
132
  }
766
133
  ],