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
@@ -1,812 +0,0 @@
1
- """HTTP-level transformers for proxy service."""
2
-
3
- from typing import TYPE_CHECKING, Any
4
-
5
- import structlog
6
- from typing_extensions import TypedDict
7
-
8
- from ccproxy.core.transformers import RequestTransformer, ResponseTransformer
9
- from ccproxy.core.types import ProxyRequest, ProxyResponse, TransformContext
10
-
11
-
12
- if TYPE_CHECKING:
13
- pass
14
-
15
-
16
- logger = structlog.get_logger(__name__)
17
-
18
- # Claude Code system prompt constants
19
- claude_code_prompt = "You are Claude Code, Anthropic's official CLI for Claude."
20
-
21
- # claude_code_prompt = "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# important-instruction-reminders\nDo what has been asked; nothing more, nothing less.\nNEVER create files unless they're absolutely necessary for achieving your goal.\nALWAYS prefer editing an existing file to creating a new one.\nNEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n\n \n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>\n"
22
-
23
-
24
- def get_detected_system_field(
25
- app_state: Any = None, injection_mode: str = "minimal"
26
- ) -> Any:
27
- """Get the detected system field for injection.
28
-
29
- Args:
30
- app_state: App state containing detection data
31
- injection_mode: 'minimal' or 'full' mode
32
-
33
- Returns:
34
- The system field to inject (preserving exact Claude CLI structure), or None if no detection data available
35
- """
36
- if not app_state or not hasattr(app_state, "claude_detection_data"):
37
- return None
38
-
39
- claude_data = app_state.claude_detection_data
40
- detected_system = claude_data.system_prompt.system_field
41
-
42
- if injection_mode == "full":
43
- # Return the complete detected system field exactly as Claude CLI sent it
44
- return detected_system
45
- else:
46
- # Minimal mode: extract just the first system message, preserving its structure
47
- if isinstance(detected_system, str):
48
- return detected_system
49
- elif isinstance(detected_system, list) and detected_system:
50
- # Return only the first message object with its complete structure (type, text, cache_control)
51
- return [detected_system[0]]
52
-
53
- return None
54
-
55
-
56
- def get_fallback_system_field() -> list[dict[str, Any]]:
57
- """Get fallback system field when no detection data is available."""
58
- return [
59
- {
60
- "type": "text",
61
- "text": claude_code_prompt,
62
- "cache_control": {"type": "ephemeral"},
63
- }
64
- ]
65
-
66
-
67
- class RequestData(TypedDict):
68
- """Typed structure for transformed request data."""
69
-
70
- method: str
71
- url: str
72
- headers: dict[str, str]
73
- body: bytes | None
74
-
75
-
76
- class ResponseData(TypedDict):
77
- """Typed structure for transformed response data."""
78
-
79
- status_code: int
80
- headers: dict[str, str]
81
- body: bytes
82
-
83
-
84
- class HTTPRequestTransformer(RequestTransformer):
85
- """HTTP request transformer that implements the abstract RequestTransformer interface."""
86
-
87
- def __init__(self) -> None:
88
- """Initialize HTTP request transformer."""
89
- super().__init__()
90
-
91
- async def _transform_request(
92
- self, request: ProxyRequest, context: TransformContext | None = None
93
- ) -> ProxyRequest:
94
- """Transform a proxy request according to the abstract interface.
95
-
96
- Args:
97
- request: The structured proxy request to transform
98
- context: Optional transformation context
99
-
100
- Returns:
101
- The transformed proxy request
102
- """
103
- # Transform path
104
- transformed_path = self.transform_path(
105
- request.url.split("?")[0].split("/", 3)[-1]
106
- if "/" in request.url
107
- else request.url
108
- )
109
-
110
- # Build new URL with transformed path
111
- base_url = "https://api.anthropic.com"
112
- new_url = f"{base_url}{transformed_path}"
113
-
114
- # Add query parameters
115
- if request.params:
116
- import urllib.parse
117
-
118
- query_string = urllib.parse.urlencode(request.params)
119
- new_url = f"{new_url}?{query_string}"
120
-
121
- # Transform headers (requires access token from context)
122
- access_token = ""
123
- if context and hasattr(context, "access_token"):
124
- access_token = context.access_token
125
- elif context and isinstance(context, dict):
126
- access_token = context.get("access_token", "")
127
-
128
- # Extract app_state from context if available
129
- app_state = None
130
- if context and hasattr(context, "app_state"):
131
- app_state = context.app_state
132
- elif context and isinstance(context, dict):
133
- app_state = context.get("app_state")
134
-
135
- transformed_headers = self.create_proxy_headers(
136
- request.headers, access_token, self.proxy_mode, app_state
137
- )
138
-
139
- # Transform body
140
- transformed_body = request.body
141
- if request.body:
142
- if isinstance(request.body, bytes):
143
- transformed_body = self.transform_request_body(
144
- request.body, transformed_path, self.proxy_mode, app_state
145
- )
146
- elif isinstance(request.body, str):
147
- transformed_body = self.transform_request_body(
148
- request.body.encode("utf-8"),
149
- transformed_path,
150
- self.proxy_mode,
151
- app_state,
152
- )
153
- elif isinstance(request.body, dict):
154
- import json
155
-
156
- transformed_body = self.transform_request_body(
157
- json.dumps(request.body).encode("utf-8"),
158
- transformed_path,
159
- self.proxy_mode,
160
- app_state,
161
- )
162
-
163
- # Create new transformed request
164
- return ProxyRequest(
165
- method=request.method,
166
- url=new_url,
167
- headers=transformed_headers,
168
- params={}, # Already included in URL
169
- body=transformed_body,
170
- protocol=request.protocol,
171
- timeout=request.timeout,
172
- metadata=request.metadata,
173
- )
174
-
175
- async def transform_proxy_request(
176
- self,
177
- method: str,
178
- path: str,
179
- headers: dict[str, str],
180
- body: bytes | None,
181
- query_params: dict[str, str | list[str]] | None,
182
- access_token: str,
183
- target_base_url: str = "https://api.anthropic.com",
184
- app_state: Any = None,
185
- injection_mode: str = "minimal",
186
- ) -> RequestData:
187
- """Transform request using direct parameters from ProxyService.
188
-
189
- This method provides the same functionality as ProxyService._transform_request()
190
- but is properly located in the transformer layer.
191
-
192
- Args:
193
- method: HTTP method
194
- path: Request path
195
- headers: Request headers
196
- body: Request body
197
- query_params: Query parameters
198
- access_token: OAuth access token
199
- target_base_url: Base URL for the target API
200
- app_state: Optional app state containing detection data
201
- injection_mode: System prompt injection mode
202
-
203
- Returns:
204
- Dictionary with transformed request data (method, url, headers, body)
205
- """
206
- import urllib.parse
207
-
208
- # Transform path
209
- transformed_path = self.transform_path(path, self.proxy_mode)
210
- target_url = f"{target_base_url.rstrip('/')}{transformed_path}"
211
-
212
- # Add beta=true query parameter for /v1/messages requests if not already present
213
- if transformed_path == "/v1/messages":
214
- if query_params is None:
215
- query_params = {}
216
- elif "beta" not in query_params:
217
- query_params = dict(query_params) # Make a copy
218
-
219
- if "beta" not in query_params:
220
- query_params["beta"] = "true"
221
-
222
- # Transform body first (as it might change size)
223
- proxy_body = None
224
- if body:
225
- proxy_body = self.transform_request_body(
226
- body, path, self.proxy_mode, app_state, injection_mode
227
- )
228
-
229
- # Transform headers (and update Content-Length if body changed)
230
- proxy_headers = self.create_proxy_headers(
231
- headers, access_token, self.proxy_mode, app_state
232
- )
233
-
234
- # Update Content-Length if body was transformed and size changed
235
- if proxy_body and body and len(proxy_body) != len(body):
236
- # Remove any existing content-length headers (case-insensitive)
237
- proxy_headers = {
238
- k: v for k, v in proxy_headers.items() if k.lower() != "content-length"
239
- }
240
- proxy_headers["Content-Length"] = str(len(proxy_body))
241
- elif proxy_body and not body:
242
- # New body was created where none existed
243
- proxy_headers["Content-Length"] = str(len(proxy_body))
244
-
245
- # Add query parameters to URL if present
246
- if query_params:
247
- query_string = urllib.parse.urlencode(query_params)
248
- target_url = f"{target_url}?{query_string}"
249
-
250
- return RequestData(
251
- method=method,
252
- url=target_url,
253
- headers=proxy_headers,
254
- body=proxy_body,
255
- )
256
-
257
- def transform_path(self, path: str, proxy_mode: str = "full") -> str:
258
- """Transform request path."""
259
- # Remove /api prefix if present (for new proxy endpoints)
260
- if path.startswith("/api"):
261
- path = path[4:] # Remove "/api" prefix
262
-
263
- # Remove /openai prefix if present
264
- if path.startswith("/openai"):
265
- path = path[7:] # Remove "/openai" prefix
266
-
267
- # Convert OpenAI chat completions to Anthropic messages
268
- if path == "/v1/chat/completions":
269
- return "/v1/messages"
270
-
271
- return path
272
-
273
- def create_proxy_headers(
274
- self,
275
- headers: dict[str, str],
276
- access_token: str,
277
- proxy_mode: str = "full",
278
- app_state: Any = None,
279
- ) -> dict[str, str]:
280
- """Create proxy headers from original headers with Claude CLI identity."""
281
- proxy_headers = {}
282
-
283
- # Strip potentially problematic headers
284
- excluded_headers = {
285
- "host",
286
- "x-forwarded-for",
287
- "x-forwarded-proto",
288
- "x-forwarded-host",
289
- "forwarded",
290
- # Authentication headers to be replaced
291
- "authorization",
292
- "x-api-key",
293
- # Compression headers to avoid decompression issues
294
- "accept-encoding",
295
- "content-encoding",
296
- # CORS headers - should not be forwarded to upstream
297
- "origin",
298
- "access-control-request-method",
299
- "access-control-request-headers",
300
- "access-control-allow-origin",
301
- "access-control-allow-methods",
302
- "access-control-allow-headers",
303
- "access-control-allow-credentials",
304
- "access-control-max-age",
305
- "access-control-expose-headers",
306
- }
307
-
308
- # Copy important headers (excluding problematic ones)
309
- for key, value in headers.items():
310
- lower_key = key.lower()
311
- if lower_key not in excluded_headers:
312
- proxy_headers[key] = value
313
-
314
- # Set authentication with OAuth token
315
- if access_token:
316
- proxy_headers["Authorization"] = f"Bearer {access_token}"
317
-
318
- # Set defaults for essential headers
319
- if "content-type" not in [k.lower() for k in proxy_headers]:
320
- proxy_headers["Content-Type"] = "application/json"
321
- if "accept" not in [k.lower() for k in proxy_headers]:
322
- proxy_headers["Accept"] = "application/json"
323
- if "connection" not in [k.lower() for k in proxy_headers]:
324
- proxy_headers["Connection"] = "keep-alive"
325
-
326
- # Use detected Claude CLI headers when available
327
- if app_state and hasattr(app_state, "claude_detection_data"):
328
- claude_data = app_state.claude_detection_data
329
- detected_headers = claude_data.headers.to_headers_dict()
330
- proxy_headers.update(detected_headers)
331
- logger.debug("using_detected_headers", version=claude_data.claude_version)
332
- else:
333
- # Fallback to hardcoded Claude/Anthropic headers
334
- proxy_headers["anthropic-beta"] = (
335
- "claude-code-20250219,oauth-2025-04-20,"
336
- "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
337
- )
338
- proxy_headers["anthropic-version"] = "2023-06-01"
339
- proxy_headers["anthropic-dangerous-direct-browser-access"] = "true"
340
-
341
- # Claude CLI identity headers
342
- proxy_headers["x-app"] = "cli"
343
- proxy_headers["User-Agent"] = "claude-cli/1.0.60 (external, cli)"
344
-
345
- # Stainless SDK compatibility headers
346
- proxy_headers["X-Stainless-Lang"] = "js"
347
- proxy_headers["X-Stainless-Retry-Count"] = "0"
348
- proxy_headers["X-Stainless-Timeout"] = "60"
349
- proxy_headers["X-Stainless-Package-Version"] = "0.55.1"
350
- proxy_headers["X-Stainless-OS"] = "Linux"
351
- proxy_headers["X-Stainless-Arch"] = "x64"
352
- proxy_headers["X-Stainless-Runtime"] = "node"
353
- proxy_headers["X-Stainless-Runtime-Version"] = "v24.3.0"
354
- logger.debug("using_fallback_headers")
355
-
356
- # Standard HTTP headers for proper API interaction
357
- proxy_headers["accept-language"] = "*"
358
- proxy_headers["sec-fetch-mode"] = "cors"
359
- # Note: accept-encoding removed to avoid compression issues
360
- # HTTPX handles compression automatically
361
-
362
- return proxy_headers
363
-
364
- def _count_cache_control_blocks(self, data: dict[str, Any]) -> dict[str, int]:
365
- """Count cache_control blocks in different parts of the request.
366
-
367
- Returns:
368
- Dictionary with counts for 'injected_system', 'user_system', and 'messages'
369
- """
370
- counts = {"injected_system": 0, "user_system": 0, "messages": 0}
371
-
372
- # Count in system field
373
- system = data.get("system")
374
- if system:
375
- if isinstance(system, str):
376
- # String system prompts don't have cache_control
377
- pass
378
- elif isinstance(system, list):
379
- # Count cache_control in system prompt blocks
380
- # The first block(s) are injected, rest are user's
381
- injected_count = 0
382
- for i, block in enumerate(system):
383
- if isinstance(block, dict) and "cache_control" in block:
384
- # Check if this is the injected prompt (contains Claude Code identity)
385
- text = block.get("text", "")
386
- if "Claude Code" in text or "Anthropic's official CLI" in text:
387
- counts["injected_system"] += 1
388
- injected_count = max(injected_count, i + 1)
389
- elif i < injected_count:
390
- # Part of injected system (multiple blocks)
391
- counts["injected_system"] += 1
392
- else:
393
- counts["user_system"] += 1
394
-
395
- # Count in messages
396
- messages = data.get("messages", [])
397
- for msg in messages:
398
- content = msg.get("content")
399
- if isinstance(content, list):
400
- for block in content:
401
- if isinstance(block, dict) and "cache_control" in block:
402
- counts["messages"] += 1
403
-
404
- return counts
405
-
406
- def _limit_cache_control_blocks(
407
- self, data: dict[str, Any], max_blocks: int = 4
408
- ) -> dict[str, Any]:
409
- """Limit the number of cache_control blocks to comply with Anthropic's limit.
410
-
411
- Priority order:
412
- 1. Injected system prompt cache_control (highest priority - Claude Code identity)
413
- 2. User's system prompt cache_control
414
- 3. User's message cache_control (lowest priority)
415
-
416
- Args:
417
- data: Request data dictionary
418
- max_blocks: Maximum number of cache_control blocks allowed (default: 4)
419
-
420
- Returns:
421
- Modified data dictionary with cache_control blocks limited
422
- """
423
- import copy
424
-
425
- # Deep copy to avoid modifying original
426
- data = copy.deepcopy(data)
427
-
428
- # Count existing blocks
429
- counts = self._count_cache_control_blocks(data)
430
- total = counts["injected_system"] + counts["user_system"] + counts["messages"]
431
-
432
- if total <= max_blocks:
433
- # No need to remove anything
434
- return data
435
-
436
- logger.warning(
437
- "cache_control_limit_exceeded",
438
- total_blocks=total,
439
- max_blocks=max_blocks,
440
- injected=counts["injected_system"],
441
- user_system=counts["user_system"],
442
- messages=counts["messages"],
443
- )
444
-
445
- # Calculate how many to remove
446
- to_remove = total - max_blocks
447
- removed = 0
448
-
449
- # Remove from messages first (lowest priority)
450
- if to_remove > 0 and counts["messages"] > 0:
451
- messages = data.get("messages", [])
452
- for msg in reversed(messages): # Remove from end first
453
- if removed >= to_remove:
454
- break
455
- content = msg.get("content")
456
- if isinstance(content, list):
457
- for block in reversed(content):
458
- if removed >= to_remove:
459
- break
460
- if isinstance(block, dict) and "cache_control" in block:
461
- del block["cache_control"]
462
- removed += 1
463
- logger.debug("removed_cache_control", location="message")
464
-
465
- # Remove from user system prompts next
466
- if removed < to_remove and counts["user_system"] > 0:
467
- system = data.get("system")
468
- if isinstance(system, list):
469
- # Find and remove cache_control from user system blocks (non-injected)
470
- for block in reversed(system):
471
- if removed >= to_remove:
472
- break
473
- if isinstance(block, dict) and "cache_control" in block:
474
- text = block.get("text", "")
475
- # Skip injected prompts (highest priority)
476
- if (
477
- "Claude Code" not in text
478
- and "Anthropic's official CLI" not in text
479
- ):
480
- del block["cache_control"]
481
- removed += 1
482
- logger.debug(
483
- "removed_cache_control", location="user_system"
484
- )
485
-
486
- # In theory, we should never need to remove injected system cache_control
487
- # but include this for completeness
488
- if removed < to_remove:
489
- logger.error(
490
- "cannot_preserve_injected_cache_control",
491
- needed_to_remove=to_remove,
492
- actually_removed=removed,
493
- )
494
-
495
- return data
496
-
497
- def transform_request_body(
498
- self,
499
- body: bytes,
500
- path: str,
501
- proxy_mode: str = "full",
502
- app_state: Any = None,
503
- injection_mode: str = "minimal",
504
- ) -> bytes:
505
- """Transform request body."""
506
- if not body:
507
- return body
508
-
509
- # Check if this is an OpenAI request and transform it
510
- if self._is_openai_request(path, body):
511
- # Transform OpenAI format to Anthropic format
512
- body = self._transform_openai_to_anthropic(body)
513
-
514
- # Apply system prompt transformation for Claude Code identity
515
- return self.transform_system_prompt(body, app_state, injection_mode)
516
-
517
- def transform_system_prompt(
518
- self, body: bytes, app_state: Any = None, injection_mode: str = "minimal"
519
- ) -> bytes:
520
- """Transform system prompt based on injection mode.
521
-
522
- Args:
523
- body: Original request body as bytes
524
- app_state: Optional app state containing detection data
525
- injection_mode: System prompt injection mode ('minimal' or 'full')
526
-
527
- Returns:
528
- Transformed request body as bytes with system prompt injection
529
- """
530
- try:
531
- import json
532
-
533
- data = json.loads(body.decode("utf-8"))
534
- except (json.JSONDecodeError, UnicodeDecodeError) as e:
535
- # Return original if not valid JSON
536
- logger.warning(
537
- "http_transform_json_decode_failed",
538
- error=str(e),
539
- body_preview=body[:200].decode("utf-8", errors="replace")
540
- if body
541
- else None,
542
- body_length=len(body) if body else 0,
543
- )
544
- return body
545
-
546
- # Get the system field to inject
547
- detected_system = get_detected_system_field(app_state, injection_mode)
548
- if detected_system is None:
549
- # No detection data, use fallback
550
- detected_system = get_fallback_system_field()
551
-
552
- # Always inject the system prompt (detected or fallback)
553
- if "system" not in data:
554
- # No existing system prompt, inject the detected/fallback one
555
- data["system"] = detected_system
556
- else:
557
- # Request has existing system prompt, prepend the detected/fallback one
558
- existing_system = data["system"]
559
-
560
- if isinstance(detected_system, str):
561
- # Detected system is a string
562
- if isinstance(existing_system, str):
563
- # Both are strings, convert to list format
564
- data["system"] = [
565
- {"type": "text", "text": detected_system},
566
- {"type": "text", "text": existing_system},
567
- ]
568
- elif isinstance(existing_system, list):
569
- # Detected is string, existing is list
570
- data["system"] = [
571
- {"type": "text", "text": detected_system}
572
- ] + existing_system
573
- elif isinstance(detected_system, list):
574
- # Detected system is a list
575
- if isinstance(existing_system, str):
576
- # Detected is list, existing is string
577
- data["system"] = detected_system + [
578
- {"type": "text", "text": existing_system}
579
- ]
580
- elif isinstance(existing_system, list):
581
- # Both are lists, concatenate
582
- data["system"] = detected_system + existing_system
583
-
584
- # Limit cache_control blocks to comply with Anthropic's limit
585
- data = self._limit_cache_control_blocks(data)
586
-
587
- return json.dumps(data).encode("utf-8")
588
-
589
- def _is_openai_request(self, path: str, body: bytes) -> bool:
590
- """Check if this is an OpenAI API request."""
591
- # Check path-based indicators
592
- if "/openai/" in path or "/chat/completions" in path:
593
- return True
594
-
595
- # Check body-based indicators
596
- if body:
597
- try:
598
- import json
599
-
600
- data = json.loads(body.decode("utf-8"))
601
- # Look for OpenAI-specific patterns
602
- model = data.get("model", "")
603
- if model.startswith(("gpt-", "o1-", "text-davinci")):
604
- return True
605
- # Check for OpenAI message format with system in messages
606
- messages = data.get("messages", [])
607
- if messages and any(msg.get("role") == "system" for msg in messages):
608
- return True
609
- except (json.JSONDecodeError, UnicodeDecodeError) as e:
610
- logger.warning(
611
- "openai_request_detection_json_decode_failed",
612
- error=str(e),
613
- body_preview=body[:100].decode("utf-8", errors="replace")
614
- if body
615
- else None,
616
- )
617
- pass
618
-
619
- return False
620
-
621
- def _transform_openai_to_anthropic(self, body: bytes) -> bytes:
622
- """Transform OpenAI request format to Anthropic format."""
623
- try:
624
- # Use the OpenAI adapter for transformation
625
- import json
626
-
627
- from ccproxy.adapters.openai.adapter import OpenAIAdapter
628
-
629
- adapter = OpenAIAdapter()
630
- openai_data = json.loads(body.decode("utf-8"))
631
- anthropic_data = adapter.adapt_request(openai_data)
632
- return json.dumps(anthropic_data).encode("utf-8")
633
-
634
- except Exception as e:
635
- logger.warning(
636
- "openai_transformation_failed",
637
- error=str(e),
638
- operation="transform_openai_to_anthropic",
639
- )
640
- # Return original body if transformation fails
641
- return body
642
-
643
-
644
- class HTTPResponseTransformer(ResponseTransformer):
645
- """HTTP response transformer that implements the abstract ResponseTransformer interface."""
646
-
647
- def __init__(self) -> None:
648
- """Initialize HTTP response transformer."""
649
- super().__init__()
650
-
651
- async def _transform_response(
652
- self, response: ProxyResponse, context: TransformContext | None = None
653
- ) -> ProxyResponse:
654
- """Transform a proxy response according to the abstract interface.
655
-
656
- Args:
657
- response: The structured proxy response to transform
658
- context: Optional transformation context
659
-
660
- Returns:
661
- The transformed proxy response
662
- """
663
- # Extract original path from context for transformation decisions
664
- original_path = ""
665
- if context and hasattr(context, "original_path"):
666
- original_path = context.original_path
667
- elif context and isinstance(context, dict):
668
- original_path = context.get("original_path", "")
669
-
670
- # Transform response body
671
- transformed_body = response.body
672
- if response.body:
673
- if isinstance(response.body, bytes):
674
- transformed_body = self.transform_response_body(
675
- response.body, original_path
676
- )
677
- elif isinstance(response.body, str):
678
- body_bytes = response.body.encode("utf-8")
679
- transformed_body = self.transform_response_body(
680
- body_bytes, original_path
681
- )
682
- elif isinstance(response.body, dict):
683
- import json
684
-
685
- body_bytes = json.dumps(response.body).encode("utf-8")
686
- transformed_body = self.transform_response_body(
687
- body_bytes, original_path
688
- )
689
-
690
- # Calculate content length for transformed body
691
- content_length = 0
692
- if transformed_body:
693
- if isinstance(transformed_body, bytes):
694
- content_length = len(transformed_body)
695
- elif isinstance(transformed_body, str):
696
- content_length = len(transformed_body.encode("utf-8"))
697
- else:
698
- content_length = len(str(transformed_body))
699
-
700
- # Transform response headers
701
- transformed_headers = self.transform_response_headers(
702
- response.headers, original_path, content_length
703
- )
704
-
705
- # Create new transformed response
706
- return ProxyResponse(
707
- status_code=response.status_code,
708
- headers=transformed_headers,
709
- body=transformed_body,
710
- metadata=response.metadata,
711
- )
712
-
713
- async def transform_proxy_response(
714
- self,
715
- status_code: int,
716
- headers: dict[str, str],
717
- body: bytes,
718
- original_path: str,
719
- proxy_mode: str = "full",
720
- ) -> ResponseData:
721
- """Transform response using direct parameters from ProxyService.
722
-
723
- This method provides the same functionality as ProxyService._transform_response()
724
- but is properly located in the transformer layer.
725
-
726
- Args:
727
- status_code: HTTP status code
728
- headers: Response headers
729
- body: Response body
730
- original_path: Original request path for context
731
- proxy_mode: Proxy transformation mode
732
-
733
- Returns:
734
- Dictionary with transformed response data (status_code, headers, body)
735
- """
736
- # For error responses, handle OpenAI transformation if needed
737
- if status_code >= 400:
738
- transformed_error_body = body
739
- if self._is_openai_request(original_path):
740
- try:
741
- import json
742
-
743
- from ccproxy.adapters.openai.adapter import OpenAIAdapter
744
-
745
- error_data = json.loads(body.decode("utf-8"))
746
- openai_adapter = OpenAIAdapter()
747
- openai_error = openai_adapter.adapt_error(error_data)
748
- transformed_error_body = json.dumps(openai_error).encode("utf-8")
749
- except (json.JSONDecodeError, UnicodeDecodeError):
750
- # Keep original error if parsing fails
751
- pass
752
-
753
- return ResponseData(
754
- status_code=status_code,
755
- headers=headers,
756
- body=transformed_error_body,
757
- )
758
-
759
- # For successful responses, transform normally
760
- transformed_body = self.transform_response_body(body, original_path, proxy_mode)
761
-
762
- transformed_headers = self.transform_response_headers(
763
- headers, original_path, len(transformed_body), proxy_mode
764
- )
765
-
766
- return ResponseData(
767
- status_code=status_code,
768
- headers=transformed_headers,
769
- body=transformed_body,
770
- )
771
-
772
- def transform_response_body(
773
- self, body: bytes, path: str, proxy_mode: str = "full"
774
- ) -> bytes:
775
- """Transform response body."""
776
- # Basic body transformation - pass through for now
777
- return body
778
-
779
- def transform_response_headers(
780
- self,
781
- headers: dict[str, str],
782
- path: str,
783
- content_length: int,
784
- proxy_mode: str = "full",
785
- ) -> dict[str, str]:
786
- """Transform response headers."""
787
- transformed_headers = {}
788
-
789
- # Copy important headers
790
- for key, value in headers.items():
791
- lower_key = key.lower()
792
- if lower_key not in [
793
- "content-length",
794
- "transfer-encoding",
795
- "content-encoding",
796
- "date", # Remove upstream date header to avoid conflicts
797
- ]:
798
- transformed_headers[key] = value
799
-
800
- # Set content length
801
- transformed_headers["Content-Length"] = str(content_length)
802
-
803
- # Add CORS headers
804
- transformed_headers["Access-Control-Allow-Origin"] = "*"
805
- transformed_headers["Access-Control-Allow-Headers"] = "*"
806
- transformed_headers["Access-Control-Allow-Methods"] = "*"
807
-
808
- return transformed_headers
809
-
810
- def _is_openai_request(self, path: str) -> bool:
811
- """Check if this is an OpenAI API request."""
812
- return "/openai/" in path or "/chat/completions" in path