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,574 @@
1
+ """Codex/OpenAI OAuth provider for plugin registration."""
2
+
3
+ import hashlib
4
+ from base64 import urlsafe_b64encode
5
+ from typing import Any
6
+ from urllib.parse import urlencode
7
+
8
+ import httpx
9
+
10
+ from ccproxy.auth.oauth.protocol import ProfileLoggingMixin, StandardProfileFields
11
+ from ccproxy.auth.oauth.registry import CliAuthConfig, FlowType, OAuthProviderInfo
12
+ from ccproxy.config.settings import Settings
13
+ from ccproxy.core.logging import get_plugin_logger
14
+
15
+ from .client import CodexOAuthClient
16
+ from .config import CodexOAuthConfig
17
+ from .models import OpenAICredentials
18
+ from .storage import CodexTokenStorage
19
+
20
+
21
+ logger = get_plugin_logger()
22
+
23
+
24
+ class CodexOAuthProvider(ProfileLoggingMixin):
25
+ """Codex/OpenAI OAuth provider implementation for registry."""
26
+
27
+ def __init__(
28
+ self,
29
+ config: CodexOAuthConfig | None = None,
30
+ storage: CodexTokenStorage | None = None,
31
+ http_client: httpx.AsyncClient | None = None,
32
+ hook_manager: Any | None = None,
33
+ settings: Settings | None = None,
34
+ ):
35
+ """Initialize Codex OAuth provider.
36
+
37
+ Args:
38
+ config: OAuth configuration
39
+ storage: Token storage
40
+ http_client: Optional HTTP client (for request tracing support)
41
+ hook_manager: Optional hook manager for emitting events
42
+ settings: Optional settings for HTTP client configuration
43
+ """
44
+ self.config = config or CodexOAuthConfig()
45
+ self.storage = storage or CodexTokenStorage()
46
+ self.hook_manager = hook_manager
47
+ self.http_client = http_client
48
+ self.settings = settings
49
+
50
+ self.client = CodexOAuthClient(
51
+ self.config,
52
+ self.storage,
53
+ http_client,
54
+ hook_manager=hook_manager,
55
+ settings=settings,
56
+ )
57
+
58
+ @property
59
+ def provider_name(self) -> str:
60
+ """Internal provider name."""
61
+ return "codex"
62
+
63
+ @property
64
+ def provider_display_name(self) -> str:
65
+ """Display name for UI."""
66
+ return "OpenAI Codex"
67
+
68
+ @property
69
+ def supports_pkce(self) -> bool:
70
+ """Whether this provider supports PKCE."""
71
+ return self.config.use_pkce
72
+
73
+ @property
74
+ def supports_refresh(self) -> bool:
75
+ """Whether this provider supports token refresh."""
76
+ return True
77
+
78
+ @property
79
+ def requires_client_secret(self) -> bool:
80
+ """Whether this provider requires a client secret."""
81
+ return False # OpenAI uses PKCE flow without client secret
82
+
83
+ async def get_authorization_url(
84
+ self,
85
+ state: str,
86
+ code_verifier: str | None = None,
87
+ redirect_uri: str | None = None,
88
+ ) -> str:
89
+ """Get the authorization URL for OAuth flow.
90
+
91
+ Args:
92
+ state: OAuth state parameter for CSRF protection
93
+ code_verifier: PKCE code verifier (if PKCE is supported)
94
+
95
+ Returns:
96
+ Authorization URL to redirect user to
97
+ """
98
+ params = {
99
+ "response_type": "code",
100
+ "client_id": self.config.client_id,
101
+ "redirect_uri": redirect_uri or self.config.get_redirect_uri(),
102
+ "scope": " ".join(self.config.scopes),
103
+ "state": state,
104
+ }
105
+
106
+ # Add PKCE challenge if supported and verifier provided
107
+ if self.config.use_pkce and code_verifier:
108
+ code_challenge = (
109
+ urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
110
+ .decode()
111
+ .rstrip("=")
112
+ )
113
+ params["code_challenge"] = code_challenge
114
+ params["code_challenge_method"] = "S256"
115
+
116
+ auth_url = f"{self.config.authorize_url}?{urlencode(params)}"
117
+
118
+ logger.info(
119
+ "codex_oauth_auth_url_generated",
120
+ state=state,
121
+ has_pkce=bool(code_verifier and self.config.use_pkce),
122
+ category="auth",
123
+ )
124
+
125
+ return auth_url
126
+
127
+ async def handle_callback(
128
+ self,
129
+ code: str,
130
+ state: str,
131
+ code_verifier: str | None = None,
132
+ redirect_uri: str | None = None,
133
+ ) -> Any:
134
+ """Handle OAuth callback and exchange code for tokens.
135
+
136
+ Args:
137
+ code: Authorization code from OAuth callback
138
+ state: State parameter for validation
139
+ code_verifier: PKCE code verifier (if PKCE is used)
140
+ redirect_uri: Redirect URI used in authorization (optional)
141
+
142
+ Returns:
143
+ OpenAI credentials object
144
+ """
145
+ # Use the client's handle_callback method which includes code exchange
146
+ # If a specific redirect_uri was provided, create a temporary client with that URI
147
+ if redirect_uri and redirect_uri != self.client.redirect_uri:
148
+ # Create temporary config with the specific redirect URI
149
+ temp_config = CodexOAuthConfig(
150
+ client_id=self.config.client_id,
151
+ redirect_uri=redirect_uri,
152
+ scopes=self.config.scopes,
153
+ base_url=self.config.base_url,
154
+ authorize_url=self.config.authorize_url,
155
+ token_url=self.config.token_url,
156
+ audience=self.config.audience,
157
+ use_pkce=self.config.use_pkce,
158
+ )
159
+
160
+ # Create temporary client with the correct redirect URI
161
+ temp_client = CodexOAuthClient(
162
+ temp_config,
163
+ self.storage,
164
+ self.http_client,
165
+ hook_manager=self.hook_manager,
166
+ settings=self.settings,
167
+ )
168
+
169
+ credentials = await temp_client.handle_callback(
170
+ code, state, code_verifier or ""
171
+ )
172
+ else:
173
+ # Use the regular client
174
+ credentials = await self.client.handle_callback(
175
+ code, state, code_verifier or ""
176
+ )
177
+
178
+ # The client already saves to storage if available, but we can save again
179
+ # to our specific storage if needed
180
+ if self.storage:
181
+ await self.storage.save(credentials)
182
+
183
+ logger.info(
184
+ "codex_oauth_callback_handled",
185
+ state=state,
186
+ has_credentials=bool(credentials),
187
+ has_id_token=bool(credentials.id_token),
188
+ category="auth",
189
+ )
190
+
191
+ return credentials
192
+
193
+ async def refresh_access_token(self, refresh_token: str) -> Any:
194
+ """Refresh access token using refresh token.
195
+
196
+ Args:
197
+ refresh_token: Refresh token from previous auth
198
+
199
+ Returns:
200
+ New token response
201
+ """
202
+ credentials = await self.client.refresh_token(refresh_token)
203
+
204
+ # Store updated credentials
205
+ if self.storage:
206
+ await self.storage.save(credentials)
207
+
208
+ logger.info("codex_oauth_token_refreshed", category="auth")
209
+
210
+ return credentials
211
+
212
+ async def revoke_token(self, token: str) -> None:
213
+ """Revoke an access or refresh token.
214
+
215
+ Args:
216
+ token: Token to revoke
217
+ """
218
+ # OpenAI doesn't have a revoke endpoint, so we just delete stored credentials
219
+ if self.storage:
220
+ await self.storage.delete()
221
+
222
+ logger.info("codex_oauth_token_revoked_locally", category="auth")
223
+
224
+ def get_provider_info(self) -> OAuthProviderInfo:
225
+ """Get provider information for discovery.
226
+
227
+ Returns:
228
+ Provider information
229
+ """
230
+ return OAuthProviderInfo(
231
+ name=self.provider_name,
232
+ display_name=self.provider_display_name,
233
+ description="OAuth authentication for OpenAI Codex",
234
+ supports_pkce=self.supports_pkce,
235
+ scopes=self.config.scopes,
236
+ is_available=True,
237
+ plugin_name="oauth_codex",
238
+ )
239
+
240
+ async def validate_token(self, access_token: str) -> bool:
241
+ """Validate an access token.
242
+
243
+ Args:
244
+ access_token: Token to validate
245
+
246
+ Returns:
247
+ True if token is valid
248
+ """
249
+ # OpenAI doesn't have a validation endpoint, so we check if stored token matches
250
+ if self.storage:
251
+ credentials = await self.storage.load()
252
+ if credentials:
253
+ return credentials.access_token == access_token
254
+ return False
255
+
256
+ async def get_user_info(self, access_token: str) -> dict[str, Any] | None:
257
+ """Get user information using access token.
258
+
259
+ Args:
260
+ access_token: Valid access token
261
+
262
+ Returns:
263
+ User information or None
264
+ """
265
+ # Load stored credentials
266
+ if self.storage:
267
+ credentials = await self.storage.load()
268
+ if credentials:
269
+ info = {
270
+ "account_id": credentials.account_id,
271
+ "active": credentials.active,
272
+ "has_id_token": bool(credentials.id_token),
273
+ }
274
+
275
+ # Try to extract info from ID token if present
276
+ if credentials.id_token:
277
+ try:
278
+ import jwt
279
+
280
+ decoded = jwt.decode(
281
+ credentials.id_token,
282
+ options={"verify_signature": False},
283
+ )
284
+ info.update(
285
+ {
286
+ "email": decoded.get("email"),
287
+ "name": decoded.get("name"),
288
+ "sub": decoded.get("sub"),
289
+ }
290
+ )
291
+ except Exception:
292
+ pass
293
+
294
+ return info
295
+ return None
296
+
297
+ def get_storage(self) -> Any:
298
+ """Get storage implementation for this provider.
299
+
300
+ Returns:
301
+ Storage implementation
302
+ """
303
+ return self.storage
304
+
305
+ def get_config(self) -> Any:
306
+ """Get configuration for this provider.
307
+
308
+ Returns:
309
+ Configuration implementation
310
+ """
311
+ return self.config
312
+
313
+ async def save_credentials(
314
+ self, credentials: Any, custom_path: Any | None = None
315
+ ) -> bool:
316
+ """Save credentials using provider's storage mechanism.
317
+
318
+ Args:
319
+ credentials: OpenAI credentials object
320
+ custom_path: Optional custom storage path (Path object)
321
+
322
+ Returns:
323
+ True if saved successfully, False otherwise
324
+ """
325
+ from pathlib import Path
326
+
327
+ from ccproxy.auth.storage.generic import GenericJsonStorage
328
+
329
+ from .manager import CodexTokenManager
330
+ from .models import OpenAICredentials
331
+
332
+ try:
333
+ if custom_path:
334
+ # Use custom path for storage
335
+ storage = GenericJsonStorage(Path(custom_path), OpenAICredentials)
336
+ manager = await CodexTokenManager.create(storage=storage)
337
+ else:
338
+ # Use default storage
339
+ manager = await CodexTokenManager.create()
340
+
341
+ return await manager.save_credentials(credentials)
342
+ except Exception as e:
343
+ logger.error(
344
+ "Failed to save OpenAI credentials",
345
+ error=str(e),
346
+ exc_info=e,
347
+ has_custom_path=bool(custom_path),
348
+ )
349
+ return False
350
+
351
+ async def load_credentials(self, custom_path: Any | None = None) -> Any | None:
352
+ """Load credentials from provider's storage.
353
+
354
+ Args:
355
+ custom_path: Optional custom storage path (Path object)
356
+
357
+ Returns:
358
+ Credentials if found, None otherwise
359
+ """
360
+ from pathlib import Path
361
+
362
+ from ccproxy.auth.storage.generic import GenericJsonStorage
363
+
364
+ from .manager import CodexTokenManager
365
+ from .models import OpenAICredentials
366
+
367
+ try:
368
+ if custom_path:
369
+ # Load from custom path
370
+ storage = GenericJsonStorage(Path(custom_path), OpenAICredentials)
371
+ manager = await CodexTokenManager.create(storage=storage)
372
+ else:
373
+ # Load from default storage
374
+ manager = await CodexTokenManager.create()
375
+
376
+ credentials = await manager.load_credentials()
377
+
378
+ # Use standardized profile logging
379
+ self._log_credentials_loaded("codex", credentials)
380
+
381
+ return credentials
382
+ except Exception as e:
383
+ logger.error(
384
+ "Failed to load OpenAI credentials",
385
+ error=str(e),
386
+ exc_info=e,
387
+ has_custom_path=bool(custom_path),
388
+ )
389
+ return None
390
+
391
+ async def create_token_manager(self, storage: Any | None = None) -> Any:
392
+ """Create and return the token manager instance.
393
+
394
+ Provided to allow core/CLI code to obtain a manager without
395
+ importing plugin classes directly.
396
+ """
397
+ from .manager import CodexTokenManager
398
+
399
+ return await CodexTokenManager.create(storage=storage)
400
+
401
+ def _extract_standard_profile(
402
+ self, credentials: OpenAICredentials
403
+ ) -> StandardProfileFields:
404
+ """Extract standardized profile fields from OpenAI credentials for UI display.
405
+
406
+ Args:
407
+ credentials: OpenAI credentials with JWT tokens
408
+
409
+ Returns:
410
+ StandardProfileFields with clean, UI-friendly data
411
+ """
412
+ # Initialize with basic credential info
413
+ from typing import Any
414
+
415
+ profile_data: dict[str, Any] = {
416
+ "account_id": credentials.account_id,
417
+ "provider_type": "codex",
418
+ "active": credentials.active,
419
+ "expired": credentials.is_expired(),
420
+ "has_refresh_token": bool(credentials.refresh_token),
421
+ "has_id_token": bool(credentials.id_token),
422
+ "token_expires_at": credentials.expires_at,
423
+ }
424
+
425
+ # Store raw credential data for debugging
426
+ raw_data: dict[str, Any] = {
427
+ "last_refresh": credentials.last_refresh,
428
+ "expires_at": str(credentials.expires_at),
429
+ }
430
+
431
+ # Extract information from ID token
432
+ if credentials.id_token:
433
+ try:
434
+ import jwt
435
+
436
+ id_claims = jwt.decode(
437
+ credentials.id_token, options={"verify_signature": False}
438
+ )
439
+
440
+ # Extract UI-friendly profile info
441
+ profile_data.update(
442
+ {
443
+ "email": id_claims.get("email"),
444
+ "email_verified": id_claims.get("email_verified"),
445
+ "display_name": id_claims.get("name")
446
+ or id_claims.get("given_name"),
447
+ }
448
+ )
449
+
450
+ # Extract subscription information
451
+ auth_claims = id_claims.get("https://api.openai.com/auth", {})
452
+ if isinstance(auth_claims, dict):
453
+ plan_type = auth_claims.get(
454
+ "chatgpt_plan_type"
455
+ ) # 'plus', 'pro', etc.
456
+ profile_data.update(
457
+ {
458
+ "subscription_type": plan_type,
459
+ "subscription_status": "active" if plan_type else None,
460
+ }
461
+ )
462
+
463
+ # Parse subscription dates
464
+ if auth_claims.get("chatgpt_subscription_active_until"):
465
+ try:
466
+ from datetime import datetime
467
+
468
+ expires_str = auth_claims[
469
+ "chatgpt_subscription_active_until"
470
+ ]
471
+ profile_data["subscription_expires_at"] = (
472
+ datetime.fromisoformat(
473
+ expires_str.replace("+00:00", "")
474
+ )
475
+ )
476
+ except Exception:
477
+ pass
478
+
479
+ # Extract organization info
480
+ orgs = auth_claims.get("organizations", [])
481
+ if orgs:
482
+ primary_org = orgs[0] if isinstance(orgs, list) else {}
483
+ if isinstance(primary_org, dict):
484
+ profile_data.update(
485
+ {
486
+ "organization_name": primary_org.get("title"),
487
+ "organization_role": primary_org.get("role"),
488
+ }
489
+ )
490
+
491
+ # Store full claims for debugging
492
+ raw_data["id_token_claims"] = id_claims
493
+
494
+ except Exception as e:
495
+ logger.debug(
496
+ "Failed to decode ID token for profile extraction", error=str(e)
497
+ )
498
+ raw_data["id_token_decode_error"] = str(e)
499
+
500
+ # Extract access token information
501
+ if credentials.access_token:
502
+ try:
503
+ import jwt
504
+
505
+ access_claims = jwt.decode(
506
+ credentials.access_token, options={"verify_signature": False}
507
+ )
508
+
509
+ # Store access token info in raw data
510
+ raw_data["access_token_claims"] = {
511
+ "scopes": access_claims.get("scp", []),
512
+ "client_id": access_claims.get("client_id"),
513
+ "audience": access_claims.get("aud"),
514
+ }
515
+
516
+ except Exception as e:
517
+ logger.debug(
518
+ "Failed to decode access token for profile extraction", error=str(e)
519
+ )
520
+ raw_data["access_token_decode_error"] = str(e)
521
+
522
+ # Add provider-specific features
523
+ if profile_data.get("subscription_type"):
524
+ profile_data["features"] = {
525
+ "chatgpt_plus": profile_data["subscription_type"] == "plus",
526
+ "has_subscription": True,
527
+ }
528
+
529
+ profile_data["raw_profile_data"] = raw_data
530
+
531
+ return StandardProfileFields(**profile_data)
532
+
533
+ async def exchange_manual_code(self, code: str) -> Any:
534
+ """Exchange manual authorization code for tokens.
535
+
536
+ Args:
537
+ code: Authorization code from manual entry
538
+
539
+ Returns:
540
+ OpenAI credentials object
541
+ """
542
+ # For manual code flow, use OOB redirect URI and no state validation
543
+ credentials: OpenAICredentials = await self.client.handle_callback(
544
+ code, "manual", ""
545
+ )
546
+
547
+ if self.storage:
548
+ await self.storage.save(credentials)
549
+
550
+ logger.info(
551
+ "codex_oauth_manual_code_exchanged",
552
+ has_credentials=bool(credentials),
553
+ category="auth",
554
+ )
555
+
556
+ return credentials
557
+
558
+ @property
559
+ def cli(self) -> CliAuthConfig:
560
+ """Get CLI authentication configuration for this provider."""
561
+ return CliAuthConfig(
562
+ preferred_flow=FlowType.browser,
563
+ callback_port=1455,
564
+ callback_path="/auth/callback",
565
+ supports_manual_code=True,
566
+ supports_device_flow=False,
567
+ fixed_redirect_uri=None,
568
+ manual_redirect_uri="https://platform.openai.com/oauth/callback",
569
+ )
570
+
571
+ async def cleanup(self) -> None:
572
+ """Cleanup resources."""
573
+ if self.client:
574
+ await self.client.close()
@@ -0,0 +1,92 @@
1
+ """Token storage for Codex OAuth plugin."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ccproxy.auth.storage.base import BaseJsonStorage
6
+ from ccproxy.core.logging import get_plugin_logger
7
+
8
+ from .models import OpenAICredentials
9
+
10
+
11
+ logger = get_plugin_logger()
12
+
13
+
14
+ class CodexTokenStorage(BaseJsonStorage[OpenAICredentials]):
15
+ """Codex/OpenAI OAuth-specific token storage implementation."""
16
+
17
+ def __init__(self, storage_path: Path | None = None):
18
+ """Initialize Codex token storage.
19
+
20
+ Args:
21
+ storage_path: Path to storage file
22
+ """
23
+ if storage_path is None:
24
+ # Default to standard OpenAI credentials location
25
+ storage_path = Path.home() / ".codex" / "auth.json"
26
+
27
+ super().__init__(storage_path)
28
+ self.provider_name = "codex"
29
+
30
+ async def save(self, credentials: OpenAICredentials) -> bool:
31
+ """Save OpenAI credentials.
32
+
33
+ Args:
34
+ credentials: OpenAI credentials to save
35
+
36
+ Returns:
37
+ True if saved successfully, False otherwise
38
+ """
39
+ try:
40
+ # Convert to dict for storage
41
+ data = credentials.model_dump(mode="json", exclude_none=True)
42
+
43
+ # Use parent class's atomic write with backup
44
+ await self._write_json(data)
45
+
46
+ logger.info(
47
+ "codex_oauth_credentials_saved",
48
+ has_refresh_token=bool(credentials.refresh_token),
49
+ storage_path=str(self.file_path),
50
+ category="auth",
51
+ )
52
+ return True
53
+ except Exception as e:
54
+ logger.error(
55
+ "codex_oauth_save_failed", error=str(e), exc_info=e, category="auth"
56
+ )
57
+ return False
58
+
59
+ async def load(self) -> OpenAICredentials | None:
60
+ """Load OpenAI credentials.
61
+
62
+ Returns:
63
+ Stored credentials or None
64
+ """
65
+ try:
66
+ # Use parent class's read method (avoid redundant exists() checks)
67
+ data = await self._read_json()
68
+ if not data:
69
+ logger.debug(
70
+ "codex_auth_file_empty",
71
+ storage_path=str(self.file_path),
72
+ category="auth",
73
+ )
74
+ return None
75
+
76
+ credentials = OpenAICredentials.model_validate(data)
77
+ logger.info(
78
+ "codex_oauth_credentials_loaded",
79
+ has_refresh_token=bool(credentials.refresh_token),
80
+ category="auth",
81
+ )
82
+ return credentials
83
+ except Exception as e:
84
+ logger.error(
85
+ "codex_oauth_credentials_load_error",
86
+ error=str(e),
87
+ exc_info=e,
88
+ category="auth",
89
+ )
90
+ return None
91
+
92
+ # The exists(), delete(), and get_location() methods are inherited from BaseJsonStorage
@@ -0,0 +1,28 @@
1
+ # Permissions Plugin
2
+
3
+ Provides interactive approval flows for tool calls and other privileged actions.
4
+
5
+ ## Highlights
6
+ - Starts the permission service that tracks and resolves pending requests
7
+ - Exposes SSE and MCP routes for UI, terminal, or IDE integrations
8
+ - Supports configurable timeouts and optional terminal UI prompts
9
+
10
+ ## Configuration
11
+ - `PermissionsConfig` toggles enablement, stream support, and timeouts
12
+ - Pending requests are handled only when the plugin is enabled
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin permissions --config-class PermissionsConfig`
15
+
16
+ ```toml
17
+ [plugins.permissions]
18
+ # enabled = true
19
+ # timeout_seconds = 30
20
+ # enable_terminal_ui = true
21
+ # enable_sse_stream = true
22
+ # cleanup_after_minutes = 5
23
+ ```
24
+
25
+ ## Related Components
26
+ - `service.py`: permission service entrypoint
27
+ - `routes.py`: FastAPI router for SSE streaming
28
+ - `mcp/`: MCP server routes used by Claude Code