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,324 @@
1
+ """Codex-specific streaming metrics extraction utilities.
2
+
3
+ This module provides utilities for extracting token usage from
4
+ OpenAI/Codex streaming responses.
5
+ """
6
+
7
+ import json
8
+ from typing import Any
9
+
10
+ from ccproxy.core.logging import get_plugin_logger
11
+ from ccproxy.streaming import StreamingMetrics
12
+
13
+
14
+ logger = get_plugin_logger(__name__)
15
+
16
+
17
+ def extract_usage_from_codex_chunk(chunk_data: Any) -> dict[str, Any] | None:
18
+ """Extract usage information from OpenAI/Codex streaming response chunk.
19
+
20
+ OpenAI/Codex sends usage information in the final streaming chunk where
21
+ usage is not null. Earlier chunks have usage=null.
22
+
23
+ Args:
24
+ chunk_data: Streaming response chunk dictionary
25
+
26
+ Returns:
27
+ Dictionary with token counts or None if no usage found
28
+ """
29
+ if not isinstance(chunk_data, dict):
30
+ return None
31
+
32
+ # Extract model if present
33
+ model = chunk_data.get("model")
34
+
35
+ # Check for different Codex response formats
36
+ # 1. Standard OpenAI format (chat.completion.chunk)
37
+ object_type = chunk_data.get("object", "")
38
+ usage = chunk_data.get("usage")
39
+
40
+ if usage and object_type.startswith(("chat.completion", "codex.response")):
41
+ # Extract basic tokens
42
+ result = {
43
+ "input_tokens": usage.get("prompt_tokens") or usage.get("input_tokens", 0),
44
+ "output_tokens": usage.get("completion_tokens")
45
+ or usage.get("output_tokens", 0),
46
+ "total_tokens": usage.get("total_tokens"),
47
+ "event_type": "openai_completion",
48
+ "model": model,
49
+ }
50
+
51
+ # Extract detailed token information if available
52
+ if "input_tokens_details" in usage:
53
+ result["cache_read_tokens"] = usage["input_tokens_details"].get(
54
+ "cached_tokens", 0
55
+ )
56
+
57
+ if "output_tokens_details" in usage:
58
+ result["reasoning_tokens"] = usage["output_tokens_details"].get(
59
+ "reasoning_tokens", 0
60
+ )
61
+
62
+ return result
63
+
64
+ # 2. Codex CLI response format (response.completed event)
65
+ event_type = chunk_data.get("type", "")
66
+ if event_type == "response.completed" and "response" in chunk_data:
67
+ response_data = chunk_data["response"]
68
+ if isinstance(response_data, dict) and "usage" in response_data:
69
+ usage = response_data["usage"]
70
+ if usage:
71
+ # Codex CLI uses various formats
72
+ result = {
73
+ "input_tokens": usage.get("input_tokens")
74
+ or usage.get("prompt_tokens", 0),
75
+ "output_tokens": usage.get("output_tokens")
76
+ or usage.get("completion_tokens", 0),
77
+ "total_tokens": usage.get("total_tokens"),
78
+ "event_type": "codex_cli_response",
79
+ "model": response_data.get("model") or model,
80
+ }
81
+
82
+ # Check for detailed tokens
83
+ if "input_tokens_details" in usage:
84
+ result["cache_read_tokens"] = usage["input_tokens_details"].get(
85
+ "cached_tokens", 0
86
+ )
87
+
88
+ if "output_tokens_details" in usage:
89
+ result["reasoning_tokens"] = usage["output_tokens_details"].get(
90
+ "reasoning_tokens", 0
91
+ )
92
+
93
+ return result
94
+
95
+ return None
96
+
97
+
98
+ class CodexStreamingMetricsCollector:
99
+ """Collects and manages token metrics during Codex streaming responses.
100
+
101
+ Implements IStreamingMetricsCollector interface for Codex/OpenAI.
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ request_id: str | None = None,
107
+ pricing_service: Any = None,
108
+ model: str | None = None,
109
+ ) -> None:
110
+ """Initialize the metrics collector.
111
+
112
+ Args:
113
+ request_id: Optional request ID for logging context
114
+ pricing_service: Optional pricing service for cost calculation
115
+ model: Optional model name for cost calculation (can also be extracted from chunks)
116
+ """
117
+ self.request_id = request_id
118
+ self.pricing_service = pricing_service
119
+ self.model = model
120
+ self.reasoning_tokens: int | None = None # Store reasoning tokens separately
121
+ self.metrics: StreamingMetrics = {
122
+ "tokens_input": None,
123
+ "tokens_output": None,
124
+ "cache_read_tokens": None, # OpenAI might support in the future
125
+ "cache_write_tokens": None,
126
+ "cost_usd": None,
127
+ }
128
+
129
+ def process_raw_chunk(self, chunk_str: str) -> bool:
130
+ """Process raw Codex format chunk before any conversion.
131
+
132
+ This handles Codex's native response.completed event format.
133
+ """
134
+ return self.process_chunk(chunk_str)
135
+
136
+ def process_converted_chunk(self, chunk_str: str) -> bool:
137
+ """Process chunk after conversion to OpenAI format.
138
+
139
+ When Codex responses are converted to OpenAI chat completion format,
140
+ this method extracts metrics from the converted OpenAI format.
141
+ """
142
+ # After conversion, we'd see standard OpenAI format
143
+ # For now, delegate to main process_chunk which handles both
144
+ return self.process_chunk(chunk_str)
145
+
146
+ def process_chunk(self, chunk_str: str) -> bool:
147
+ """Process a streaming chunk to extract OpenAI/Codex token metrics.
148
+
149
+ Args:
150
+ chunk_str: Raw chunk string from streaming response
151
+
152
+ Returns:
153
+ True if this was the final chunk with complete metrics, False otherwise
154
+ """
155
+ # Check if this chunk contains usage information
156
+ if "usage" not in chunk_str:
157
+ return False
158
+
159
+ logger.debug(
160
+ "processing_chunk",
161
+ chunk_preview=chunk_str[:300],
162
+ request_id=self.request_id,
163
+ )
164
+
165
+ try:
166
+ # Parse SSE data lines to find usage information
167
+ # Codex sends complete JSON on a single line after "data: "
168
+ for line in chunk_str.split("\n"):
169
+ if line.startswith("data: "):
170
+ data_str = line[6:].strip()
171
+ if data_str and data_str != "[DONE]":
172
+ event_data = json.loads(data_str)
173
+
174
+ # Log event type for debugging
175
+ event_type = event_data.get("type", "")
176
+ if event_type == "response.completed":
177
+ logger.debug(
178
+ "completed_event_found",
179
+ has_response=("response" in event_data),
180
+ has_usage=("usage" in event_data.get("response", {}))
181
+ if "response" in event_data
182
+ else False,
183
+ request_id=self.request_id,
184
+ )
185
+
186
+ usage_data = extract_usage_from_codex_chunk(event_data)
187
+
188
+ if usage_data:
189
+ # Store token counts from the event
190
+ self.metrics["tokens_input"] = usage_data.get(
191
+ "input_tokens"
192
+ )
193
+ self.metrics["tokens_output"] = usage_data.get(
194
+ "output_tokens"
195
+ )
196
+ self.metrics["cache_read_tokens"] = usage_data.get(
197
+ "cache_read_tokens", 0
198
+ )
199
+ self.reasoning_tokens = usage_data.get(
200
+ "reasoning_tokens", 0
201
+ )
202
+
203
+ # Extract model from the chunk if we don't have it yet
204
+ if not self.model and usage_data.get("model"):
205
+ self.model = usage_data.get("model")
206
+ logger.debug(
207
+ "model_extracted_from_stream",
208
+ plugin="codex",
209
+ model=self.model,
210
+ request_id=self.request_id,
211
+ )
212
+
213
+ # Calculate cost synchronously when we have complete metrics
214
+ if self.pricing_service:
215
+ if self.model:
216
+ try:
217
+ # Import pricing exceptions
218
+ from ccproxy.plugins.pricing.exceptions import (
219
+ ModelPricingNotFoundError,
220
+ PricingDataNotLoadedError,
221
+ PricingServiceDisabledError,
222
+ )
223
+
224
+ cost_decimal = self.pricing_service.calculate_cost_sync(
225
+ model_name=self.model,
226
+ input_tokens=self.metrics["tokens_input"]
227
+ or 0,
228
+ output_tokens=self.metrics["tokens_output"]
229
+ or 0,
230
+ cache_read_tokens=self.metrics[
231
+ "cache_read_tokens"
232
+ ]
233
+ or 0,
234
+ cache_write_tokens=0, # OpenAI doesn't have cache write
235
+ )
236
+ self.metrics["cost_usd"] = float(cost_decimal)
237
+ logger.debug(
238
+ "streaming_cost_calculated",
239
+ model=self.model,
240
+ cost_usd=self.metrics["cost_usd"],
241
+ tokens_input=self.metrics["tokens_input"],
242
+ tokens_output=self.metrics["tokens_output"],
243
+ request_id=self.request_id,
244
+ )
245
+ except ModelPricingNotFoundError as e:
246
+ logger.warning(
247
+ "model_pricing_not_found",
248
+ model=self.model,
249
+ message=str(e),
250
+ tokens_input=self.metrics["tokens_input"],
251
+ tokens_output=self.metrics["tokens_output"],
252
+ request_id=self.request_id,
253
+ )
254
+ except PricingDataNotLoadedError as e:
255
+ logger.warning(
256
+ "pricing_data_not_loaded",
257
+ model=self.model,
258
+ message=str(e),
259
+ request_id=self.request_id,
260
+ )
261
+ except PricingServiceDisabledError as e:
262
+ logger.debug(
263
+ "pricing_service_disabled",
264
+ message=str(e),
265
+ request_id=self.request_id,
266
+ )
267
+ except Exception as e:
268
+ logger.debug(
269
+ "streaming_cost_calculation_failed",
270
+ error=str(e),
271
+ model=self.model,
272
+ request_id=self.request_id,
273
+ )
274
+ else:
275
+ logger.warning(
276
+ "streaming_cost_calculation_skipped_no_model",
277
+ plugin="codex",
278
+ request_id=self.request_id,
279
+ tokens_input=self.metrics["tokens_input"],
280
+ tokens_output=self.metrics["tokens_output"],
281
+ message="Model not found in streaming response, cannot calculate cost",
282
+ )
283
+
284
+ logger.debug(
285
+ "token_metrics_extracted",
286
+ plugin="codex",
287
+ tokens_input=self.metrics["tokens_input"],
288
+ tokens_output=self.metrics["tokens_output"],
289
+ cache_read_tokens=self.metrics["cache_read_tokens"],
290
+ reasoning_tokens=self.reasoning_tokens,
291
+ total_tokens=usage_data.get("total_tokens"),
292
+ event_type=usage_data.get("event_type"),
293
+ cost_usd=self.metrics.get("cost_usd"),
294
+ request_id=self.request_id,
295
+ )
296
+ return True # This is the final event with complete metrics
297
+
298
+ break # Only process first valid data line
299
+
300
+ except (json.JSONDecodeError, KeyError) as e:
301
+ logger.debug(
302
+ "metrics_parse_failed",
303
+ plugin="codex",
304
+ error=str(e),
305
+ request_id=self.request_id,
306
+ )
307
+
308
+ return False
309
+
310
+ def get_metrics(self) -> StreamingMetrics:
311
+ """Get the current collected metrics.
312
+
313
+ Returns:
314
+ Current token metrics
315
+ """
316
+ return self.metrics.copy()
317
+
318
+ def get_reasoning_tokens(self) -> int | None:
319
+ """Get reasoning tokens if available (for o1 models).
320
+
321
+ Returns:
322
+ Reasoning tokens count or None
323
+ """
324
+ return self.reasoning_tokens
@@ -0,0 +1,106 @@
1
+ """Scheduled tasks for Codex plugin."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.scheduler.tasks import BaseScheduledTask
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from .detection_service import CodexDetectionService
11
+
12
+
13
+ logger = get_plugin_logger()
14
+
15
+
16
+ class CodexDetectionRefreshTask(BaseScheduledTask):
17
+ """Task to periodically refresh Codex CLI detection headers."""
18
+
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ interval_seconds: float,
23
+ detection_service: "CodexDetectionService",
24
+ enabled: bool = True,
25
+ skip_initial_run: bool = True,
26
+ **kwargs: Any,
27
+ ) -> None:
28
+ """Initialize the Codex detection refresh task.
29
+
30
+ Args:
31
+ name: Task name
32
+ interval_seconds: Interval between refreshes
33
+ detection_service: The Codex detection service to refresh
34
+ enabled: Whether the task is enabled
35
+ skip_initial_run: Whether to skip the initial run at startup
36
+ **kwargs: Additional arguments for BaseScheduledTask
37
+ """
38
+ super().__init__(
39
+ name=name,
40
+ interval_seconds=interval_seconds,
41
+ enabled=enabled,
42
+ **kwargs,
43
+ )
44
+ self.detection_service = detection_service
45
+ self.skip_initial_run = skip_initial_run
46
+ self._first_run = True
47
+
48
+ async def run(self) -> bool:
49
+ """Execute the detection refresh.
50
+
51
+ Returns:
52
+ True if refresh was successful, False otherwise
53
+ """
54
+ # Skip the first run if configured to do so
55
+ if self._first_run and self.skip_initial_run:
56
+ self._first_run = False
57
+ logger.debug(
58
+ "codex_detection_refresh_skipped_initial",
59
+ task_name=self.name,
60
+ reason="Initial run skipped to avoid duplicate detection at startup",
61
+ )
62
+ return True # Return success to avoid triggering backoff
63
+
64
+ self._first_run = False
65
+
66
+ try:
67
+ logger.info(
68
+ "codex_detection_refresh_starting",
69
+ task_name=self.name,
70
+ )
71
+
72
+ # Refresh the detection data
73
+ detection_data = await self.detection_service.initialize_detection()
74
+
75
+ logger.info(
76
+ "codex_detection_refresh_completed",
77
+ task_name=self.name,
78
+ version=detection_data.codex_version if detection_data else "unknown",
79
+ has_cached_data=detection_data is not None,
80
+ )
81
+
82
+ return True
83
+
84
+ except Exception as e:
85
+ logger.error(
86
+ "codex_detection_refresh_failed",
87
+ task_name=self.name,
88
+ error=str(e),
89
+ error_type=type(e).__name__,
90
+ )
91
+ return False
92
+
93
+ async def setup(self) -> None:
94
+ """Perform any setup required before task execution starts."""
95
+ logger.debug(
96
+ "codex_detection_refresh_setup",
97
+ task_name=self.name,
98
+ interval_seconds=self.interval_seconds,
99
+ )
100
+
101
+ async def cleanup(self) -> None:
102
+ """Perform any cleanup required after task execution stops."""
103
+ logger.info(
104
+ "codex_detection_refresh_cleanup",
105
+ task_name=self.name,
106
+ )
@@ -0,0 +1 @@
1
+ """Utility modules for Codex plugin."""
@@ -0,0 +1,106 @@
1
+ """SSE (Server-Sent Events) parser for Codex responses."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def parse_sse_line(line: str) -> tuple[str | None, Any | None]:
8
+ """Parse a single SSE line.
9
+
10
+ Args:
11
+ line: SSE line to parse
12
+
13
+ Returns:
14
+ Tuple of (event_type, data) or (None, None) if not parseable
15
+ """
16
+ line = line.strip()
17
+
18
+ if not line:
19
+ return None, None
20
+
21
+ if line.startswith("event:"):
22
+ return line[6:].strip(), None
23
+
24
+ if line.startswith("data:"):
25
+ data_str = line[5:].strip()
26
+
27
+ if data_str == "[DONE]":
28
+ return "done", None
29
+
30
+ try:
31
+ return "data", json.loads(data_str)
32
+ except json.JSONDecodeError:
33
+ return None, None
34
+
35
+ return None, None
36
+
37
+
38
+ def extract_final_response(sse_content: str) -> dict[str, Any] | None:
39
+ """Extract the final response from SSE content.
40
+
41
+ Looks for the response.completed event in SSE stream.
42
+
43
+ Args:
44
+ sse_content: Complete SSE response content
45
+
46
+ Returns:
47
+ Final response data or None if not found
48
+ """
49
+ lines = sse_content.strip().split("\n")
50
+ final_response = None
51
+
52
+ for line in lines:
53
+ event_type, data = parse_sse_line(line)
54
+
55
+ if event_type == "data" and data and isinstance(data, dict):
56
+ # Check for response.completed event
57
+ if data.get("type") == "response.completed":
58
+ # Found the completed response
59
+ if "response" in data:
60
+ final_response = data["response"]
61
+ else:
62
+ final_response = data
63
+ elif data.get("type") == "response.in_progress" and "response" in data:
64
+ # Update with in-progress data, but keep looking
65
+ final_response = data["response"]
66
+
67
+ return final_response
68
+
69
+
70
+ def parse_sse_stream(chunks: list[bytes]) -> dict[str, Any] | None:
71
+ """Parse SSE stream chunks to extract final response.
72
+
73
+ Args:
74
+ chunks: List of byte chunks from SSE stream
75
+
76
+ Returns:
77
+ Final response data or None if not found
78
+ """
79
+ # Combine all chunks
80
+ full_content = b"".join(chunks).decode("utf-8", errors="replace")
81
+ return extract_final_response(full_content)
82
+
83
+
84
+ def is_sse_response(content: bytes | str) -> bool:
85
+ """Check if content appears to be SSE format.
86
+
87
+ Args:
88
+ content: Response content to check
89
+
90
+ Returns:
91
+ True if content appears to be SSE format
92
+ """
93
+ if isinstance(content, bytes):
94
+ try:
95
+ content = content.decode("utf-8", errors="replace")
96
+ except Exception:
97
+ return False
98
+
99
+ # Check for SSE markers
100
+ content_start = content[:100].strip()
101
+ return (
102
+ content_start.startswith("event:")
103
+ or content_start.startswith("data:")
104
+ or "\nevent:" in content_start
105
+ or "\ndata:" in content_start
106
+ )
@@ -0,0 +1,34 @@
1
+ # Command Replay Plugin
2
+
3
+ Generates reproducible `curl` and `xh` commands for captured provider requests.
4
+
5
+ ## Highlights
6
+ - Subscribes to provider hook events to snapshot raw HTTP payloads
7
+ - Emits commands to stdout or disk with configurable file layout
8
+ - Supports URL include/exclude filters and provider-only logging
9
+
10
+ ## Configuration
11
+ - `CommandReplayConfig` toggles command types, log directory, and filters
12
+ - Enable via `plugins.command_replay` settings or matching environment vars
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin command_replay --config-class CommandReplayConfig`
15
+
16
+ ```toml
17
+ [plugins.command_replay]
18
+ # enabled = true
19
+ # generate_curl = true
20
+ # generate_xh = true
21
+ # log_dir = "/tmp/ccproxy/command_replay"
22
+ # write_to_files = true
23
+ # separate_files_per_command = true
24
+ # include_url_patterns = ["api.anthropic.com", "api.openai.com", "claude.ai", "chatgpt.com"]
25
+ # exclude_url_patterns = []
26
+ # log_to_console = false
27
+ # log_level = "TRACE"
28
+ # only_provider_requests = false
29
+ ```
30
+
31
+ ## Related Components
32
+ - `hook.py`: assembles commands from hook payloads
33
+ - `formatter.py`: file naming and formatting helpers
34
+ - `plugin.py`: runtime wiring and hook registration
@@ -0,0 +1,17 @@
1
+ """Command Replay Plugin - Generate curl and xh commands for provider requests."""
2
+
3
+ from .config import CommandReplayConfig
4
+ from .hook import CommandReplayHook
5
+ from .plugin import CommandReplayFactory, CommandReplayRuntime
6
+
7
+
8
+ # Export the factory for auto-discovery
9
+ factory = CommandReplayFactory()
10
+
11
+ __all__ = [
12
+ "CommandReplayConfig",
13
+ "CommandReplayHook",
14
+ "CommandReplayRuntime",
15
+ "CommandReplayFactory",
16
+ "factory",
17
+ ]