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,7 +1,20 @@
1
- """OpenAI Codex-specific configuration settings."""
1
+ """Codex plugin-specific configuration settings."""
2
+
3
+ from typing import Literal
2
4
 
3
5
  from pydantic import BaseModel, Field, field_validator
4
6
 
7
+ from ccproxy.core.constants import (
8
+ FORMAT_ANTHROPIC_MESSAGES,
9
+ FORMAT_OPENAI_CHAT,
10
+ FORMAT_OPENAI_RESPONSES,
11
+ )
12
+ from ccproxy.models.provider import ModelCard, ModelMappingRule, ProviderConfig
13
+ from ccproxy.plugins.codex.model_defaults import (
14
+ DEFAULT_CODEX_MODEL_CARDS,
15
+ DEFAULT_CODEX_MODEL_MAPPINGS,
16
+ )
17
+
5
18
 
6
19
  class OAuthSettings(BaseModel):
7
20
  """OAuth configuration for OpenAI authentication."""
@@ -30,19 +43,12 @@ class OAuthSettings(BaseModel):
30
43
  return v.rstrip("/")
31
44
 
32
45
 
33
- class CodexSettings(BaseModel):
34
- """OpenAI Codex-specific configuration settings."""
35
-
36
- enabled: bool = Field(
37
- default=True,
38
- description="Enable OpenAI Codex provider support",
39
- )
46
+ class CodexSettings(ProviderConfig):
47
+ """Codex plugin configuration extending base ProviderConfig."""
40
48
 
41
- base_url: str = Field(
42
- default="https://chatgpt.com/backend-api/codex",
43
- description="OpenAI Codex API base URL",
44
- )
49
+ # Base ProviderConfig fields will be inherited
45
50
 
51
+ # Codex-specific OAuth settings
46
52
  oauth: OAuthSettings = Field(
47
53
  default_factory=OAuthSettings,
48
54
  description="OAuth configuration settings",
@@ -65,6 +71,66 @@ class CodexSettings(BaseModel):
65
71
  description="Enable verbose logging for Codex operations",
66
72
  )
67
73
 
74
+ # NEW: Auth manager override support
75
+ auth_manager: str | None = Field(
76
+ default=None,
77
+ description="Override auth manager name (e.g., 'oauth_codex_lb' for load balancing)",
78
+ )
79
+
80
+ # Override base_url default for Codex
81
+ base_url: str = Field(
82
+ default="https://chatgpt.com/backend-api/codex",
83
+ description="OpenAI Codex API base URL",
84
+ )
85
+
86
+ # Set defaults for inherited fields
87
+ name: str = Field(default="codex", description="Provider name")
88
+ supports_streaming: bool = Field(
89
+ default=True, description="Whether the provider supports streaming"
90
+ )
91
+ requires_auth: bool = Field(
92
+ default=True, description="Whether the provider requires authentication"
93
+ )
94
+ auth_type: str | None = Field(
95
+ default="oauth", description="Authentication type (bearer, api_key, etc.)"
96
+ )
97
+ model_mappings: list[ModelMappingRule] = Field(
98
+ default_factory=lambda: [
99
+ rule.model_copy(deep=True) for rule in DEFAULT_CODEX_MODEL_MAPPINGS
100
+ ],
101
+ description="List of client-to-upstream model mapping rules",
102
+ )
103
+ models_endpoint: list[ModelCard] = Field(
104
+ default_factory=lambda: [
105
+ card.model_copy(deep=True) for card in DEFAULT_CODEX_MODEL_CARDS
106
+ ],
107
+ description="Model metadata served via the /models endpoint",
108
+ )
109
+
110
+ supported_input_formats: list[str] = Field(
111
+ default_factory=lambda: [
112
+ FORMAT_OPENAI_RESPONSES,
113
+ FORMAT_OPENAI_CHAT,
114
+ FORMAT_ANTHROPIC_MESSAGES,
115
+ ],
116
+ description="List of supported input formats",
117
+ )
118
+ preferred_upstream_mode: Literal["streaming", "non_streaming"] = Field(
119
+ default="streaming", description="Preferred upstream mode for requests"
120
+ )
121
+ buffer_non_streaming: bool = Field(
122
+ default=True, description="Whether to buffer non-streaming requests"
123
+ )
124
+ enable_format_registry: bool = Field(
125
+ default=True, description="Whether to enable format adapter registry"
126
+ )
127
+
128
+ # Detection configuration
129
+ detection_home_mode: Literal["temp", "home"] = Field(
130
+ default="temp",
131
+ description="Home directory mode for CLI detection: 'temp' uses temporary directory, 'home' uses actual user HOME",
132
+ )
133
+
68
134
  @field_validator("base_url")
69
135
  @classmethod
70
136
  def validate_base_url(cls, v: str) -> str:
@@ -0,0 +1,544 @@
1
+ """Service for detecting Codex CLI using centralized detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import socket
9
+ import tempfile
10
+ from contextlib import suppress
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+
14
+ from fastapi import FastAPI, Request, Response
15
+
16
+ from ccproxy.config.settings import Settings
17
+ from ccproxy.config.utils import get_ccproxy_cache_dir
18
+ from ccproxy.core.logging import get_plugin_logger
19
+ from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
20
+ from ccproxy.services.cli_detection import CLIDetectionService
21
+ from ccproxy.utils.caching import async_ttl_cache
22
+ from ccproxy.utils.headers import extract_request_headers
23
+
24
+ from .config import CodexSettings
25
+ from .models import CodexCacheData, CodexCliInfo
26
+
27
+
28
+ logger = get_plugin_logger()
29
+
30
+
31
+ class CodexDetectionService:
32
+ """Service for automatically detecting Codex CLI headers at startup."""
33
+
34
+ # Headers whose values are redacted in cache (lowercase)
35
+ REDACTED_HEADERS = [
36
+ "authorization",
37
+ "session_id",
38
+ "conversation_id",
39
+ "chatgpt-account-id",
40
+ "host",
41
+ ]
42
+ # Headers to ignore at injection time (lowercase). Cache retains keys with empty values to preserve order.
43
+ ignores_header: list[str] = [
44
+ "host",
45
+ "content-length",
46
+ "authorization",
47
+ "x-api-key",
48
+ "session_id",
49
+ "conversation_id",
50
+ "chatgpt-account-id",
51
+ ]
52
+
53
+ def __init__(
54
+ self,
55
+ settings: Settings,
56
+ cli_service: CLIDetectionService | None = None,
57
+ codex_settings: CodexSettings | None = None,
58
+ redact_sensitive_cache: bool = True,
59
+ ) -> None:
60
+ """Initialize Codex detection service.
61
+
62
+ Args:
63
+ settings: Application settings
64
+ cli_service: Optional CLI detection service for dependency injection.
65
+ If None, creates its own instance.
66
+ codex_settings: Optional Codex plugin settings for plugin-specific configuration.
67
+ If None, uses default configuration.
68
+ """
69
+ self.settings = settings
70
+ self.codex_settings = codex_settings if codex_settings else CodexSettings()
71
+ self.cache_dir = get_ccproxy_cache_dir()
72
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
73
+ self._cached_data: CodexCacheData | None = None
74
+ self._cli_service = cli_service or CLIDetectionService(settings)
75
+ self._cli_info: CodexCliInfo | None = None
76
+ self._redact_sensitive_cache = redact_sensitive_cache
77
+
78
+ async def initialize_detection(self) -> CodexCacheData:
79
+ """Initialize Codex detection at startup."""
80
+ try:
81
+ # Get current Codex version
82
+ current_version = await self._get_codex_version()
83
+
84
+ detected_data = None
85
+ # Try to load from cache first
86
+ cached = False
87
+ try:
88
+ detected_data = self._load_from_cache(current_version)
89
+ cached = detected_data is not None
90
+ except Exception as e:
91
+ logger.warning(
92
+ "invalid_cache_file",
93
+ error=str(e),
94
+ category="plugin",
95
+ exc_info=e,
96
+ )
97
+
98
+ if not cached:
99
+ # No cache or version changed - detect fresh
100
+ detected_data = await self._detect_codex_headers(current_version)
101
+ # Cache the results
102
+ self._save_to_cache(detected_data)
103
+
104
+ self._cached_data = detected_data
105
+
106
+ logger.trace(
107
+ "detection_headers_completed",
108
+ version=current_version,
109
+ cached=cached,
110
+ )
111
+
112
+ if detected_data is None:
113
+ raise ValueError("Codex detection failed")
114
+ return detected_data
115
+
116
+ except Exception as e:
117
+ logger.warning(
118
+ "detection_codex_headers_failed",
119
+ fallback=True,
120
+ exc_info=e,
121
+ category="plugin",
122
+ )
123
+ # Return fallback data
124
+ fallback_data = self._get_fallback_data()
125
+ self._cached_data = fallback_data
126
+ return fallback_data
127
+
128
+ def get_cached_data(self) -> CodexCacheData | None:
129
+ """Get currently cached detection data."""
130
+ return self._cached_data
131
+
132
+ def get_detected_headers(self) -> DetectedHeaders:
133
+ """Return cached headers as structured data."""
134
+
135
+ data = self.get_cached_data()
136
+ if not data:
137
+ return DetectedHeaders()
138
+ return data.headers
139
+
140
+ def get_detected_prompts(self) -> DetectedPrompts:
141
+ """Return cached prompt metadata as structured data."""
142
+
143
+ data = self.get_cached_data()
144
+ if not data:
145
+ return DetectedPrompts()
146
+ return data.prompts
147
+
148
+ def get_ignored_headers(self) -> list[str]:
149
+ """Headers that should be ignored when forwarding CLI values."""
150
+
151
+ return list(self.ignores_header)
152
+
153
+ def get_redacted_headers(self) -> list[str]:
154
+ """Headers that must always be removed before forwarding."""
155
+
156
+ return list(getattr(self, "REDACTED_HEADERS", []))
157
+
158
+ def get_version(self) -> str:
159
+ """Get the Codex CLI version.
160
+
161
+ Returns:
162
+ Version string or "unknown" if not available
163
+ """
164
+ data = self.get_cached_data()
165
+ return data.codex_version if data else "unknown"
166
+
167
+ def get_cli_path(self) -> list[str] | None:
168
+ """Get the Codex CLI command with caching.
169
+
170
+ Returns:
171
+ Command list to execute Codex CLI if found, None otherwise
172
+ """
173
+ info = self._cli_service.get_cli_info("codex")
174
+ return info["command"] if info["is_available"] else None
175
+
176
+ def get_binary_path(self) -> list[str] | None:
177
+ """Alias for get_cli_path for backward compatibility."""
178
+ return self.get_cli_path()
179
+
180
+ def get_cli_health_info(self) -> CodexCliInfo:
181
+ """Get lightweight CLI health info using centralized detection, cached locally.
182
+
183
+ Returns:
184
+ CodexCliInfo with availability, version, and binary path
185
+ """
186
+ from .models import CodexCliInfo, CodexCliStatus
187
+
188
+ if self._cli_info is not None:
189
+ return self._cli_info
190
+
191
+ info = self._cli_service.get_cli_info("codex")
192
+ status = (
193
+ CodexCliStatus.AVAILABLE
194
+ if info["is_available"]
195
+ else CodexCliStatus.NOT_INSTALLED
196
+ )
197
+ cli_info = CodexCliInfo(
198
+ status=status,
199
+ version=info.get("version"),
200
+ binary_path=info.get("path"),
201
+ )
202
+ self._cli_info = cli_info
203
+ return cli_info
204
+
205
+ @async_ttl_cache(maxsize=16, ttl=900.0) # 15 minute cache for version
206
+ async def _get_codex_version(self) -> str:
207
+ """Get Codex CLI version with caching."""
208
+ try:
209
+ # Custom parser for Codex version format
210
+ def parse_codex_version(output: str) -> str:
211
+ # Handle "codex 0.21.0" format
212
+ if " " in output:
213
+ return output.split()[-1]
214
+ return output
215
+
216
+ # Use centralized CLI detection
217
+ result = await self._cli_service.detect_cli(
218
+ binary_name="codex",
219
+ package_name="@openai/codex",
220
+ version_flag="--version",
221
+ version_parser=parse_codex_version,
222
+ cache_key="codex_version",
223
+ )
224
+
225
+ if result.is_available and result.version:
226
+ return result.version
227
+ else:
228
+ raise FileNotFoundError("Codex CLI not found")
229
+
230
+ except Exception as e:
231
+ logger.warning(
232
+ "codex_version_detection_failed", error=str(e), category="plugin"
233
+ )
234
+ return "unknown"
235
+
236
+ async def _detect_codex_headers(self, version: str) -> CodexCacheData:
237
+ """Execute Codex CLI with proxy to capture headers and instructions."""
238
+ # Data captured from the request
239
+ captured_data: dict[str, Any] = {}
240
+
241
+ async def capture_handler(request: Request) -> Response:
242
+ """Capture the Codex CLI request."""
243
+ # Capture headers and request metadata
244
+ headers_dict = extract_request_headers(request)
245
+ captured_data["headers"] = headers_dict
246
+ captured_data["method"] = request.method
247
+ captured_data["url"] = str(request.url)
248
+ captured_data["path"] = request.url.path
249
+ captured_data["query_params"] = (
250
+ dict(request.query_params) if request.query_params else {}
251
+ )
252
+
253
+ # Capture raw body
254
+ raw_body = await request.body()
255
+ captured_data["body"] = raw_body
256
+
257
+ # Parse body as JSON if possible
258
+ try:
259
+ if raw_body:
260
+ captured_data["body_json"] = json.loads(raw_body.decode("utf-8"))
261
+ else:
262
+ captured_data["body_json"] = None
263
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
264
+ logger.debug("body_parsing_failed", error=str(e), category="plugin")
265
+ captured_data["body_json"] = None
266
+
267
+ logger.debug(
268
+ "request_captured",
269
+ method=request.method,
270
+ path=request.url.path,
271
+ headers_count=len(headers_dict),
272
+ body_size=len(raw_body),
273
+ category="plugin",
274
+ )
275
+
276
+ # Return a mock response to satisfy Codex CLI
277
+ return Response(
278
+ content='{"choices": [{"message": {"content": "Test response"}}]}',
279
+ media_type="application/json",
280
+ status_code=200,
281
+ )
282
+
283
+ # Create temporary FastAPI app
284
+ temp_app = FastAPI()
285
+ # Current Codex endpoint used by CLI
286
+ temp_app.post("/backend-api/codex/responses")(capture_handler)
287
+
288
+ # from starlette.middleware.base import BaseHTTPMiddleware
289
+ # from starlette.requests import Request
290
+ #
291
+ # Another way to recover the headers
292
+ # class DumpHeadersMiddleware(BaseHTTPMiddleware):
293
+ # async def dispatch(self, request: Request, call_next):
294
+ # # Print all headers
295
+ # print("Request Headers:")
296
+ # for name, value in request.headers.items():
297
+ # print(f"{name}: {value}")
298
+ # response = await call_next(request)
299
+ # return response
300
+ #
301
+ # temp_app.add_middleware(DumpHeadersMiddleware)
302
+
303
+ # Find available port
304
+ sock = socket.socket()
305
+ sock.bind(("", 0))
306
+ port = sock.getsockname()[1]
307
+ sock.close()
308
+
309
+ # Start server in background
310
+ from uvicorn import Config, Server
311
+
312
+ config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
313
+ server = Server(config)
314
+
315
+ server_ready = asyncio.Event()
316
+
317
+ @temp_app.on_event("startup")
318
+ async def signal_server_ready() -> None:
319
+ """Mark the in-process server as ready once startup completes."""
320
+
321
+ server_ready.set()
322
+
323
+ logger.debug("start", category="plugin")
324
+ server_task = asyncio.create_task(server.serve())
325
+ ready_task = asyncio.create_task(server_ready.wait())
326
+
327
+ try:
328
+ done, _pending = await asyncio.wait(
329
+ {ready_task, server_task},
330
+ timeout=5,
331
+ return_when=asyncio.FIRST_COMPLETED,
332
+ )
333
+ if ready_task in done:
334
+ await ready_task
335
+ elif server_task in done:
336
+ await server_task
337
+ raise RuntimeError(
338
+ "Codex detection server exited before signalling readiness"
339
+ )
340
+ else:
341
+ raise TimeoutError(
342
+ "Timed out waiting for Codex detection server startup"
343
+ )
344
+
345
+ stdout, stderr = b"", b""
346
+
347
+ # Determine home directory mode based on configuration
348
+ home_path = os.environ.get("HOME")
349
+ cwd_path = Path.cwd()
350
+
351
+ temp_context: tempfile.TemporaryDirectory[str] | None = None
352
+ if (
353
+ self.codex_settings
354
+ and self.codex_settings.detection_home_mode == "temp"
355
+ ):
356
+ temp_context = tempfile.TemporaryDirectory()
357
+ temp_dir_path = Path(temp_context.name)
358
+ home_path = str(temp_dir_path)
359
+ cwd_path = temp_dir_path
360
+
361
+ logger.debug(
362
+ "detection_service_using",
363
+ home_dir=home_path,
364
+ cwd=cwd_path,
365
+ category="plugin",
366
+ )
367
+
368
+ try:
369
+ # Execute Codex CLI with proxy
370
+ env: dict[str, str] = dict(os.environ)
371
+ env["OPENAI_BASE_URL"] = f"http://127.0.0.1:{port}/backend-api/codex"
372
+ env["OPENAI_API_KEY"] = "dummy-key-for-detection"
373
+ if home_path is not None:
374
+ env["HOME"] = home_path
375
+ del env["OPENAI_API_KEY"]
376
+
377
+ # Get codex command from CLI service
378
+ cli_info = self._cli_service.get_cli_info("codex")
379
+ if not cli_info["is_available"] or not cli_info["command"]:
380
+ raise FileNotFoundError("Codex CLI not found for header detection")
381
+
382
+ # Prepare command
383
+ cmd = cli_info["command"] + [
384
+ "exec",
385
+ "--cd",
386
+ str(cwd_path),
387
+ "--skip-git-repo-check",
388
+ "test",
389
+ ]
390
+
391
+ process = await asyncio.create_subprocess_exec(
392
+ *cmd,
393
+ env=env,
394
+ stdout=asyncio.subprocess.PIPE,
395
+ stderr=asyncio.subprocess.PIPE,
396
+ )
397
+ # Wait for process with timeout
398
+ try:
399
+ await asyncio.wait_for(process.wait(), timeout=300)
400
+ except TimeoutError:
401
+ process.kill()
402
+ await process.wait()
403
+
404
+ stdout = await process.stdout.read() if process.stdout else b""
405
+ stderr = await process.stderr.read() if process.stderr else b""
406
+
407
+ finally:
408
+ # Clean up temporary directory if used
409
+ if temp_context is not None:
410
+ temp_context.cleanup()
411
+
412
+ finally:
413
+ if not ready_task.done():
414
+ ready_task.cancel()
415
+ with suppress(asyncio.CancelledError):
416
+ await ready_task
417
+
418
+ server.should_exit = True
419
+ await server_task
420
+
421
+ if not captured_data:
422
+ logger.error(
423
+ "failed_to_capture_codex_cli_request",
424
+ stdout=stdout.decode(errors="ignore"),
425
+ stderr=stderr.decode(errors="ignore"),
426
+ category="plugin",
427
+ )
428
+ raise RuntimeError("Failed to capture Codex CLI request")
429
+
430
+ # Sanitize headers/body for cache
431
+ headers_dict = (
432
+ self._sanitize_headers_for_cache(captured_data.get("headers", {}))
433
+ if self._redact_sensitive_cache
434
+ else captured_data.get("headers", {})
435
+ )
436
+ body_json = (
437
+ self._sanitize_body_json_for_cache(captured_data.get("body_json"))
438
+ if self._redact_sensitive_cache
439
+ else captured_data.get("body_json")
440
+ )
441
+
442
+ prompts = DetectedPrompts.from_body(body_json)
443
+
444
+ return CodexCacheData(
445
+ codex_version=version,
446
+ headers=DetectedHeaders(headers_dict),
447
+ prompts=prompts,
448
+ body_json=body_json,
449
+ method=captured_data.get("method"),
450
+ url=captured_data.get("url"),
451
+ path=captured_data.get("path"),
452
+ query_params=captured_data.get("query_params"),
453
+ )
454
+
455
+ def _load_from_cache(self, version: str) -> CodexCacheData | None:
456
+ """Load cached data for specific Codex version."""
457
+ cache_file = self.cache_dir / f"codex_headers_{version}.json"
458
+
459
+ if not cache_file.exists():
460
+ return None
461
+
462
+ with cache_file.open("r") as f:
463
+ data = json.load(f)
464
+ return CodexCacheData.model_validate(data)
465
+
466
+ def _save_to_cache(self, data: CodexCacheData) -> None:
467
+ """Save detection data to cache."""
468
+ cache_file = self.cache_dir / f"codex_headers_{data.codex_version}.json"
469
+
470
+ try:
471
+ with cache_file.open("w") as f:
472
+ json.dump(data.model_dump(), f, indent=2, default=str)
473
+ logger.debug(
474
+ "cache_saved",
475
+ file=str(cache_file),
476
+ version=data.codex_version,
477
+ category="plugin",
478
+ )
479
+ except Exception as e:
480
+ logger.warning(
481
+ "cache_save_failed",
482
+ file=str(cache_file),
483
+ error=str(e),
484
+ category="plugin",
485
+ )
486
+
487
+ def _get_fallback_data(self) -> CodexCacheData:
488
+ """Get fallback data when detection fails."""
489
+ logger.warning("using_fallback_codex_data", category="plugin")
490
+
491
+ # Load fallback data from package data file
492
+ package_data_file = (
493
+ Path(__file__).resolve().parents[2] / "data" / "codex_headers_fallback.json"
494
+ )
495
+ with package_data_file.open("r") as f:
496
+ fallback_data_dict = json.load(f)
497
+ return CodexCacheData.model_validate(fallback_data_dict)
498
+
499
+ def invalidate_cache(self) -> None:
500
+ """Clear all cached detection data."""
501
+ # Clear the async cache for _get_codex_version
502
+ if hasattr(self._get_codex_version, "cache_clear"):
503
+ self._get_codex_version.cache_clear()
504
+ self._cli_info = None
505
+ logger.debug("detection_cache_cleared", category="plugin")
506
+
507
+ # --- Helpers ---
508
+ def _sanitize_headers_for_cache(self, headers: dict[str, str]) -> dict[str, str]:
509
+ """Redact sensitive headers for cache while preserving keys and order."""
510
+ sanitized: dict[str, str] = {}
511
+ for k, v in headers.items():
512
+ lk = k.lower()
513
+ if lk in self.REDACTED_HEADERS:
514
+ sanitized[lk] = "" if len(str(v)) < 8 else str(v)[:8] + "..."
515
+ else:
516
+ sanitized[lk] = v
517
+ return sanitized
518
+
519
+ def _sanitize_body_json_for_cache(
520
+ self, body: dict[str, Any] | None
521
+ ) -> dict[str, Any] | None:
522
+ if body is None:
523
+ return None
524
+
525
+ def redact(obj: Any) -> Any:
526
+ if isinstance(obj, dict):
527
+ out: dict[str, Any] = {}
528
+ for k, v in obj.items():
529
+ if k == "conversation_id":
530
+ out[k] = ""
531
+ else:
532
+ out[k] = redact(v)
533
+ return out
534
+ elif isinstance(obj, list):
535
+ return [redact(x) for x in obj]
536
+ else:
537
+ return obj
538
+
539
+ return cast(dict[str, Any] | None, redact(body))
540
+
541
+ def get_system_prompt(self, mode: str | None = None) -> dict[str, Any]:
542
+ """Return an instructions dict for injection based on cached prompts."""
543
+ prompts = self.get_detected_prompts()
544
+ return prompts.instructions_payload()