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,327 @@
1
+ """Caching utilities for CCProxy.
2
+
3
+ This module provides caching decorators and utilities to improve performance
4
+ by caching frequently accessed data like detection results and auth status.
5
+ """
6
+
7
+ import functools
8
+ import threading
9
+ import time
10
+ from collections.abc import Callable, Hashable
11
+ from typing import Any, TypeVar
12
+
13
+ from ccproxy.core.logging import TraceBoundLogger, get_logger
14
+
15
+
16
+ logger: TraceBoundLogger = get_logger(__name__)
17
+
18
+
19
+ def _trace(message: str, **kwargs: Any) -> None:
20
+ """Trace-level logger helper with debug fallback."""
21
+ if hasattr(logger, "trace"):
22
+ logger.trace(message, **kwargs)
23
+ else:
24
+ logger.debug(message, **kwargs)
25
+
26
+
27
+ F = TypeVar("F", bound=Callable[..., Any])
28
+
29
+
30
+ class TTLCache:
31
+ """Thread-safe TTL (Time To Live) cache with LRU eviction."""
32
+
33
+ def __init__(self, maxsize: int = 128, ttl: float = 300.0):
34
+ """Initialize TTL cache.
35
+
36
+ Args:
37
+ maxsize: Maximum number of entries to cache
38
+ ttl: Time to live for entries in seconds
39
+ """
40
+ self.maxsize = maxsize
41
+ self.ttl = ttl
42
+ self._cache: dict[Hashable, tuple[Any, float]] = {}
43
+ self._access_order: dict[Hashable, int] = {}
44
+ self._access_counter = 0
45
+ self._lock = threading.RLock()
46
+
47
+ def get(self, key: Hashable) -> Any | None:
48
+ """Get value from cache."""
49
+ with self._lock:
50
+ if key not in self._cache:
51
+ return None
52
+
53
+ value, expiry_time = self._cache[key]
54
+
55
+ # Check if expired
56
+ if time.time() > expiry_time:
57
+ self._cache.pop(key, None)
58
+ self._access_order.pop(key, None)
59
+ return None
60
+
61
+ # Update access order
62
+ self._access_counter += 1
63
+ self._access_order[key] = self._access_counter
64
+
65
+ return value
66
+
67
+ def set(self, key: Hashable, value: Any) -> None:
68
+ """Set value in cache."""
69
+ with self._lock:
70
+ now = time.time()
71
+ expiry_time = now + self.ttl
72
+
73
+ # Add/update entry
74
+ self._cache[key] = (value, expiry_time)
75
+ self._access_counter += 1
76
+ self._access_order[key] = self._access_counter
77
+
78
+ # Evict expired entries first
79
+ self._evict_expired()
80
+
81
+ # Evict oldest entries if over maxsize
82
+ while len(self._cache) > self.maxsize:
83
+ self._evict_oldest()
84
+
85
+ def delete(self, key: Hashable) -> bool:
86
+ """Delete entry from cache."""
87
+ with self._lock:
88
+ if key in self._cache:
89
+ del self._cache[key]
90
+ self._access_order.pop(key, None)
91
+ return True
92
+ return False
93
+
94
+ def clear(self) -> None:
95
+ """Clear all cache entries."""
96
+ with self._lock:
97
+ self._cache.clear()
98
+ self._access_order.clear()
99
+ self._access_counter = 0
100
+
101
+ def _evict_expired(self) -> None:
102
+ """Remove expired entries."""
103
+ now = time.time()
104
+ expired_keys = [
105
+ key for key, (_, expiry_time) in self._cache.items() if now > expiry_time
106
+ ]
107
+
108
+ for key in expired_keys:
109
+ self._cache.pop(key, None)
110
+ self._access_order.pop(key, None)
111
+
112
+ def _evict_oldest(self) -> None:
113
+ """Remove oldest accessed entry."""
114
+ if not self._access_order:
115
+ return
116
+
117
+ oldest_key = min(self._access_order, key=lambda k: self._access_order[k])
118
+ self._cache.pop(oldest_key, None)
119
+ self._access_order.pop(oldest_key, None)
120
+
121
+ def stats(self) -> dict[str, Any]:
122
+ """Get cache statistics."""
123
+ with self._lock:
124
+ return {
125
+ "size": len(self._cache),
126
+ "maxsize": self.maxsize,
127
+ "ttl": self.ttl,
128
+ }
129
+
130
+
131
+ def ttl_cache(maxsize: int = 128, ttl: float = 300.0) -> Callable[[F], F]:
132
+ """TTL cache decorator for functions.
133
+
134
+ Args:
135
+ maxsize: Maximum number of entries to cache
136
+ ttl: Time to live for cached results in seconds
137
+ """
138
+
139
+ def decorator(func: F) -> F:
140
+ cache = TTLCache(maxsize=maxsize, ttl=ttl)
141
+
142
+ @functools.wraps(func)
143
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
144
+ # Create cache key from function args/kwargs
145
+ key = _make_cache_key(func.__name__, args, kwargs)
146
+
147
+ # Try to get from cache first
148
+ cached_result = cache.get(key)
149
+ if cached_result is not None:
150
+ _trace(
151
+ "cache_hit",
152
+ function=func.__name__,
153
+ key_hash=hash(key) if isinstance(key, tuple) else key,
154
+ )
155
+ return cached_result
156
+
157
+ # Call function and cache result
158
+ result = func(*args, **kwargs)
159
+ cache.set(key, result)
160
+
161
+ _trace(
162
+ "cache_miss_and_set",
163
+ function=func.__name__,
164
+ key_hash=hash(key) if isinstance(key, tuple) else key,
165
+ cache_size=len(cache._cache),
166
+ )
167
+
168
+ return result
169
+
170
+ # Add cache management methods
171
+ wrapper.cache_info = cache.stats # type: ignore
172
+ wrapper.cache_clear = cache.clear # type: ignore
173
+
174
+ return wrapper # type: ignore
175
+
176
+ return decorator
177
+
178
+
179
+ def async_ttl_cache(
180
+ maxsize: int = 128, ttl: float = 300.0
181
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
182
+ """TTL cache decorator for async functions.
183
+
184
+ Args:
185
+ maxsize: Maximum number of entries to cache
186
+ ttl: Time to live for cached results in seconds
187
+ """
188
+
189
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
190
+ cache = TTLCache(maxsize=maxsize, ttl=ttl)
191
+
192
+ @functools.wraps(func)
193
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
194
+ # Create cache key from function args/kwargs
195
+ key = _make_cache_key(func.__name__, args, kwargs)
196
+
197
+ # Try to get from cache first
198
+ cached_result = cache.get(key)
199
+ if cached_result is not None:
200
+ _trace(
201
+ "async_cache_hit",
202
+ function=func.__name__,
203
+ key_hash=hash(key) if isinstance(key, tuple) else key,
204
+ )
205
+ return cached_result
206
+
207
+ # Call async function and cache result
208
+ result = await func(*args, **kwargs)
209
+ cache.set(key, result)
210
+
211
+ _trace(
212
+ "async_cache_miss_and_set",
213
+ function=func.__name__,
214
+ key_hash=hash(key) if isinstance(key, tuple) else key,
215
+ cache_size=len(cache._cache),
216
+ )
217
+
218
+ return result
219
+
220
+ # Add cache management methods
221
+ wrapper.cache_info = cache.stats # type: ignore
222
+ wrapper.cache_clear = cache.clear # type: ignore
223
+
224
+ return wrapper
225
+
226
+ return decorator
227
+
228
+
229
+ def _make_cache_key(
230
+ func_name: str, args: tuple[Any, ...], kwargs: dict[str, Any]
231
+ ) -> Hashable:
232
+ """Create a hashable cache key from function arguments."""
233
+ try:
234
+ # Try to create a simple key for basic types
235
+ key_parts = [func_name]
236
+
237
+ # Add positional args
238
+ for arg in args:
239
+ if hasattr(arg, "__dict__"):
240
+ # For objects, use class name and id (weak ref to avoid memory leaks)
241
+ key_parts.append(f"{type(arg).__name__}:{id(arg)}")
242
+ else:
243
+ key_parts.append(arg)
244
+
245
+ # Add keyword args (sorted for consistency)
246
+ for k, v in sorted(kwargs.items()):
247
+ if hasattr(v, "__dict__"):
248
+ key_parts.append(f"{k}={type(v).__name__}:{id(v)}")
249
+ else:
250
+ key_parts.append(f"{k}={v}")
251
+
252
+ return tuple(key_parts)
253
+
254
+ except (TypeError, ValueError):
255
+ # Fallback to string representation
256
+ return f"{func_name}:{hash(str(args))}:{hash(str(sorted(kwargs.items())))}"
257
+
258
+
259
+ class AuthStatusCache:
260
+ """Specialized cache for auth status checks with shorter TTL."""
261
+
262
+ def __init__(self, ttl: float = 60.0): # 1 minute TTL for auth status
263
+ """Initialize auth status cache.
264
+
265
+ Args:
266
+ ttl: Time to live for auth status in seconds
267
+ """
268
+ self._cache = TTLCache(maxsize=32, ttl=ttl)
269
+
270
+ def get_auth_status(self, provider: str) -> bool | None:
271
+ """Get cached auth status for provider."""
272
+ return self._cache.get(f"auth_status:{provider}")
273
+
274
+ def set_auth_status(self, provider: str, is_authenticated: bool) -> None:
275
+ """Cache auth status for provider."""
276
+ self._cache.set(f"auth_status:{provider}", is_authenticated)
277
+
278
+ def invalidate_auth_status(self, provider: str) -> None:
279
+ """Invalidate auth status for provider."""
280
+ self._cache.delete(f"auth_status:{provider}")
281
+
282
+ def clear(self) -> None:
283
+ """Clear all auth status cache."""
284
+ self._cache.clear()
285
+
286
+
287
+ # Global instances for common use cases
288
+ _detection_cache = TTLCache(maxsize=64, ttl=600.0) # 10 minute TTL for detection
289
+ _auth_cache = AuthStatusCache(ttl=60.0) # 1 minute TTL for auth status
290
+ _config_cache = TTLCache(maxsize=32, ttl=300.0) # 5 minute TTL for plugin configs
291
+
292
+
293
+ def cache_detection_result(key: str, result: Any) -> None:
294
+ """Cache a detection result."""
295
+ _detection_cache.set(f"detection:{key}", result)
296
+
297
+
298
+ def get_cached_detection_result(key: str) -> Any | None:
299
+ """Get cached detection result."""
300
+ return _detection_cache.get(f"detection:{key}")
301
+
302
+
303
+ def cache_plugin_config(plugin_name: str, config: Any) -> None:
304
+ """Cache plugin configuration."""
305
+ _config_cache.set(f"plugin_config:{plugin_name}", config)
306
+
307
+
308
+ def get_cached_plugin_config(plugin_name: str) -> Any | None:
309
+ """Get cached plugin configuration."""
310
+ return _config_cache.get(f"plugin_config:{plugin_name}")
311
+
312
+
313
+ def clear_all_caches() -> None:
314
+ """Clear all global caches."""
315
+ _detection_cache.clear()
316
+ _auth_cache.clear()
317
+ _config_cache.clear()
318
+ logger.info("all_caches_cleared", category="cache")
319
+
320
+
321
+ def get_cache_stats() -> dict[str, Any]:
322
+ """Get statistics for all caches."""
323
+ return {
324
+ "detection_cache": _detection_cache.stats(),
325
+ "auth_cache": _auth_cache._cache.stats(),
326
+ "config_cache": _config_cache.stats(),
327
+ }
@@ -0,0 +1,101 @@
1
+ """Dynamic CLI logging utilities."""
2
+
3
+ from typing import Any
4
+
5
+ import structlog
6
+
7
+ from .binary_resolver import CLIInfo
8
+
9
+
10
+ logger = structlog.get_logger(__name__)
11
+
12
+
13
+ def log_cli_info(cli_info_dict: dict[str, CLIInfo], context: str = "plugin") -> None:
14
+ """Log CLI information dynamically for each CLI found.
15
+
16
+ Args:
17
+ cli_info_dict: Dictionary of CLI name -> CLIInfo
18
+ context: Context for logging (e.g., "plugin", "startup", "detection")
19
+ """
20
+ for cli_name, cli_info in cli_info_dict.items():
21
+ if cli_info["is_available"]:
22
+ logger.debug(
23
+ f"{context}_cli_available",
24
+ cli_name=cli_name,
25
+ version=cli_info["version"],
26
+ source=cli_info["source"],
27
+ path=cli_info["path"],
28
+ command=cli_info["command"],
29
+ package_manager=cli_info["package_manager"],
30
+ )
31
+ else:
32
+ logger.warning(
33
+ f"{context}_cli_unavailable",
34
+ cli_name=cli_name,
35
+ expected_version=cli_info["version"],
36
+ )
37
+
38
+
39
+ def log_plugin_summary(summary: dict[str, Any], plugin_name: str) -> None:
40
+ """Log plugin summary with dynamic CLI information.
41
+
42
+ Args:
43
+ summary: Plugin summary dictionary
44
+ plugin_name: Name of the plugin
45
+ """
46
+ # Log basic plugin info
47
+ basic_info = {k: v for k, v in summary.items() if k != "cli_info"}
48
+ logger.debug(
49
+ "plugin_summary",
50
+ plugin_name=plugin_name,
51
+ **basic_info,
52
+ )
53
+
54
+ # Log CLI info dynamically if present
55
+ if "cli_info" in summary:
56
+ log_cli_info(summary["cli_info"], f"{plugin_name}_plugin")
57
+
58
+
59
+ def format_cli_info_for_display(cli_info: CLIInfo) -> dict[str, str]:
60
+ """Format CLI info for human-readable display.
61
+
62
+ Args:
63
+ cli_info: CLI information dictionary
64
+
65
+ Returns:
66
+ Formatted dictionary for display
67
+ """
68
+ if not cli_info["is_available"]:
69
+ return {
70
+ "status": "unavailable",
71
+ "name": cli_info["name"],
72
+ }
73
+
74
+ display_info = {
75
+ "status": "available",
76
+ "name": cli_info["name"],
77
+ "version": cli_info["version"] or "unknown",
78
+ "source": cli_info["source"],
79
+ }
80
+
81
+ if cli_info["source"] == "path":
82
+ display_info["path"] = cli_info["path"] or "unknown"
83
+ elif cli_info["source"] == "package_manager":
84
+ display_info["package_manager"] = cli_info["package_manager"] or "unknown"
85
+ display_info["command"] = " ".join(cli_info["command"])
86
+
87
+ return display_info
88
+
89
+
90
+ def create_cli_summary_table(cli_info_dict: dict[str, CLIInfo]) -> list[dict[str, str]]:
91
+ """Create a table-ready summary of all CLI information.
92
+
93
+ Args:
94
+ cli_info_dict: Dictionary of CLI name -> CLIInfo
95
+
96
+ Returns:
97
+ List of formatted CLI info for table display
98
+ """
99
+ return [
100
+ format_cli_info_for_display(cli_info) for cli_info in cli_info_dict.values()
101
+ ]
@@ -0,0 +1,251 @@
1
+ """Utilities for generating command line tools (curl, xh) from HTTP request data."""
2
+
3
+ import json
4
+ import shlex
5
+ from typing import Any
6
+
7
+
8
+ def generate_curl_command(
9
+ method: str,
10
+ url: str,
11
+ headers: dict[str, str] | None = None,
12
+ body: Any = None,
13
+ is_json: bool = False,
14
+ pretty: bool = True,
15
+ ) -> str:
16
+ """Generate a curl command from HTTP request parameters.
17
+
18
+ Args:
19
+ method: HTTP method (GET, POST, etc.)
20
+ url: Target URL
21
+ headers: HTTP headers dictionary
22
+ body: Request body (can be dict, str, bytes)
23
+ is_json: Whether the body should be treated as JSON
24
+ pretty: Whether to format the command for readability
25
+
26
+ Returns:
27
+ Complete curl command string
28
+ """
29
+ parts = ["curl"]
30
+
31
+ # Add verbose flag for debugging
32
+ parts.append("-v")
33
+
34
+ # Add method if not GET
35
+ if method.upper() != "GET":
36
+ parts.extend(["-X", method.upper()])
37
+
38
+ # Add headers
39
+ if headers:
40
+ for key, value in headers.items():
41
+ parts.extend(["-H", f"{key}: {value}"])
42
+
43
+ # Add body
44
+ if isinstance(body, bytes):
45
+ body_str = body.decode()
46
+ parts.extend(["-d", body_str])
47
+
48
+ # Add URL (always last)
49
+ parts.append(url)
50
+
51
+ if pretty:
52
+ # Format for readability with line continuations
53
+ cmd_parts = []
54
+ for i, part in enumerate(parts):
55
+ if i == 0:
56
+ cmd_parts.append(part)
57
+ elif part in ["-X", "-H", "-d"]:
58
+ cmd_parts.append(f" \\\n {part}")
59
+ elif i == len(parts) - 1: # URL
60
+ cmd_parts.append(f" \\\n {shlex.quote(part)}")
61
+ else:
62
+ cmd_parts.append(f" {shlex.quote(part)}")
63
+ return "".join(cmd_parts)
64
+ else:
65
+ # Single line, properly quoted
66
+ return " ".join(shlex.quote(part) for part in parts)
67
+
68
+
69
+ def generate_xh_command(
70
+ method: str,
71
+ url: str,
72
+ headers: dict[str, str] | None = None,
73
+ body: Any = None,
74
+ is_json: bool = False,
75
+ pretty: bool = True,
76
+ ) -> str:
77
+ """Generate an xh (HTTPie-like) command from HTTP request parameters.
78
+
79
+ Args:
80
+ method: HTTP method (GET, POST, etc.)
81
+ url: Target URL
82
+ headers: HTTP headers dictionary
83
+ body: Request body (can be dict, str, bytes)
84
+ is_json: Whether the body should be treated as JSON
85
+ pretty: Whether to format the command for readability
86
+
87
+ Returns:
88
+ Complete xh command string
89
+ """
90
+ parts = ["xh"]
91
+
92
+ # Add verbose flag for debugging
93
+ parts.append("--verbose")
94
+
95
+ # Add method and URL
96
+ parts.append(f"{method.upper()}")
97
+ parts.append(url)
98
+
99
+ # Add headers
100
+ if headers:
101
+ for key, value in headers.items():
102
+ # Quote the entire header to handle special characters and spaces
103
+ parts.append(f"{key}:{value}")
104
+
105
+ # Add body
106
+ if isinstance(body, bytes):
107
+ body_str = body.decode()
108
+ parts.extend(["-d", body_str])
109
+
110
+ if pretty:
111
+ # Format for readability with line continuations
112
+ cmd_parts = []
113
+ for i, part in enumerate(parts):
114
+ if i == 0:
115
+ cmd_parts.append(part)
116
+ elif part == "--verbose" or i == 1:
117
+ cmd_parts.append(f" {part}")
118
+ elif i == 2: # URL
119
+ cmd_parts.append(f" \\\n {shlex.quote(part)}")
120
+ elif part in ("--raw", "-d"): # flags
121
+ cmd_parts.append(f" \\\n {part}")
122
+ elif ":" in part and not part.startswith("http"): # header
123
+ cmd_parts.append(f" \\\n {shlex.quote(part)}")
124
+ else:
125
+ cmd_parts.append(f" {shlex.quote(part)}")
126
+ return "".join(cmd_parts)
127
+ else:
128
+ # Single line, properly quoted
129
+ return " ".join(shlex.quote(part) for part in parts)
130
+
131
+
132
+ def generate_curl_shell_script(
133
+ method: str,
134
+ url: str,
135
+ headers: dict[str, str] | None = None,
136
+ body: Any = None,
137
+ is_json: bool = False,
138
+ ) -> str:
139
+ """Generate a shell script with curl command using proper JSON handling.
140
+
141
+ This creates a more robust shell script that handles JSON safely by:
142
+ 1. Storing JSON in a variable using heredoc or printf
143
+ 2. Using the variable in the curl command
144
+
145
+ Args:
146
+ method: HTTP method (GET, POST, etc.)
147
+ url: Target URL
148
+ headers: HTTP headers dictionary
149
+ body: Request body (can be dict, str, bytes)
150
+ is_json: Whether the body should be treated as JSON
151
+
152
+ Returns:
153
+ Complete shell script content
154
+ """
155
+ script_lines = ["#!/bin/bash", "set -e", ""]
156
+
157
+ # Process JSON body safely
158
+ json_data = None
159
+ if body is not None and (is_json or isinstance(body, dict)):
160
+ if isinstance(body, dict):
161
+ json_data = json.dumps(
162
+ body, indent=2, separators=(",", ": "), ensure_ascii=False
163
+ )
164
+ else:
165
+ # Clean up string body
166
+ body_str = str(body)
167
+ if (body_str.startswith("b'") and body_str.endswith("'")) or (
168
+ body_str.startswith('b"') and body_str.endswith('"')
169
+ ):
170
+ body_str = body_str[2:-1]
171
+
172
+ body_str = body_str.replace('\\"', '"').replace("\\'", "'")
173
+
174
+ try:
175
+ parsed = json.loads(body_str)
176
+ json_data = json.dumps(
177
+ parsed, indent=2, separators=(",", ": "), ensure_ascii=False
178
+ )
179
+ except (json.JSONDecodeError, ValueError):
180
+ json_data = body_str
181
+
182
+ # Build curl command parts
183
+ curl_parts = ["curl", "-v"]
184
+
185
+ if method.upper() != "GET":
186
+ curl_parts.extend(["-X", shlex.quote(method.upper())])
187
+
188
+ # Add headers
189
+ if headers:
190
+ for key, value in headers.items():
191
+ curl_parts.extend(["-H", shlex.quote(f"{key}: {value}")])
192
+
193
+ # Handle JSON body with heredoc
194
+ if json_data:
195
+ script_lines.append("# JSON payload")
196
+ script_lines.append("JSON_DATA=$(cat <<'EOF'")
197
+ script_lines.append(json_data)
198
+ script_lines.append("EOF")
199
+ script_lines.append(")")
200
+ script_lines.append("")
201
+
202
+ curl_parts.extend(["-d", '"$JSON_DATA"'])
203
+
204
+ # Add content-type if not present
205
+ if not headers or not any(k.lower() == "content-type" for k in headers):
206
+ curl_parts.extend(["-H", shlex.quote("Content-Type: application/json")])
207
+ elif body is not None:
208
+ # Non-JSON body
209
+ curl_parts.extend(["-d", shlex.quote(str(body))])
210
+
211
+ # Add URL
212
+ curl_parts.append(shlex.quote(url))
213
+
214
+ # Build final command
215
+ script_lines.append("# Execute curl command")
216
+ script_lines.append(" ".join(curl_parts))
217
+ script_lines.append("")
218
+
219
+ return "\n".join(script_lines)
220
+
221
+
222
+ def format_command_output(
223
+ request_id: str,
224
+ curl_command: str,
225
+ xh_command: str,
226
+ provider: str | None = None,
227
+ ) -> str:
228
+ """Format the command output for logging.
229
+
230
+ Args:
231
+ request_id: Request ID for correlation
232
+ curl_command: Generated curl command
233
+ xh_command: Generated xh command
234
+ provider: Provider name (optional)
235
+
236
+ Returns:
237
+ Formatted output string
238
+ """
239
+ provider_info = f" ({provider})" if provider else ""
240
+
241
+ return f"""
242
+ 🔄 Request Replay Commands{provider_info} [ID: {request_id}]
243
+
244
+ 📋 curl:
245
+ {curl_command}
246
+
247
+ 📋 xh:
248
+ {xh_command}
249
+
250
+ ─────────────────────────────────────────────────────────────────────
251
+ """