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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +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.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -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.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,635 @@
1
+ import contextlib
2
+ import json
3
+ import uuid
4
+ from typing import Any, cast
5
+ from urllib.parse import urlparse
6
+
7
+ import httpx
8
+ from fastapi import Request
9
+ from starlette.responses import JSONResponse, Response, StreamingResponse
10
+
11
+ from ccproxy.auth.exceptions import OAuthTokenRefreshError
12
+ from ccproxy.core.logging import get_plugin_logger
13
+ from ccproxy.core.plugins.interfaces import (
14
+ DetectionServiceProtocol,
15
+ ProfiledTokenManagerProtocol,
16
+ )
17
+ from ccproxy.services.adapters.chain_composer import compose_from_chain
18
+ from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
19
+ from ccproxy.services.handler_config import HandlerConfig
20
+ from ccproxy.streaming import DeferredStreaming, StreamingBufferService
21
+ from ccproxy.utils.headers import (
22
+ extract_request_headers,
23
+ extract_response_headers,
24
+ filter_request_headers,
25
+ filter_response_headers,
26
+ )
27
+ from ccproxy.utils.model_mapper import restore_model_aliases
28
+
29
+
30
+ logger = get_plugin_logger()
31
+
32
+
33
+ class CodexAdapter(BaseHTTPAdapter):
34
+ """Simplified Codex adapter."""
35
+
36
+ def __init__(
37
+ self,
38
+ detection_service: DetectionServiceProtocol,
39
+ config: Any = None,
40
+ **kwargs: Any,
41
+ ) -> None:
42
+ super().__init__(config=config, **kwargs)
43
+ self.detection_service: DetectionServiceProtocol = detection_service
44
+ self.token_manager: ProfiledTokenManagerProtocol = cast(
45
+ ProfiledTokenManagerProtocol, self.auth_manager
46
+ )
47
+ self.base_url = self.config.base_url.rstrip("/")
48
+
49
+ async def handle_request(
50
+ self, request: Request
51
+ ) -> Response | StreamingResponse | DeferredStreaming:
52
+ """Handle request with Codex-specific streaming behavior.
53
+
54
+ Codex upstream only supports streaming. If the client requests a non-streaming
55
+ response, we internally stream and buffer it, then return a standard Response.
56
+ """
57
+ # Context + request info
58
+ ctx = request.state.context
59
+ self._ensure_tool_accumulator(ctx)
60
+ endpoint = ctx.metadata.get("endpoint", "")
61
+ body = await request.body()
62
+ body = await self._map_request_model(ctx, body)
63
+ headers = extract_request_headers(request)
64
+
65
+ # Determine client streaming intent from body flag (fallback to False)
66
+ wants_stream = False
67
+ try:
68
+ data = json.loads(body.decode()) if body else {}
69
+ wants_stream = bool(data.get("stream", False))
70
+ except Exception: # Malformed/missing JSON -> assume non-streaming
71
+ wants_stream = False
72
+ logger.trace(
73
+ "codex_adapter_request_intent",
74
+ wants_stream=wants_stream,
75
+ endpoint=endpoint,
76
+ format_chain=getattr(ctx, "format_chain", []),
77
+ category="streaming",
78
+ )
79
+
80
+ # Explicitly set service_type for downstream helpers
81
+ with contextlib.suppress(Exception):
82
+ ctx.metadata.setdefault("service_type", "codex")
83
+
84
+ # If client wants streaming, delegate to streaming handler directly
85
+ if wants_stream and self.streaming_handler:
86
+ logger.trace(
87
+ "codex_adapter_delegating_streaming",
88
+ endpoint=endpoint,
89
+ category="streaming",
90
+ )
91
+ return await self.handle_streaming(request, endpoint)
92
+
93
+ # Otherwise, buffer the upstream streaming response into a standard one
94
+ if getattr(self.config, "buffer_non_streaming", True):
95
+ # 1) Prepare provider request (adds auth, sets stream=true, etc.)
96
+ # Apply request format conversion if specified
97
+ if ctx.format_chain and len(ctx.format_chain) > 1:
98
+ try:
99
+ request_payload = self._decode_json_body(
100
+ body, context="codex_request"
101
+ )
102
+ request_payload = await self._apply_format_chain(
103
+ data=request_payload,
104
+ format_chain=ctx.format_chain,
105
+ stage="request",
106
+ )
107
+ body = self._encode_json_body(request_payload)
108
+ except Exception as e:
109
+ logger.error(
110
+ "codex_format_chain_request_failed",
111
+ error=str(e),
112
+ exc_info=e,
113
+ category="transform",
114
+ )
115
+ return JSONResponse(
116
+ status_code=400,
117
+ content={
118
+ "error": {
119
+ "type": "invalid_request_error",
120
+ "message": "Failed to convert request using format chain",
121
+ "details": str(e),
122
+ }
123
+ },
124
+ )
125
+
126
+ prepared_body, prepared_headers = await self.prepare_provider_request(
127
+ body, headers, endpoint
128
+ )
129
+ logger.trace(
130
+ "codex_adapter_prepared_provider_request",
131
+ header_keys=list(prepared_headers.keys()),
132
+ body_size=len(prepared_body or b""),
133
+ category="http",
134
+ )
135
+
136
+ # 2) Build handler config using composed adapter from format_chain (unified path)
137
+
138
+ composed_adapter = (
139
+ compose_from_chain(
140
+ registry=self.format_registry, chain=ctx.format_chain
141
+ )
142
+ if self.format_registry and ctx.format_chain
143
+ else None
144
+ )
145
+
146
+ handler_config = HandlerConfig(
147
+ supports_streaming=True,
148
+ request_transformer=None,
149
+ response_adapter=composed_adapter,
150
+ format_context=None,
151
+ )
152
+
153
+ # 3) Use StreamingBufferService to convert upstream stream -> regular response
154
+ target_url = await self.get_target_url(endpoint)
155
+ # Try to use a client with base_url for better hook integration
156
+ http_client = await self.http_pool_manager.get_client()
157
+ hook_manager = (
158
+ getattr(self.streaming_handler, "hook_manager", None)
159
+ if self.streaming_handler
160
+ else None
161
+ )
162
+ buffer_service = StreamingBufferService(
163
+ http_client=http_client,
164
+ request_tracer=None,
165
+ hook_manager=hook_manager,
166
+ http_pool_manager=self.http_pool_manager,
167
+ )
168
+
169
+ buffered_response = await buffer_service.handle_buffered_streaming_request(
170
+ method=request.method,
171
+ url=target_url,
172
+ headers=prepared_headers,
173
+ body=prepared_body,
174
+ handler_config=handler_config,
175
+ request_context=ctx,
176
+ provider_name="codex",
177
+ )
178
+ logger.trace(
179
+ "codex_adapter_buffered_response_ready",
180
+ status_code=buffered_response.status_code,
181
+ buffer_respones_preview=buffered_response.body[:300],
182
+ category="streaming",
183
+ format_chain=getattr(ctx, "format_chain", []),
184
+ )
185
+
186
+ # 4) Apply reverse format chain on buffered body if needed
187
+ if ctx.format_chain and len(ctx.format_chain) > 1:
188
+ from typing import Literal
189
+
190
+ mode: Literal["error", "response"] = (
191
+ "error" if buffered_response.status_code >= 400 else "response"
192
+ )
193
+ try:
194
+ body_bytes = (
195
+ buffered_response.body
196
+ if isinstance(buffered_response.body, bytes)
197
+ else bytes(buffered_response.body)
198
+ )
199
+ response_payload = self._decode_json_body(
200
+ body_bytes, context=f"codex_{mode}"
201
+ )
202
+ response_payload = await self._apply_format_chain(
203
+ data=response_payload,
204
+ format_chain=ctx.format_chain,
205
+ stage=mode,
206
+ )
207
+ metadata = getattr(ctx, "metadata", None)
208
+ alias_map = getattr(ctx, "_model_alias_map", None)
209
+ if isinstance(metadata, dict):
210
+ if (
211
+ isinstance(alias_map, dict)
212
+ and isinstance(response_payload, dict)
213
+ and isinstance(response_payload.get("model"), str)
214
+ ):
215
+ response_payload["model"] = alias_map.get(
216
+ response_payload["model"], response_payload["model"]
217
+ )
218
+ restore_model_aliases(response_payload, metadata)
219
+ converted_body = self._encode_json_body(response_payload)
220
+ except Exception as e:
221
+ logger.error(
222
+ "codex_format_chain_response_failed",
223
+ error=str(e),
224
+ mode=mode,
225
+ exc_info=e,
226
+ category="transform",
227
+ )
228
+ return JSONResponse(
229
+ status_code=502,
230
+ content={
231
+ "error": {
232
+ "type": "server_error",
233
+ "message": "Failed to convert provider response using format chain",
234
+ "details": str(e),
235
+ }
236
+ },
237
+ )
238
+
239
+ headers_out = filter_response_headers(dict(buffered_response.headers))
240
+ return Response(
241
+ content=converted_body,
242
+ status_code=buffered_response.status_code,
243
+ headers=headers_out,
244
+ media_type="application/json",
245
+ )
246
+
247
+ # No conversion needed; return buffered response as-is
248
+ return buffered_response
249
+
250
+ # Fallback: no buffering requested, use base non-streaming flow
251
+ return await super().handle_request(request)
252
+
253
+ async def get_target_url(self, endpoint: str) -> str:
254
+ return f"{self.base_url}/responses"
255
+
256
+ async def prepare_provider_request(
257
+ self, body: bytes, headers: dict[str, str], endpoint: str
258
+ ) -> tuple[bytes, dict[str, str]]:
259
+ token_value = await self._resolve_access_token()
260
+
261
+ # Get profile to extract chatgpt_account_id
262
+ profile = await self.token_manager.get_profile_quick()
263
+ chatgpt_account_id = (
264
+ getattr(profile, "chatgpt_account_id", None) if profile else None
265
+ )
266
+
267
+ # Parse body (format conversion is now handled by format chain)
268
+ body_data = json.loads(body.decode()) if body else {}
269
+
270
+ # Inject instructions mandatory for being allow to
271
+ # to used the Codex API endpoint
272
+ # Fetch detected instructions from detection service
273
+ instructions = self._get_instructions()
274
+
275
+ existing_instructions = body_data.get("instructions")
276
+ if isinstance(existing_instructions, str) and existing_instructions:
277
+ if instructions:
278
+ instructions = instructions + "\n" + existing_instructions
279
+ else:
280
+ instructions = existing_instructions
281
+
282
+ body_data["instructions"] = instructions
283
+
284
+ # Codex backend requires stream=true, always override
285
+ body_data["stream"] = True
286
+ body_data["store"] = False
287
+
288
+ # Remove unsupported keys for Codex
289
+ for key in ("max_output_tokens", "max_completion_tokens", "temperature"):
290
+ body_data.pop(key, None)
291
+
292
+ list_input = body_data.get("input", [])
293
+ # Remove any input types that Codex does not support
294
+ body_data["input"] = [
295
+ input for input in list_input if input.get("type") != "item_reference"
296
+ ]
297
+
298
+ #
299
+ # Remove any prefixed metadata fields that shouldn't be sent to the API
300
+ body_data = self._remove_metadata_fields(body_data)
301
+
302
+ # Filter and add headers
303
+ filtered_headers = filter_request_headers(headers, preserve_auth=False)
304
+
305
+ session_id = filtered_headers.get("session_id") or str(uuid.uuid4())
306
+ conversation_id = filtered_headers.get("conversation_id") or str(uuid.uuid4())
307
+
308
+ base_headers = {
309
+ "authorization": f"Bearer {token_value}",
310
+ "content-type": "application/json",
311
+ "session_id": session_id,
312
+ "conversation_id": conversation_id,
313
+ }
314
+
315
+ if chatgpt_account_id is not None:
316
+ base_headers["chatgpt-account-id"] = chatgpt_account_id
317
+
318
+ filtered_headers.update(base_headers)
319
+
320
+ cli_headers = self._collect_cli_headers()
321
+ if cli_headers:
322
+ filtered_headers.update(cli_headers)
323
+
324
+ return json.dumps(body_data).encode(), filtered_headers
325
+
326
+ async def process_provider_response(
327
+ self, response: httpx.Response, endpoint: str
328
+ ) -> Response | StreamingResponse:
329
+ """Return a plain Response; streaming handled upstream by BaseHTTPAdapter.
330
+
331
+ The BaseHTTPAdapter is responsible for detecting streaming and delegating
332
+ to the shared StreamingHandler. For non-streaming responses, adapters
333
+ should return a simple Starlette Response.
334
+ """
335
+ response_headers = extract_response_headers(response)
336
+ return Response(
337
+ content=response.content,
338
+ status_code=response.status_code,
339
+ headers=response_headers,
340
+ media_type=response.headers.get("content-type"),
341
+ )
342
+
343
+ async def _resolve_access_token(self) -> str:
344
+ """Resolve an access token suitable for Codex requests.
345
+
346
+ If the auth manager/credential balancer is not configured, raise a
347
+ unified AuthenticationError so middleware can return a clean 401
348
+ without leaking stack traces.
349
+ """
350
+
351
+ # Guard: token manager must be configured via plugin auth_manager
352
+ if not getattr(self, "token_manager", None):
353
+ from ccproxy.core.errors import AuthenticationError
354
+
355
+ logger.warning(
356
+ "auth_manager_override_not_resolved",
357
+ plugin="codex",
358
+ auth_manager_name="codex_credential_balancer",
359
+ category="auth",
360
+ )
361
+ raise AuthenticationError(
362
+ "Authentication manager not configured for Codex provider"
363
+ )
364
+
365
+ token_manager = self.token_manager
366
+
367
+ async def _snapshot_token() -> str | None:
368
+ snapshot = await token_manager.get_token_snapshot()
369
+ if snapshot and snapshot.access_token:
370
+ return snapshot.access_token
371
+ return None
372
+
373
+ credentials = await token_manager.load_credentials()
374
+ if credentials and token_manager.should_refresh(credentials):
375
+ try:
376
+ refreshed = await token_manager.get_access_token_with_refresh()
377
+ if refreshed:
378
+ return refreshed
379
+ except OAuthTokenRefreshError as exc:
380
+ logger.warning(
381
+ "codex_token_refresh_failed",
382
+ error=str(exc),
383
+ category="auth",
384
+ )
385
+ fallback = await _snapshot_token()
386
+ if fallback:
387
+ return fallback
388
+
389
+ token = None
390
+ try:
391
+ token = await token_manager.get_access_token()
392
+ except OAuthTokenRefreshError as exc:
393
+ logger.warning(
394
+ "codex_token_refresh_failed",
395
+ error=str(exc),
396
+ category="auth",
397
+ )
398
+ fallback = await _snapshot_token()
399
+ if fallback:
400
+ return fallback
401
+
402
+ if token:
403
+ return token
404
+
405
+ try:
406
+ refreshed = await token_manager.get_access_token_with_refresh()
407
+ if refreshed:
408
+ return refreshed
409
+ except OAuthTokenRefreshError as exc:
410
+ logger.warning(
411
+ "codex_token_refresh_failed",
412
+ error=str(exc),
413
+ category="auth",
414
+ )
415
+ fallback = await _snapshot_token()
416
+ if fallback:
417
+ return fallback
418
+
419
+ fallback = await _snapshot_token()
420
+ if fallback:
421
+ return fallback
422
+
423
+ raise ValueError("No authentication credentials available")
424
+
425
+ def _collect_cli_headers(self) -> dict[str, str]:
426
+ """Collect safe CLI headers from detection cache for forwarding."""
427
+
428
+ if not self.detection_service:
429
+ return {}
430
+
431
+ headers_data = self.detection_service.get_detected_headers()
432
+ if not headers_data:
433
+ return {}
434
+
435
+ ignores = {
436
+ header.lower() for header in self.detection_service.get_ignored_headers()
437
+ }
438
+ redacted = {
439
+ header.lower() for header in self.detection_service.get_redacted_headers()
440
+ }
441
+
442
+ return headers_data.filtered(ignores=ignores, redacted=redacted)
443
+
444
+ async def handle_streaming(
445
+ self, request: Request, endpoint: str, **kwargs: Any
446
+ ) -> StreamingResponse | DeferredStreaming:
447
+ """Handle streaming with request conversion for Codex.
448
+
449
+ Applies request format conversion (e.g., anthropic.messages -> openai.responses) before
450
+ preparing the provider request, then delegates to StreamingHandler with
451
+ a streaming response adapter for reverse conversion as needed.
452
+ """
453
+ if not self.streaming_handler:
454
+ # Fallback to base behavior
455
+ return await super().handle_streaming(request, endpoint, **kwargs)
456
+
457
+ # Get context
458
+ ctx = request.state.context
459
+ self._ensure_tool_accumulator(ctx)
460
+
461
+ # Extract body and headers
462
+ body = await request.body()
463
+ body = await self._map_request_model(ctx, body)
464
+ headers = extract_request_headers(request)
465
+
466
+ # Ensure format adapters are available when required
467
+ self._ensure_format_registry(ctx.format_chain, endpoint)
468
+
469
+ # Apply request format conversion if a chain is defined
470
+ if ctx.format_chain and len(ctx.format_chain) > 1:
471
+ try:
472
+ request_payload = self._decode_json_body(
473
+ body, context="codex_stream_request"
474
+ )
475
+ request_payload = await self._apply_format_chain(
476
+ data=request_payload,
477
+ format_chain=ctx.format_chain,
478
+ stage="request",
479
+ )
480
+ self._record_tool_definitions(ctx, request_payload)
481
+ body = self._encode_json_body(request_payload)
482
+ except Exception as e:
483
+ logger.error(
484
+ "codex_format_chain_request_failed",
485
+ error=str(e),
486
+ exc_info=e,
487
+ category="transform",
488
+ )
489
+ # Convert error to streaming response
490
+
491
+ error_content = {
492
+ "error": {
493
+ "type": "invalid_request_error",
494
+ "message": "Failed to convert request using format chain",
495
+ "details": str(e),
496
+ }
497
+ }
498
+ error_bytes = json.dumps(error_content).encode("utf-8")
499
+
500
+ async def error_generator() -> (
501
+ Any
502
+ ): # AsyncGenerator[bytes, None] would be more specific
503
+ yield error_bytes
504
+
505
+ return StreamingResponse(
506
+ content=error_generator(),
507
+ status_code=400,
508
+ media_type="application/json",
509
+ )
510
+
511
+ # Provider-specific preparation (adds auth, sets stream=true)
512
+ prepared_body, prepared_headers = await self.prepare_provider_request(
513
+ body, headers, endpoint
514
+ )
515
+
516
+ # Get format adapter for streaming reverse conversion
517
+ streaming_format_adapter = None
518
+ if ctx.format_chain and len(ctx.format_chain) > 1 and self.format_registry:
519
+ from_format = ctx.format_chain[-1]
520
+ to_format = ctx.format_chain[0]
521
+ try:
522
+ streaming_format_adapter = self.format_registry.get_if_exists(
523
+ from_format, to_format
524
+ )
525
+ except Exception:
526
+ streaming_format_adapter = None
527
+
528
+ handler_config = HandlerConfig(
529
+ supports_streaming=True,
530
+ request_transformer=None,
531
+ response_adapter=streaming_format_adapter,
532
+ format_context=None,
533
+ )
534
+
535
+ target_url = await self.get_target_url(endpoint)
536
+
537
+ parsed_url = urlparse(target_url)
538
+ base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
539
+
540
+ return await self.streaming_handler.handle_streaming_request(
541
+ method=request.method,
542
+ url=target_url,
543
+ headers=prepared_headers,
544
+ body=prepared_body,
545
+ handler_config=handler_config,
546
+ request_context=ctx,
547
+ client=await self.http_pool_manager.get_client(base_url=base_url),
548
+ )
549
+
550
+ # Helper methods
551
+ def _remove_metadata_fields(self, data: dict[str, Any]) -> dict[str, Any]:
552
+ """Remove fields that start with '_' as they are internal metadata.
553
+
554
+ Args:
555
+ data: Dictionary that may contain metadata fields
556
+
557
+ Returns:
558
+ Cleaned dictionary without metadata fields
559
+ """
560
+ if not isinstance(data, dict):
561
+ return data
562
+
563
+ # Create a new dict without keys starting with '_'
564
+ cleaned_data: dict[str, Any] = {}
565
+ for key, value in data.items():
566
+ if not key.startswith("_"):
567
+ # Recursively clean nested dictionaries
568
+ if isinstance(value, dict):
569
+ cleaned_data[key] = self._remove_metadata_fields(value)
570
+ elif isinstance(value, list):
571
+ # Clean list items if they are dictionaries
572
+ cleaned_items: list[Any] = []
573
+ for item in value:
574
+ if isinstance(item, dict):
575
+ cleaned_items.append(self._remove_metadata_fields(item))
576
+ else:
577
+ cleaned_items.append(item)
578
+ cleaned_data[key] = cleaned_items
579
+ else:
580
+ cleaned_data[key] = value
581
+
582
+ return cleaned_data
583
+
584
+ def _get_instructions(self) -> str:
585
+ if not self.detection_service:
586
+ return ""
587
+
588
+ prompts = self.detection_service.get_detected_prompts()
589
+ if prompts.has_instructions():
590
+ return prompts.instructions or ""
591
+
592
+ injection = self.detection_service.get_system_prompt()
593
+ if isinstance(injection, dict):
594
+ instructions = injection.get("instructions")
595
+ if isinstance(instructions, str):
596
+ return instructions
597
+
598
+ fallback = getattr(self.detection_service, "instructions_value", None)
599
+ if isinstance(fallback, str):
600
+ return fallback
601
+
602
+ return ""
603
+
604
+ def adapt_error(self, error_body: dict[str, Any]) -> dict[str, Any]:
605
+ """Convert Codex error format to appropriate API error format.
606
+
607
+ Args:
608
+ error_body: Codex error response
609
+
610
+ Returns:
611
+ API-formatted error response
612
+ """
613
+ # Handle the specific "Stream must be set to true" error
614
+ if isinstance(error_body, dict) and "detail" in error_body:
615
+ detail = error_body["detail"]
616
+ if "Stream must be set to true" in detail:
617
+ # Convert to generic invalid request error
618
+ return {
619
+ "error": {
620
+ "type": "invalid_request_error",
621
+ "message": "Invalid streaming parameter",
622
+ }
623
+ }
624
+
625
+ # Handle other error formats that might have "error" key
626
+ if "error" in error_body:
627
+ return error_body
628
+
629
+ # Default: wrap non-standard errors
630
+ return {
631
+ "error": {
632
+ "type": "internal_server_error",
633
+ "message": "An error occurred processing the request",
634
+ }
635
+ }