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
@@ -0,0 +1,133 @@
1
+ """Configuration for the Command Replay plugin."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class CommandReplayConfig(BaseModel):
7
+ """Configuration for command replay generation.
8
+
9
+ Generates curl and xh commands for provider requests to enable
10
+ easy replay and debugging of API calls.
11
+ """
12
+
13
+ # Enable/disable entire plugin
14
+ enabled: bool = Field(
15
+ default=True, description="Enable or disable the command replay plugin"
16
+ )
17
+
18
+ # Command generation options
19
+ generate_curl: bool = Field(default=True, description="Generate curl commands")
20
+ generate_xh: bool = Field(default=True, description="Generate xh commands")
21
+
22
+ # Formatting options
23
+ pretty_format: bool = Field(
24
+ default=True,
25
+ description="Use pretty formatting with line continuations for readability",
26
+ )
27
+
28
+ # Request filtering
29
+ include_url_patterns: list[str] = Field(
30
+ default_factory=lambda: [
31
+ "api.anthropic.com",
32
+ "api.openai.com",
33
+ "claude.ai",
34
+ "chatgpt.com",
35
+ ],
36
+ description="Only generate commands for URLs matching these patterns",
37
+ )
38
+ exclude_url_patterns: list[str] = Field(
39
+ default_factory=list,
40
+ description="Skip generating commands for URLs matching these patterns",
41
+ )
42
+
43
+ # File output control
44
+ log_dir: str = Field(
45
+ default="/tmp/ccproxy/command_replay",
46
+ description="Directory for command replay files",
47
+ )
48
+ write_to_files: bool = Field(default=True, description="Write commands to files")
49
+ separate_files_per_command: bool = Field(
50
+ default=True,
51
+ description="Create separate files for curl and xh (False = single combined file)",
52
+ )
53
+
54
+ # Console output control
55
+ log_to_console: bool = Field(
56
+ default=False, description="Log commands to console via logger"
57
+ )
58
+ log_level: str = Field(
59
+ default="TRACE",
60
+ description="Log level for command output (DEBUG, INFO, WARNING)",
61
+ )
62
+
63
+ # Request type filtering
64
+ only_provider_requests: bool = Field(
65
+ default=False,
66
+ description="Only generate commands for provider requests (not client requests)",
67
+ )
68
+ include_client_requests: bool = Field(
69
+ default=True,
70
+ description="Generate commands for client requests to non-provider URLs",
71
+ )
72
+
73
+ model_config = ConfigDict()
74
+
75
+ def should_generate_for_url(
76
+ self, url: str, is_provider_request: bool | None = None
77
+ ) -> bool:
78
+ """Check if commands should be generated for the given URL.
79
+
80
+ Args:
81
+ url: The request URL to check
82
+ is_provider_request: Whether this is a provider request (None = auto-detect)
83
+
84
+ Returns:
85
+ True if commands should be generated for this URL
86
+ """
87
+ # Check exclude patterns first
88
+ if self.exclude_url_patterns:
89
+ if any(pattern in url for pattern in self.exclude_url_patterns):
90
+ return False
91
+
92
+ # Auto-detect if this is a provider request if not specified
93
+ if is_provider_request is None:
94
+ provider_domains = [
95
+ "api.anthropic.com",
96
+ "claude.ai",
97
+ "api.openai.com",
98
+ "chatgpt.com",
99
+ ]
100
+ is_provider_request = any(
101
+ domain in url.lower() for domain in provider_domains
102
+ )
103
+
104
+ # Apply request type filtering
105
+ if self.only_provider_requests and not is_provider_request:
106
+ return False
107
+
108
+ if not self.include_client_requests and not is_provider_request:
109
+ return False
110
+
111
+ # For provider requests, check include patterns
112
+ if is_provider_request:
113
+ if self.include_url_patterns:
114
+ return any(pattern in url for pattern in self.include_url_patterns)
115
+ else:
116
+ # For client requests, be more permissive
117
+ # Only filter if there are specific include patterns that don't match
118
+ if self.include_url_patterns:
119
+ # If include patterns are all provider domains, allow client requests
120
+ provider_only = all(
121
+ any(
122
+ provider in pattern.lower()
123
+ for provider in ["anthropic", "openai", "claude", "chatgpt"]
124
+ )
125
+ for pattern in self.include_url_patterns
126
+ )
127
+ if provider_only:
128
+ return True
129
+ # Otherwise apply normal include pattern matching
130
+ return any(pattern in url for pattern in self.include_url_patterns)
131
+
132
+ # Default: generate for all URLs if no patterns specified
133
+ return True
@@ -0,0 +1,432 @@
1
+ """File formatter for command replay output."""
2
+
3
+ import stat
4
+ import time
5
+ import uuid
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import aiofiles
11
+
12
+ from ccproxy.core.logging import get_plugin_logger
13
+ from ccproxy.utils.command_line import generate_curl_shell_script
14
+
15
+
16
+ logger = get_plugin_logger()
17
+
18
+
19
+ class CommandFileFormatter:
20
+ """Formats and writes command replay data to files."""
21
+
22
+ def __init__(
23
+ self,
24
+ log_dir: str = "/tmp/ccproxy/command_replay",
25
+ enabled: bool = True,
26
+ separate_files_per_command: bool = False,
27
+ ) -> None:
28
+ """Initialize with configuration.
29
+
30
+ Args:
31
+ log_dir: Directory for command replay files
32
+ enabled: Enable file writing
33
+ separate_files_per_command: Create separate files for curl/xh vs combined
34
+ """
35
+ self.enabled = enabled
36
+ self.log_dir = Path(log_dir)
37
+ self.separate_files_per_command = separate_files_per_command
38
+
39
+ if self.enabled:
40
+ # Create log directory if it doesn't exist
41
+ try:
42
+ self.log_dir.mkdir(parents=True, exist_ok=True)
43
+ except OSError as e:
44
+ logger.error(
45
+ "failed_to_create_command_replay_directory",
46
+ log_dir=str(self.log_dir),
47
+ error=str(e),
48
+ exc_info=e,
49
+ )
50
+ # Disable file writing if we can't create the directory
51
+ self.enabled = False
52
+
53
+ # Track which files we've already created (for logging purposes only)
54
+ self._created_files: set[str] = set()
55
+
56
+ def _compose_file_id(self, request_id: str | None) -> str:
57
+ """Generate base file ID from request ID.
58
+
59
+ Args:
60
+ request_id: Request ID for correlation
61
+
62
+ Returns:
63
+ Base file ID string
64
+ """
65
+ if request_id:
66
+ # Clean up request ID for filesystem safety
67
+ safe_id = "".join(
68
+ c if c.isalnum() or c in "-_" else "_" for c in request_id
69
+ )
70
+ return safe_id[:50] # Limit length
71
+ else:
72
+ return str(uuid.uuid4())[:8]
73
+
74
+ def _compose_file_id_with_timestamp(self, request_id: str | None) -> str:
75
+ """Build filename ID with timestamp suffix for better organization.
76
+
77
+ Format: {base_id}_{timestamp}_{nanos}
78
+ Where timestamp is in format: YYYYMMDD_HHMMSS_microseconds
79
+ And nanos is a counter to prevent collisions
80
+ """
81
+ base_id = self._compose_file_id(request_id)
82
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
83
+
84
+ # Add a high-resolution timestamp with nanoseconds for uniqueness
85
+ nanos = time.time_ns() % 1000000 # Get nanosecond portion
86
+ return f"{base_id}_{timestamp}_{nanos:06d}"
87
+
88
+ def should_write_files(self) -> bool:
89
+ """Check if file writing is enabled."""
90
+ return bool(self.enabled)
91
+
92
+ async def write_commands(
93
+ self,
94
+ request_id: str,
95
+ curl_command: str,
96
+ xh_command: str,
97
+ provider: str | None = None,
98
+ timestamp_prefix: str | None = None,
99
+ method: str | None = None,
100
+ url: str | None = None,
101
+ headers: dict[str, str] | None = None,
102
+ body: Any = None,
103
+ is_json: bool = False,
104
+ ) -> list[str]:
105
+ """Write command replay data to files.
106
+
107
+ Args:
108
+ request_id: Request ID for correlation
109
+ curl_command: Generated curl command
110
+ xh_command: Generated xh command
111
+ provider: Provider name (anthropic, openai, etc.)
112
+ timestamp_prefix: Optional timestamp prefix from RequestContext
113
+ method: HTTP method for shell script generation
114
+ url: Request URL for shell script generation
115
+ headers: HTTP headers for shell script generation
116
+ body: Request body for shell script generation
117
+ is_json: Whether body is JSON for shell script generation
118
+
119
+ Returns:
120
+ List of file paths that were written
121
+ """
122
+ if not self.enabled:
123
+ return []
124
+
125
+ written_files = []
126
+
127
+ # Use provided timestamp prefix or generate our own
128
+ if timestamp_prefix:
129
+ base_id = f"{self._compose_file_id(request_id)}_{timestamp_prefix}"
130
+ else:
131
+ base_id = self._compose_file_id_with_timestamp(request_id)
132
+
133
+ # Add provider to filename if available
134
+ if provider:
135
+ base_id = f"{base_id}_{provider}"
136
+
137
+ try:
138
+ if self.separate_files_per_command:
139
+ # Write separate files for curl and xh
140
+ if curl_command:
141
+ curl_file = await self._write_single_command_file(
142
+ base_id, "curl", curl_command, request_id
143
+ )
144
+ if curl_file:
145
+ written_files.append(curl_file)
146
+
147
+ if xh_command:
148
+ xh_file = await self._write_single_command_file(
149
+ base_id, "xh", xh_command, request_id
150
+ )
151
+ if xh_file:
152
+ written_files.append(xh_file)
153
+ else:
154
+ # Write combined file with both commands
155
+ combined_file = await self._write_combined_command_file(
156
+ base_id, curl_command, xh_command, request_id, provider
157
+ )
158
+ if combined_file:
159
+ written_files.append(combined_file)
160
+
161
+ # Generate executable shell script if we have raw request data
162
+ if method and url:
163
+ shell_script_file = await self._write_shell_script_file(
164
+ base_id, request_id, method, url, headers, body, is_json, provider
165
+ )
166
+ if shell_script_file:
167
+ written_files.append(shell_script_file)
168
+
169
+ # Make files executable
170
+ await self._make_files_executable(written_files)
171
+
172
+ except Exception as e:
173
+ logger.error(
174
+ "command_replay_file_write_error",
175
+ request_id=request_id,
176
+ error=str(e),
177
+ exc_info=e,
178
+ )
179
+
180
+ return written_files
181
+
182
+ async def _write_single_command_file(
183
+ self,
184
+ base_id: str,
185
+ command_type: str,
186
+ command: str,
187
+ request_id: str,
188
+ ) -> str | None:
189
+ """Write a single command to its own file.
190
+
191
+ Args:
192
+ base_id: Base filename identifier
193
+ command_type: Command type (curl, xh)
194
+ command: Command string to write
195
+ request_id: Request ID for logging
196
+
197
+ Returns:
198
+ File path if successful, None if failed
199
+ """
200
+ file_path = self.log_dir / f"{base_id}_{command_type}.sh"
201
+
202
+ # Log file creation (only once per unique file path)
203
+ if str(file_path) not in self._created_files:
204
+ self._created_files.add(str(file_path))
205
+ logger.debug(
206
+ "command_replay_file_created",
207
+ request_id=request_id,
208
+ command_type=command_type,
209
+ file_path=str(file_path),
210
+ mode="separate",
211
+ )
212
+
213
+ try:
214
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
215
+ await f.write("#!/usr/bin/env bash\n")
216
+ await f.write(f"# {command_type.upper()} Command Replay\n")
217
+ await f.write(f"# Request ID: {request_id}\n")
218
+ await f.write(f"# Generated: {datetime.now().isoformat()}\n")
219
+ await f.write("#\n")
220
+ await f.write(
221
+ f"# Run this file directly: ./{base_id}_{command_type}.sh\n"
222
+ )
223
+ await f.write("\n")
224
+ await f.write(command)
225
+ await f.write("\n")
226
+
227
+ return str(file_path)
228
+
229
+ except Exception as e:
230
+ logger.error(
231
+ "command_replay_single_file_write_error",
232
+ request_id=request_id,
233
+ command_type=command_type,
234
+ file_path=str(file_path),
235
+ error=str(e),
236
+ )
237
+ return None
238
+
239
+ async def _write_combined_command_file(
240
+ self,
241
+ base_id: str,
242
+ curl_command: str,
243
+ xh_command: str,
244
+ request_id: str,
245
+ provider: str | None = None,
246
+ ) -> str | None:
247
+ """Write both commands to a single combined file.
248
+
249
+ Args:
250
+ base_id: Base filename identifier
251
+ curl_command: curl command string
252
+ xh_command: xh command string
253
+ request_id: Request ID for logging
254
+ provider: Provider name for header
255
+
256
+ Returns:
257
+ File path if successful, None if failed
258
+ """
259
+ file_path = self.log_dir / f"{base_id}_commands.sh"
260
+
261
+ # Log file creation (only once per unique file path)
262
+ if str(file_path) not in self._created_files:
263
+ self._created_files.add(str(file_path))
264
+ logger.debug(
265
+ "command_replay_file_created",
266
+ request_id=request_id,
267
+ command_type="combined",
268
+ file_path=str(file_path),
269
+ mode="combined",
270
+ )
271
+
272
+ try:
273
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
274
+ # Write shebang and header
275
+ await f.write("#!/usr/bin/env bash\n")
276
+ await f.write("# Command Replay File\n")
277
+ await f.write(f"# Request ID: {request_id}\n")
278
+ if provider:
279
+ await f.write(f"# Provider: {provider}\n")
280
+ await f.write(f"# Generated: {datetime.now().isoformat()}\n")
281
+ await f.write("#\n")
282
+ await f.write("# This file contains both curl and xh commands.\n")
283
+ await f.write("# Uncomment the command you want to run.\n")
284
+ await f.write("\n")
285
+
286
+ # Write curl command
287
+ if curl_command:
288
+ await f.write("# CURL Command\n")
289
+ await f.write("# " + "=" * 50 + "\n")
290
+ # Comment out the command so it doesn't run accidentally
291
+ for line in curl_command.split("\n"):
292
+ if line.strip():
293
+ await f.write(f"# {line}\n")
294
+ else:
295
+ await f.write("#\n")
296
+ await f.write("\n")
297
+
298
+ # Write xh command
299
+ if xh_command:
300
+ await f.write("# XH Command\n")
301
+ await f.write("# " + "=" * 50 + "\n")
302
+ # Comment out the command so it doesn't run accidentally
303
+ for line in xh_command.split("\n"):
304
+ if line.strip():
305
+ await f.write(f"# {line}\n")
306
+ else:
307
+ await f.write("#\n")
308
+ await f.write("\n")
309
+
310
+ # Add footer with instructions
311
+ await f.write("# " + "=" * 60 + "\n")
312
+ await f.write("# Instructions:\n")
313
+ await f.write("# 1. Uncomment the command you want to use\n")
314
+ await f.write("# 2. Make sure you have curl or xh installed\n")
315
+ await f.write("# 3. Run: chmod +x this_file.sh && ./this_file.sh\n")
316
+ await f.write("# " + "=" * 60 + "\n")
317
+
318
+ return str(file_path)
319
+
320
+ except Exception as e:
321
+ logger.error(
322
+ "command_replay_combined_file_write_error",
323
+ request_id=request_id,
324
+ file_path=str(file_path),
325
+ error=str(e),
326
+ )
327
+ return None
328
+
329
+ def get_log_dir(self) -> str:
330
+ """Get the log directory path."""
331
+ return str(self.log_dir)
332
+
333
+ async def _make_files_executable(self, file_paths: list[str]) -> None:
334
+ """Make the generated files executable.
335
+
336
+ Args:
337
+ file_paths: List of file paths to make executable
338
+ """
339
+
340
+ for file_path_str in file_paths:
341
+ try:
342
+ file_path = Path(file_path_str)
343
+ # Add execute permission for owner, group, and others
344
+ current_mode = file_path.stat().st_mode
345
+ new_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
346
+ file_path.chmod(new_mode)
347
+
348
+ logger.debug(
349
+ "command_replay_file_made_executable",
350
+ file_path=file_path_str,
351
+ )
352
+ except Exception as e:
353
+ logger.warning(
354
+ "command_replay_chmod_failed",
355
+ file_path=file_path_str,
356
+ error=str(e),
357
+ )
358
+
359
+ async def _write_shell_script_file(
360
+ self,
361
+ base_id: str,
362
+ request_id: str,
363
+ method: str,
364
+ url: str,
365
+ headers: dict[str, str] | None,
366
+ body: Any,
367
+ is_json: bool,
368
+ provider: str | None = None,
369
+ ) -> str | None:
370
+ """Write an executable shell script file.
371
+
372
+ Args:
373
+ base_id: Base filename identifier
374
+ request_id: Request ID for logging
375
+ method: HTTP method
376
+ url: Request URL
377
+ headers: HTTP headers
378
+ body: Request body
379
+ is_json: Whether body is JSON
380
+ provider: Provider name
381
+
382
+ Returns:
383
+ File path if successful, None if failed
384
+ """
385
+ file_path = self.log_dir / f"{base_id}_script.sh"
386
+
387
+ # Log file creation
388
+ if str(file_path) not in self._created_files:
389
+ self._created_files.add(str(file_path))
390
+ logger.debug(
391
+ "command_replay_file_created",
392
+ request_id=request_id,
393
+ command_type="shell_script",
394
+ file_path=str(file_path),
395
+ mode="executable",
396
+ )
397
+
398
+ try:
399
+ # Generate shell-safe script content
400
+ script_content = generate_curl_shell_script(
401
+ method=method,
402
+ url=url,
403
+ headers=headers,
404
+ body=body,
405
+ is_json=is_json,
406
+ )
407
+
408
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
409
+ await f.write("#!/bin/bash\n")
410
+ await f.write("# Executable Shell Script for Request Replay\n")
411
+ await f.write(f"# Request ID: {request_id}\n")
412
+ if provider:
413
+ await f.write(f"# Provider: {provider}\n")
414
+ await f.write(f"# Generated: {datetime.now().isoformat()}\n")
415
+ await f.write(f"# Usage: bash {file_path.name} or ./{file_path.name}\n")
416
+ await f.write("\n")
417
+ await f.write(script_content)
418
+
419
+ return str(file_path)
420
+
421
+ except Exception as e:
422
+ logger.error(
423
+ "command_replay_shell_script_write_error",
424
+ request_id=request_id,
425
+ file_path=str(file_path),
426
+ error=str(e),
427
+ )
428
+ return None
429
+
430
+ def cleanup(self) -> None:
431
+ """Clean up resources (if any)."""
432
+ self._created_files.clear()