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
@@ -1,166 +0,0 @@
1
- """OpenAI credentials management for Codex authentication."""
2
-
3
- from datetime import UTC, datetime
4
- from typing import Any
5
-
6
- import jwt
7
- import structlog
8
- from pydantic import BaseModel, Field, field_validator
9
-
10
- from .storage import OpenAITokenStorage
11
-
12
-
13
- logger = structlog.get_logger(__name__)
14
-
15
-
16
- class OpenAICredentials(BaseModel):
17
- """OpenAI authentication credentials model."""
18
-
19
- access_token: str = Field(..., description="OpenAI access token (JWT)")
20
- refresh_token: str = Field(..., description="OpenAI refresh token")
21
- expires_at: datetime = Field(..., description="Token expiration timestamp")
22
- account_id: str = Field(..., description="OpenAI account ID extracted from token")
23
- active: bool = Field(default=True, description="Whether credentials are active")
24
-
25
- @field_validator("expires_at", mode="before")
26
- @classmethod
27
- def parse_expires_at(cls, v: Any) -> datetime:
28
- """Parse expiration timestamp."""
29
- if isinstance(v, datetime):
30
- # Ensure timezone-aware datetime
31
- if v.tzinfo is None:
32
- return v.replace(tzinfo=UTC)
33
- return v
34
-
35
- if isinstance(v, str):
36
- # Handle ISO format strings
37
- try:
38
- dt = datetime.fromisoformat(v.replace("Z", "+00:00"))
39
- if dt.tzinfo is None:
40
- dt = dt.replace(tzinfo=UTC)
41
- return dt
42
- except ValueError as e:
43
- raise ValueError(f"Invalid datetime format: {v}") from e
44
-
45
- if isinstance(v, int | float):
46
- # Handle Unix timestamps
47
- return datetime.fromtimestamp(v, tz=UTC)
48
-
49
- raise ValueError(f"Cannot parse datetime from {type(v)}: {v}")
50
-
51
- @field_validator("account_id", mode="before")
52
- @classmethod
53
- def extract_account_id(cls, v: Any, info: Any) -> str:
54
- """Extract account ID from access token if not provided."""
55
- if isinstance(v, str) and v:
56
- return v
57
-
58
- # Try to extract from access_token
59
- access_token = None
60
- if hasattr(info, "data") and info.data and isinstance(info.data, dict):
61
- access_token = info.data.get("access_token")
62
-
63
- if access_token and isinstance(access_token, str):
64
- try:
65
- # Decode JWT without verification to extract claims
66
- decoded = jwt.decode(access_token, options={"verify_signature": False})
67
- if "org_id" in decoded and isinstance(decoded["org_id"], str):
68
- return decoded["org_id"]
69
- elif "sub" in decoded and isinstance(decoded["sub"], str):
70
- return decoded["sub"]
71
- elif "account_id" in decoded and isinstance(decoded["account_id"], str):
72
- return decoded["account_id"]
73
- except Exception as e:
74
- logger.warning("Failed to extract account_id from token", error=str(e))
75
-
76
- raise ValueError(
77
- "account_id is required and could not be extracted from access_token"
78
- )
79
-
80
- def is_expired(self) -> bool:
81
- """Check if the access token is expired."""
82
- now = datetime.now(UTC)
83
- return now >= self.expires_at
84
-
85
- def expires_in_seconds(self) -> int:
86
- """Get seconds until token expires."""
87
- now = datetime.now(UTC)
88
- delta = self.expires_at - now
89
- return max(0, int(delta.total_seconds()))
90
-
91
- def to_dict(self) -> dict[str, Any]:
92
- """Convert to dictionary for storage."""
93
- return {
94
- "access_token": self.access_token,
95
- "refresh_token": self.refresh_token,
96
- "expires_at": self.expires_at.isoformat(),
97
- "account_id": self.account_id,
98
- "active": self.active,
99
- }
100
-
101
- @classmethod
102
- def from_dict(cls, data: dict[str, Any]) -> "OpenAICredentials":
103
- """Create from dictionary."""
104
- return cls(**data)
105
-
106
-
107
- class OpenAITokenManager:
108
- """Manages OpenAI token storage and refresh operations."""
109
-
110
- def __init__(self, storage: OpenAITokenStorage | None = None):
111
- """Initialize token manager.
112
-
113
- Args:
114
- storage: Token storage backend. If None, uses default TOML file storage.
115
- """
116
- self.storage = storage or OpenAITokenStorage()
117
-
118
- async def load_credentials(self) -> OpenAICredentials | None:
119
- """Load credentials from storage."""
120
- try:
121
- return await self.storage.load()
122
- except Exception as e:
123
- logger.error("Failed to load OpenAI credentials", error=str(e))
124
- return None
125
-
126
- async def save_credentials(self, credentials: OpenAICredentials) -> bool:
127
- """Save credentials to storage."""
128
- try:
129
- return await self.storage.save(credentials)
130
- except Exception as e:
131
- logger.error("Failed to save OpenAI credentials", error=str(e))
132
- return False
133
-
134
- async def delete_credentials(self) -> bool:
135
- """Delete credentials from storage."""
136
- try:
137
- return await self.storage.delete()
138
- except Exception as e:
139
- logger.error("Failed to delete OpenAI credentials", error=str(e))
140
- return False
141
-
142
- async def has_credentials(self) -> bool:
143
- """Check if credentials exist."""
144
- try:
145
- return await self.storage.exists()
146
- except Exception:
147
- return False
148
-
149
- async def get_valid_token(self) -> str | None:
150
- """Get a valid access token, refreshing if necessary."""
151
- credentials = await self.load_credentials()
152
- if not credentials or not credentials.active:
153
- return None
154
-
155
- # If token is not expired, return it
156
- if not credentials.is_expired():
157
- return credentials.access_token
158
-
159
- # TODO: Implement token refresh logic
160
- # For now, return None if expired (user needs to re-authenticate)
161
- logger.warning("OpenAI token expired, refresh not yet implemented")
162
- return None
163
-
164
- def get_storage_location(self) -> str:
165
- """Get storage location description."""
166
- return self.storage.get_location()
@@ -1,334 +0,0 @@
1
- """OpenAI OAuth PKCE client implementation."""
2
-
3
- import asyncio
4
- import base64
5
- import contextlib
6
- import hashlib
7
- import secrets
8
- import urllib.parse
9
- import webbrowser
10
- from datetime import UTC, datetime, timedelta
11
-
12
- import httpx
13
- import structlog
14
- import uvicorn
15
- from fastapi import FastAPI, Request, Response
16
- from fastapi.responses import HTMLResponse
17
-
18
- from ccproxy.config.codex import CodexSettings
19
-
20
- from .credentials import OpenAICredentials, OpenAITokenManager
21
-
22
-
23
- logger = structlog.get_logger(__name__)
24
-
25
-
26
- class OpenAIOAuthClient:
27
- """OpenAI OAuth PKCE flow client."""
28
-
29
- def __init__(
30
- self, settings: CodexSettings, token_manager: OpenAITokenManager | None = None
31
- ):
32
- """Initialize OAuth client.
33
-
34
- Args:
35
- settings: Codex configuration settings
36
- token_manager: Token manager for credential storage
37
- """
38
- self.settings = settings
39
- self.token_manager = token_manager or OpenAITokenManager()
40
- self._server_task: asyncio.Task[None] | None = None
41
- self._auth_complete = asyncio.Event()
42
- self._auth_result: OpenAICredentials | None = None
43
- self._auth_error: str | None = None
44
-
45
- def _generate_pkce_pair(self) -> tuple[str, str]:
46
- """Generate PKCE code verifier and challenge."""
47
- # Generate code verifier (43-128 characters)
48
- code_verifier = (
49
- base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip("=")
50
- )
51
-
52
- # Generate code challenge
53
- code_challenge = (
54
- base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
55
- .decode()
56
- .rstrip("=")
57
- )
58
-
59
- return code_verifier, code_challenge
60
-
61
- def _build_auth_url(self, code_challenge: str, state: str) -> str:
62
- """Build OAuth authorization URL."""
63
- params = {
64
- "response_type": "code",
65
- "client_id": self.settings.oauth.client_id,
66
- "redirect_uri": self.settings.get_redirect_uri(),
67
- "scope": " ".join(self.settings.oauth.scopes),
68
- "state": state,
69
- "code_challenge": code_challenge,
70
- "code_challenge_method": "S256",
71
- }
72
-
73
- query_string = urllib.parse.urlencode(params)
74
- return f"{self.settings.oauth.base_url}/oauth/authorize?{query_string}"
75
-
76
- async def _exchange_code_for_tokens(
77
- self, code: str, code_verifier: str
78
- ) -> OpenAICredentials:
79
- """Exchange authorization code for tokens."""
80
- token_url = f"{self.settings.oauth.base_url}/oauth/token"
81
-
82
- data = {
83
- "grant_type": "authorization_code",
84
- "code": code,
85
- "redirect_uri": self.settings.get_redirect_uri(),
86
- "client_id": self.settings.oauth.client_id,
87
- "code_verifier": code_verifier,
88
- }
89
-
90
- headers = {
91
- "Content-Type": "application/x-www-form-urlencoded",
92
- "Accept": "application/json",
93
- }
94
-
95
- async with httpx.AsyncClient() as client:
96
- try:
97
- response = await client.post(
98
- token_url, data=data, headers=headers, timeout=30.0
99
- )
100
- response.raise_for_status()
101
-
102
- token_data = response.json()
103
-
104
- # Calculate expiration time
105
- expires_in = token_data.get("expires_in", 3600) # Default 1 hour
106
- expires_at = datetime.now(UTC).replace(microsecond=0) + timedelta(
107
- seconds=expires_in
108
- )
109
-
110
- # Create credentials (account_id will be extracted from access_token)
111
- credentials = OpenAICredentials(
112
- access_token=token_data["access_token"],
113
- refresh_token=token_data.get("refresh_token", ""),
114
- expires_at=expires_at,
115
- account_id="", # Will be auto-extracted by validator
116
- active=True,
117
- )
118
-
119
- return credentials
120
-
121
- except httpx.HTTPStatusError as e:
122
- error_detail = "Unknown error"
123
- try:
124
- error_data = e.response.json()
125
- error_detail = error_data.get(
126
- "error_description", error_data.get("error", str(e))
127
- )
128
- except Exception:
129
- error_detail = str(e)
130
-
131
- raise ValueError(f"Token exchange failed: {error_detail}") from e
132
- except Exception as e:
133
- raise ValueError(f"Token exchange request failed: {e}") from e
134
-
135
- def _create_callback_app(self, code_verifier: str, expected_state: str) -> FastAPI:
136
- """Create FastAPI app to handle OAuth callback."""
137
- app = FastAPI(title="OpenAI OAuth Callback")
138
-
139
- @app.get("/auth/callback")
140
- async def oauth_callback(request: Request) -> Response:
141
- """Handle OAuth callback."""
142
- params = dict(request.query_params)
143
-
144
- # Check for error in callback
145
- if "error" in params:
146
- error_desc = params.get("error_description", params["error"])
147
- self._auth_error = f"OAuth error: {error_desc}"
148
- self._auth_complete.set()
149
- return HTMLResponse(
150
- """
151
- <html>
152
- <head><title>Authentication Failed</title></head>
153
- <body>
154
- <h1>Authentication Failed</h1>
155
- <p>Error: """
156
- + error_desc
157
- + """</p>
158
- <p>You can close this window.</p>
159
- <script>setTimeout(() => window.close(), 3000);</script>
160
- </body>
161
- </html>
162
- """,
163
- status_code=400,
164
- )
165
-
166
- # Verify state parameter
167
- received_state = params.get("state")
168
- if received_state != expected_state:
169
- self._auth_error = "Invalid state parameter"
170
- self._auth_complete.set()
171
- return HTMLResponse(
172
- """
173
- <html>
174
- <head><title>Authentication Failed</title></head>
175
- <body>
176
- <h1>Authentication Failed</h1>
177
- <p>Invalid state parameter. Possible CSRF attack.</p>
178
- <p>You can close this window.</p>
179
- <script>setTimeout(() => window.close(), 3000);</script>
180
- </body>
181
- </html>
182
- """,
183
- status_code=400,
184
- )
185
-
186
- # Get authorization code
187
- auth_code = params.get("code")
188
- if not auth_code:
189
- self._auth_error = "No authorization code received"
190
- self._auth_complete.set()
191
- return HTMLResponse(
192
- """
193
- <html>
194
- <head><title>Authentication Failed</title></head>
195
- <body>
196
- <h1>Authentication Failed</h1>
197
- <p>No authorization code received.</p>
198
- <p>You can close this window.</p>
199
- <script>setTimeout(() => window.close(), 3000);</script>
200
- </body>
201
- </html>
202
- """,
203
- status_code=400,
204
- )
205
-
206
- # Exchange code for tokens
207
- try:
208
- credentials = await self._exchange_code_for_tokens(
209
- auth_code, code_verifier
210
- )
211
-
212
- # Save credentials
213
- success = await self.token_manager.save_credentials(credentials)
214
- if not success:
215
- raise ValueError("Failed to save credentials")
216
-
217
- self._auth_result = credentials
218
- self._auth_complete.set()
219
-
220
- return HTMLResponse(
221
- """
222
- <html>
223
- <head><title>Authentication Successful</title></head>
224
- <body>
225
- <h1>Authentication Successful!</h1>
226
- <p>You have successfully authenticated with OpenAI.</p>
227
- <p>You can close this window and return to the terminal.</p>
228
- <script>setTimeout(() => window.close(), 3000);</script>
229
- </body>
230
- </html>
231
- """
232
- )
233
-
234
- except Exception as e:
235
- logger.error("Token exchange failed", error=str(e))
236
- self._auth_error = f"Token exchange failed: {e}"
237
- self._auth_complete.set()
238
- return HTMLResponse(
239
- f"""
240
- <html>
241
- <head><title>Authentication Failed</title></head>
242
- <body>
243
- <h1>Authentication Failed</h1>
244
- <p>Token exchange failed: {e}</p>
245
- <p>You can close this window.</p>
246
- <script>setTimeout(() => window.close(), 3000);</script>
247
- </body>
248
- </html>
249
- """,
250
- status_code=500,
251
- )
252
-
253
- return app
254
-
255
- async def _run_callback_server(self, app: FastAPI) -> None:
256
- """Run callback server."""
257
- config = uvicorn.Config(
258
- app=app,
259
- host="127.0.0.1",
260
- port=self.settings.callback_port,
261
- log_level="warning", # Reduce noise
262
- access_log=False,
263
- )
264
- server = uvicorn.Server(config)
265
- await server.serve()
266
-
267
- async def authenticate(self, open_browser: bool = True) -> OpenAICredentials:
268
- """Perform OAuth PKCE flow.
269
-
270
- Args:
271
- open_browser: Whether to automatically open browser
272
-
273
- Returns:
274
- OpenAI credentials
275
-
276
- Raises:
277
- ValueError: If authentication fails
278
- """
279
- # Reset state
280
- self._auth_complete.clear()
281
- self._auth_result = None
282
- self._auth_error = None
283
-
284
- # Generate PKCE parameters
285
- code_verifier, code_challenge = self._generate_pkce_pair()
286
- state = secrets.token_urlsafe(32)
287
-
288
- # Create callback app
289
- app = self._create_callback_app(code_verifier, state)
290
-
291
- # Start callback server
292
- self._server_task = asyncio.create_task(self._run_callback_server(app))
293
-
294
- # Give server time to start
295
- await asyncio.sleep(1)
296
-
297
- # Build authorization URL
298
- auth_url = self._build_auth_url(code_challenge, state)
299
-
300
- logger.info("Starting OpenAI OAuth flow")
301
- print("\nPlease visit this URL to authenticate with OpenAI:")
302
- print(f"{auth_url}\n")
303
-
304
- if open_browser:
305
- try:
306
- webbrowser.open(auth_url)
307
- print("Opening browser...")
308
- except Exception as e:
309
- logger.warning("Failed to open browser automatically", error=str(e))
310
- print("Please copy and paste the URL above into your browser.")
311
-
312
- print("Waiting for authentication to complete...")
313
-
314
- try:
315
- # Wait for authentication to complete (with timeout)
316
- await asyncio.wait_for(self._auth_complete.wait(), timeout=300) # 5 minutes
317
-
318
- if self._auth_error:
319
- raise ValueError(self._auth_error)
320
-
321
- if not self._auth_result:
322
- raise ValueError("Authentication completed but no credentials received")
323
-
324
- logger.info("OpenAI authentication successful") # type: ignore[unreachable]
325
- return self._auth_result
326
-
327
- except TimeoutError as e:
328
- raise ValueError("Authentication timed out (5 minutes)") from e
329
- finally:
330
- # Clean up server
331
- if self._server_task and not self._server_task.done():
332
- self._server_task.cancel()
333
- with contextlib.suppress(asyncio.CancelledError):
334
- await self._server_task
@@ -1,184 +0,0 @@
1
- """JSON file storage for OpenAI credentials using Codex format."""
2
-
3
- import contextlib
4
- import json
5
- from datetime import UTC, datetime
6
- from pathlib import Path
7
- from typing import TYPE_CHECKING, Any
8
-
9
- import jwt
10
- import structlog
11
-
12
-
13
- if TYPE_CHECKING:
14
- from .credentials import OpenAICredentials
15
-
16
-
17
- logger = structlog.get_logger(__name__)
18
-
19
-
20
- class OpenAITokenStorage:
21
- """JSON file-based storage for OpenAI credentials using Codex format."""
22
-
23
- def __init__(self, file_path: Path | None = None):
24
- """Initialize storage with file path.
25
-
26
- Args:
27
- file_path: Path to JSON file. If None, uses ~/.codex/auth.json
28
- """
29
- self.file_path = file_path or Path.home() / ".codex" / "auth.json"
30
-
31
- async def load(self) -> "OpenAICredentials | None":
32
- """Load credentials from Codex JSON file."""
33
- if not self.file_path.exists():
34
- return None
35
-
36
- try:
37
- with self.file_path.open("r") as f:
38
- data = json.load(f)
39
-
40
- # Extract tokens section
41
- tokens = data.get("tokens", {})
42
- if not tokens:
43
- logger.warning("No tokens section found in Codex auth file")
44
- return None
45
-
46
- # Get required fields
47
- access_token = tokens.get("access_token")
48
- refresh_token = tokens.get("refresh_token")
49
- account_id = tokens.get("account_id")
50
-
51
- if not access_token:
52
- logger.warning("No access_token found in Codex auth file")
53
- return None
54
-
55
- # Extract expiration from JWT token
56
- expires_at = self._extract_expiration_from_token(access_token)
57
- if not expires_at:
58
- logger.warning("Could not extract expiration from access token")
59
- return None
60
-
61
- # Import here to avoid circular import
62
- from .credentials import OpenAICredentials
63
-
64
- # Create credentials object
65
- credentials_data = {
66
- "access_token": access_token,
67
- "refresh_token": refresh_token or "",
68
- "expires_at": expires_at,
69
- "account_id": account_id or "",
70
- "active": True,
71
- }
72
-
73
- return OpenAICredentials.from_dict(credentials_data)
74
-
75
- except Exception as e:
76
- logger.error(
77
- "Failed to load OpenAI credentials from Codex auth file",
78
- file_path=str(self.file_path),
79
- error=str(e),
80
- )
81
- return None
82
-
83
- def _extract_expiration_from_token(self, access_token: str) -> datetime | None:
84
- """Extract expiration time from JWT access token."""
85
- try:
86
- decoded = jwt.decode(access_token, options={"verify_signature": False})
87
- exp_timestamp = decoded.get("exp")
88
- if exp_timestamp:
89
- return datetime.fromtimestamp(exp_timestamp, tz=UTC)
90
- except Exception as e:
91
- logger.warning("Failed to decode JWT token for expiration", error=str(e))
92
- return None
93
-
94
- async def save(self, credentials: "OpenAICredentials") -> bool:
95
- """Save credentials to Codex JSON file."""
96
- try:
97
- # Create directory if it doesn't exist
98
- self.file_path.parent.mkdir(parents=True, exist_ok=True)
99
-
100
- # Load existing file or create new structure
101
- existing_data: dict[str, Any] = {}
102
- if self.file_path.exists():
103
- try:
104
- with self.file_path.open("r") as f:
105
- existing_data = json.load(f)
106
- except Exception:
107
- logger.warning(
108
- "Could not load existing auth file, creating new one"
109
- )
110
-
111
- # Prepare Codex JSON data structure
112
- codex_data = {
113
- "OPENAI_API_KEY": existing_data.get("OPENAI_API_KEY"),
114
- "tokens": {
115
- "id_token": existing_data.get("tokens", {}).get("id_token"),
116
- "access_token": credentials.access_token,
117
- "refresh_token": credentials.refresh_token,
118
- "account_id": credentials.account_id,
119
- },
120
- "last_refresh": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
121
- }
122
-
123
- # Write atomically by writing to temp file then renaming
124
- temp_file = self.file_path.with_suffix(f"{self.file_path.suffix}.tmp")
125
-
126
- with temp_file.open("w") as f:
127
- json.dump(codex_data, f, indent=2)
128
-
129
- # Set restrictive permissions (readable only by owner)
130
- temp_file.chmod(0o600)
131
-
132
- # Atomic rename
133
- temp_file.replace(self.file_path)
134
-
135
- logger.info(
136
- "Saved OpenAI credentials to Codex auth file",
137
- file_path=str(self.file_path),
138
- )
139
- return True
140
-
141
- except Exception as e:
142
- logger.error(
143
- "Failed to save OpenAI credentials to Codex auth file",
144
- file_path=str(self.file_path),
145
- error=str(e),
146
- )
147
- # Clean up temp file if it exists
148
- temp_file = self.file_path.with_suffix(f"{self.file_path.suffix}.tmp")
149
- if temp_file.exists():
150
- with contextlib.suppress(Exception):
151
- temp_file.unlink()
152
- return False
153
-
154
- async def exists(self) -> bool:
155
- """Check if credentials file exists."""
156
- if not self.file_path.exists():
157
- return False
158
-
159
- try:
160
- with self.file_path.open("r") as f:
161
- data = json.load(f)
162
- tokens = data.get("tokens", {})
163
- return bool(tokens.get("access_token"))
164
- except Exception:
165
- return False
166
-
167
- async def delete(self) -> bool:
168
- """Delete credentials file."""
169
- try:
170
- if self.file_path.exists():
171
- self.file_path.unlink()
172
- logger.info("Deleted Codex auth file", file_path=str(self.file_path))
173
- return True
174
- except Exception as e:
175
- logger.error(
176
- "Failed to delete Codex auth file",
177
- file_path=str(self.file_path),
178
- error=str(e),
179
- )
180
- return False
181
-
182
- def get_location(self) -> str:
183
- """Get storage location description."""
184
- return str(self.file_path)