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
@@ -5,14 +5,25 @@ import secrets
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ import structlog
8
9
  import typer
9
10
  from click import get_current_context
10
11
  from pydantic import BaseModel
11
12
  from pydantic.fields import FieldInfo
12
13
 
13
- from ccproxy._version import __version__
14
14
  from ccproxy.cli.helpers import get_rich_toolkit
15
- from ccproxy.config.settings import Settings, get_settings
15
+ from ccproxy.config.settings import Settings
16
+ from ccproxy.core._version import __version__
17
+ from ccproxy.services.container import ServiceContainer
18
+
19
+
20
+ logger = structlog.get_logger(__name__)
21
+
22
+
23
+ def _get_service_container() -> ServiceContainer:
24
+ """Create a service container for the config commands."""
25
+ settings = Settings.from_config(config_path=get_config_path_from_context())
26
+ return ServiceContainer(settings)
16
27
 
17
28
 
18
29
  def _create_config_table(title: str, rows: list[tuple[str, str, str]]) -> Any:
@@ -20,9 +31,9 @@ def _create_config_table(title: str, rows: list[tuple[str, str, str]]) -> Any:
20
31
  from rich.table import Table
21
32
 
22
33
  table = Table(title=title, show_header=True, header_style="bold magenta")
23
- table.add_column("Setting", style="cyan", width=20)
24
- table.add_column("Value", style="green")
25
- table.add_column("Description", style="dim")
34
+ table.add_column("Setting", style="cyan", overflow="fold")
35
+ table.add_column("Value", style="green", overflow="fold")
36
+ table.add_column("Description", style="dim", overflow="fold")
26
37
 
27
38
  for setting, value, description in rows:
28
39
  table.add_row(setting, value, description)
@@ -39,7 +50,6 @@ def _format_value(value: Any) -> str:
39
50
  elif isinstance(value, str):
40
51
  if not value:
41
52
  return "[dim]Not set[/dim]"
42
- # Special handling for sensitive values
43
53
  if any(
44
54
  keyword in value.lower()
45
55
  for keyword in ["token", "key", "secret", "password"]
@@ -64,7 +74,6 @@ def _get_field_description(field_info: FieldInfo) -> str:
64
74
  """Get a human-readable description from a Pydantic field."""
65
75
  if field_info.description:
66
76
  return field_info.description
67
- # Generate a basic description from the field name
68
77
  return "Configuration setting"
69
78
 
70
79
 
@@ -74,14 +83,13 @@ def _generate_config_rows_from_model(
74
83
  """Generate configuration rows from a Pydantic model dynamically."""
75
84
  rows = []
76
85
 
77
- for field_name, _field_info in model.model_fields.items():
86
+ field_definitions = model.__class__.model_fields
87
+
88
+ for field_name, _field_info in field_definitions.items():
78
89
  field_value = getattr(model, field_name)
79
90
  display_name = f"{prefix}{field_name}" if prefix else field_name
80
91
 
81
- # If the field value is also a BaseModel, we might want to flatten it
82
92
  if isinstance(field_value, BaseModel):
83
- # For nested models, we can either flatten or show as a summary
84
- # For now, let's show a summary and then add sub-rows
85
93
  model_name = field_value.__class__.__name__
86
94
  rows.append(
87
95
  (
@@ -91,11 +99,9 @@ def _generate_config_rows_from_model(
91
99
  )
92
100
  )
93
101
 
94
- # Add sub-rows for the nested model
95
- sub_rows = _generate_config_rows_from_model(field_value, f"{display_name}_")
102
+ sub_rows = _generate_config_rows_from_model(field_value, f"{display_name}.")
96
103
  rows.extend(sub_rows)
97
104
  else:
98
- # Regular field
99
105
  formatted_value = _format_value(field_value)
100
106
  description = _get_field_description(_field_info)
101
107
  rows.append((display_name, formatted_value, description))
@@ -109,41 +115,68 @@ def _group_config_rows(
109
115
  """Group configuration rows by their top-level section."""
110
116
  groups: dict[str, list[tuple[str, str, str]]] = {}
111
117
 
118
+ CATEGORY_PREFIXES = {
119
+ "server_": "Server Configuration",
120
+ "security_": "Security Configuration",
121
+ "cors_": "CORS Configuration",
122
+ "claude_": "Claude CLI Configuration",
123
+ "auth_": "Authentication Configuration",
124
+ "docker_": "Docker Configuration",
125
+ "observability_": "Observability Configuration",
126
+ "scheduler_": "Scheduler Configuration",
127
+ "pricing_": "Pricing Configuration",
128
+ }
129
+
112
130
  for setting, value, description in rows:
113
- # Determine the group based on the setting name
114
- if setting.startswith("server"):
115
- group_name = "Server Configuration"
116
- elif setting.startswith("security"):
117
- group_name = "Security Configuration"
118
- elif setting.startswith("cors"):
119
- group_name = "CORS Configuration"
120
- elif setting.startswith("claude"):
121
- group_name = "Claude CLI Configuration"
122
- elif setting.startswith("reverse_proxy"):
123
- group_name = "Reverse Proxy Configuration"
124
- elif setting.startswith("auth"):
125
- group_name = "Authentication Configuration"
126
- elif setting.startswith("docker"):
127
- group_name = "Docker Configuration"
128
- elif setting.startswith("observability"):
129
- group_name = "Observability Configuration"
130
- elif setting.startswith("scheduler"):
131
- group_name = "Scheduler Configuration"
132
- elif setting.startswith("pricing"):
133
- group_name = "Pricing Configuration"
131
+ normalized_setting = setting
132
+ group_name = "General Configuration"
133
+
134
+ for prefix, group in CATEGORY_PREFIXES.items():
135
+ if setting.startswith(prefix):
136
+ normalized_setting = setting[len(prefix) :]
137
+ group_name = group
138
+ break
134
139
  else:
135
- group_name = "General Configuration"
140
+ if setting.startswith("server"):
141
+ group_name = "Server Configuration"
142
+ elif setting.startswith("security"):
143
+ group_name = "Security Configuration"
144
+ elif setting.startswith("cors"):
145
+ group_name = "CORS Configuration"
146
+ elif setting.startswith("claude"):
147
+ group_name = "Claude CLI Configuration"
148
+ elif setting.startswith("auth"):
149
+ group_name = "Authentication Configuration"
150
+ elif setting.startswith("docker"):
151
+ group_name = "Docker Configuration"
152
+ elif setting.startswith("observability"):
153
+ group_name = "Observability Configuration"
154
+ elif setting.startswith("scheduler"):
155
+ group_name = "Scheduler Configuration"
156
+ elif setting.startswith("pricing"):
157
+ group_name = "Pricing Configuration"
158
+
159
+ if "." in normalized_setting:
160
+ normalized_setting = normalized_setting.split(".")[-1]
136
161
 
137
162
  if group_name not in groups:
138
163
  groups[group_name] = []
139
164
 
140
- # Clean up the setting name by removing the prefix
141
- clean_setting = setting.split("_", 1)[1] if "_" in setting else setting
142
- groups[group_name].append((clean_setting, value, description))
165
+ groups[group_name].append((normalized_setting, value, description))
143
166
 
144
167
  return groups
145
168
 
146
169
 
170
+ def _is_hidden_in_example(field_info: FieldInfo) -> bool:
171
+ """Determine if a field should be omitted from generated example configs."""
172
+
173
+ if bool(field_info.exclude):
174
+ return True
175
+
176
+ extra = getattr(field_info, "json_schema_extra", None) or {}
177
+ return bool(extra.get("config_example_hidden"))
178
+
179
+
147
180
  def get_config_path_from_context() -> Path | None:
148
181
  """Get config path from typer context if available."""
149
182
  try:
@@ -152,7 +185,6 @@ def get_config_path_from_context() -> Path | None:
152
185
  config_path = ctx.obj["config_path"]
153
186
  return config_path if config_path is None else Path(config_path)
154
187
  except RuntimeError:
155
- # No active click context (e.g., in tests)
156
188
  pass
157
189
  return None
158
190
 
@@ -169,10 +201,13 @@ app = typer.Typer(
169
201
  @app.command(name="list")
170
202
  def config_list() -> None:
171
203
  """Show current configuration."""
204
+ from ccproxy.cli._settings_help import print_settings_help
205
+
172
206
  toolkit = get_rich_toolkit()
173
207
 
174
208
  try:
175
- settings = get_settings(config_path=get_config_path_from_context())
209
+ container = _get_service_container()
210
+ settings = container.get_service(Settings)
176
211
 
177
212
  from rich.console import Console
178
213
  from rich.panel import Panel
@@ -180,34 +215,18 @@ def config_list() -> None:
180
215
 
181
216
  console = Console()
182
217
 
183
- # Generate configuration rows dynamically from the Settings model
184
- all_rows = _generate_config_rows_from_model(settings)
185
-
186
- # Add computed fields that aren't part of the model but are useful to display
187
- all_rows.append(
188
- ("server_url", settings.server_url, "Complete server URL (computed)")
189
- )
190
-
191
- # Group rows by configuration section
192
- grouped_rows = _group_config_rows(all_rows)
193
-
194
- # Display header
218
+ # Display header panel
195
219
  console.print(
196
220
  Panel.fit(
197
221
  f"[bold]CCProxy API Configuration[/bold]\n[dim]Version: {__version__}[/dim]",
198
222
  border_style="blue",
199
223
  )
200
224
  )
201
- console.print()
202
225
 
203
- # Display each configuration section as a table
204
- for section_name, section_rows in grouped_rows.items():
205
- if section_rows: # Only show sections that have data
206
- table = _create_config_table(section_name, section_rows)
207
- console.print(table)
208
- console.print()
226
+ # Use generic settings display
227
+ print_settings_help(Settings, settings)
209
228
 
210
- # Show configuration file sources
229
+ # Display footer panel
211
230
  info_text = Text()
212
231
  info_text.append("Configuration loaded from: ", style="bold")
213
232
  info_text.append(
@@ -218,7 +237,20 @@ def config_list() -> None:
218
237
  Panel(info_text, title="Configuration Sources", border_style="green")
219
238
  )
220
239
 
240
+ except (OSError, PermissionError) as e:
241
+ logger.error("config_list_file_access_error", error=str(e), exc_info=e)
242
+ toolkit.print(f"Error accessing configuration files: {e}", tag="error")
243
+ raise typer.Exit(1) from e
244
+ except (json.JSONDecodeError, ValueError) as e:
245
+ logger.error("config_list_parsing_error", error=str(e), exc_info=e)
246
+ toolkit.print(f"Configuration parsing error: {e}", tag="error")
247
+ raise typer.Exit(1) from e
248
+ except ImportError as e:
249
+ logger.error("config_list_import_error", error=str(e), exc_info=e)
250
+ toolkit.print(f"Module import error: {e}", tag="error")
251
+ raise typer.Exit(1) from e
221
252
  except Exception as e:
253
+ logger.error("config_list_unexpected_error", error=str(e), exc_info=e)
222
254
  toolkit.print(f"Error loading configuration: {e}", tag="error")
223
255
  raise typer.Exit(1) from e
224
256
 
@@ -243,16 +275,7 @@ def config_init(
243
275
  help="Overwrite existing configuration files",
244
276
  ),
245
277
  ) -> None:
246
- """Generate example configuration files.
247
-
248
- This command creates example configuration files with all available options
249
- and documentation comments.
250
-
251
- Examples:
252
- ccproxy config init # Create TOML config in default location
253
- ccproxy config init --output-dir ./config # Create in specific directory
254
- """
255
- # Validate format
278
+ """Generate example configuration files."""
256
279
  if format != "toml":
257
280
  toolkit = get_rich_toolkit()
258
281
  toolkit.print(
@@ -264,19 +287,15 @@ def config_init(
264
287
  toolkit = get_rich_toolkit()
265
288
 
266
289
  try:
267
- from ccproxy.config.discovery import get_ccproxy_config_dir
290
+ from ccproxy.config.utils import get_ccproxy_config_dir
268
291
 
269
- # Determine output directory
270
292
  if output_dir is None:
271
293
  output_dir = get_ccproxy_config_dir()
272
294
 
273
- # Create output directory if it doesn't exist
274
295
  output_dir.mkdir(parents=True, exist_ok=True)
275
296
 
276
- # Generate configuration dynamically from Settings model
277
297
  example_config = _generate_default_config_from_model(Settings)
278
298
 
279
- # Determine output file name
280
299
  if format == "toml":
281
300
  output_file = output_dir / "config.toml"
282
301
  if output_file.exists() and not force:
@@ -286,7 +305,6 @@ def config_init(
286
305
  )
287
306
  raise typer.Exit(1)
288
307
 
289
- # Write TOML with comments using dynamic generation
290
308
  _write_toml_config_with_comments(output_file, example_config, Settings)
291
309
 
292
310
  toolkit.print(
@@ -300,7 +318,24 @@ def config_init(
300
318
  toolkit.print(f" export CONFIG_FILE={output_file}", tag="command")
301
319
  toolkit.print(" ccproxy api", tag="command")
302
320
 
321
+ except (OSError, PermissionError) as e:
322
+ logger.error("config_init_file_access_error", error=str(e), exc_info=e)
323
+ toolkit.print(
324
+ f"Error creating configuration file (permission/IO error): {e}", tag="error"
325
+ )
326
+ raise typer.Exit(1) from e
327
+ except ImportError as e:
328
+ logger.error("config_init_import_error", error=str(e), exc_info=e)
329
+ toolkit.print(f"Module import error: {e}", tag="error")
330
+ raise typer.Exit(1) from e
331
+ except ValueError as e:
332
+ logger.error("config_init_value_error", error=str(e), exc_info=e)
333
+ toolkit.print(f"Configuration value error: {e}", tag="error")
334
+ raise typer.Exit(1) from e
303
335
  except Exception as e:
336
+ if isinstance(e, typer.Exit):
337
+ raise
338
+ logger.error("config_init_unexpected_error", error=str(e), exc_info=e)
304
339
  toolkit.print(f"Error creating configuration file: {e}", tag="error")
305
340
  raise typer.Exit(1) from e
306
341
 
@@ -325,23 +360,10 @@ def generate_token(
325
360
  help="Overwrite existing auth_token without confirmation",
326
361
  ),
327
362
  ) -> None:
328
- """Generate a secure random token for API authentication.
329
-
330
- This command generates a secure authentication token that can be used with
331
- both Anthropic and OpenAI compatible APIs.
332
-
333
- Use --save to write the token to a TOML configuration file.
334
-
335
- Examples:
336
- ccproxy config generate-token # Generate and display token
337
- ccproxy config generate-token --save # Generate and save to config
338
- ccproxy config generate-token --save --config-file custom.toml # Save to TOML config
339
- ccproxy config generate-token --save --force # Overwrite existing token
340
- """
363
+ """Generate a secure random token for API authentication."""
341
364
  toolkit = get_rich_toolkit()
342
365
 
343
366
  try:
344
- # Generate a secure token
345
367
  token = secrets.token_urlsafe(32)
346
368
 
347
369
  from rich.console import Console
@@ -349,7 +371,6 @@ def generate_token(
349
371
 
350
372
  console = Console()
351
373
 
352
- # Display the generated token
353
374
  console.print()
354
375
  console.print(
355
376
  Panel.fit(
@@ -359,7 +380,6 @@ def generate_token(
359
380
  )
360
381
  console.print()
361
382
 
362
- # Show environment variable commands - server first, then clients
363
383
  console.print("[bold]Server Environment Variables:[/bold]")
364
384
  console.print(f"[cyan]export SECURITY__AUTH_TOKEN={token}[/cyan]")
365
385
  console.print()
@@ -385,47 +405,40 @@ def generate_token(
385
405
 
386
406
  console.print("[bold]Usage with curl (using environment variables):[/bold]")
387
407
  console.print("[dim]Anthropic API:[/dim]")
388
- console.print('[cyan]curl -H "x-api-key: $ANTHROPIC_API_KEY" \\\\[/cyan]')
389
- console.print('[cyan] -H "Content-Type: application/json" \\\\[/cyan]')
408
+ console.print(r'[cyan]curl -H "x-api-key: $ANTHROPIC_API_KEY" \ [/cyan]')
409
+ console.print(r'[cyan] -H "Content-Type: application/json" \ [/cyan]')
390
410
  console.print('[cyan] "$ANTHROPIC_BASE_URL/v1/messages"[/cyan]')
391
411
  console.print()
392
412
  console.print("[dim]OpenAI API:[/dim]")
393
413
  console.print(
394
- '[cyan]curl -H "Authorization: Bearer $OPENAI_API_KEY" \\\\[/cyan]'
414
+ r'[cyan]curl -H "Authorization: Bearer $OPENAI_API_KEY" \ [/cyan]'
395
415
  )
396
- console.print('[cyan] -H "Content-Type: application/json" \\\\[/cyan]')
416
+ console.print(r'[cyan] -H "Content-Type: application/json" \ [/cyan]')
397
417
  console.print('[cyan] "$OPENAI_BASE_URL/v1/chat/completions"[/cyan]')
398
418
  console.print()
399
419
 
400
- # Mention the save functionality if not using it
401
420
  if not save:
402
421
  console.print(
403
422
  "[dim]Tip: Use --save to write this token to a configuration file[/dim]"
404
423
  )
405
424
  console.print()
406
425
 
407
- # Save to config file if requested
408
426
  if save:
409
- # Determine config file path
410
427
  if config_file is None:
411
- # Try to find existing config file or create default
412
- from ccproxy.config.discovery import find_toml_config_file
428
+ from ccproxy.config.utils import find_toml_config_file
413
429
 
414
430
  config_file = find_toml_config_file()
415
431
 
416
432
  if config_file is None:
417
- # Create default config file in current directory
418
433
  config_file = Path(".ccproxy.toml")
419
434
 
420
435
  console.print(
421
436
  f"[bold]Saving token to configuration file:[/bold] {config_file}"
422
437
  )
423
438
 
424
- # Detect file format from extension
425
439
  file_format = _detect_config_format(config_file)
426
440
  console.print(f"[dim]Detected format: {file_format.upper()}[/dim]")
427
441
 
428
- # Read existing config or create new one using existing Settings functionality
429
442
  config_data = {}
430
443
  existing_token = None
431
444
 
@@ -436,7 +449,32 @@ def generate_token(
436
449
  config_data = Settings.load_config_file(config_file)
437
450
  existing_token = config_data.get("auth_token")
438
451
  console.print("[dim]Found existing configuration file[/dim]")
452
+ except (OSError, PermissionError) as e:
453
+ logger.warning(
454
+ "generate_token_config_file_access_error",
455
+ error=str(e),
456
+ exc_info=e,
457
+ )
458
+ console.print(
459
+ f"[yellow]Warning: Could not access existing config file: {e}[/yellow]"
460
+ )
461
+ console.print("[dim]Will create new configuration file[/dim]")
462
+ except (json.JSONDecodeError, ValueError) as e:
463
+ logger.warning(
464
+ "generate_token_config_file_parse_error",
465
+ error=str(e),
466
+ exc_info=e,
467
+ )
468
+ console.print(
469
+ f"[yellow]Warning: Could not parse existing config file: {e}[/yellow]"
470
+ )
471
+ console.print("[dim]Will create new configuration file[/dim]")
439
472
  except Exception as e:
473
+ logger.warning(
474
+ "generate_token_config_file_read_error",
475
+ error=str(e),
476
+ exc_info=e,
477
+ )
440
478
  console.print(
441
479
  f"[yellow]Warning: Could not read existing config file: {e}[/yellow]"
442
480
  )
@@ -444,7 +482,6 @@ def generate_token(
444
482
  else:
445
483
  console.print("[dim]Will create new configuration file[/dim]")
446
484
 
447
- # Check for existing token and ask for confirmation if needed
448
485
  if existing_token and not force:
449
486
  console.print()
450
487
  console.print(
@@ -458,10 +495,8 @@ def generate_token(
458
495
  console.print("[dim]Token generation cancelled[/dim]")
459
496
  return
460
497
 
461
- # Update auth_token in config
462
498
  config_data["auth_token"] = token
463
499
 
464
- # Write updated config in the appropriate format
465
500
  _write_config_file(config_file, config_data, file_format)
466
501
 
467
502
  console.print(f"[green]✓[/green] Token saved to {config_file}")
@@ -473,7 +508,20 @@ def generate_token(
473
508
  console.print(f"[cyan]export CONFIG_FILE={config_file}[/cyan]")
474
509
  console.print("[cyan]ccproxy api[/cyan]")
475
510
 
511
+ except (OSError, PermissionError) as e:
512
+ logger.error("generate_token_file_write_error", error=str(e), exc_info=e)
513
+ toolkit.print(f"Error writing configuration file: {e}", tag="error")
514
+ raise typer.Exit(1) from e
515
+ except ValueError as e:
516
+ logger.error("generate_token_value_error", error=str(e), exc_info=e)
517
+ toolkit.print(f"Token generation configuration error: {e}", tag="error")
518
+ raise typer.Exit(1) from e
519
+ except ImportError as e:
520
+ logger.error("generate_token_import_error", error=str(e), exc_info=e)
521
+ toolkit.print(f"Module import error: {e}", tag="error")
522
+ raise typer.Exit(1) from e
476
523
  except Exception as e:
524
+ logger.error("generate_token_unexpected_error", error=str(e), exc_info=e)
477
525
  toolkit.print(f"Error generating token: {e}", tag="error")
478
526
  raise typer.Exit(1) from e
479
527
 
@@ -484,7 +532,6 @@ def _detect_config_format(config_file: Path) -> str:
484
532
  if suffix in [".toml"]:
485
533
  return "toml"
486
534
  else:
487
- # Only TOML is supported
488
535
  return "toml"
489
536
 
490
537
 
@@ -492,22 +539,30 @@ def _generate_default_config_from_model(
492
539
  settings_class: type[Settings],
493
540
  ) -> dict[str, Any]:
494
541
  """Generate a default configuration dictionary from the Settings model."""
495
- # Create a default instance to get all default values
542
+ from ccproxy.config.settings import DEFAULT_ENABLED_PLUGINS
543
+
496
544
  default_settings = settings_class()
497
545
 
498
- config_data = {}
546
+ config_data: dict[str, Any] = {}
547
+
548
+ for field_name, field_info in settings_class.model_fields.items():
549
+ if _is_hidden_in_example(field_info):
550
+ continue
499
551
 
500
- # Iterate through all fields and extract their default values
501
- for field_name, _field_info in settings_class.model_fields.items():
502
552
  field_value = getattr(default_settings, field_name)
503
553
 
554
+ # Special case: enabled_plugins should use DEFAULT_ENABLED_PLUGINS for config init
555
+ if field_name == "enabled_plugins" and field_value is None:
556
+ config_data[field_name] = DEFAULT_ENABLED_PLUGINS
557
+ continue
558
+
504
559
  if isinstance(field_value, BaseModel):
505
- # For nested models, recursively generate their config
506
- config_data[field_name] = _generate_nested_config_from_model(field_value)
560
+ nested_config = _generate_nested_config_from_model(field_value)
561
+ if nested_config:
562
+ config_data[field_name] = nested_config
507
563
  else:
508
- # Convert Path objects to strings for JSON serialization
509
564
  if isinstance(field_value, Path):
510
- config_data[field_name] = str(field_value) # type: ignore[assignment]
565
+ config_data[field_name] = str(field_value)
511
566
  else:
512
567
  config_data[field_name] = field_value
513
568
 
@@ -516,17 +571,21 @@ def _generate_default_config_from_model(
516
571
 
517
572
  def _generate_nested_config_from_model(model: BaseModel) -> dict[str, Any]:
518
573
  """Generate configuration for nested models."""
519
- config_data = {}
574
+ config_data: dict[str, Any] = {}
575
+
576
+ for field_name, field_info in model.model_fields.items():
577
+ if _is_hidden_in_example(field_info):
578
+ continue
520
579
 
521
- for field_name, _field_info in model.model_fields.items():
522
580
  field_value = getattr(model, field_name)
523
581
 
524
582
  if isinstance(field_value, BaseModel):
525
- config_data[field_name] = _generate_nested_config_from_model(field_value)
583
+ nested_config = _generate_nested_config_from_model(field_value)
584
+ if nested_config:
585
+ config_data[field_name] = nested_config
526
586
  else:
527
- # Convert Path objects to strings for JSON serialization
528
587
  if isinstance(field_value, Path):
529
- config_data[field_name] = str(field_value) # type: ignore[assignment]
588
+ config_data[field_name] = str(field_value)
530
589
  else:
531
590
  config_data[field_name] = field_value
532
591
 
@@ -543,19 +602,34 @@ def _write_toml_config_with_comments(
543
602
  f.write("# Most settings are commented out with their default values\n")
544
603
  f.write("# Uncomment and modify as needed\n\n")
545
604
 
546
- # Write each top-level section
547
- for field_name, _field_info in settings_class.model_fields.items():
605
+ # Reorder fields to put enabled_plugins first
606
+ field_items = list(settings_class.model_fields.items())
607
+ priority_fields = ["enabled_plugins", "disabled_plugins"]
608
+
609
+ # Separate priority fields from others
610
+ priority_items = [
611
+ (name, info) for name, info in field_items if name in priority_fields
612
+ ]
613
+ other_items = [
614
+ (name, info) for name, info in field_items if name not in priority_fields
615
+ ]
616
+
617
+ # Combine with priority fields first
618
+ ordered_items = priority_items + other_items
619
+
620
+ for field_name, field_info in ordered_items:
621
+ if _is_hidden_in_example(field_info):
622
+ continue
623
+
548
624
  field_value = config_data.get(field_name)
549
- description = _get_field_description(_field_info)
625
+ description = _get_field_description(field_info)
550
626
 
551
627
  f.write(f"# {description}\n")
552
628
 
553
629
  if isinstance(field_value, dict):
554
- # This is a nested model - write as a TOML section
555
630
  f.write(f"# [{field_name}]\n")
556
631
  _write_toml_section(f, field_value, prefix="# ", level=0)
557
632
  else:
558
- # Simple field - write as commented line
559
633
  formatted_value = _format_config_value_for_toml(field_value)
560
634
  f.write(f"# {field_name} = {formatted_value}\n")
561
635
 
@@ -568,11 +642,9 @@ def _write_toml_section(
568
642
  """Write a TOML section with proper indentation and commenting."""
569
643
  for key, value in data.items():
570
644
  if isinstance(value, dict):
571
- # Nested section
572
645
  f.write(f"{prefix}[{key}]\n")
573
646
  _write_toml_section(f, value, prefix, level + 1)
574
647
  else:
575
- # Simple value
576
648
  formatted_value = _format_config_value_for_toml(value)
577
649
  f.write(f"{prefix}{key} = {formatted_value}\n")
578
650
 
@@ -584,28 +656,30 @@ def _format_config_value_for_toml(value: Any) -> str:
584
656
  elif isinstance(value, bool):
585
657
  return "true" if value else "false"
586
658
  elif isinstance(value, str):
587
- return f'"{value}"'
659
+ return f'"{value}"' # Correctly escape quotes within strings
588
660
  elif isinstance(value, int | float):
589
661
  return str(value)
590
662
  elif isinstance(value, list):
591
663
  if not value:
592
664
  return "[]"
593
- # Format list items
594
665
  formatted_items = []
595
666
  for item in value:
596
667
  if isinstance(item, str):
597
- formatted_items.append(f'"{item}"')
668
+ formatted_items.append(
669
+ f'"{item}"'
670
+ ) # Correctly escape quotes within list strings
598
671
  else:
599
672
  formatted_items.append(str(item))
600
673
  return f"[{', '.join(formatted_items)}]"
601
674
  elif isinstance(value, dict):
602
675
  if not value:
603
- return "{}"
604
- # Format dict as inline table
676
+ return "{{}}"
605
677
  formatted_items = []
606
678
  for k, v in value.items():
607
679
  if isinstance(v, str):
608
- formatted_items.append(f'{k} = "{v}"')
680
+ formatted_items.append(
681
+ f'{k} = "{v}"'
682
+ ) # Correctly escape quotes within dict strings
609
683
  else:
610
684
  formatted_items.append(f"{k} = {v}")
611
685
  return f"{{{', '.join(formatted_items)}}}"
@@ -613,32 +687,6 @@ def _format_config_value_for_toml(value: Any) -> str:
613
687
  return str(value)
614
688
 
615
689
 
616
- def _write_json_config_with_comments(
617
- config_file: Path, config_data: dict[str, Any]
618
- ) -> None:
619
- """Write configuration data to a JSON file with formatting."""
620
-
621
- def convert_for_json(obj: Any) -> Any:
622
- """Convert objects to JSON-serializable format."""
623
- if isinstance(obj, Path):
624
- return str(obj)
625
- elif isinstance(obj, dict):
626
- return {k: convert_for_json(v) for k, v in obj.items()}
627
- elif isinstance(obj, list):
628
- return [convert_for_json(item) for item in obj]
629
- elif hasattr(obj, "__dict__"):
630
- # Handle complex objects by converting to string
631
- return str(obj)
632
- else:
633
- return obj
634
-
635
- serializable_data = convert_for_json(config_data)
636
-
637
- with config_file.open("w", encoding="utf-8") as f:
638
- json.dump(serializable_data, f, indent=2, sort_keys=True)
639
- f.write("\n")
640
-
641
-
642
690
  def _write_config_file(
643
691
  config_file: Path, config_data: dict[str, Any], file_format: str
644
692
  ) -> None:
@@ -649,118 +697,3 @@ def _write_config_file(
649
697
  raise ValueError(
650
698
  f"Unsupported config format: {file_format}. Only TOML is supported."
651
699
  )
652
-
653
-
654
- def _write_toml_config(config_file: Path, config_data: dict[str, Any]) -> None:
655
- """Write configuration data to a TOML file with proper formatting."""
656
- try:
657
- # Create a nicely formatted TOML file
658
- with config_file.open("w", encoding="utf-8") as f:
659
- f.write("# CCProxy API Configuration\n")
660
- f.write("# Generated by ccproxy config generate-token\n\n")
661
-
662
- # Write server settings
663
- if any(
664
- key in config_data
665
- for key in ["host", "port", "log_level", "workers", "reload"]
666
- ):
667
- f.write("# Server configuration\n")
668
- if "host" in config_data:
669
- f.write(f'host = "{config_data["host"]}"\n')
670
- if "port" in config_data:
671
- f.write(f"port = {config_data['port']}\n")
672
- if "log_level" in config_data:
673
- f.write(f'log_level = "{config_data["log_level"]}"\n')
674
- if "workers" in config_data:
675
- f.write(f"workers = {config_data['workers']}\n")
676
- if "reload" in config_data:
677
- f.write(f"reload = {str(config_data['reload']).lower()}\n")
678
- f.write("\n")
679
-
680
- # Write security settings
681
- if any(key in config_data for key in ["auth_token", "cors_origins"]):
682
- f.write("# Security configuration\n")
683
- if "auth_token" in config_data:
684
- f.write(f'auth_token = "{config_data["auth_token"]}"\n')
685
- if "cors_origins" in config_data:
686
- origins = config_data["cors_origins"]
687
- if isinstance(origins, list):
688
- origins_str = '", "'.join(origins)
689
- f.write(f'cors_origins = ["{origins_str}"]\n')
690
- else:
691
- f.write(f'cors_origins = ["{origins}"]\n')
692
- f.write("\n")
693
-
694
- # Write Claude CLI configuration
695
- if "claude_cli_path" in config_data:
696
- f.write("# Claude CLI configuration\n")
697
- if config_data["claude_cli_path"]:
698
- f.write(f'claude_cli_path = "{config_data["claude_cli_path"]}"\n')
699
- else:
700
- f.write(
701
- '# claude_cli_path = "/path/to/claude" # Auto-detect if not set\n'
702
- )
703
- f.write("\n")
704
-
705
- # Write Docker settings
706
- if "docker" in config_data:
707
- docker_settings = config_data["docker"]
708
- f.write("# Docker configuration\n")
709
- f.write("[docker]\n")
710
-
711
- for key, value in docker_settings.items():
712
- if isinstance(value, str):
713
- f.write(f'{key} = "{value}"\n')
714
- elif isinstance(value, bool):
715
- f.write(f"{key} = {str(value).lower()}\n")
716
- elif isinstance(value, int | float):
717
- f.write(f"{key} = {value}\n")
718
- elif isinstance(value, list):
719
- if value: # Only write non-empty lists
720
- if all(isinstance(item, str) for item in value):
721
- items_str = '", "'.join(value)
722
- f.write(f'{key} = ["{items_str}"]\n')
723
- else:
724
- f.write(f"{key} = {value}\n")
725
- else:
726
- f.write(f"{key} = []\n")
727
- elif isinstance(value, dict):
728
- if value: # Only write non-empty dicts
729
- f.write(f"{key} = {json.dumps(value)}\n")
730
- else:
731
- f.write(f"{key} = {{}}\n")
732
- elif value is None:
733
- f.write(f"# {key} = null # Not configured\n")
734
- f.write("\n")
735
-
736
- # Write any remaining top-level settings
737
- written_keys = {
738
- "host",
739
- "port",
740
- "log_level",
741
- "workers",
742
- "reload",
743
- "auth_token",
744
- "cors_origins",
745
- "claude_cli_path",
746
- "docker",
747
- }
748
- remaining_keys = set(config_data.keys()) - written_keys
749
-
750
- if remaining_keys:
751
- f.write("# Additional settings\n")
752
- for key in sorted(remaining_keys):
753
- value = config_data[key]
754
- if isinstance(value, str):
755
- f.write(f'{key} = "{value}"\n')
756
- elif isinstance(value, bool):
757
- f.write(f"{key} = {str(value).lower()}\n")
758
- elif isinstance(value, int | float):
759
- f.write(f"{key} = {value}\n")
760
- elif isinstance(value, list | dict):
761
- f.write(f"{key} = {json.dumps(value)}\n")
762
- elif value is None:
763
- f.write(f"# {key} = null\n")
764
-
765
- except Exception as e:
766
- raise ValueError(f"Failed to write TOML configuration: {e}") from e