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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1231
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,212 @@
1
+ """Token storage for Claude OAuth plugin."""
2
+
3
+ import asyncio
4
+ import json
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Any, cast
8
+
9
+ from ccproxy.auth.storage.base import BaseJsonStorage
10
+ from ccproxy.core.logging import get_plugin_logger
11
+
12
+ from .models import ClaudeCredentials, ClaudeProfileInfo
13
+
14
+
15
+ logger = get_plugin_logger()
16
+
17
+
18
+ class ClaudeOAuthStorage(BaseJsonStorage[ClaudeCredentials]):
19
+ """Claude OAuth-specific token storage implementation."""
20
+
21
+ def __init__(self, storage_path: Path | None = None):
22
+ """Initialize Claude OAuth token storage.
23
+
24
+ Args:
25
+ storage_path: Path to storage file
26
+ """
27
+ if storage_path is None:
28
+ # Default to standard Claude credentials location
29
+ storage_path = Path.home() / ".claude" / ".credentials.json"
30
+
31
+ super().__init__(storage_path)
32
+ self.provider_name = "claude-api"
33
+
34
+ async def save(self, credentials: ClaudeCredentials) -> bool:
35
+ """Save Claude credentials.
36
+
37
+ Args:
38
+ credentials: Claude credentials to save
39
+
40
+ Returns:
41
+ True if saved successfully, False otherwise
42
+ """
43
+ try:
44
+ # Convert to dict for storage (uses by_alias=True by default)
45
+ data = credentials.model_dump(mode="json", exclude_none=True)
46
+
47
+ # Use parent class's atomic write with backup
48
+ await self._write_json(data)
49
+
50
+ logger.debug(
51
+ "claude_oauth_credentials_saved",
52
+ has_oauth=bool(credentials.claude_ai_oauth),
53
+ storage_path=str(self.file_path),
54
+ category="auth",
55
+ )
56
+ return True
57
+ except Exception as e:
58
+ logger.error(
59
+ "claude_oauth_save_failed", error=str(e), exc_info=e, category="auth"
60
+ )
61
+ return False
62
+
63
+ async def load(self) -> ClaudeCredentials | None:
64
+ """Load Claude credentials.
65
+
66
+ Returns:
67
+ Stored credentials or None
68
+ """
69
+ try:
70
+ # Use parent class's read method
71
+ data = await self._read_json()
72
+ if not data:
73
+ return None
74
+
75
+ credentials = ClaudeCredentials.model_validate(data)
76
+ logger.debug(
77
+ "claude_oauth_credentials_loaded",
78
+ has_oauth=bool(credentials.claude_ai_oauth),
79
+ category="auth",
80
+ )
81
+ return credentials
82
+ except Exception as e:
83
+ logger.error(
84
+ "claude_oauth_credentials_load_error",
85
+ error=str(e),
86
+ exc_info=e,
87
+ category="auth",
88
+ )
89
+ return None
90
+
91
+ # The exists(), delete(), and get_location() methods are inherited from BaseJsonStorage
92
+
93
+
94
+ class ClaudeProfileStorage:
95
+ """Claude profile storage implementation for .account.json."""
96
+
97
+ def __init__(self, storage_path: Path | None = None):
98
+ """Initialize Claude profile storage.
99
+
100
+ Args:
101
+ storage_path: Path to storage file
102
+ """
103
+ if storage_path is None:
104
+ # Default to standard Claude account location
105
+ storage_path = Path.home() / ".claude" / ".account.json"
106
+
107
+ self.file_path = storage_path
108
+
109
+ async def _write_json(self, data: dict[str, Any]) -> None:
110
+ """Write JSON data to file atomically.
111
+
112
+ Args:
113
+ data: JSON data to write
114
+ """
115
+ # Ensure parent directory exists
116
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
117
+
118
+ # Write to temp file first for atomic operation
119
+ def write_file() -> None:
120
+ with tempfile.NamedTemporaryFile(
121
+ mode="w",
122
+ dir=self.file_path.parent,
123
+ delete=False,
124
+ prefix=".tmp_",
125
+ suffix=".json",
126
+ ) as tmp_file:
127
+ json.dump(data, tmp_file, indent=2)
128
+ tmp_path = Path(tmp_file.name)
129
+
130
+ # Set proper permissions before moving
131
+ tmp_path.chmod(0o600)
132
+ # Atomic rename
133
+ tmp_path.replace(self.file_path)
134
+
135
+ await asyncio.to_thread(write_file)
136
+
137
+ async def _read_json(self) -> dict[str, Any] | None:
138
+ """Read JSON data from file.
139
+
140
+ Returns:
141
+ Parsed JSON data or None if file doesn't exist
142
+ """
143
+ if not self.file_path.exists():
144
+ return None
145
+
146
+ def read_file() -> dict[str, Any]:
147
+ with self.file_path.open("r") as f:
148
+ return cast(dict[str, Any], json.load(f))
149
+
150
+ return cast(dict[str, Any], await asyncio.to_thread(read_file))
151
+
152
+ async def save_profile(self, profile_data: dict[str, Any]) -> bool:
153
+ """Save Claude profile data.
154
+
155
+ Args:
156
+ profile_data: Raw profile data from API
157
+
158
+ Returns:
159
+ True if saved successfully, False otherwise
160
+ """
161
+ try:
162
+ # Write the raw profile data
163
+ await self._write_json(profile_data)
164
+
165
+ # Extract key info for logging
166
+ account = profile_data.get("account", {})
167
+ logger.info(
168
+ "claude_profile_saved",
169
+ account_id=account.get("uuid"),
170
+ email=account.get("email"),
171
+ has_claude_pro=account.get("has_claude_pro"),
172
+ has_claude_max=account.get("has_claude_max"),
173
+ storage_path=str(self.file_path),
174
+ category="auth",
175
+ )
176
+ return True
177
+ except Exception as e:
178
+ logger.error(
179
+ "claude_profile_save_failed",
180
+ error=str(e),
181
+ exc_info=e,
182
+ category="auth",
183
+ )
184
+ return False
185
+
186
+ async def load_profile(self) -> ClaudeProfileInfo | None:
187
+ """Load Claude profile.
188
+
189
+ Returns:
190
+ ClaudeProfileInfo or None if not found
191
+ """
192
+ try:
193
+ data = await self._read_json()
194
+ if not data:
195
+ return None
196
+
197
+ profile = ClaudeProfileInfo.from_api_response(data)
198
+ logger.debug(
199
+ "claude_profile_loaded",
200
+ account_id=profile.account_id,
201
+ email=profile.email,
202
+ category="auth",
203
+ )
204
+ return profile
205
+ except Exception as e:
206
+ logger.error(
207
+ "claude_profile_load_error",
208
+ error=str(e),
209
+ exc_info=e,
210
+ category="auth",
211
+ )
212
+ return None
@@ -0,0 +1,38 @@
1
+ # OAuth Codex Plugin
2
+
3
+ OAuth-only plugin that issues and refreshes tokens for the Codex provider.
4
+
5
+ ## Highlights
6
+ - Provides `CodexOAuthProvider` implementing the OAuth dance for Codex APIs
7
+ - Registers an auth manager consumable by `codex` and related plugins
8
+ - Leverages shared HTTP client and hook infrastructure from the container
9
+
10
+ ## Configuration
11
+ - `CodexOAuthConfig` configures client ID, secrets, scopes, and cache paths
12
+ - Enable when using Codex endpoints that require OAuth tokens
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin oauth_codex --config-class CodexOAuthConfig`
15
+
16
+ ```toml
17
+ [plugins.oauth_codex]
18
+ # enabled = true
19
+ # base_url = "https://auth.openai.com"
20
+ # token_url = "https://auth.openai.com/oauth/token"
21
+ # authorize_url = "https://auth.openai.com/oauth/authorize"
22
+ # profile_url = "https://api.openai.com/oauth/profile"
23
+ # client_id = "app_EMoamEEZ73f0CkXaXp7hrann"
24
+ # redirect_uri = "http://localhost:1455/auth/callback"
25
+ # scopes = ["openid", "profile", "email", "offline_access"]
26
+ # headers = {"User-Agent" = "Codex-Code/1.0.43"}
27
+ # audience = "https://api.openai.com/v1"
28
+ # user_agent = "Codex-Code/1.0.43"
29
+ # request_timeout = 30
30
+ # callback_timeout = 300
31
+ # callback_port = 1455
32
+ # use_pkce = true
33
+ ```
34
+
35
+ ## Related Components
36
+ - `provider.py`: OAuth implementation for Codex credentials
37
+ - `plugin.py`: runtime lifecycle for the auth plugin
38
+ - `config.py`: settings model for OAuth parameters
@@ -0,0 +1,14 @@
1
+ """OAuth Codex plugin for standalone OpenAI Codex OAuth authentication."""
2
+
3
+ from .client import CodexOAuthClient
4
+ from .config import CodexOAuthConfig
5
+ from .provider import CodexOAuthProvider
6
+ from .storage import CodexTokenStorage
7
+
8
+
9
+ __all__ = [
10
+ "CodexOAuthClient",
11
+ "CodexOAuthConfig",
12
+ "CodexOAuthProvider",
13
+ "CodexTokenStorage",
14
+ ]
@@ -0,0 +1,224 @@
1
+ """Codex/OpenAI OAuth client implementation."""
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import Any
5
+
6
+ import httpx
7
+ import jwt
8
+ from pydantic import SecretStr
9
+
10
+ from ccproxy.auth.exceptions import OAuthError
11
+ from ccproxy.auth.oauth.base import BaseOAuthClient
12
+ from ccproxy.auth.storage.base import TokenStorage
13
+ from ccproxy.config.settings import Settings
14
+ from ccproxy.core.logging import get_plugin_logger
15
+
16
+ from .config import CodexOAuthConfig
17
+ from .models import OpenAICredentials, OpenAITokens
18
+
19
+
20
+ logger = get_plugin_logger()
21
+
22
+
23
+ class CodexOAuthClient(BaseOAuthClient[OpenAICredentials]):
24
+ """Codex/OpenAI OAuth implementation for the OAuth Codex plugin."""
25
+
26
+ def __init__(
27
+ self,
28
+ config: CodexOAuthConfig,
29
+ storage: TokenStorage[OpenAICredentials] | None = None,
30
+ http_client: httpx.AsyncClient | None = None,
31
+ hook_manager: Any | None = None,
32
+ settings: Settings | None = None,
33
+ ):
34
+ """Initialize Codex OAuth client.
35
+
36
+ Args:
37
+ config: OAuth configuration
38
+ storage: Token storage backend
39
+ http_client: Optional HTTP client (for request tracing support)
40
+ hook_manager: Optional hook manager for emitting events
41
+ settings: Optional settings for HTTP client configuration
42
+ """
43
+ self.oauth_config = config
44
+
45
+ # Resolve effective redirect URI from config
46
+ redirect_uri = config.get_redirect_uri()
47
+
48
+ # Initialize base class
49
+ super().__init__(
50
+ client_id=config.client_id,
51
+ redirect_uri=redirect_uri,
52
+ base_url=config.base_url,
53
+ scopes=config.scopes,
54
+ storage=storage,
55
+ http_client=http_client,
56
+ hook_manager=hook_manager,
57
+ settings=settings,
58
+ )
59
+
60
+ def _get_auth_endpoint(self) -> str:
61
+ """Get OpenAI OAuth authorization endpoint.
62
+
63
+ Returns:
64
+ Full authorization endpoint URL
65
+ """
66
+ return self.oauth_config.authorize_url
67
+
68
+ def _get_token_endpoint(self) -> str:
69
+ """Get OpenAI OAuth token exchange endpoint.
70
+
71
+ Returns:
72
+ Full token endpoint URL
73
+ """
74
+ return self.oauth_config.token_url
75
+
76
+ def get_custom_auth_params(self) -> dict[str, str]:
77
+ """Get OpenAI-specific authorization parameters.
78
+
79
+ Returns:
80
+ Dictionary of custom parameters
81
+ """
82
+ # OpenAI does not use the audience parameter in authorization requests
83
+ return {}
84
+
85
+ def get_custom_headers(self) -> dict[str, str]:
86
+ """Get OpenAI-specific HTTP headers.
87
+
88
+ Returns:
89
+ Dictionary of custom headers
90
+ """
91
+ return {
92
+ "User-Agent": self.oauth_config.user_agent,
93
+ }
94
+
95
+ async def parse_token_response(self, data: dict[str, Any]) -> OpenAICredentials:
96
+ """Parse OpenAI-specific token response.
97
+
98
+ Args:
99
+ data: Raw token response from OpenAI
100
+
101
+ Returns:
102
+ OpenAI credentials object
103
+
104
+ Raises:
105
+ OAuthError: If response parsing fails
106
+ """
107
+ try:
108
+ # Extract tokens
109
+ access_token: str = data["access_token"]
110
+ refresh_token: str = data.get("refresh_token", "")
111
+ id_token: str = data.get("id_token", "")
112
+
113
+ # Build credentials in the current nested schema; legacy inputs are also accepted
114
+ # by the model's validator if needed.
115
+ tokens = OpenAITokens(
116
+ id_token=SecretStr(id_token),
117
+ access_token=SecretStr(access_token),
118
+ refresh_token=SecretStr(refresh_token or ""),
119
+ account_id="",
120
+ )
121
+ credentials = OpenAICredentials(
122
+ OPENAI_API_KEY=None,
123
+ tokens=tokens,
124
+ last_refresh=datetime.now(UTC).replace(microsecond=0).isoformat(),
125
+ active=True,
126
+ )
127
+
128
+ # Try to extract account_id from JWT claims (id_token preferred)
129
+ try:
130
+ token_to_decode = id_token or access_token
131
+ decoded = jwt.decode(
132
+ token_to_decode, options={"verify_signature": False}
133
+ )
134
+ account_id = (
135
+ decoded.get("sub")
136
+ or decoded.get("account_id")
137
+ or decoded.get("org_id")
138
+ or ""
139
+ )
140
+ # Pydantic model has properties mapping; update underlying field
141
+ credentials.tokens.account_id = str(account_id)
142
+ logger.debug(
143
+ "codex_oauth_id_token_decoded",
144
+ sub=decoded.get("sub"),
145
+ email=decoded.get("email"),
146
+ category="auth",
147
+ )
148
+ except Exception as e:
149
+ logger.warning(
150
+ "codex_oauth_id_token_decode_error",
151
+ error=str(e),
152
+ exc_info=e,
153
+ category="auth",
154
+ )
155
+
156
+ logger.info(
157
+ "codex_oauth_credentials_parsed",
158
+ has_refresh_token=bool(refresh_token),
159
+ has_id_token=bool(id_token),
160
+ account_id=credentials.account_id,
161
+ category="auth",
162
+ )
163
+
164
+ return credentials
165
+
166
+ except KeyError as e:
167
+ logger.error(
168
+ "codex_oauth_token_response_missing_field",
169
+ missing_field=str(e),
170
+ response_keys=list(data.keys()),
171
+ category="auth",
172
+ )
173
+ raise OAuthError(f"Missing required field in token response: {e}") from e
174
+ except Exception as e:
175
+ logger.error(
176
+ "codex_oauth_token_response_parse_error",
177
+ error=str(e),
178
+ error_type=type(e).__name__,
179
+ category="auth",
180
+ )
181
+ raise OAuthError(f"Failed to parse OpenAI token response: {e}") from e
182
+
183
+ async def refresh_token(self, refresh_token: str) -> OpenAICredentials:
184
+ """Refresh OpenAI access token.
185
+
186
+ Args:
187
+ refresh_token: Refresh token
188
+
189
+ Returns:
190
+ New OpenAI credentials
191
+
192
+ Raises:
193
+ OAuthError: If refresh fails
194
+ """
195
+ token_endpoint = self._get_token_endpoint()
196
+ data = {
197
+ "grant_type": "refresh_token",
198
+ "refresh_token": refresh_token,
199
+ "client_id": self.client_id,
200
+ "scope": "openid profile email offline_access",
201
+ }
202
+ headers = self.get_custom_headers()
203
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
204
+
205
+ try:
206
+ response = await self.http_client.post(
207
+ token_endpoint,
208
+ data=data, # OpenAI uses form encoding
209
+ headers=headers,
210
+ timeout=30.0,
211
+ )
212
+ response.raise_for_status()
213
+
214
+ token_response = response.json()
215
+ return await self.parse_token_response(token_response)
216
+
217
+ except Exception as e:
218
+ logger.error(
219
+ "codex_oauth_token_refresh_failed",
220
+ error=str(e),
221
+ exc_info=False,
222
+ category="auth",
223
+ )
224
+ raise OAuthError(f"Failed to refresh OpenAI token: {e}") from e
@@ -0,0 +1,95 @@
1
+ """OpenAI Codex-specific configuration settings."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class CodexOAuthConfig(BaseModel):
7
+ """OAuth-specific configuration for OpenAI Codex."""
8
+
9
+ enabled: bool = Field(
10
+ default=True,
11
+ description="Enable the plugin",
12
+ )
13
+
14
+ # Core OAuth endpoints and identifiers (aligns with Claude config structure)
15
+ base_url: str = Field(
16
+ default="https://auth.openai.com",
17
+ description="Base URL for OAuth API endpoints",
18
+ )
19
+ token_url: str = Field(
20
+ default="https://auth.openai.com/oauth/token",
21
+ description="OAuth token endpoint URL",
22
+ )
23
+ authorize_url: str = Field(
24
+ default="https://auth.openai.com/oauth/authorize",
25
+ description="OAuth authorization endpoint URL",
26
+ )
27
+ profile_url: str = Field(
28
+ default="https://api.openai.com/oauth/profile",
29
+ description="OAuth profile endpoint URL",
30
+ )
31
+ client_id: str = Field(
32
+ default="app_EMoamEEZ73f0CkXaXp7hrann",
33
+ description="OpenAI OAuth client ID",
34
+ )
35
+ redirect_uri: str | None = Field(
36
+ default=None,
37
+ description="OAuth redirect URI (auto-generated from callback_port if not set)",
38
+ )
39
+ scopes: list[str] = Field(
40
+ default_factory=lambda: [
41
+ "openid",
42
+ "profile",
43
+ "email",
44
+ "offline_access",
45
+ ],
46
+ description="OAuth scopes to request",
47
+ )
48
+
49
+ # Additional request configuration (mirrors Claude config shape)
50
+ headers: dict[str, str] = Field(
51
+ default_factory=lambda: {
52
+ "User-Agent": "Codex-Code/1.0.43", # Match default user agent in config
53
+ },
54
+ description="Additional headers for OAuth requests",
55
+ )
56
+ # Optional audience parameter for auth requests (OpenAI specific)
57
+ audience: str = Field(
58
+ default="https://api.openai.com/v1",
59
+ description="OAuth audience parameter for OpenAI",
60
+ )
61
+ # Convenience user agent string (mirrors headers[\"User-Agent\"]) for typed access
62
+ user_agent: str = Field(
63
+ default="Codex-Code/1.0.43",
64
+ description="User-Agent header value for OAuth requests",
65
+ )
66
+ request_timeout: int = Field(
67
+ default=30,
68
+ description="Timeout in seconds for OAuth requests",
69
+ )
70
+ callback_timeout: int = Field(
71
+ default=300,
72
+ description="Timeout in seconds for OAuth callback",
73
+ ge=60,
74
+ le=600,
75
+ )
76
+ callback_port: int = Field(
77
+ default=1455,
78
+ description="Port for OAuth callback server",
79
+ ge=1024,
80
+ le=65535,
81
+ )
82
+
83
+ def get_redirect_uri(self) -> str:
84
+ """Return redirect URI, auto-generated from callback_port when unset.
85
+
86
+ Uses the standard plugin callback path: `/auth/callback`.
87
+ """
88
+ if self.redirect_uri:
89
+ return self.redirect_uri
90
+ return f"http://localhost:{self.callback_port}/auth/callback"
91
+
92
+ use_pkce: bool = Field(
93
+ default=True,
94
+ description="Whether to use PKCE flow (OpenAI requires it)",
95
+ )