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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -1,231 +0,0 @@
1
- """Configuration validation utilities."""
2
-
3
- import re
4
- from pathlib import Path
5
- from typing import Any
6
- from urllib.parse import urlparse
7
-
8
-
9
- class ConfigValidationError(Exception):
10
- """Configuration validation error."""
11
-
12
- pass
13
-
14
-
15
- def validate_host(host: str) -> str:
16
- """Validate host address.
17
-
18
- Args:
19
- host: Host address to validate
20
-
21
- Returns:
22
- The validated host address
23
-
24
- Raises:
25
- ConfigValidationError: If host is invalid
26
- """
27
- if not host:
28
- raise ConfigValidationError("Host cannot be empty")
29
-
30
- # Allow localhost, IP addresses, and domain names
31
- if host in ["localhost", "0.0.0.0", "127.0.0.1"]:
32
- return host
33
-
34
- # Basic IP address validation
35
- if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", host):
36
- parts = host.split(".")
37
- if all(0 <= int(part) <= 255 for part in parts):
38
- return host
39
- raise ConfigValidationError(f"Invalid IP address: {host}")
40
-
41
- # Basic domain name validation
42
- if re.match(r"^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", host):
43
- return host
44
-
45
- return host # Allow other formats for flexibility
46
-
47
-
48
- def validate_port(port: int | str) -> int:
49
- """Validate port number.
50
-
51
- Args:
52
- port: Port number to validate
53
-
54
- Returns:
55
- The validated port number
56
-
57
- Raises:
58
- ConfigValidationError: If port is invalid
59
- """
60
- if isinstance(port, str):
61
- try:
62
- port = int(port)
63
- except ValueError as e:
64
- raise ConfigValidationError(f"Port must be a valid integer: {port}") from e
65
-
66
- if not isinstance(port, int):
67
- raise ConfigValidationError(f"Port must be an integer: {port}")
68
-
69
- if port < 1 or port > 65535:
70
- raise ConfigValidationError(f"Port must be between 1 and 65535: {port}")
71
-
72
- return port
73
-
74
-
75
- def validate_url(url: str) -> str:
76
- """Validate URL format.
77
-
78
- Args:
79
- url: URL to validate
80
-
81
- Returns:
82
- The validated URL
83
-
84
- Raises:
85
- ConfigValidationError: If URL is invalid
86
- """
87
- if not url:
88
- raise ConfigValidationError("URL cannot be empty")
89
-
90
- try:
91
- result = urlparse(url)
92
- if not result.scheme or not result.netloc:
93
- raise ConfigValidationError(f"Invalid URL format: {url}")
94
- except Exception as e:
95
- raise ConfigValidationError(f"Invalid URL: {url}") from e
96
-
97
- return url
98
-
99
-
100
- def validate_path(path: str | Path) -> Path:
101
- """Validate file path.
102
-
103
- Args:
104
- path: Path to validate
105
-
106
- Returns:
107
- The validated Path object
108
-
109
- Raises:
110
- ConfigValidationError: If path is invalid
111
- """
112
- if isinstance(path, str):
113
- path = Path(path)
114
-
115
- if not isinstance(path, Path):
116
- raise ConfigValidationError(f"Path must be a string or Path object: {path}")
117
-
118
- return path
119
-
120
-
121
- def validate_log_level(level: str) -> str:
122
- """Validate log level.
123
-
124
- Args:
125
- level: Log level to validate
126
-
127
- Returns:
128
- The validated log level
129
-
130
- Raises:
131
- ConfigValidationError: If log level is invalid
132
- """
133
- valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
134
- level = level.upper()
135
-
136
- if level not in valid_levels:
137
- raise ConfigValidationError(
138
- f"Invalid log level: {level}. Must be one of: {valid_levels}"
139
- )
140
-
141
- return level
142
-
143
-
144
- def validate_cors_origins(origins: list[str]) -> list[str]:
145
- """Validate CORS origins.
146
-
147
- Args:
148
- origins: List of origin URLs to validate
149
-
150
- Returns:
151
- The validated list of origins
152
-
153
- Raises:
154
- ConfigValidationError: If any origin is invalid
155
- """
156
- if not isinstance(origins, list):
157
- raise ConfigValidationError("CORS origins must be a list")
158
-
159
- validated_origins = []
160
- for origin in origins:
161
- if origin == "*":
162
- validated_origins.append(origin)
163
- else:
164
- validated_origins.append(validate_url(origin))
165
-
166
- return validated_origins
167
-
168
-
169
- def validate_timeout(timeout: int | float) -> int | float:
170
- """Validate timeout value.
171
-
172
- Args:
173
- timeout: Timeout value to validate
174
-
175
- Returns:
176
- The validated timeout value
177
-
178
- Raises:
179
- ConfigValidationError: If timeout is invalid
180
- """
181
- if not isinstance(timeout, int | float):
182
- raise ConfigValidationError(f"Timeout must be a number: {timeout}")
183
-
184
- if timeout <= 0:
185
- raise ConfigValidationError(f"Timeout must be positive: {timeout}")
186
-
187
- return timeout
188
-
189
-
190
- def validate_config_dict(config: dict[str, Any]) -> dict[str, Any]:
191
- """Validate configuration dictionary.
192
-
193
- Args:
194
- config: Configuration dictionary to validate
195
-
196
- Returns:
197
- The validated configuration dictionary
198
-
199
- Raises:
200
- ConfigValidationError: If configuration is invalid
201
- """
202
- if not isinstance(config, dict):
203
- raise ConfigValidationError("Configuration must be a dictionary")
204
-
205
- validated_config: dict[str, Any] = {}
206
-
207
- # Validate specific fields if present
208
- if "host" in config:
209
- validated_config["host"] = validate_host(config["host"])
210
-
211
- if "port" in config:
212
- validated_config["port"] = validate_port(config["port"])
213
-
214
- if "target_url" in config:
215
- validated_config["target_url"] = validate_url(config["target_url"])
216
-
217
- if "log_level" in config:
218
- validated_config["log_level"] = validate_log_level(config["log_level"])
219
-
220
- if "cors_origins" in config:
221
- validated_config["cors_origins"] = validate_cors_origins(config["cors_origins"])
222
-
223
- if "timeout" in config:
224
- validated_config["timeout"] = validate_timeout(config["timeout"])
225
-
226
- # Copy other fields without validation
227
- for key, value in config.items():
228
- if key not in validated_config:
229
- validated_config[key] = value
230
-
231
- return validated_config
@@ -1,389 +0,0 @@
1
- """Codex-specific transformers for request/response transformation."""
2
-
3
- import json
4
-
5
- import structlog
6
- from typing_extensions import TypedDict
7
-
8
- from ccproxy.core.transformers import RequestTransformer
9
- from ccproxy.core.types import ProxyRequest, TransformContext
10
- from ccproxy.models.detection import CodexCacheData
11
-
12
-
13
- logger = structlog.get_logger(__name__)
14
-
15
-
16
- class CodexRequestData(TypedDict):
17
- """Typed structure for transformed Codex request data."""
18
-
19
- method: str
20
- url: str
21
- headers: dict[str, str]
22
- body: bytes | None
23
-
24
-
25
- class CodexRequestTransformer(RequestTransformer):
26
- """Codex request transformer for header and instructions field injection."""
27
-
28
- def __init__(self) -> None:
29
- """Initialize Codex request transformer."""
30
- super().__init__()
31
-
32
- async def _transform_request(
33
- self, request: ProxyRequest, context: TransformContext | None = None
34
- ) -> ProxyRequest:
35
- """Transform a proxy request for Codex API.
36
-
37
- Args:
38
- request: The structured proxy request to transform
39
- context: Optional transformation context
40
-
41
- Returns:
42
- The transformed proxy request
43
- """
44
- # Extract required data from context
45
- access_token = ""
46
- session_id = ""
47
- account_id = ""
48
- codex_detection_data = None
49
-
50
- if context:
51
- if hasattr(context, "access_token"):
52
- access_token = context.access_token
53
- elif isinstance(context, dict):
54
- access_token = context.get("access_token", "")
55
-
56
- if hasattr(context, "session_id"):
57
- session_id = context.session_id
58
- elif isinstance(context, dict):
59
- session_id = context.get("session_id", "")
60
-
61
- if hasattr(context, "account_id"):
62
- account_id = context.account_id
63
- elif isinstance(context, dict):
64
- account_id = context.get("account_id", "")
65
-
66
- if hasattr(context, "codex_detection_data"):
67
- codex_detection_data = context.codex_detection_data
68
- elif isinstance(context, dict):
69
- codex_detection_data = context.get("codex_detection_data")
70
-
71
- # Transform URL - remove codex prefix and forward to ChatGPT backend
72
- transformed_url = self._transform_codex_url(request.url)
73
-
74
- # Convert request body to bytes for header processing
75
- body_bytes = None
76
- if request.body:
77
- if isinstance(request.body, bytes):
78
- body_bytes = request.body
79
- elif isinstance(request.body, str):
80
- body_bytes = request.body.encode("utf-8")
81
- elif isinstance(request.body, dict):
82
- body_bytes = json.dumps(request.body).encode("utf-8")
83
-
84
- # Transform headers with Codex CLI identity
85
- transformed_headers = self.create_codex_headers(
86
- request.headers,
87
- access_token,
88
- session_id,
89
- account_id,
90
- body_bytes,
91
- codex_detection_data,
92
- )
93
-
94
- # Transform body to inject instructions
95
- transformed_body = request.body
96
- if request.body:
97
- if isinstance(request.body, bytes):
98
- transformed_body = self.transform_codex_body(
99
- request.body, codex_detection_data
100
- )
101
- else:
102
- # Convert to bytes if needed
103
- body_bytes = (
104
- json.dumps(request.body).encode("utf-8")
105
- if isinstance(request.body, dict)
106
- else str(request.body).encode("utf-8")
107
- )
108
- transformed_body = self.transform_codex_body(
109
- body_bytes, codex_detection_data
110
- )
111
-
112
- # Create new transformed request
113
- return ProxyRequest(
114
- method=request.method,
115
- url=transformed_url,
116
- headers=transformed_headers,
117
- params={}, # Query params handled in URL
118
- body=transformed_body,
119
- protocol=request.protocol,
120
- timeout=request.timeout,
121
- metadata=request.metadata,
122
- )
123
-
124
- async def transform_codex_request(
125
- self,
126
- method: str,
127
- path: str,
128
- headers: dict[str, str],
129
- body: bytes | None,
130
- access_token: str,
131
- session_id: str,
132
- account_id: str,
133
- codex_detection_data: CodexCacheData | None = None,
134
- target_base_url: str = "https://chatgpt.com/backend-api/codex",
135
- ) -> CodexRequestData:
136
- """Transform Codex request using direct parameters from ProxyService.
137
-
138
- Args:
139
- method: HTTP method
140
- path: Request path
141
- headers: Request headers
142
- body: Request body
143
- access_token: OAuth access token
144
- session_id: Codex session ID
145
- account_id: ChatGPT account ID
146
- codex_detection_data: Optional Codex detection data
147
- target_base_url: Base URL for the Codex API
148
-
149
- Returns:
150
- Dictionary with transformed request data (method, url, headers, body)
151
- """
152
- # Transform URL path
153
- transformed_path = self._transform_codex_path(path)
154
- target_url = f"{target_base_url.rstrip('/')}{transformed_path}"
155
-
156
- # Transform body first (inject instructions)
157
- codex_body = None
158
- if body:
159
- # body is guaranteed to be bytes due to parameter type
160
- codex_body = self.transform_codex_body(body, codex_detection_data)
161
-
162
- # Transform headers with Codex CLI identity and authentication
163
- codex_headers = self.create_codex_headers(
164
- headers, access_token, session_id, account_id, body, codex_detection_data
165
- )
166
-
167
- # Update Content-Length if body was transformed and size changed
168
- if codex_body and body and len(codex_body) != len(body):
169
- # Remove any existing content-length headers (case-insensitive)
170
- codex_headers = {
171
- k: v for k, v in codex_headers.items() if k.lower() != "content-length"
172
- }
173
- codex_headers["Content-Length"] = str(len(codex_body))
174
- elif codex_body and not body:
175
- # New body was created where none existed
176
- codex_headers["Content-Length"] = str(len(codex_body))
177
-
178
- return CodexRequestData(
179
- method=method,
180
- url=target_url,
181
- headers=codex_headers,
182
- body=codex_body,
183
- )
184
-
185
- def _transform_codex_url(self, url: str) -> str:
186
- """Transform URL from proxy format to ChatGPT backend format."""
187
- # Extract base URL and path
188
- if "://" in url:
189
- protocol, rest = url.split("://", 1)
190
- if "/" in rest:
191
- domain, path = rest.split("/", 1)
192
- path = "/" + path
193
- else:
194
- path = "/"
195
- else:
196
- path = url if url.startswith("/") else "/" + url
197
-
198
- # Transform path and build target URL
199
- transformed_path = self._transform_codex_path(path)
200
- return f"https://chatgpt.com/backend-api/codex{transformed_path}"
201
-
202
- def _transform_codex_path(self, path: str) -> str:
203
- """Transform request path for Codex API."""
204
- # Remove /codex prefix if present
205
- if path.startswith("/codex"):
206
- path = path[6:] # Remove "/codex" prefix
207
-
208
- # Ensure we have a valid path
209
- if not path or path == "/":
210
- path = "/responses"
211
-
212
- # Handle session_id in path for /codex/{session_id}/responses pattern
213
- if path.startswith("/") and "/" in path[1:]:
214
- # This might be /{session_id}/responses - extract the responses part
215
- parts = path.strip("/").split("/")
216
- if len(parts) >= 2 and parts[-1] == "responses":
217
- # Keep the /responses endpoint, session_id will be in headers
218
- path = "/responses"
219
-
220
- return path
221
-
222
- def create_codex_headers(
223
- self,
224
- headers: dict[str, str],
225
- access_token: str,
226
- session_id: str,
227
- account_id: str,
228
- body: bytes | None = None,
229
- codex_detection_data: CodexCacheData | None = None,
230
- ) -> dict[str, str]:
231
- """Create Codex headers with CLI identity and authentication."""
232
- codex_headers = {}
233
-
234
- # Strip potentially problematic headers
235
- excluded_headers = {
236
- "host",
237
- "x-forwarded-for",
238
- "x-forwarded-proto",
239
- "x-forwarded-host",
240
- "forwarded",
241
- # Authentication headers to be replaced
242
- "authorization",
243
- "x-api-key",
244
- # Compression headers to avoid decompression issues
245
- "accept-encoding",
246
- "content-encoding",
247
- # CORS headers - should not be forwarded to upstream
248
- "origin",
249
- "access-control-request-method",
250
- "access-control-request-headers",
251
- "access-control-allow-origin",
252
- "access-control-allow-methods",
253
- "access-control-allow-headers",
254
- "access-control-allow-credentials",
255
- "access-control-max-age",
256
- "access-control-expose-headers",
257
- }
258
-
259
- # Copy important headers (excluding problematic ones)
260
- for key, value in headers.items():
261
- lower_key = key.lower()
262
- if lower_key not in excluded_headers:
263
- codex_headers[key] = value
264
-
265
- # Set authentication with OAuth token
266
- if access_token:
267
- codex_headers["Authorization"] = f"Bearer {access_token}"
268
-
269
- # Set defaults for essential headers
270
- if "content-type" not in [k.lower() for k in codex_headers]:
271
- codex_headers["Content-Type"] = "application/json"
272
- if "accept" not in [k.lower() for k in codex_headers]:
273
- codex_headers["Accept"] = "application/json"
274
-
275
- # Use detected Codex CLI headers when available
276
- if codex_detection_data:
277
- detected_headers = codex_detection_data.headers.to_headers_dict()
278
- # Override with session-specific values
279
- detected_headers["session_id"] = session_id
280
- if account_id:
281
- detected_headers["chatgpt-account-id"] = account_id
282
- codex_headers.update(detected_headers)
283
- logger.debug(
284
- "using_detected_codex_headers",
285
- version=codex_detection_data.codex_version,
286
- )
287
- else:
288
- # Fallback to hardcoded Codex headers
289
- codex_headers.update(
290
- {
291
- "session_id": session_id,
292
- "originator": "codex_cli_rs",
293
- "openai-beta": "responses=experimental",
294
- "version": "0.21.0",
295
- }
296
- )
297
- if account_id:
298
- codex_headers["chatgpt-account-id"] = account_id
299
- logger.debug("using_fallback_codex_headers")
300
-
301
- # Don't set Accept header - let the backend handle it based on stream parameter
302
- # Setting Accept: text/event-stream with stream:true in body causes 400 Bad Request
303
- # The backend will determine the response format based on the stream parameter
304
-
305
- return codex_headers
306
-
307
- def _is_streaming_request(self, body: bytes | None) -> bool:
308
- """Check if the request body indicates a streaming request (including injected default)."""
309
- if not body:
310
- return False
311
-
312
- try:
313
- data = json.loads(body.decode("utf-8"))
314
- return data.get("stream", False) is True
315
- except (json.JSONDecodeError, UnicodeDecodeError):
316
- return False
317
-
318
- def _is_user_streaming_request(self, body: bytes | None) -> bool:
319
- """Check if the user explicitly requested streaming (has 'stream' field in original body)."""
320
- if not body:
321
- return False
322
-
323
- try:
324
- data = json.loads(body.decode("utf-8"))
325
- # Only return True if user explicitly included "stream" field (regardless of its value)
326
- return "stream" in data and data.get("stream") is True
327
- except (json.JSONDecodeError, UnicodeDecodeError):
328
- return False
329
-
330
- def transform_codex_body(
331
- self, body: bytes, codex_detection_data: CodexCacheData | None = None
332
- ) -> bytes:
333
- """Transform request body to inject Codex CLI instructions."""
334
- if not body:
335
- return body
336
-
337
- try:
338
- data = json.loads(body.decode("utf-8"))
339
- except (json.JSONDecodeError, UnicodeDecodeError) as e:
340
- # Return original if not valid JSON
341
- logger.warning(
342
- "codex_transform_json_decode_failed",
343
- error=str(e),
344
- body_preview=body[:200].decode("utf-8", errors="replace")
345
- if body
346
- else None,
347
- body_length=len(body) if body else 0,
348
- )
349
- return body
350
-
351
- # Check if this request already has the full Codex instructions
352
- # If instructions field exists and is longer than 1000 chars, it's already set
353
- if (
354
- "instructions" in data
355
- and data["instructions"]
356
- and len(data["instructions"]) > 1000
357
- ):
358
- # This already has full Codex instructions, don't replace them
359
- logger.debug("skipping_codex_transform_has_full_instructions")
360
- return body
361
-
362
- # Get the instructions to inject
363
- detected_instructions = None
364
- if codex_detection_data:
365
- detected_instructions = codex_detection_data.instructions.instructions_field
366
- else:
367
- # Fallback instructions from req.json
368
- detected_instructions = (
369
- "You are a coding agent running in the Codex CLI, a terminal-based coding assistant. "
370
- "Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.\n\n"
371
- "Your capabilities:\n"
372
- "- Receive user prompts and other context provided by the harness, such as files in the workspace.\n"
373
- "- Communicate with the user by streaming thinking & responses, and by making & updating plans.\n"
374
- "- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, "
375
- "you can request that these function calls be escalated to the user for approval before running. "
376
- 'More on this in the "Sandbox and approvals" section.\n\n'
377
- "Within this context, Codex refers to the open-source agentic coding interface "
378
- "(not the old Codex language model built by OpenAI)."
379
- )
380
-
381
- # Always inject/override the instructions field
382
- data["instructions"] = detected_instructions
383
-
384
- # Only inject stream: true if user explicitly requested streaming or didn't specify
385
- # For now, we'll inject stream: true by default since Codex seems to expect it
386
- if "stream" not in data:
387
- data["stream"] = True
388
-
389
- return json.dumps(data, separators=(",", ":")).encode("utf-8")