ccproxy-api 0.1.6__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 +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  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 +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  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 +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  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 +95 -342
  387. ccproxy/utils/version_checker.py +279 -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.6.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 -1231
  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 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  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.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.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.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/config/core.py ADDED
@@ -0,0 +1,227 @@
1
+ """Core configuration settings - server, HTTP, CORS, logging, and plugins."""
2
+
3
+ from pathlib import Path
4
+ from typing import Literal, cast
5
+
6
+ from pydantic import BaseModel, Field, field_validator
7
+
8
+ from ccproxy.core.system import get_xdg_config_home
9
+
10
+
11
+ class ServerSettings(BaseModel):
12
+ """Server-specific configuration settings."""
13
+
14
+ host: str = Field(
15
+ default="127.0.0.1",
16
+ description="Server host address",
17
+ )
18
+
19
+ port: int = Field(
20
+ default=8000,
21
+ description="Server port number",
22
+ ge=1,
23
+ le=65535,
24
+ )
25
+
26
+ workers: int = Field(
27
+ default=1,
28
+ description="Number of worker processes",
29
+ ge=1,
30
+ le=32,
31
+ )
32
+
33
+ reload: bool = Field(
34
+ default=False,
35
+ description="Enable auto-reload for development",
36
+ )
37
+
38
+ bypass_mode: bool = Field(
39
+ default=False,
40
+ description="Enable bypass mode for testing (uses mock responses instead of real API calls)",
41
+ )
42
+
43
+
44
+ class HTTPSettings(BaseModel):
45
+ """HTTP client configuration settings.
46
+
47
+ Controls how the core HTTP client handles compression and other HTTP-level settings.
48
+ """
49
+
50
+ compression_enabled: bool = Field(
51
+ default=True,
52
+ description="Enable compression for provider requests (Accept-Encoding header)",
53
+ )
54
+
55
+ accept_encoding: str = Field(
56
+ default="gzip, deflate",
57
+ description="Accept-Encoding header value when compression is enabled",
58
+ )
59
+
60
+
61
+ class CORSSettings(BaseModel):
62
+ """CORS-specific configuration settings."""
63
+
64
+ origins: list[str] = Field(
65
+ default_factory=lambda: [
66
+ "vscode-file://vscode-app",
67
+ "http://localhost/*",
68
+ "http://localhost:*/*",
69
+ "http://127.0.0.1:*/*",
70
+ "http://127.0.0.1:/*",
71
+ ],
72
+ description="CORS allowed origins (avoid using '*' for security)",
73
+ )
74
+
75
+ credentials: bool = Field(
76
+ default=True,
77
+ description="CORS allow credentials",
78
+ )
79
+
80
+ methods: list[str] = Field(
81
+ default_factory=lambda: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
82
+ description="CORS allowed methods",
83
+ )
84
+
85
+ headers: list[str] = Field(
86
+ default_factory=lambda: [
87
+ "Content-Type",
88
+ "Authorization",
89
+ "Accept",
90
+ "Origin",
91
+ "X-Requested-With",
92
+ ],
93
+ description="CORS allowed headers",
94
+ )
95
+
96
+ origin_regex: str | None = Field(
97
+ default=None,
98
+ description="CORS origin regex pattern",
99
+ )
100
+
101
+ expose_headers: list[str] = Field(
102
+ default_factory=list,
103
+ description="CORS exposed headers",
104
+ )
105
+
106
+ max_age: int = Field(
107
+ default=600,
108
+ description="CORS preflight max age in seconds",
109
+ ge=0,
110
+ )
111
+
112
+
113
+ # === Logging Configuration ===
114
+
115
+
116
+ LogLevelName = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "TRACE"]
117
+ LOG_LEVEL_OPTIONS: tuple[str, ...] = (
118
+ "DEBUG",
119
+ "INFO",
120
+ "WARNING",
121
+ "ERROR",
122
+ "CRITICAL",
123
+ "TRACE",
124
+ )
125
+
126
+ LogFormatName = Literal["auto", "rich", "json", "plain"]
127
+ LOG_FORMAT_OPTIONS: tuple[str, ...] = ("auto", "rich", "json", "plain")
128
+
129
+ LOG_FORMAT_DESCRIPTION = "Logging format: 'rich', 'json', 'plain', or 'auto' (auto-selects based on environment)"
130
+
131
+
132
+ class LoggingSettings(BaseModel):
133
+ """Centralized logging configuration - core app only."""
134
+
135
+ level: LogLevelName = Field(
136
+ default="INFO",
137
+ description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL, TRACE)",
138
+ )
139
+
140
+ format: LogFormatName = Field(
141
+ default="auto",
142
+ description=LOG_FORMAT_DESCRIPTION,
143
+ )
144
+
145
+ file: str | None = Field(
146
+ default=None,
147
+ description="Path to JSON log file. If specified, logs will be written to this file in JSON format",
148
+ )
149
+
150
+ verbose_api: bool = Field(
151
+ default=False,
152
+ description="Enable verbose API request/response logging",
153
+ )
154
+
155
+ request_log_dir: str | None = Field(
156
+ default=None,
157
+ description="Directory to save individual request/response logs when verbose_api is enabled",
158
+ )
159
+
160
+ plugin_log_base_dir: str = Field(
161
+ default="/tmp/ccproxy",
162
+ description="Shared base directory for all plugin log outputs",
163
+ )
164
+
165
+ @field_validator("level", mode="before")
166
+ @classmethod
167
+ def validate_log_level(cls, value: LogLevelName | str) -> LogLevelName:
168
+ """Validate and normalize log level."""
169
+ if isinstance(value, str):
170
+ candidate = value.upper()
171
+ else:
172
+ candidate = value
173
+
174
+ if candidate not in LOG_LEVEL_OPTIONS:
175
+ raise ValueError(
176
+ f"Invalid log level: {value}. Must be one of {list(LOG_LEVEL_OPTIONS)}"
177
+ )
178
+
179
+ return cast(LogLevelName, candidate)
180
+
181
+ @field_validator("format", mode="before")
182
+ @classmethod
183
+ def validate_log_format(cls, value: LogFormatName | str) -> LogFormatName:
184
+ """Validate and normalize log format."""
185
+ if isinstance(value, str):
186
+ candidate = value.lower()
187
+ else:
188
+ candidate = value
189
+
190
+ if candidate not in LOG_FORMAT_OPTIONS:
191
+ raise ValueError(
192
+ f"Invalid log format: {value}. Must be one of {list(LOG_FORMAT_OPTIONS)}"
193
+ )
194
+
195
+ return cast(LogFormatName, candidate)
196
+
197
+
198
+ def _default_plugin_directories() -> list[Path]:
199
+ """Default directories scanned for filesystem plugins."""
200
+
201
+ # package_plugins = Path(__file__).resolve().parent.parent.parent / "plugins"
202
+ # we don't include packages from ccproxy.plugins because they
203
+ # are using importlib.metadata entry points
204
+ user_plugins = get_xdg_config_home() / "ccproxy" / "plugins"
205
+
206
+ seen: set[Path] = set()
207
+ ordered: list[Path] = []
208
+ for candidate in [user_plugins]:
209
+ normalized = candidate.resolve()
210
+ if normalized in seen:
211
+ continue
212
+ seen.add(normalized)
213
+ ordered.append(candidate)
214
+ return ordered
215
+
216
+
217
+ class PluginDiscoverySettings(BaseModel):
218
+ """Configuration for filesystem plugin discovery."""
219
+
220
+ directories: list[Path] = Field(
221
+ default_factory=_default_plugin_directories,
222
+ description=(
223
+ "Ordered directories scanned for local plugins."
224
+ " Defaults to the bundled ccproxy/plugins directory and"
225
+ " ${XDG_CONFIG_HOME}/ccproxy/plugins."
226
+ ),
227
+ )
@@ -0,0 +1,232 @@
1
+ """Utility functions for generating environment variable configuration from Pydantic models."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, TextIO, TypeVar
7
+
8
+ from pydantic import BaseModel
9
+ from pydantic.fields import FieldInfo
10
+
11
+
12
+ T = TypeVar("T", bound=BaseModel)
13
+
14
+
15
+ def is_hidden_in_example(field_info: FieldInfo) -> bool:
16
+ """Determine if a field should be omitted from generated example configs."""
17
+ if bool(field_info.exclude):
18
+ return True
19
+
20
+ extra = getattr(field_info, "json_schema_extra", None) or {}
21
+ return bool(extra.get("config_example_hidden"))
22
+
23
+
24
+ def get_field_description(field_info: FieldInfo) -> str:
25
+ """Get a human-readable description from a Pydantic field."""
26
+ if field_info.description:
27
+ return field_info.description
28
+ return "Configuration setting"
29
+
30
+
31
+ def format_value_for_env(value: Any) -> str:
32
+ """Format a configuration value for environment variable output.
33
+
34
+ Args:
35
+ value: The value to format
36
+
37
+ Returns:
38
+ String representation suitable for environment variables
39
+ """
40
+ if value is None:
41
+ return ""
42
+ elif isinstance(value, bool):
43
+ return "true" if value else "false"
44
+ elif isinstance(value, str):
45
+ return value
46
+ elif isinstance(value, int | float):
47
+ return str(value)
48
+ elif isinstance(value, list | dict):
49
+ # For complex types, try JSON representation
50
+ try:
51
+ return json.dumps(value)
52
+ except (TypeError, ValueError):
53
+ # If JSON serialization fails, use string representation
54
+ return str(value)
55
+ else:
56
+ return str(value)
57
+
58
+
59
+ def generate_env_vars_from_model(
60
+ model_class: type[T],
61
+ prefix: str = "",
62
+ include_hidden: bool = False,
63
+ ) -> list[tuple[str, Any, str]]:
64
+ """Generate environment variable names and values from a Pydantic model.
65
+
66
+ Args:
67
+ model_class: The Pydantic model class to generate env vars from
68
+ prefix: Prefix for env var names (e.g., "SERVER" or "PLUGINS__MAX_TOKENS")
69
+ include_hidden: Whether to include fields marked as hidden in examples
70
+
71
+ Returns:
72
+ List of tuples: (env_var_name, value, description)
73
+ """
74
+ default_instance = model_class()
75
+ env_vars: list[tuple[str, Any, str]] = []
76
+
77
+ for field_name, field_info in model_class.model_fields.items():
78
+ if not include_hidden and is_hidden_in_example(field_info):
79
+ continue
80
+
81
+ field_value = getattr(default_instance, field_name)
82
+ env_var_name = (
83
+ f"{prefix}__{field_name.upper()}" if prefix else field_name.upper()
84
+ )
85
+ description = get_field_description(field_info)
86
+
87
+ if isinstance(field_value, BaseModel):
88
+ # Recursively handle nested models
89
+ nested_vars = generate_env_vars_from_model(
90
+ field_value.__class__, env_var_name, include_hidden
91
+ )
92
+ env_vars.extend(nested_vars)
93
+ else:
94
+ # Convert Path to string
95
+ if isinstance(field_value, Path):
96
+ field_value = str(field_value)
97
+
98
+ env_vars.append((env_var_name, field_value, description))
99
+
100
+ return env_vars
101
+
102
+
103
+ def generate_env_config(
104
+ model_class: type[BaseModel],
105
+ prefix: str = "",
106
+ include_hidden: bool = False,
107
+ commented: bool = True,
108
+ header_comment: str | None = None,
109
+ export_format: bool = True,
110
+ ) -> str:
111
+ """Generate environment variable configuration string.
112
+
113
+ Args:
114
+ model_class: The Pydantic model class to generate config from
115
+ prefix: Prefix for env var names (e.g., "SERVER" or "PLUGINS__MAX_TOKENS")
116
+ include_hidden: Whether to include fields marked as hidden in examples
117
+ commented: Whether to comment out all settings (default True)
118
+ header_comment: Optional custom header comment
119
+ export_format: Whether to use 'export VAR=value' format (True) or 'VAR=value' (False)
120
+
121
+ Returns:
122
+ Environment variable configuration as a string
123
+ """
124
+ lines = []
125
+
126
+ # Write header
127
+ if header_comment:
128
+ for line in header_comment.split("\n"):
129
+ lines.append(f"# {line}" if line else "#")
130
+ else:
131
+ lines.append("# Environment Variable Configuration")
132
+ lines.append("# This file contains environment variables for the application")
133
+ if commented:
134
+ lines.append("# Uncomment and set values as needed")
135
+ lines.append("")
136
+
137
+ # Generate environment variables
138
+ env_vars = generate_env_vars_from_model(model_class, prefix, include_hidden)
139
+
140
+ comment_prefix = "# " if commented else ""
141
+ export_prefix = "export " if export_format else ""
142
+
143
+ for env_var_name, value, description in env_vars:
144
+ # Write description as comment
145
+ lines.append(f"# {description}")
146
+
147
+ # Format the value
148
+ formatted_value = format_value_for_env(value)
149
+
150
+ # Write the environment variable
151
+ if formatted_value:
152
+ # Quote values that contain spaces or special characters
153
+ if isinstance(value, str) and (
154
+ " " in value or any(c in value for c in ["$", '"', "'", "\\"])
155
+ ):
156
+ lines.append(
157
+ f'{comment_prefix}{export_prefix}{env_var_name}="{formatted_value}"'
158
+ )
159
+ else:
160
+ lines.append(
161
+ f"{comment_prefix}{export_prefix}{env_var_name}={formatted_value}"
162
+ )
163
+ else:
164
+ # Empty value
165
+ lines.append(f'{comment_prefix}{export_prefix}{env_var_name}=""')
166
+
167
+ lines.append("")
168
+
169
+ return "\n".join(lines)
170
+
171
+
172
+ def write_env_config(
173
+ output: TextIO | Path | str,
174
+ model_class: type[BaseModel],
175
+ prefix: str = "",
176
+ include_hidden: bool = False,
177
+ commented: bool = True,
178
+ header_comment: str | None = None,
179
+ export_format: bool = True,
180
+ ) -> None:
181
+ """Write environment variable configuration directly to a stream or file.
182
+
183
+ Args:
184
+ output: Output destination - can be a TextIO stream (file, StringIO, stdout),
185
+ a Path object, or a string path to a file
186
+ model_class: The Pydantic model class to generate config from
187
+ prefix: Prefix for env var names (e.g., "SERVER" or "PLUGINS__MAX_TOKENS")
188
+ include_hidden: Whether to include fields marked as hidden in examples
189
+ commented: Whether to comment out all settings (default True)
190
+ header_comment: Optional custom header comment
191
+ export_format: Whether to use 'export VAR=value' format (default True)
192
+
193
+ Examples:
194
+ # Write to stdout
195
+ write_env_config(sys.stdout, Settings)
196
+
197
+ # Write to file
198
+ write_env_config("env.sh", Settings)
199
+ write_env_config(Path("env.sh"), Settings)
200
+
201
+ # Write to StringIO
202
+ buffer = StringIO()
203
+ write_env_config(buffer, Settings)
204
+ content = buffer.getvalue()
205
+
206
+ # Write with prefix for plugin
207
+ write_env_config(sys.stdout, MaxTokensConfig, prefix="PLUGINS__MAX_TOKENS")
208
+
209
+ # Write without export (for .env files)
210
+ write_env_config("env.sh", Settings, export_format=False)
211
+ """
212
+ # Generate env config string
213
+ env_string = generate_env_config(
214
+ model_class=model_class,
215
+ prefix=prefix,
216
+ include_hidden=include_hidden,
217
+ commented=commented,
218
+ header_comment=header_comment,
219
+ export_format=export_format,
220
+ )
221
+
222
+ # Determine output type and write
223
+ if isinstance(output, str | Path):
224
+ # Write to file path
225
+ Path(output).write_text(env_string, encoding="utf-8")
226
+ else:
227
+ # Write to stream (TextIO, stdout, StringIO, etc.)
228
+ output.write(env_string)
229
+ if output not in (sys.stdout, sys.stderr):
230
+ # Ensure trailing newline for files (not needed for stdout/stderr)
231
+ if not env_string.endswith("\n"):
232
+ output.write("\n")
@@ -0,0 +1,67 @@
1
+ """Runtime configuration settings - binary resolution configuration."""
2
+
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+
6
+ # === Binary Resolution Configuration ===
7
+
8
+
9
+ class BinarySettings(BaseModel):
10
+ """Binary resolution and package manager fallback settings."""
11
+
12
+ fallback_enabled: bool = Field(
13
+ default=True,
14
+ description="Enable package manager fallback when binaries are not found",
15
+ )
16
+
17
+ package_manager_only: bool = Field(
18
+ default=True,
19
+ description="Skip direct binary lookup and use package managers exclusively",
20
+ )
21
+
22
+ preferred_package_manager: str | None = Field(
23
+ default=None,
24
+ description="Preferred package manager (bunx, pnpm, npx). If not set, auto-detects based on availability",
25
+ )
26
+
27
+ package_manager_priority: list[str] = Field(
28
+ default_factory=lambda: ["bunx", "pnpm", "npx"],
29
+ description="Priority order for trying package managers when preferred is not set",
30
+ )
31
+
32
+ cache_results: bool = Field(
33
+ default=True,
34
+ description="Cache binary resolution results to avoid repeated lookups",
35
+ )
36
+
37
+ @field_validator("preferred_package_manager")
38
+ @classmethod
39
+ def validate_preferred_package_manager(cls, v: str | None) -> str | None:
40
+ """Validate preferred package manager."""
41
+ if v is not None:
42
+ valid_managers = ["bunx", "pnpm", "npx"]
43
+ if v not in valid_managers:
44
+ raise ValueError(
45
+ f"Invalid package manager: {v}. Must be one of {valid_managers}"
46
+ )
47
+ return v
48
+
49
+ @field_validator("package_manager_priority")
50
+ @classmethod
51
+ def validate_package_manager_priority(cls, v: list[str]) -> list[str]:
52
+ """Validate package manager priority list."""
53
+ valid_managers = {"bunx", "pnpm", "npx"}
54
+ for manager in v:
55
+ if manager not in valid_managers:
56
+ raise ValueError(
57
+ f"Invalid package manager in priority list: {manager}. "
58
+ f"Must be one of {valid_managers}"
59
+ )
60
+ # Remove duplicates while preserving order
61
+ seen = set()
62
+ result = []
63
+ for manager in v:
64
+ if manager not in seen:
65
+ seen.add(manager)
66
+ result.append(manager)
67
+ return result
@@ -1,16 +1,49 @@
1
- """Security configuration settings."""
1
+ """Security and authentication configuration settings."""
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
6
+
7
+
8
+ # === Authentication Configuration ===
9
+
10
+
11
+ class AuthSettings(BaseModel):
12
+ """Configuration for authentication behavior and caching."""
13
+
14
+ model_config = ConfigDict(extra="ignore")
15
+
16
+ credentials_ttl_seconds: float = Field(
17
+ 3600.0,
18
+ description=(
19
+ "Cache duration for loaded credentials before rechecking storage. "
20
+ "Use nested env var AUTH__CREDENTIALS_TTL_SECONDS to override."
21
+ ),
22
+ ge=0.0,
23
+ )
24
+
25
+
26
+ # === Security Configuration ===
4
27
 
5
28
 
6
29
  class SecuritySettings(BaseModel):
7
30
  """Security-specific configuration settings."""
8
31
 
9
- auth_token: str | None = Field(
32
+ auth_token: SecretStr | None = Field(
10
33
  default=None,
11
34
  description="Bearer token for API authentication (optional)",
12
35
  )
13
36
 
37
+ @field_validator("auth_token", mode="before")
38
+ @classmethod
39
+ def validate_auth_token(cls, v: Any) -> Any:
40
+ """Convert string values to SecretStr."""
41
+ if v is None:
42
+ return None
43
+ if isinstance(v, str):
44
+ return SecretStr(v)
45
+ return v
46
+
14
47
  confirmation_timeout_seconds: int = Field(
15
48
  default=30,
16
49
  ge=5,