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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,437 @@
1
+ """Centralized CLI detection service for all plugins.
2
+
3
+ This module provides a unified interface for detecting CLI binaries,
4
+ checking versions, and managing CLI-related state across all plugins.
5
+ It eliminates duplicate CLI detection logic by consolidating common patterns.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import re
11
+ from typing import Any, NamedTuple
12
+
13
+ import structlog
14
+
15
+ from ccproxy.config.settings import Settings
16
+ from ccproxy.config.utils import get_ccproxy_cache_dir
17
+ from ccproxy.utils.binary_resolver import BinaryResolver, CLIInfo
18
+ from ccproxy.utils.caching import TTLCache
19
+
20
+
21
+ logger = structlog.get_logger(__name__)
22
+
23
+
24
+ class CLIDetectionResult(NamedTuple):
25
+ """Result of CLI detection for a specific binary."""
26
+
27
+ name: str
28
+ version: str | None
29
+ command: list[str] | None
30
+ is_available: bool
31
+ source: str # "path", "package_manager", "fallback", or "unknown"
32
+ package_manager: str | None = None
33
+ cached: bool = False
34
+ fallback_data: dict[str, Any] | None = None
35
+
36
+
37
+ class CLIDetectionService:
38
+ """Centralized service for CLI detection across all plugins.
39
+
40
+ This service provides:
41
+ - Unified binary detection using BinaryResolver
42
+ - Version detection with caching
43
+ - Fallback data support for when CLI is not available
44
+ - Consistent logging and error handling
45
+ """
46
+
47
+ def __init__(
48
+ self, settings: Settings, binary_resolver: BinaryResolver | None = None
49
+ ) -> None:
50
+ """Initialize the CLI detection service.
51
+
52
+ Args:
53
+ settings: Application settings
54
+ binary_resolver: Optional binary resolver instance. If None, creates a new one.
55
+ """
56
+ self.settings = settings
57
+ self.cache_dir = get_ccproxy_cache_dir()
58
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
59
+
60
+ # Use injected resolver or create from settings for backward compatibility
61
+ self.resolver = binary_resolver or BinaryResolver.from_settings(settings)
62
+
63
+ # Enhanced TTL cache for detection results (10 minute TTL)
64
+ self._detection_cache = TTLCache(maxsize=64, ttl=600.0)
65
+
66
+ # Separate cache for version info (longer TTL since versions change infrequently)
67
+ self._version_cache = TTLCache(maxsize=32, ttl=1800.0) # 30 minutes
68
+
69
+ async def detect_cli(
70
+ self,
71
+ binary_name: str,
72
+ package_name: str | None = None,
73
+ version_flag: str = "--version",
74
+ version_parser: Any | None = None,
75
+ fallback_data: dict[str, Any] | None = None,
76
+ cache_key: str | None = None,
77
+ ) -> CLIDetectionResult:
78
+ """Detect a CLI binary and its version.
79
+
80
+ Args:
81
+ binary_name: Name of the binary to detect (e.g., "claude", "codex")
82
+ package_name: NPM package name if different from binary name
83
+ version_flag: Flag to get version (default: "--version")
84
+ version_parser: Optional callable to parse version output
85
+ fallback_data: Optional fallback data if CLI is not available
86
+ cache_key: Optional cache key (defaults to binary_name)
87
+
88
+ Returns:
89
+ CLIDetectionResult with detection information
90
+ """
91
+ cache_key = cache_key or binary_name
92
+
93
+ # Check TTL cache first
94
+ cached_result = self._detection_cache.get(cache_key)
95
+ if cached_result is not None:
96
+ logger.debug(
97
+ "cli_detection_cached",
98
+ binary=binary_name,
99
+ version=cached_result.version,
100
+ available=cached_result.is_available,
101
+ cache_hit=True,
102
+ )
103
+ return cached_result # type: ignore[no-any-return]
104
+
105
+ # Try to detect the binary
106
+ result = self.resolver.find_binary(binary_name, package_name)
107
+
108
+ if result:
109
+ # Binary found - get version
110
+ version = await self._get_cli_version(
111
+ result.command, version_flag, version_parser
112
+ )
113
+
114
+ # Determine source
115
+ source = "path" if result.is_direct else "package_manager"
116
+
117
+ detection_result = CLIDetectionResult(
118
+ name=binary_name,
119
+ version=version,
120
+ command=result.command,
121
+ is_available=True,
122
+ source=source,
123
+ package_manager=result.package_manager,
124
+ cached=False,
125
+ )
126
+
127
+ logger.debug(
128
+ "cli_detection_success",
129
+ binary=binary_name,
130
+ version=version,
131
+ source=source,
132
+ package_manager=result.package_manager,
133
+ command=result.command,
134
+ cached=cached_result is not None,
135
+ )
136
+
137
+ elif fallback_data:
138
+ # Use fallback data
139
+ detection_result = CLIDetectionResult(
140
+ name=binary_name,
141
+ version=fallback_data.get("version", "unknown"),
142
+ command=None,
143
+ is_available=False,
144
+ source="fallback",
145
+ package_manager=None,
146
+ cached=False,
147
+ fallback_data=fallback_data,
148
+ )
149
+
150
+ logger.warning(
151
+ "cli_detection_using_fallback",
152
+ binary=binary_name,
153
+ reason="CLI not found",
154
+ )
155
+
156
+ else:
157
+ # Not found and no fallback
158
+ detection_result = CLIDetectionResult(
159
+ name=binary_name,
160
+ version=None,
161
+ command=None,
162
+ is_available=False,
163
+ source="unknown",
164
+ package_manager=None,
165
+ cached=False,
166
+ )
167
+
168
+ logger.error(
169
+ "cli_detection_failed",
170
+ binary=binary_name,
171
+ package=package_name,
172
+ )
173
+
174
+ # Cache the result with TTL
175
+ self._detection_cache.set(cache_key, detection_result)
176
+
177
+ return detection_result
178
+
179
+ async def _get_cli_version(
180
+ self,
181
+ cli_command: list[str],
182
+ version_flag: str,
183
+ version_parser: Any | None = None,
184
+ ) -> str | None:
185
+ """Get CLI version by executing version command with caching.
186
+
187
+ Args:
188
+ cli_command: Command list to execute CLI
189
+ version_flag: Flag to get version
190
+ version_parser: Optional callable to parse version output
191
+
192
+ Returns:
193
+ Version string if successful, None otherwise
194
+ """
195
+ # Create cache key from command and flag
196
+ cache_key = f"version:{':'.join(cli_command)}:{version_flag}"
197
+
198
+ # Check version cache first (longer TTL since versions change infrequently)
199
+ cached_version = self._version_cache.get(cache_key)
200
+ if cached_version is not None:
201
+ logger.debug(
202
+ "cli_version_cached",
203
+ command=cli_command[0],
204
+ version=cached_version,
205
+ cache_hit=True,
206
+ )
207
+ return cached_version # type: ignore[no-any-return]
208
+
209
+ try:
210
+ # Prepare command with version flag
211
+ cmd = cli_command + [version_flag]
212
+
213
+ # Run command with timeout
214
+ process = await asyncio.create_subprocess_exec(
215
+ *cmd,
216
+ stdout=asyncio.subprocess.PIPE,
217
+ stderr=asyncio.subprocess.PIPE,
218
+ )
219
+
220
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
221
+
222
+ version = None
223
+ if process.returncode == 0 and stdout:
224
+ version_output = stdout.decode().strip()
225
+
226
+ # Use custom parser if provided
227
+ if version_parser:
228
+ parsed = version_parser(version_output)
229
+ version = str(parsed) if parsed is not None else None
230
+ else:
231
+ # Default parsing logic
232
+ version = self._parse_version_output(version_output)
233
+
234
+ # Try stderr as some CLIs output version there
235
+ if not version and stderr:
236
+ version_output = stderr.decode().strip()
237
+ if version_parser:
238
+ parsed = version_parser(version_output)
239
+ version = str(parsed) if parsed is not None else None
240
+ else:
241
+ version = self._parse_version_output(version_output)
242
+
243
+ # Cache the version result (even if None)
244
+ self._version_cache.set(cache_key, version)
245
+
246
+ return version
247
+
248
+ except TimeoutError:
249
+ logger.debug("cli_version_timeout", command=cli_command)
250
+ # Cache timeout result briefly to avoid repeated attempts
251
+ self._version_cache.set(cache_key, None)
252
+ return None
253
+ except Exception as e:
254
+ logger.debug("cli_version_error", command=cli_command, error=str(e))
255
+ # Cache error result briefly to avoid repeated attempts
256
+ self._version_cache.set(cache_key, None)
257
+ return None
258
+
259
+ def _parse_version_output(self, output: str) -> str:
260
+ """Parse version from CLI output using common patterns.
261
+
262
+ Args:
263
+ output: Raw version command output
264
+
265
+ Returns:
266
+ Parsed version string
267
+ """
268
+ # Handle various common formats
269
+ if "/" in output:
270
+ # Handle "tool/1.0.0" format
271
+ output = output.split("/")[-1]
272
+
273
+ if "(" in output:
274
+ # Handle "1.0.0 (Tool Name)" format
275
+ output = output.split("(")[0].strip()
276
+
277
+ # Extract version number pattern (e.g., "1.0.0", "v1.0.0")
278
+ version_pattern = r"v?(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)"
279
+ match = re.search(version_pattern, output)
280
+ if match:
281
+ return match.group(1)
282
+
283
+ # Return cleaned output if no pattern matches
284
+ return output.strip()
285
+
286
+ def load_cached_version(
287
+ self, binary_name: str, cache_file: str | None = None
288
+ ) -> str | None:
289
+ """Load cached version for a binary.
290
+
291
+ Args:
292
+ binary_name: Name of the binary
293
+ cache_file: Optional cache file name
294
+
295
+ Returns:
296
+ Cached version string or None
297
+ """
298
+ cache_file_name = cache_file or f"{binary_name}_version.json"
299
+ cache_path = self.cache_dir / cache_file_name
300
+
301
+ if not cache_path.exists():
302
+ return None
303
+
304
+ try:
305
+ with cache_path.open("r") as f:
306
+ data = json.load(f)
307
+ version = data.get("version")
308
+ return str(version) if version is not None else None
309
+ except Exception as e:
310
+ logger.debug("cache_load_error", file=str(cache_path), error=str(e))
311
+ return None
312
+
313
+ def save_cached_version(
314
+ self,
315
+ binary_name: str,
316
+ version: str,
317
+ cache_file: str | None = None,
318
+ additional_data: dict[str, Any] | None = None,
319
+ ) -> None:
320
+ """Save version to cache.
321
+
322
+ Args:
323
+ binary_name: Name of the binary
324
+ version: Version string to cache
325
+ cache_file: Optional cache file name
326
+ additional_data: Additional data to cache
327
+ """
328
+ cache_file_name = cache_file or f"{binary_name}_version.json"
329
+ cache_path = self.cache_dir / cache_file_name
330
+
331
+ try:
332
+ data = {"binary": binary_name, "version": version}
333
+ if additional_data:
334
+ data.update(additional_data)
335
+
336
+ with cache_path.open("w") as f:
337
+ json.dump(data, f, indent=2)
338
+
339
+ logger.debug("cache_saved", file=str(cache_path), version=version)
340
+ except Exception as e:
341
+ logger.warning("cache_save_error", file=str(cache_path), error=str(e))
342
+
343
+ def get_cli_info(self, binary_name: str) -> CLIInfo:
344
+ """Get CLI information in standard format.
345
+
346
+ Args:
347
+ binary_name: Name of the binary
348
+
349
+ Returns:
350
+ CLIInfo dictionary with structured information
351
+ """
352
+ # Check if we have cached detection result
353
+ cached_result = self._detection_cache.get(binary_name)
354
+ if cached_result is not None:
355
+ return CLIInfo(
356
+ name=cached_result.name,
357
+ version=cached_result.version,
358
+ source=cached_result.source,
359
+ path=cached_result.command[0] if cached_result.command else None,
360
+ command=cached_result.command or [],
361
+ package_manager=cached_result.package_manager,
362
+ is_available=cached_result.is_available,
363
+ )
364
+
365
+ # Fall back to resolver
366
+ return self.resolver.get_cli_info(binary_name)
367
+
368
+ def clear_cache(self) -> None:
369
+ """Clear all detection caches."""
370
+ self._detection_cache.clear()
371
+ self._version_cache.clear()
372
+ self.resolver.clear_cache()
373
+ logger.debug("cli_detection_cache_cleared")
374
+
375
+ def get_all_detected(self) -> dict[str, CLIDetectionResult]:
376
+ """Get all detected CLI binaries.
377
+
378
+ Returns:
379
+ Dictionary of binary name to detection result
380
+ """
381
+ # Extract all cached results from TTLCache
382
+ results: dict[str, CLIDetectionResult] = {}
383
+ if hasattr(self._detection_cache, "_cache"):
384
+ for key, (result, _) in self._detection_cache._cache.items():
385
+ if isinstance(key, str) and isinstance(result, CLIDetectionResult):
386
+ results[key] = result
387
+ return results
388
+
389
+ async def detect_multiple(
390
+ self,
391
+ binaries: list[tuple[str, str | None]],
392
+ parallel: bool = True,
393
+ ) -> dict[str, CLIDetectionResult]:
394
+ """Detect multiple CLI binaries.
395
+
396
+ Args:
397
+ binaries: List of (binary_name, package_name) tuples
398
+ parallel: Whether to detect in parallel
399
+
400
+ Returns:
401
+ Dictionary of binary name to detection result
402
+ """
403
+ if parallel:
404
+ # Detect in parallel
405
+ tasks = [
406
+ self.detect_cli(binary_name, package_name)
407
+ for binary_name, package_name in binaries
408
+ ]
409
+ results = await asyncio.gather(*tasks, return_exceptions=True)
410
+
411
+ detected: dict[str, CLIDetectionResult] = {}
412
+ for (binary_name, _), result in zip(binaries, results, strict=False):
413
+ if isinstance(result, Exception):
414
+ logger.error(
415
+ "cli_detection_error",
416
+ binary=binary_name,
417
+ error=str(result),
418
+ )
419
+ elif isinstance(result, CLIDetectionResult):
420
+ detected[binary_name] = result
421
+
422
+ return detected
423
+ else:
424
+ # Detect sequentially
425
+ detected = {}
426
+ for binary_name, package_name in binaries:
427
+ try:
428
+ result = await self.detect_cli(binary_name, package_name)
429
+ detected[binary_name] = result
430
+ except Exception as e:
431
+ logger.error(
432
+ "cli_detection_error",
433
+ binary=binary_name,
434
+ error=str(e),
435
+ )
436
+
437
+ return detected
@@ -0,0 +1,6 @@
1
+ """Configuration management services."""
2
+
3
+ from ccproxy.services.config.proxy_configuration import ProxyConfiguration
4
+
5
+
6
+ __all__ = ["ProxyConfiguration"]
@@ -0,0 +1,111 @@
1
+ """Proxy and SSL configuration management service."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import httpx
8
+ import structlog
9
+
10
+
11
+ logger = structlog.get_logger(__name__)
12
+
13
+
14
+ class ProxyConfiguration:
15
+ """Manages proxy and SSL configuration from environment."""
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize by reading environment variables.
19
+
20
+ - Calls _init_proxy_url()
21
+ - Calls _init_ssl_context()
22
+ - Caches configuration
23
+ """
24
+ self._proxy_url = self._init_proxy_url()
25
+ self._ssl_verify = self._init_ssl_context()
26
+
27
+ if self._proxy_url:
28
+ logger.info("proxy_configuration_detected", proxy_url=self._proxy_url)
29
+ if isinstance(self._ssl_verify, str):
30
+ logger.info("custom_ca_bundle_configured", ca_bundle=self._ssl_verify)
31
+ elif not self._ssl_verify:
32
+ logger.warning("ssl_verification_disabled_not_recommended_for_production")
33
+
34
+ def _init_proxy_url(self) -> str | None:
35
+ """Extract proxy URL from environment.
36
+
37
+ - Checks HTTPS_PROXY (highest priority)
38
+ - Falls back to ALL_PROXY
39
+ - Falls back to HTTP_PROXY
40
+ - Handles case variations
41
+ """
42
+ # Check in order of priority
43
+ proxy_vars = [
44
+ "HTTPS_PROXY",
45
+ "https_proxy",
46
+ "ALL_PROXY",
47
+ "all_proxy",
48
+ "HTTP_PROXY",
49
+ "http_proxy",
50
+ ]
51
+
52
+ for var in proxy_vars:
53
+ proxy_url = os.getenv(var)
54
+ if proxy_url:
55
+ return proxy_url
56
+
57
+ return None
58
+
59
+ def _init_ssl_context(self) -> str | bool:
60
+ """Configure SSL verification and CA bundle.
61
+
62
+ - Checks REQUESTS_CA_BUNDLE for custom CA
63
+ - Checks SSL_CERT_FILE as fallback
64
+ - Checks SSL_VERIFY for disabling (not recommended)
65
+ - Returns: path | True | False
66
+ """
67
+ # Check for custom CA bundle
68
+ ca_bundle = os.getenv("REQUESTS_CA_BUNDLE") or os.getenv("SSL_CERT_FILE")
69
+ if ca_bundle:
70
+ ca_path = Path(ca_bundle)
71
+ if ca_path.exists() and ca_path.is_file():
72
+ return str(ca_path)
73
+ else:
74
+ logger.warning("ca_bundle_file_not_found", ca_bundle=ca_bundle)
75
+
76
+ # Check if SSL verification should be disabled
77
+ ssl_verify = os.getenv("SSL_VERIFY", "true").lower()
78
+ return ssl_verify not in ("false", "0", "no", "off")
79
+
80
+ @property
81
+ def proxy_url(self) -> str | None:
82
+ """Get configured proxy URL if any."""
83
+ return self._proxy_url
84
+
85
+ @property
86
+ def ssl_verify(self) -> str | bool:
87
+ """Get SSL verification setting."""
88
+ return self._ssl_verify
89
+
90
+ def get_httpx_client_config(self) -> dict[str, Any]:
91
+ """Build configuration dict for httpx.AsyncClient.
92
+
93
+ - Includes 'proxy' if proxy configured
94
+ - Includes 'verify' for SSL settings
95
+ - Ready to pass to client constructor
96
+ """
97
+ config = {
98
+ "verify": self._ssl_verify,
99
+ "timeout": 120.0, # Default timeout
100
+ "follow_redirects": False,
101
+ "limits": httpx.Limits(
102
+ max_keepalive_connections=100,
103
+ max_connections=1000,
104
+ keepalive_expiry=30.0,
105
+ ),
106
+ }
107
+
108
+ if self._proxy_url:
109
+ config["proxy"] = self._proxy_url
110
+
111
+ return config