ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,134 @@
1
+ """Generic storage implementation using Pydantic validation."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, TypeVar
6
+
7
+ from pydantic import SecretStr, TypeAdapter
8
+
9
+ from ccproxy.auth.models.credentials import BaseCredentials
10
+ from ccproxy.auth.storage.base import BaseJsonStorage
11
+ from ccproxy.core.logging import get_logger
12
+
13
+
14
+ logger = get_logger(__name__)
15
+
16
+ T = TypeVar("T", bound=BaseCredentials)
17
+
18
+
19
+ class GenericJsonStorage(BaseJsonStorage[T]):
20
+ """Generic storage implementation using Pydantic validation.
21
+
22
+ This replaces provider-specific storage classes with a single
23
+ implementation that handles any Pydantic model.
24
+ """
25
+
26
+ def __init__(self, file_path: Path, model_class: type[T]):
27
+ """Initialize generic storage.
28
+
29
+ Args:
30
+ file_path: Path to JSON file
31
+ model_class: Pydantic model class for validation
32
+ """
33
+ super().__init__(file_path)
34
+ self.model_class = model_class
35
+ self.type_adapter = TypeAdapter(model_class)
36
+
37
+ async def load(self) -> T | None:
38
+ """Load and validate credentials with Pydantic.
39
+
40
+ Returns:
41
+ Validated model instance or None if file doesn't exist
42
+ """
43
+ try:
44
+ data = await self._read_json()
45
+ except FileNotFoundError:
46
+ # File doesn't exist - this is normal for uninitialized credentials
47
+ logger.debug(
48
+ "credential_file_not_found",
49
+ path=str(self.file_path),
50
+ category="auth",
51
+ )
52
+ return None
53
+ except Exception as e:
54
+ # Handle JSON decode errors and other file read issues with clear warning
55
+ error_type = type(e).__name__
56
+ logger.warning(
57
+ "credential_file_read_failed",
58
+ error_type=error_type,
59
+ error=str(e),
60
+ exc_info=e,
61
+ path=str(self.file_path),
62
+ category="auth",
63
+ )
64
+ return None
65
+
66
+ if not data:
67
+ return None
68
+
69
+ try:
70
+ # Pydantic handles all validation and conversion
71
+ return self.type_adapter.validate_python(data)
72
+ except Exception as e:
73
+ # Log validation errors with clean warning (not error)
74
+ error_type = type(e).__name__
75
+ logger.warning(
76
+ "credential_validation_failed",
77
+ error_type=error_type,
78
+ error=str(e),
79
+ exc_info=e,
80
+ model=self.model_class.__name__,
81
+ path=str(self.file_path),
82
+ category="auth",
83
+ )
84
+ return None
85
+
86
+ async def save(self, obj: T) -> bool:
87
+ """Save model using Pydantic serialization.
88
+
89
+ Args:
90
+ obj: Pydantic model instance to save
91
+
92
+ Returns:
93
+ True if saved successfully
94
+ """
95
+ try:
96
+ # Preserve original JSON structure using aliases
97
+ # Use dump_python without mode="json" to get actual values
98
+ data = self.type_adapter.dump_python(
99
+ obj,
100
+ by_alias=True, # Use field aliases from original models
101
+ exclude_none=True,
102
+ )
103
+ # Convert SecretStr values to their actual values
104
+ data = self._unmask_secrets(data)
105
+ await self._write_json(data)
106
+ return True
107
+ except Exception as e:
108
+ logger.error(
109
+ "Failed to save credentials",
110
+ error=str(e),
111
+ exc_info=e,
112
+ model=self.model_class.__name__,
113
+ )
114
+ return False
115
+
116
+ def _unmask_secrets(self, data: Any) -> Any:
117
+ """Recursively unmask SecretStr values in data structure.
118
+
119
+ Args:
120
+ data: Data structure potentially containing SecretStr values
121
+
122
+ Returns:
123
+ Data with SecretStr values replaced by their actual values
124
+ """
125
+ if isinstance(data, dict):
126
+ return {k: self._unmask_secrets(v) for k, v in data.items()}
127
+ elif isinstance(data, list):
128
+ return [self._unmask_secrets(item) for item in data]
129
+ elif isinstance(data, SecretStr):
130
+ return data.get_secret_value()
131
+ elif isinstance(data, datetime):
132
+ return data.isoformat()
133
+ else:
134
+ return data
ccproxy/cli/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .commands.serve import api, claude
1
+ from .commands.serve import api
2
2
  from .helpers import get_rich_toolkit
3
3
  from .main import app, app_main, main, version_callback
4
4
 
@@ -8,7 +8,6 @@ __all__ = [
8
8
  "main",
9
9
  "version_callback",
10
10
  "api",
11
- "claude",
12
11
  "app_main",
13
12
  "get_rich_toolkit",
14
13
  ]
@@ -0,0 +1,351 @@
1
+ """Generic Pydantic model introspection and display utility for settings help."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import enum
7
+ import inspect
8
+ from typing import Any, get_args, get_origin
9
+
10
+ from pydantic import BaseModel, SecretStr
11
+ from pydantic.fields import FieldInfo
12
+ from rich.box import HEAVY_HEAD
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+
17
+ console = Console()
18
+
19
+ ELLIPSIS = "…"
20
+
21
+
22
+ def _is_model(obj: Any) -> bool:
23
+ """Check if an object is a Pydantic BaseModel subclass."""
24
+ return inspect.isclass(obj) and issubclass(obj, BaseModel)
25
+
26
+
27
+ def _typename(tp: Any) -> str:
28
+ """Return human-friendly type name for Unions, Literals, containers, etc."""
29
+ origin = get_origin(tp)
30
+ if origin is None:
31
+ if isinstance(tp, type):
32
+ try:
33
+ if issubclass(tp, enum.Enum):
34
+ return f"Enum[{tp.__name__}]"
35
+ except TypeError:
36
+ pass
37
+ name_str: str = tp.__name__
38
+ return name_str
39
+ return str(tp)
40
+
41
+ args_str = ", ".join(_typename(a) for a in get_args(tp))
42
+ name = getattr(origin, "__name__", str(origin))
43
+
44
+ # Handle Union types (including | syntax)
45
+ if name in {"Union", "types.UnionType", "UnionType"}:
46
+ return " | ".join(_typename(a) for a in get_args(tp))
47
+
48
+ # Handle common generic types
49
+ if name in {"list", "List"}:
50
+ return f"list[{args_str}]"
51
+ if name in {"dict", "Dict"}:
52
+ return f"dict[{args_str}]"
53
+ if name in {"Annotated"}:
54
+ # Show inner type for Annotated
55
+ inner_args = get_args(tp)
56
+ return _typename(inner_args[0]) if inner_args else "Any"
57
+
58
+ result: str = f"{name}[{args_str}]" if args_str else name
59
+ return result
60
+
61
+
62
+ def _is_secret_field(name: str) -> bool:
63
+ """Check if a field name suggests it contains secret data."""
64
+ secret_patterns = ["token", "key", "secret", "password", "credential", "auth"]
65
+ return any(pattern in name.lower() for pattern in secret_patterns)
66
+
67
+
68
+ def _render_value(val: Any, width: int = 22, field_name: str = "") -> str:
69
+ """Render a value for display with truncation and secret masking."""
70
+ # Handle None
71
+ if val is None:
72
+ return "—"
73
+
74
+ # Mask secrets
75
+ if isinstance(val, SecretStr):
76
+ return "***"
77
+ if field_name and _is_secret_field(field_name):
78
+ return "***" if val else "—"
79
+
80
+ # Handle enums
81
+ if isinstance(val, enum.Enum):
82
+ return str(val.value)
83
+
84
+ # Handle Pydantic models - show class name only
85
+ if isinstance(val, BaseModel):
86
+ return val.__class__.__name__
87
+
88
+ # Handle dataclasses
89
+ if dataclasses.is_dataclass(val):
90
+ return val.__class__.__name__
91
+
92
+ # Convert to string and truncate if needed
93
+ s = repr(val)
94
+ if len(s) > width:
95
+ return s[: width - 1] + ELLIPSIS
96
+ return s
97
+
98
+
99
+ def _default_for_field(field: FieldInfo) -> Any:
100
+ """Extract default value or factory from a Pydantic field."""
101
+ from pydantic_core import PydanticUndefined
102
+
103
+ # Check if field has a default value
104
+ if hasattr(field, "default") and field.default is not PydanticUndefined:
105
+ return field.default
106
+
107
+ # Check if field has a default_factory
108
+ if hasattr(field, "default_factory") and field.default_factory is not None:
109
+ factory = field.default_factory
110
+ if callable(factory):
111
+ try:
112
+ return factory() # type: ignore[call-arg]
113
+ except Exception:
114
+ return "<factory>"
115
+ return "<factory>"
116
+
117
+ # Field is required
118
+ return "required"
119
+
120
+
121
+ def _choices_from_type(tp: Any) -> list[str]:
122
+ """Extract enum/literal choices from a type annotation."""
123
+ origin = get_origin(tp)
124
+
125
+ # Handle direct enum types
126
+ if origin is None:
127
+ try:
128
+ if inspect.isclass(tp) and issubclass(tp, enum.Enum):
129
+ return [str(m.value) for m in tp]
130
+ except TypeError:
131
+ pass
132
+ return []
133
+
134
+ # Handle Literal types
135
+ if hasattr(origin, "__name__") and origin.__name__ in {"Literal"}:
136
+ return [repr(x) for x in get_args(tp)]
137
+
138
+ return []
139
+
140
+
141
+ def _is_field_hidden(field: FieldInfo) -> bool:
142
+ """Check if a field should be hidden from display.
143
+
144
+ Fields can be hidden by setting:
145
+ - field.exclude = True
146
+ - field.json_schema_extra = {"config_example_hidden": True}
147
+ """
148
+ # Check if field is explicitly excluded
149
+ if field.exclude:
150
+ return True
151
+
152
+ # Check json_schema_extra for config_example_hidden flag
153
+ json_schema_extra = field.json_schema_extra
154
+ if json_schema_extra:
155
+ # Handle both dict and callable forms
156
+ if callable(json_schema_extra):
157
+ try:
158
+ # Pydantic v2 callable signature: (schema_dict, handler)
159
+ extra_dict = json_schema_extra({})
160
+ if isinstance(extra_dict, dict):
161
+ return bool(extra_dict.get("config_example_hidden", False))
162
+ except Exception:
163
+ pass
164
+ elif isinstance(json_schema_extra, dict):
165
+ return bool(json_schema_extra.get("config_example_hidden", False))
166
+
167
+ return False
168
+
169
+
170
+ def build_table_for_model(
171
+ model_cls: type[BaseModel],
172
+ instance: BaseModel | None = None,
173
+ *,
174
+ title: str | None = None,
175
+ show_value: bool = True,
176
+ ) -> Table:
177
+ """Build a Rich table for a Pydantic model with field information.
178
+
179
+ Args:
180
+ model_cls: The Pydantic model class to display
181
+ instance: Optional instance to show actual values
182
+ title: Optional table title (defaults to model class name)
183
+ show_value: Whether to include the Value column (useful for schema-only display)
184
+
185
+ Returns:
186
+ A Rich Table ready for printing
187
+ """
188
+ table = Table(
189
+ title=title or model_cls.__name__,
190
+ box=HEAVY_HEAD,
191
+ show_lines=False,
192
+ header_style="bold",
193
+ )
194
+
195
+ table.add_column("Field", style="bold")
196
+ table.add_column("Type", style="cyan")
197
+ if show_value:
198
+ table.add_column("Value", style="green")
199
+ table.add_column("Default", style="yellow")
200
+ table.add_column("Description", style="dim")
201
+
202
+ schema = model_cls.model_json_schema()
203
+ required_fields = set(schema.get("required", []))
204
+
205
+ for field_name, field in model_cls.model_fields.items():
206
+ # Skip hidden fields
207
+ if _is_field_hidden(field):
208
+ continue
209
+ prop = schema.get("properties", {}).get(field_name, {})
210
+
211
+ # Get type string
212
+ tp_str = (
213
+ _typename(field.annotation)
214
+ if field.annotation is not None
215
+ else prop.get("type", "object")
216
+ )
217
+
218
+ # Get default value
219
+ default = _default_for_field(field)
220
+
221
+ # Get actual value if instance provided
222
+ if show_value and instance is not None:
223
+ val = getattr(
224
+ instance, field_name, default if default != "required" else None
225
+ )
226
+ value_str = _render_value(val, width=22, field_name=field_name)
227
+ else:
228
+ value_str = None
229
+
230
+ # Get description
231
+ desc = prop.get("description", "") or field.description or ""
232
+
233
+ # Add choices to description if available
234
+ choices = _choices_from_type(field.annotation)
235
+ if choices and len(choices) <= 5: # Only show if reasonable number
236
+ choices_str = ", ".join(choices[:5])
237
+ if len(choices) > 5:
238
+ choices_str += "..."
239
+ desc = f"{desc} Choices: {choices_str}".strip()
240
+
241
+ # Mark required fields with *
242
+ display_name = f"{field_name}*" if field_name in required_fields else field_name
243
+
244
+ # Build row
245
+ if show_value and value_str is not None:
246
+ table.add_row(
247
+ display_name,
248
+ tp_str,
249
+ value_str,
250
+ _render_value(default, width=22),
251
+ desc,
252
+ )
253
+ else:
254
+ table.add_row(
255
+ display_name,
256
+ tp_str,
257
+ _render_value(default, width=22),
258
+ desc,
259
+ )
260
+
261
+ return table
262
+
263
+
264
+ def collect_nested_models(model_cls: type[BaseModel]) -> list[type[BaseModel]]:
265
+ """Recursively find all nested BaseModel types in a model's fields.
266
+
267
+ Args:
268
+ model_cls: The Pydantic model class to scan
269
+
270
+ Returns:
271
+ List of unique BaseModel subclasses found, sorted by name
272
+ """
273
+ nested_models: set[type[BaseModel]] = set()
274
+
275
+ def walk(tp: Any) -> None:
276
+ """Recursively walk type annotations to find BaseModel subclasses."""
277
+ origin = get_origin(tp)
278
+
279
+ if origin is None:
280
+ # Direct type - check if it's a BaseModel
281
+ try:
282
+ if inspect.isclass(tp) and issubclass(tp, BaseModel):
283
+ nested_models.add(tp)
284
+ # Recursively scan this model's fields
285
+ for field in tp.model_fields.values():
286
+ if field.annotation is not None:
287
+ walk(field.annotation)
288
+ except TypeError:
289
+ pass
290
+ return
291
+
292
+ # Generic type - walk the type arguments
293
+ for arg in get_args(tp):
294
+ walk(arg)
295
+
296
+ # Scan all fields in the model
297
+ for field in model_cls.model_fields.values():
298
+ if field.annotation is not None:
299
+ walk(field.annotation)
300
+
301
+ # Exclude the model itself
302
+ nested_models.discard(model_cls)
303
+
304
+ # Return sorted by name for stable output
305
+ return sorted(nested_models, key=lambda c: c.__name__)
306
+
307
+
308
+ def print_settings_help(
309
+ model_cls: type[BaseModel],
310
+ instance: BaseModel | None = None,
311
+ *,
312
+ title_left: str = "",
313
+ version: str | None = None,
314
+ enabled: bool | None = None,
315
+ ) -> None:
316
+ """Print comprehensive settings help for a Pydantic model.
317
+
318
+ Displays:
319
+ 1. Main table with all fields (with values if instance provided)
320
+ 2. Nested Configuration Types section with schema tables for each nested model
321
+
322
+ Args:
323
+ model_cls: The Pydantic model class to display
324
+ instance: Optional instance to show actual values
325
+ title_left: Optional prefix for the title
326
+ version: Optional version string to display
327
+ enabled: Optional enabled status to display
328
+ """
329
+ # Build header
330
+ header = title_left + model_cls.__name__ if title_left else model_cls.__name__
331
+ suffix = []
332
+ if version:
333
+ suffix.append(f"v{version}")
334
+ if enabled is not None:
335
+ suffix.append("enabled" if enabled else "disabled")
336
+ if suffix:
337
+ header += " (" + ", ".join(suffix) + ")"
338
+
339
+ # Print main table
340
+ console.print(f"\n{header}", style="bold")
341
+ console.print(
342
+ build_table_for_model(model_cls, instance, show_value=instance is not None)
343
+ )
344
+
345
+ # Print nested types
346
+ nested = collect_nested_models(model_cls)
347
+ if nested:
348
+ console.print("\n[bold cyan]Nested Configuration Types:[/bold cyan]\n")
349
+ for nested_cls in nested:
350
+ console.print(build_table_for_model(nested_cls, show_value=False))
351
+ console.print()