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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1231
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,263 @@
1
+ """Codex plugin local CLI health models and detection models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from enum import Enum
7
+ from typing import Annotated, Any, Literal, TypedDict
8
+
9
+ from pydantic import (
10
+ BaseModel,
11
+ ConfigDict,
12
+ Field,
13
+ field_serializer,
14
+ field_validator,
15
+ model_validator,
16
+ )
17
+
18
+ from ccproxy.llms.models import anthropic as anthropic_models
19
+ from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
20
+
21
+
22
+ class CodexCliStatus(str, Enum):
23
+ AVAILABLE = "available"
24
+ NOT_INSTALLED = "not_installed"
25
+ BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
26
+ TIMEOUT = "timeout"
27
+ ERROR = "error"
28
+
29
+
30
+ class CodexCliInfo(BaseModel):
31
+ status: CodexCliStatus
32
+ version: str | None = None
33
+ binary_path: str | None = None
34
+ version_output: str | None = None
35
+ error: str | None = None
36
+ return_code: str | None = None
37
+
38
+
39
+ class CodexHeaders(BaseModel):
40
+ """Pydantic model for Codex CLI headers extraction with field aliases."""
41
+
42
+ session_id: str = Field(
43
+ alias="session_id",
44
+ description="Codex session identifier",
45
+ default="",
46
+ )
47
+ originator: str = Field(
48
+ description="Codex originator identifier",
49
+ default="codex_cli_rs",
50
+ )
51
+ openai_beta: str = Field(
52
+ alias="openai-beta",
53
+ description="OpenAI beta features",
54
+ default="responses=experimental",
55
+ )
56
+ version: str = Field(
57
+ description="Codex CLI version",
58
+ default="0.21.0",
59
+ )
60
+ chatgpt_account_id: str = Field(
61
+ alias="chatgpt-account-id",
62
+ description="ChatGPT account identifier",
63
+ default="",
64
+ )
65
+
66
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
67
+
68
+ def to_headers_dict(self) -> dict[str, str]:
69
+ """Convert to headers dictionary for HTTP forwarding with proper case."""
70
+ headers = {}
71
+
72
+ # Map field names to proper HTTP header names
73
+ header_mapping = {
74
+ "session_id": "session_id",
75
+ "originator": "originator",
76
+ "openai_beta": "openai-beta",
77
+ "version": "version",
78
+ "chatgpt_account_id": "chatgpt-account-id",
79
+ }
80
+
81
+ for field_name, header_name in header_mapping.items():
82
+ value = getattr(self, field_name, None)
83
+ if value is not None and value != "":
84
+ headers[header_name] = value
85
+
86
+ return headers
87
+
88
+
89
+ class CodexInstructionsData(BaseModel):
90
+ """Extracted Codex instructions information."""
91
+
92
+ instructions_field: Annotated[
93
+ str,
94
+ Field(
95
+ description="Complete instructions field as detected from Codex CLI, preserving exact text content"
96
+ ),
97
+ ]
98
+
99
+ model_config = ConfigDict(extra="allow")
100
+
101
+
102
+ class CodexCacheData(BaseModel):
103
+ """Cached Codex CLI detection data with version tracking."""
104
+
105
+ codex_version: Annotated[str, Field(description="Codex CLI version")]
106
+ headers: Annotated[
107
+ DetectedHeaders,
108
+ Field(
109
+ description="Captured headers (lowercase keys) in insertion order",
110
+ default_factory=DetectedHeaders,
111
+ ),
112
+ ]
113
+ prompts: Annotated[
114
+ DetectedPrompts,
115
+ Field(description="Captured prompt metadata", default_factory=DetectedPrompts),
116
+ ]
117
+ body_json: Annotated[
118
+ dict[str, Any] | None,
119
+ Field(
120
+ description="Legacy captured request body (deprecated)",
121
+ default=None,
122
+ exclude=True,
123
+ ),
124
+ ] = None
125
+ method: Annotated[
126
+ str | None, Field(description="Captured HTTP method", default=None)
127
+ ] = None
128
+ url: Annotated[str | None, Field(description="Captured full URL", default=None)] = (
129
+ None
130
+ )
131
+ path: Annotated[
132
+ str | None, Field(description="Captured request path", default=None)
133
+ ] = None
134
+ query_params: Annotated[
135
+ dict[str, str] | None,
136
+ Field(description="Captured query parameters", default=None),
137
+ ] = None
138
+ cached_at: datetime = Field(
139
+ description="Cache timestamp",
140
+ default_factory=lambda: datetime.now(UTC),
141
+ )
142
+
143
+ model_config = ConfigDict(extra="forbid")
144
+
145
+ @model_validator(mode="before")
146
+ @classmethod
147
+ def _coerce_legacy_format(cls, values: dict[str, Any]) -> dict[str, Any]:
148
+ if not isinstance(values, dict):
149
+ return values
150
+
151
+ if "prompts" not in values:
152
+ legacy_body = values.get("body_json")
153
+ if legacy_body is not None:
154
+ values["prompts"] = DetectedPrompts.from_body(legacy_body)
155
+ cls._log_legacy_usage("body_json")
156
+
157
+ return values
158
+
159
+ @field_validator("headers", mode="before")
160
+ @classmethod
161
+ def _validate_headers(cls, value: Any) -> DetectedHeaders:
162
+ if isinstance(value, DetectedHeaders):
163
+ return value
164
+ if isinstance(value, dict):
165
+ return DetectedHeaders(value)
166
+ if value is None:
167
+ cls._log_legacy_usage("missing_headers")
168
+ return DetectedHeaders()
169
+ raise TypeError("headers must be a mapping of strings")
170
+
171
+ @field_validator("prompts", mode="before")
172
+ @classmethod
173
+ def _validate_prompts(cls, value: Any) -> DetectedPrompts:
174
+ if isinstance(value, DetectedPrompts):
175
+ return value
176
+ if isinstance(value, dict):
177
+ return DetectedPrompts.from_body(value)
178
+ if value is None:
179
+ return DetectedPrompts()
180
+ raise TypeError("prompts must be derived from a mapping")
181
+
182
+ @field_serializer("headers")
183
+ def _serialize_headers(self, headers: DetectedHeaders) -> dict[str, str]:
184
+ return headers.as_dict()
185
+
186
+ @field_serializer("prompts")
187
+ def _serialize_prompts(self, prompts: DetectedPrompts) -> dict[str, Any]:
188
+ raw = prompts.raw or {}
189
+ if not isinstance(raw, dict):
190
+ raw = {}
191
+ if prompts.instructions and "instructions" not in raw:
192
+ raw = dict(raw)
193
+ raw["instructions"] = prompts.instructions
194
+ if prompts.system is not None and "system" not in raw:
195
+ raw = dict(raw)
196
+ raw["system"] = prompts.system
197
+ return raw
198
+
199
+ @staticmethod
200
+ def _log_legacy_usage(reason: str) -> None:
201
+ try:
202
+ from ccproxy.core.logging import get_plugin_logger
203
+
204
+ logger = get_plugin_logger()
205
+ logger.debug(
206
+ "legacy_detection_cache_format",
207
+ plugin="codex",
208
+ reason=reason,
209
+ )
210
+ except Exception: # pragma: no cover - logging best-effort only
211
+ pass
212
+
213
+
214
+ class CodexMessage(BaseModel):
215
+ """Message format for Codex requests."""
216
+
217
+ role: Annotated[Literal["user", "assistant"], Field(description="Message role")]
218
+ content: Annotated[str, Field(description="Message content")]
219
+
220
+
221
+ class CodexRequest(BaseModel):
222
+ """OpenAI Codex completion request model."""
223
+
224
+ model: Annotated[str, Field(description="Model name (e.g., gpt-5)")] = "gpt-5"
225
+ instructions: Annotated[
226
+ str | None, Field(description="System instructions for the model")
227
+ ] = None
228
+ messages: Annotated[list[CodexMessage], Field(description="Conversation messages")]
229
+ stream: Annotated[bool, Field(description="Whether to stream the response")] = True
230
+
231
+ model_config = ConfigDict(
232
+ extra="allow"
233
+ ) # Allow additional fields for compatibility
234
+
235
+
236
+ class CodexResponse(BaseModel):
237
+ """OpenAI Codex completion response model."""
238
+
239
+ id: Annotated[str, Field(description="Response ID")]
240
+ model: Annotated[str, Field(description="Model used for completion")]
241
+ content: Annotated[str, Field(description="Generated content")]
242
+ finish_reason: Annotated[
243
+ str | None, Field(description="Reason the response finished")
244
+ ] = None
245
+ usage: Annotated[
246
+ anthropic_models.Usage | None, Field(description="Token usage information")
247
+ ] = None
248
+
249
+ model_config = ConfigDict(
250
+ extra="allow"
251
+ ) # Allow additional fields for compatibility
252
+
253
+
254
+ class CodexAuthData(TypedDict, total=False):
255
+ """Authentication data for Codex/OpenAI provider.
256
+
257
+ Attributes:
258
+ access_token: Bearer token for OpenAI API authentication
259
+ chatgpt_account_id: Account ID for ChatGPT session-based requests
260
+ """
261
+
262
+ access_token: str | None
263
+ chatgpt_account_id: str | None
@@ -0,0 +1,275 @@
1
+ """Codex provider plugin v2 implementation."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from ccproxy.core.constants import (
6
+ FORMAT_OPENAI_CHAT,
7
+ FORMAT_OPENAI_RESPONSES,
8
+ )
9
+ from ccproxy.core.logging import get_plugin_logger
10
+ from ccproxy.core.plugins import (
11
+ BaseProviderPluginFactory,
12
+ FormatAdapterSpec,
13
+ FormatPair,
14
+ PluginContext,
15
+ PluginManifest,
16
+ ProviderPluginRuntime,
17
+ )
18
+ from ccproxy.core.plugins.declaration import RouterSpec
19
+ from ccproxy.llms.streaming.accumulators import OpenAIAccumulator
20
+ from ccproxy.plugins.oauth_codex.manager import CodexTokenManager
21
+
22
+ from .adapter import CodexAdapter
23
+ from .config import CodexSettings
24
+ from .detection_service import CodexDetectionService
25
+ from .routes import router as codex_router
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ pass
30
+
31
+
32
+ logger = get_plugin_logger()
33
+
34
+
35
+ class CodexRuntime(ProviderPluginRuntime):
36
+ """Runtime for Codex provider plugin."""
37
+
38
+ def __init__(self, manifest: PluginManifest):
39
+ """Initialize runtime."""
40
+ super().__init__(manifest)
41
+ self.config: CodexSettings | None = None
42
+ self.credential_manager: CodexTokenManager | None = None
43
+
44
+ async def _on_initialize(self) -> None:
45
+ """Initialize the Codex provider plugin."""
46
+ if not self.context:
47
+ raise RuntimeError("Context not set")
48
+
49
+ # Get configuration
50
+ try:
51
+ config = self.context.get(CodexSettings)
52
+ except ValueError:
53
+ logger.debug("plugin_no_config")
54
+ # Use default config if none provided
55
+ config = CodexSettings()
56
+ logger.debug("plugin_using_default_config")
57
+ self.config = config
58
+
59
+ # Get auth manager from context
60
+ try:
61
+ self.credential_manager = self.context.get(CodexTokenManager)
62
+ except ValueError:
63
+ self.credential_manager = None
64
+
65
+ # Call parent to initialize adapter and detection service
66
+ await super()._on_initialize()
67
+
68
+ # Register streaming metrics hook
69
+ await self._register_streaming_metrics_hook()
70
+
71
+ # Check CLI status
72
+ if self.detection_service:
73
+ version = self.detection_service.get_version()
74
+ cli_path = self.detection_service.get_cli_path()
75
+
76
+ if not cli_path:
77
+ logger.warning(
78
+ "cli_detection_completed",
79
+ cli_available=False,
80
+ version=None,
81
+ cli_path=None,
82
+ source="unknown",
83
+ )
84
+
85
+ # Get CLI info for consolidated logging (only for successful detection)
86
+ cli_info = {}
87
+ if self.detection_service and self.detection_service.get_cli_path():
88
+ cli_info.update(
89
+ {
90
+ "cli_available": True,
91
+ "cli_version": self.detection_service.get_version(),
92
+ "cli_path": self.detection_service.get_cli_path(),
93
+ "cli_source": "package_manager",
94
+ }
95
+ )
96
+
97
+ logger.debug(
98
+ "plugin_initialized",
99
+ plugin="codex",
100
+ version=self.manifest.version,
101
+ status="initialized",
102
+ has_credentials=self.credential_manager is not None,
103
+ has_adapter=self.adapter is not None,
104
+ has_detection=self.detection_service is not None,
105
+ **cli_info,
106
+ )
107
+
108
+ async def _get_health_details(self) -> dict[str, Any]:
109
+ """Get health check details."""
110
+ details = await super()._get_health_details()
111
+
112
+ # Add Codex-specific details
113
+ if self.config:
114
+ details.update(
115
+ {
116
+ "base_url": self.config.base_url,
117
+ "supports_streaming": self.config.supports_streaming,
118
+ "models": [card.id for card in self.config.models_endpoint],
119
+ }
120
+ )
121
+
122
+ # Add authentication status
123
+ if self.credential_manager:
124
+ try:
125
+ auth_status = await self.credential_manager.get_auth_status()
126
+ details["auth_configured"] = auth_status.get("auth_configured", False)
127
+ details["token_available"] = auth_status.get("token_available", False)
128
+ except Exception as e:
129
+ details["auth_error"] = str(e)
130
+
131
+ # Include standardized provider health check details
132
+ try:
133
+ from .health import codex_health_check
134
+
135
+ if self.config and self.detection_service:
136
+ health_result = await codex_health_check(
137
+ self.config,
138
+ self.detection_service,
139
+ self.credential_manager,
140
+ version=self.manifest.version,
141
+ )
142
+ details.update(
143
+ {
144
+ "health_check_status": health_result.status,
145
+ "health_check_detail": health_result.details,
146
+ }
147
+ )
148
+ except Exception as e:
149
+ details["health_check_error"] = str(e)
150
+
151
+ return details
152
+
153
+ async def _register_streaming_metrics_hook(self) -> None:
154
+ """Register the streaming metrics extraction hook."""
155
+ try:
156
+ if not self.context:
157
+ logger.warning(
158
+ "streaming_metrics_hook_not_registered",
159
+ reason="no_context",
160
+ plugin="codex",
161
+ )
162
+ return
163
+ # Get hook registry from context
164
+ from ccproxy.core.plugins.hooks.registry import HookRegistry
165
+
166
+ try:
167
+ hook_registry = self.context.get(HookRegistry)
168
+ except ValueError:
169
+ logger.warning(
170
+ "streaming_metrics_hook_not_registered",
171
+ reason="no_hook_registry",
172
+ plugin="codex",
173
+ context_keys=list(self.context.keys()) if self.context else [],
174
+ )
175
+ return
176
+
177
+ # Get pricing service from plugin registry if available
178
+ pricing_service = None
179
+ if "plugin_registry" in self.context:
180
+ try:
181
+ from ccproxy.plugins.pricing.service import PricingService
182
+
183
+ plugin_registry = self.context["plugin_registry"]
184
+ pricing_service = plugin_registry.get_service(
185
+ "pricing", PricingService
186
+ )
187
+ except Exception as e:
188
+ logger.debug(
189
+ "pricing_service_not_available_for_hook",
190
+ plugin="codex",
191
+ error=str(e),
192
+ )
193
+
194
+ # Create and register the hook
195
+ from .hooks import CodexStreamingMetricsHook
196
+
197
+ # Pass both pricing_service (if available now) and plugin_registry (for lazy loading)
198
+ metrics_hook = CodexStreamingMetricsHook(
199
+ pricing_service=pricing_service,
200
+ plugin_registry=self.context.get("plugin_registry"),
201
+ )
202
+ hook_registry.register(metrics_hook)
203
+
204
+ logger.debug(
205
+ "streaming_metrics_hook_registered",
206
+ plugin="codex",
207
+ hook_name=metrics_hook.name,
208
+ priority=metrics_hook.priority,
209
+ has_pricing=pricing_service is not None,
210
+ )
211
+
212
+ except Exception as e:
213
+ logger.error(
214
+ "streaming_metrics_hook_registration_failed",
215
+ plugin="codex",
216
+ error=str(e),
217
+ exc_info=e,
218
+ )
219
+
220
+
221
+ class CodexFactory(BaseProviderPluginFactory):
222
+ """Factory for Codex provider plugin."""
223
+
224
+ cli_safe = False # Heavy provider plugin - not safe for CLI
225
+
226
+ # Plugin configuration via class attributes
227
+ plugin_name = "codex"
228
+ plugin_description = (
229
+ "OpenAI Codex provider plugin with OAuth authentication and format conversion"
230
+ )
231
+ runtime_class = CodexRuntime
232
+ adapter_class = CodexAdapter
233
+ detection_service_class = CodexDetectionService
234
+ config_class = CodexSettings
235
+ # String-based auth manager reference
236
+ auth_manager_name = "oauth_codex"
237
+ credentials_manager_class = CodexTokenManager
238
+ routers = [
239
+ RouterSpec(router=codex_router, prefix="/codex"),
240
+ ]
241
+ dependencies = ["oauth_codex"]
242
+ optional_requires = ["pricing"]
243
+
244
+ # No format adapters needed - core provides all required conversions
245
+ format_adapters: list[FormatAdapterSpec] = []
246
+
247
+ # Define requirements for adapters this plugin needs
248
+ requires_format_adapters: list[FormatPair] = [
249
+ # Codex can leverage core-provided OpenAI chat ↔ responses conversion
250
+ (FORMAT_OPENAI_CHAT, FORMAT_OPENAI_RESPONSES),
251
+ ]
252
+ tool_accumulator_class = OpenAIAccumulator
253
+
254
+ def create_detection_service(self, context: PluginContext) -> CodexDetectionService:
255
+ """Create the Codex detection service with validation."""
256
+ from ccproxy.config.settings import Settings
257
+ from ccproxy.services.cli_detection import CLIDetectionService
258
+
259
+ settings = context.get(Settings)
260
+ try:
261
+ cli_service = context.get(CLIDetectionService)
262
+ except ValueError:
263
+ cli_service = None
264
+
265
+ # Get codex-specific settings
266
+ try:
267
+ codex_settings = context.get(CodexSettings)
268
+ except ValueError:
269
+ codex_settings = None
270
+
271
+ return CodexDetectionService(settings, cli_service, codex_settings)
272
+
273
+
274
+ # Export the factory instance
275
+ factory = CodexFactory()
@@ -0,0 +1,129 @@
1
+ """Codex plugin routes."""
2
+
3
+ from typing import TYPE_CHECKING, Annotated, Any, cast
4
+
5
+ from fastapi import APIRouter, Depends, Request
6
+ from starlette.responses import Response, StreamingResponse
7
+
8
+ from ccproxy.api.decorators import with_format_chain
9
+ from ccproxy.api.dependencies import (
10
+ get_plugin_adapter,
11
+ get_provider_config_dependency,
12
+ )
13
+ from ccproxy.auth.dependencies import ConditionalAuthDep
14
+ from ccproxy.core.constants import (
15
+ FORMAT_ANTHROPIC_MESSAGES,
16
+ FORMAT_OPENAI_CHAT,
17
+ FORMAT_OPENAI_RESPONSES,
18
+ UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
19
+ UPSTREAM_ENDPOINT_OPENAI_CHAT_COMPLETIONS,
20
+ UPSTREAM_ENDPOINT_OPENAI_RESPONSES,
21
+ )
22
+ from ccproxy.streaming import DeferredStreaming
23
+
24
+ from .config import CodexSettings
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ pass
29
+
30
+ CodexAdapterDep = Annotated[Any, Depends(get_plugin_adapter("codex"))]
31
+ CodexConfigDep = Annotated[
32
+ CodexSettings,
33
+ Depends(get_provider_config_dependency("codex", CodexSettings)),
34
+ ]
35
+ router = APIRouter()
36
+
37
+
38
+ # Helper to handle adapter requests
39
+ async def handle_codex_request(
40
+ request: Request,
41
+ adapter: Any,
42
+ ) -> StreamingResponse | Response | DeferredStreaming:
43
+ result = await adapter.handle_request(request)
44
+ return cast(StreamingResponse | Response | DeferredStreaming, result)
45
+
46
+
47
+ # Route definitions
48
+ async def _codex_responses_handler(
49
+ request: Request,
50
+ adapter: CodexAdapterDep,
51
+ ) -> StreamingResponse | Response | DeferredStreaming:
52
+ """Shared handler for Codex responses endpoints."""
53
+
54
+ return await handle_codex_request(request, adapter)
55
+
56
+
57
+ @router.post("/v1/responses", response_model=None)
58
+ @with_format_chain(
59
+ [FORMAT_OPENAI_RESPONSES], endpoint=UPSTREAM_ENDPOINT_OPENAI_RESPONSES
60
+ )
61
+ async def codex_responses(
62
+ request: Request,
63
+ auth: ConditionalAuthDep,
64
+ adapter: CodexAdapterDep,
65
+ ) -> StreamingResponse | Response | DeferredStreaming:
66
+ return await _codex_responses_handler(request, adapter)
67
+
68
+
69
+ @router.post("/responses", response_model=None, include_in_schema=False)
70
+ @with_format_chain(
71
+ [FORMAT_OPENAI_RESPONSES], endpoint=UPSTREAM_ENDPOINT_OPENAI_RESPONSES
72
+ )
73
+ async def codex_responses_legacy(
74
+ request: Request,
75
+ auth: ConditionalAuthDep,
76
+ adapter: CodexAdapterDep,
77
+ ) -> StreamingResponse | Response | DeferredStreaming:
78
+ return await _codex_responses_handler(request, adapter)
79
+
80
+
81
+ @router.post("/v1/chat/completions", response_model=None)
82
+ @with_format_chain(
83
+ [FORMAT_OPENAI_CHAT, FORMAT_OPENAI_RESPONSES],
84
+ endpoint=UPSTREAM_ENDPOINT_OPENAI_CHAT_COMPLETIONS,
85
+ )
86
+ async def codex_chat_completions(
87
+ request: Request,
88
+ auth: ConditionalAuthDep,
89
+ adapter: CodexAdapterDep,
90
+ ) -> StreamingResponse | Response | DeferredStreaming:
91
+ return await handle_codex_request(request, adapter)
92
+
93
+
94
+ @router.get("/v1/models", response_model=None)
95
+ async def list_models(
96
+ request: Request,
97
+ auth: ConditionalAuthDep,
98
+ config: CodexConfigDep,
99
+ ) -> dict[str, Any]:
100
+ """List available Codex models."""
101
+ models = [card.model_dump(mode="json") for card in config.models_endpoint]
102
+ return {"object": "list", "data": models}
103
+
104
+
105
+ @router.post("/v1/messages", response_model=None)
106
+ @with_format_chain(
107
+ [FORMAT_ANTHROPIC_MESSAGES, FORMAT_OPENAI_RESPONSES],
108
+ endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
109
+ )
110
+ async def codex_v1_messages(
111
+ request: Request,
112
+ auth: ConditionalAuthDep,
113
+ adapter: CodexAdapterDep,
114
+ ) -> StreamingResponse | Response | DeferredStreaming:
115
+ return await handle_codex_request(request, adapter)
116
+
117
+
118
+ @router.post("/{session_id}/v1/messages", response_model=None)
119
+ @with_format_chain(
120
+ [FORMAT_ANTHROPIC_MESSAGES, FORMAT_OPENAI_RESPONSES],
121
+ endpoint="/{session_id}/v1/messages",
122
+ )
123
+ async def codex_v1_messages_with_session(
124
+ session_id: str,
125
+ request: Request,
126
+ auth: ConditionalAuthDep,
127
+ adapter: CodexAdapterDep,
128
+ ) -> StreamingResponse | Response | DeferredStreaming:
129
+ return await handle_codex_request(request, adapter)