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,256 @@
1
+ """OpenAI/Codex token manager implementation for the Codex plugin."""
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from ccproxy.auth.exceptions import OAuthTokenRefreshError
6
+ from ccproxy.auth.managers.base import BaseTokenManager
7
+ from ccproxy.auth.managers.token_snapshot import TokenSnapshot
8
+ from ccproxy.auth.storage.base import TokenStorage
9
+ from ccproxy.core.logging import get_plugin_logger
10
+
11
+ from .models import OpenAICredentials, OpenAIProfileInfo, OpenAITokenWrapper
12
+
13
+
14
+ logger = get_plugin_logger()
15
+
16
+
17
+ class CodexTokenManager(BaseTokenManager[OpenAICredentials]):
18
+ """Manager for Codex/OpenAI token storage and operations.
19
+
20
+ Uses the generic storage and wrapper pattern for consistency.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ storage: TokenStorage[OpenAICredentials] | None = None,
26
+ ):
27
+ """Initialize Codex token manager.
28
+
29
+ Args:
30
+ storage: Optional custom storage, defaults to standard location
31
+ """
32
+ if storage is None:
33
+ # Use the Codex-specific storage for ~/.codex/auth.json
34
+ from .storage import CodexTokenStorage
35
+
36
+ storage = CodexTokenStorage()
37
+ super().__init__(storage)
38
+ self._profile_cache: OpenAIProfileInfo | None = None
39
+
40
+ @classmethod
41
+ async def create(
42
+ cls, storage: TokenStorage[OpenAICredentials] | None = None
43
+ ) -> "CodexTokenManager":
44
+ """Async factory for parity with other managers.
45
+
46
+ Codex/OpenAI does not need to preload remote data, but this keeps a
47
+ consistent async creation API across managers.
48
+ """
49
+ return cls(storage=storage)
50
+
51
+ def _build_token_snapshot(self, credentials: OpenAICredentials) -> TokenSnapshot:
52
+ """Construct a snapshot for OpenAI credentials."""
53
+ wrapper = OpenAITokenWrapper(credentials=credentials)
54
+ extras = {
55
+ "id_token_present": bool(wrapper.id_token),
56
+ }
57
+ return TokenSnapshot(
58
+ provider="codex",
59
+ account_id=wrapper.account_id,
60
+ access_token=str(wrapper.access_token_value),
61
+ refresh_token=wrapper.refresh_token_value,
62
+ expires_at=wrapper.expires_at_datetime,
63
+ extras=extras,
64
+ )
65
+
66
+ # ==================== Abstract Method Implementations ====================
67
+
68
+ async def refresh_token(self) -> OpenAICredentials | None:
69
+ """Refresh the access token using the refresh token.
70
+
71
+ Returns:
72
+ Updated credentials or None if refresh failed
73
+ """
74
+ # Load current credentials
75
+ credentials = await self.load_credentials()
76
+ if not credentials:
77
+ logger.error("no_credentials_to_refresh", category="auth")
78
+ return None
79
+
80
+ if not credentials.refresh_token:
81
+ logger.error("no_refresh_token_available", category="auth")
82
+ return None
83
+
84
+ try:
85
+ # Refresh directly using a local OAuth client/provider (no global registry)
86
+ from .provider import CodexOAuthProvider
87
+
88
+ provider = CodexOAuthProvider()
89
+ new_credentials: OpenAICredentials = await provider.refresh_access_token(
90
+ credentials.refresh_token
91
+ )
92
+
93
+ # Preserve account_id if not in new credentials
94
+ if not new_credentials.account_id and credentials.account_id:
95
+ # Preserve via nested tokens structure
96
+ new_credentials.tokens.account_id = credentials.account_id
97
+
98
+ # Save updated credentials
99
+ if await self.save_credentials(new_credentials):
100
+ logger.info(
101
+ "Token refreshed successfully",
102
+ account_id=new_credentials.account_id,
103
+ category="auth",
104
+ )
105
+ # Clear profile cache as token changed
106
+ self._profile_cache = None
107
+ return new_credentials
108
+
109
+ logger.error("failed_to_save_refreshed_credentials", category="auth")
110
+ return None
111
+
112
+ except Exception as e:
113
+ logger.error(
114
+ "Token refresh failed",
115
+ error=str(e),
116
+ exc_info=False,
117
+ category="auth",
118
+ )
119
+ return None
120
+
121
+ def is_expired(self, credentials: OpenAICredentials) -> bool:
122
+ """Check if credentials are expired using wrapper."""
123
+ if isinstance(credentials, OpenAICredentials):
124
+ wrapper = OpenAITokenWrapper(credentials=credentials)
125
+ return bool(wrapper.is_expired)
126
+
127
+ expires_at = getattr(credentials, "expires_at", None)
128
+ if not expires_at:
129
+ return False
130
+
131
+ if isinstance(expires_at, datetime):
132
+ return expires_at <= datetime.now(UTC)
133
+
134
+ return False
135
+
136
+ def get_account_id(self, credentials: OpenAICredentials) -> str | None:
137
+ """Get account ID from credentials."""
138
+ return credentials.account_id
139
+
140
+ def get_expiration_time(self, credentials: OpenAICredentials) -> datetime | None:
141
+ """Get expiration time as datetime."""
142
+ return credentials.expires_at
143
+
144
+ # ==================== OpenAI-Specific Methods ====================
145
+
146
+ async def get_profile_quick(self) -> OpenAIProfileInfo | None:
147
+ """Lightweight profile from cached data or JWT claims.
148
+
149
+ Avoids any remote calls. Uses cache if populated, otherwise derives
150
+ directly from stored credentials' JWT claims.
151
+ """
152
+ if self._profile_cache:
153
+ return self._profile_cache
154
+
155
+ credentials = await self.load_credentials()
156
+ if not credentials or self.is_expired(credentials):
157
+ return None
158
+
159
+ self._profile_cache = OpenAIProfileInfo.from_token(credentials)
160
+ return self._profile_cache
161
+
162
+ async def get_profile(self) -> OpenAIProfileInfo | None:
163
+ """Get user profile from JWT token.
164
+
165
+ OpenAI doesn't provide a profile API, so we extract
166
+ all information from the JWT token claims.
167
+
168
+ Returns:
169
+ OpenAIProfileInfo or None if not authenticated
170
+ """
171
+ if self._profile_cache:
172
+ return self._profile_cache
173
+
174
+ credentials = await self.load_credentials()
175
+ if not credentials or self.is_expired(credentials):
176
+ return None
177
+
178
+ # Extract profile from JWT token claims
179
+ self._profile_cache = OpenAIProfileInfo.from_token(credentials)
180
+ return self._profile_cache
181
+
182
+ async def get_access_token_with_refresh(self) -> str | None:
183
+ """Get valid access token, automatically refreshing if expired.
184
+
185
+ Returns:
186
+ Access token if available and valid, None otherwise
187
+ """
188
+ credentials = await self.load_credentials()
189
+ if not credentials:
190
+ logger.debug("no_credentials_found", category="auth")
191
+ return None
192
+
193
+ needs_refresh = self.should_refresh(credentials)
194
+
195
+ if needs_refresh:
196
+ logger.info(
197
+ "openai_token_refresh_needed",
198
+ reason="expired" if self.is_expired(credentials) else "expiring_soon",
199
+ expires_in=self.seconds_until_expiration(credentials),
200
+ category="auth",
201
+ )
202
+
203
+ if credentials.refresh_token:
204
+ try:
205
+ refreshed = await self.refresh_token()
206
+ except Exception as exc: # pragma: no cover - defensive
207
+ logger.warning(
208
+ "openai_token_refresh_exception",
209
+ error=str(exc),
210
+ category="auth",
211
+ )
212
+ raise OAuthTokenRefreshError("OpenAI token refresh failed") from exc
213
+
214
+ if refreshed:
215
+ logger.info("OpenAI token refreshed successfully", category="auth")
216
+ credentials = refreshed
217
+ else:
218
+ logger.warning("openai_token_refresh_failed", category="auth")
219
+ raise OAuthTokenRefreshError("OpenAI token refresh failed")
220
+ else:
221
+ logger.warning(
222
+ "Cannot refresh OpenAI token - no refresh token available",
223
+ category="auth",
224
+ )
225
+ raise OAuthTokenRefreshError("OpenAI token refresh failed")
226
+
227
+ return credentials.access_token
228
+
229
+ async def get_access_token(self) -> str | None:
230
+ """Override base method to return token even if expired.
231
+
232
+ Will attempt refresh if expired but still returns the token
233
+ even if refresh fails, letting the API handle authorization.
234
+
235
+ Returns:
236
+ Access token if available (expired or not), None only if no credentials
237
+ """
238
+ credentials = await self.load_credentials()
239
+ if not credentials:
240
+ logger.debug("no_credentials_found", category="auth")
241
+ return None
242
+
243
+ # Check if token is expired
244
+ needs_refresh = self.should_refresh(credentials)
245
+
246
+ if needs_refresh:
247
+ try:
248
+ return await self.get_access_token_with_refresh()
249
+ except OAuthTokenRefreshError as exc:
250
+ logger.warning(
251
+ "OpenAI token refresh failed, using existing token",
252
+ error=str(exc),
253
+ category="auth",
254
+ )
255
+
256
+ return credentials.access_token
@@ -0,0 +1,239 @@
1
+ """OpenAI-specific authentication models."""
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import Any, Literal
5
+
6
+ import jwt
7
+ from pydantic import (
8
+ BaseModel,
9
+ Field,
10
+ SecretStr,
11
+ computed_field,
12
+ field_serializer,
13
+ field_validator,
14
+ )
15
+
16
+ from ccproxy.auth.models.base import BaseProfileInfo, BaseTokenInfo
17
+ from ccproxy.core.logging import get_plugin_logger
18
+
19
+
20
+ logger = get_plugin_logger()
21
+
22
+
23
+ class OpenAITokens(BaseModel):
24
+ """Nested token structure from OpenAI OAuth."""
25
+
26
+ id_token: SecretStr = Field(..., description="OpenAI ID token (JWT)")
27
+ access_token: SecretStr = Field(..., description="OpenAI access token (JWT)")
28
+ refresh_token: SecretStr = Field(..., description="OpenAI refresh token")
29
+ account_id: str = Field(..., description="OpenAI account ID")
30
+
31
+ @field_serializer("id_token", "access_token", "refresh_token")
32
+ def serialize_secret(self, value: SecretStr) -> str:
33
+ """Serialize SecretStr to plain string for JSON output."""
34
+ return value.get_secret_value() if value else ""
35
+
36
+ @field_validator("id_token", "access_token", "refresh_token", mode="before")
37
+ @classmethod
38
+ def validate_tokens(cls, v: str | SecretStr | None) -> SecretStr | None:
39
+ """Convert string values to SecretStr."""
40
+ if v is None:
41
+ return None
42
+ if isinstance(v, str):
43
+ return SecretStr(v)
44
+ return v
45
+
46
+
47
+ class OpenAICredentials(BaseModel):
48
+ """OpenAI authentication credentials model matching actual auth file schema."""
49
+
50
+ OPENAI_API_KEY: str | None = Field(
51
+ None, description="Legacy API key (usually null)"
52
+ )
53
+ tokens: OpenAITokens = Field(..., description="OAuth token information")
54
+ last_refresh: str = Field(..., description="Last refresh timestamp as ISO string")
55
+ active: bool = Field(default=True, description="Whether credentials are active")
56
+ # No legacy compatibility shims; callers must provide nested `tokens` structure
57
+
58
+ @property
59
+ def access_token(self) -> str:
60
+ """Get access token from nested structure."""
61
+ return self.tokens.access_token.get_secret_value()
62
+
63
+ @property
64
+ def refresh_token(self) -> str:
65
+ """Get refresh token from nested structure."""
66
+ return self.tokens.refresh_token.get_secret_value()
67
+
68
+ @property
69
+ def id_token(self) -> str:
70
+ """Get ID token from nested structure."""
71
+ return self.tokens.id_token.get_secret_value()
72
+
73
+ @property
74
+ def account_id(self) -> str:
75
+ """Get account ID from nested structure."""
76
+ return self.tokens.account_id
77
+
78
+ @property
79
+ def expires_at(self) -> datetime:
80
+ """Extract expiration from access token JWT."""
81
+ try:
82
+ # Decode JWT without verification to extract 'exp' claim
83
+ decoded = jwt.decode(
84
+ self.tokens.access_token.get_secret_value(),
85
+ options={"verify_signature": False},
86
+ )
87
+ exp_timestamp = decoded.get("exp")
88
+ if exp_timestamp:
89
+ return datetime.fromtimestamp(exp_timestamp, tz=UTC)
90
+ except (jwt.DecodeError, jwt.InvalidTokenError, KeyError, ValueError) as e:
91
+ logger.debug("Failed to extract expiration from access token", error=str(e))
92
+
93
+ # Fallback to a reasonable default if we can't decode
94
+ return datetime.now(UTC).replace(hour=23, minute=59, second=59)
95
+
96
+ def is_expired(self) -> bool:
97
+ """Check if the access token is expired."""
98
+ now = datetime.now(UTC)
99
+ return now >= self.expires_at
100
+
101
+ def expires_in_seconds(self) -> int:
102
+ """Get seconds until token expires."""
103
+ now = datetime.now(UTC)
104
+ delta = self.expires_at - now
105
+ return max(0, int(delta.total_seconds()))
106
+
107
+ def to_dict(self) -> dict[str, Any]:
108
+ """Convert to dictionary for storage.
109
+
110
+ Implements BaseCredentials protocol.
111
+ """
112
+ return {
113
+ "OPENAI_API_KEY": self.OPENAI_API_KEY,
114
+ "tokens": {
115
+ "id_token": self.tokens.id_token.get_secret_value(),
116
+ "access_token": self.tokens.access_token.get_secret_value(),
117
+ "refresh_token": self.tokens.refresh_token.get_secret_value(),
118
+ "account_id": self.tokens.account_id,
119
+ },
120
+ "last_refresh": self.last_refresh,
121
+ "active": self.active,
122
+ }
123
+
124
+ @classmethod
125
+ def from_dict(cls, data: dict[str, Any]) -> "OpenAICredentials":
126
+ """Create from dictionary.
127
+
128
+ Implements BaseCredentials protocol.
129
+ """
130
+ return cls(**data)
131
+
132
+
133
+ class OpenAITokenWrapper(BaseTokenInfo):
134
+ """Wrapper for OpenAI credentials that adds computed properties.
135
+
136
+ This wrapper maintains the original OpenAICredentials structure
137
+ while providing a unified interface through BaseTokenInfo.
138
+ """
139
+
140
+ # Embed the original credentials to preserve JSON schema
141
+ credentials: OpenAICredentials
142
+
143
+ @computed_field
144
+ def access_token_value(self) -> str:
145
+ """Get access token (now SecretStr in OpenAI)."""
146
+ return self.credentials.access_token
147
+
148
+ @property
149
+ def refresh_token_value(self) -> str | None:
150
+ """Get refresh token."""
151
+ return self.credentials.refresh_token
152
+
153
+ @property
154
+ def expires_at_datetime(self) -> datetime:
155
+ """Get expiration (already a datetime in OpenAI)."""
156
+ return self.credentials.expires_at
157
+
158
+ @property
159
+ def account_id(self) -> str:
160
+ """Get account ID (extracted from JWT by validator)."""
161
+ return self.credentials.account_id
162
+
163
+ @property
164
+ def id_token(self) -> str | None:
165
+ """Get ID token if available."""
166
+ return self.credentials.id_token
167
+
168
+
169
+ class OpenAIProfileInfo(BaseProfileInfo):
170
+ """OpenAI-specific profile extracted from JWT tokens.
171
+
172
+ OpenAI embeds profile information in JWT claims rather
173
+ than providing a separate API endpoint.
174
+ """
175
+
176
+ provider_type: Literal["openai"] = "openai"
177
+
178
+ @classmethod
179
+ def from_token(cls, credentials: OpenAICredentials) -> "OpenAIProfileInfo":
180
+ """Extract profile from JWT token claims.
181
+
182
+ Args:
183
+ credentials: OpenAI credentials containing JWT tokens
184
+
185
+ Returns:
186
+ OpenAIProfileInfo with all JWT claims preserved
187
+ """
188
+ # Prefer id_token as it has more claims, fallback to access_token
189
+ token_to_decode = credentials.id_token or credentials.access_token
190
+
191
+ try:
192
+ # Decode without verification to extract claims
193
+ claims = jwt.decode(token_to_decode, options={"verify_signature": False})
194
+ logger.debug(
195
+ "Extracted JWT claims", num_claims=len(claims), category="auth"
196
+ )
197
+ except Exception as e:
198
+ logger.warning("failed_to_decode_jwt_token", error=str(e), category="auth")
199
+ claims = {}
200
+
201
+ # Use the account_id already extracted by OpenAICredentials validator
202
+ account_id = credentials.account_id
203
+
204
+ # Extract common fields if present in claims
205
+ email = claims.get("email", "")
206
+ display_name = claims.get("name") or claims.get("given_name")
207
+
208
+ # Store ALL JWT claims in extras for complete information
209
+ # This includes: sub, aud, iss, exp, iat, org_id, chatgpt_account_id, etc.
210
+ return cls(
211
+ account_id=account_id,
212
+ email=email,
213
+ display_name=display_name,
214
+ extras=claims, # Preserve all JWT claims
215
+ )
216
+
217
+ @property
218
+ def chatgpt_account_id(self) -> str | None:
219
+ """Get ChatGPT account ID from JWT claims."""
220
+ auth_claims = self.extras.get("https://api.openai.com/auth", {})
221
+ if isinstance(auth_claims, dict):
222
+ return auth_claims.get("chatgpt_account_id")
223
+ return None
224
+
225
+ @property
226
+ def organization_id(self) -> str | None:
227
+ """Get organization ID from JWT claims."""
228
+ # Check in auth claims first
229
+ auth_claims = self.extras.get("https://api.openai.com/auth", {})
230
+ if isinstance(auth_claims, dict) and "organization_id" in auth_claims:
231
+ return str(auth_claims["organization_id"])
232
+ # Fallback to top-level org_id
233
+ org_id = self.extras.get("org_id")
234
+ return str(org_id) if org_id is not None else None
235
+
236
+ @property
237
+ def auth0_subject(self) -> str | None:
238
+ """Get Auth0 subject (sub claim)."""
239
+ return self.extras.get("sub")
@@ -0,0 +1,146 @@
1
+ """OAuth Codex plugin v2 implementation."""
2
+
3
+ from typing import Any, cast
4
+
5
+ from ccproxy.auth.oauth import OAuthProviderProtocol
6
+ from ccproxy.core.logging import get_plugin_logger
7
+ from ccproxy.core.plugins import (
8
+ AuthProviderPluginFactory,
9
+ AuthProviderPluginRuntime,
10
+ PluginContext,
11
+ PluginManifest,
12
+ )
13
+
14
+ from .config import CodexOAuthConfig
15
+ from .provider import CodexOAuthProvider
16
+
17
+
18
+ logger = get_plugin_logger()
19
+
20
+
21
+ class OAuthCodexRuntime(AuthProviderPluginRuntime):
22
+ """Runtime for OAuth Codex plugin."""
23
+
24
+ def __init__(self, manifest: PluginManifest):
25
+ """Initialize runtime."""
26
+ super().__init__(manifest)
27
+ self.config: CodexOAuthConfig | None = None
28
+
29
+ async def _on_initialize(self) -> None:
30
+ """Initialize the OAuth Codex plugin."""
31
+ logger.debug(
32
+ "oauth_codex_initializing",
33
+ context_keys=list(self.context.keys()) if self.context else [],
34
+ )
35
+
36
+ # Get configuration
37
+ if self.context:
38
+ config = self.context.get("config")
39
+ if not isinstance(config, CodexOAuthConfig):
40
+ # Use default config if none provided
41
+ config = CodexOAuthConfig()
42
+ logger.debug("oauth_codex_using_default_config")
43
+ self.config = config
44
+
45
+ # Call parent initialization which handles provider registration
46
+ await super()._on_initialize()
47
+
48
+ logger.debug(
49
+ "oauth_codex_plugin_initialized",
50
+ status="initialized",
51
+ provider_name=self.auth_provider.provider_name
52
+ if self.auth_provider
53
+ else "unknown",
54
+ category="plugin",
55
+ )
56
+
57
+
58
+ class OAuthCodexFactory(AuthProviderPluginFactory):
59
+ """Factory for OAuth Codex plugin."""
60
+
61
+ cli_safe = True # Safe for CLI - provides auth only
62
+
63
+ def __init__(self) -> None:
64
+ """Initialize factory with manifest."""
65
+ # Create manifest with static declarations
66
+ manifest = PluginManifest(
67
+ name="oauth_codex",
68
+ version="0.1.0",
69
+ description="Standalone OpenAI Codex OAuth authentication provider plugin",
70
+ is_provider=True, # It's a provider plugin but focused on OAuth
71
+ config_class=CodexOAuthConfig,
72
+ dependencies=[],
73
+ routes=[], # No HTTP routes needed
74
+ tasks=[], # No scheduled tasks needed
75
+ )
76
+
77
+ # Initialize with manifest
78
+ super().__init__(manifest)
79
+
80
+ def create_context(self, core_services: Any) -> PluginContext:
81
+ """Create context with auth provider components.
82
+
83
+ Args:
84
+ core_services: Core services container
85
+
86
+ Returns:
87
+ Plugin context with auth provider components
88
+ """
89
+ # Start with base context
90
+ context = super().create_context(core_services)
91
+
92
+ # Create auth provider for this plugin
93
+ auth_provider = self.create_auth_provider(context)
94
+ context["auth_provider"] = auth_provider
95
+
96
+ # Add other auth-specific components if needed
97
+ storage = self.create_storage()
98
+ if storage:
99
+ context["storage"] = storage
100
+
101
+ return context
102
+
103
+ def create_runtime(self) -> OAuthCodexRuntime:
104
+ """Create runtime instance."""
105
+ return OAuthCodexRuntime(self.manifest)
106
+
107
+ def create_auth_provider(
108
+ self, context: PluginContext | None = None
109
+ ) -> OAuthProviderProtocol:
110
+ """Create OAuth provider instance.
111
+
112
+ Args:
113
+ context: Optional plugin context containing http_client
114
+
115
+ Returns:
116
+ CodexOAuthProvider instance
117
+ """
118
+ # Prefer validated config from context when available
119
+ if context and isinstance(context.get("config"), CodexOAuthConfig):
120
+ cfg = cast(CodexOAuthConfig, context.get("config"))
121
+ else:
122
+ cfg = CodexOAuthConfig()
123
+ config: CodexOAuthConfig = cfg
124
+ http_client = context.get("http_client") if context else None
125
+ hook_manager = context.get("hook_manager") if context else None
126
+ settings = context.get("settings") if context else None
127
+ provider = CodexOAuthProvider(
128
+ config,
129
+ http_client=http_client,
130
+ hook_manager=hook_manager,
131
+ settings=settings,
132
+ )
133
+ return cast(OAuthProviderProtocol, provider)
134
+
135
+ def create_storage(self) -> Any | None:
136
+ """Create storage for OAuth credentials.
137
+
138
+ Returns:
139
+ Storage instance or None to use provider's default
140
+ """
141
+ # CodexOAuthProvider manages its own storage internally
142
+ return None
143
+
144
+
145
+ # Export the factory instance
146
+ factory = OAuthCodexFactory()