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,269 +0,0 @@
1
- """Service for automatically detecting Claude CLI headers at startup."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import json
7
- import os
8
- import socket
9
- import subprocess
10
- from typing import Any
11
-
12
- import structlog
13
- from fastapi import FastAPI, Request, Response
14
-
15
- from ccproxy.config.discovery import get_ccproxy_cache_dir
16
- from ccproxy.config.settings import Settings
17
- from ccproxy.models.detection import (
18
- ClaudeCacheData,
19
- ClaudeCodeHeaders,
20
- SystemPromptData,
21
- )
22
-
23
-
24
- logger = structlog.get_logger(__name__)
25
-
26
-
27
- class ClaudeDetectionService:
28
- """Service for automatically detecting Claude CLI headers at startup."""
29
-
30
- def __init__(self, settings: Settings) -> None:
31
- """Initialize Claude detection service."""
32
- self.settings = settings
33
- self.cache_dir = get_ccproxy_cache_dir()
34
- self.cache_dir.mkdir(parents=True, exist_ok=True)
35
- self._cached_data: ClaudeCacheData | None = None
36
-
37
- async def initialize_detection(self) -> ClaudeCacheData:
38
- """Initialize Claude detection at startup."""
39
- try:
40
- # Get current Claude version
41
- current_version = await self._get_claude_version()
42
-
43
- # Try to load from cache first
44
- detected_data = self._load_from_cache(current_version)
45
- cached = detected_data is not None
46
- if cached:
47
- logger.debug("detection_claude_headers_debug", version=current_version)
48
- else:
49
- # No cache or version changed - detect fresh
50
- detected_data = await self._detect_claude_headers(current_version)
51
- # Cache the results
52
- self._save_to_cache(detected_data)
53
-
54
- self._cached_data = detected_data
55
-
56
- logger.info(
57
- "detection_claude_headers_completed",
58
- version=current_version,
59
- cached=cached,
60
- )
61
-
62
- # TODO: add proper testing without claude cli installed
63
- if detected_data is None:
64
- raise ValueError("Claude detection failed")
65
- return detected_data
66
-
67
- except Exception as e:
68
- logger.warning("detection_claude_headers_failed", fallback=True, error=e)
69
- # Return fallback data
70
- fallback_data = self._get_fallback_data()
71
- self._cached_data = fallback_data
72
- return fallback_data
73
-
74
- def get_cached_data(self) -> ClaudeCacheData | None:
75
- """Get currently cached detection data."""
76
- return self._cached_data
77
-
78
- async def _get_claude_version(self) -> str:
79
- """Get Claude CLI version."""
80
- try:
81
- result = subprocess.run(
82
- ["claude", "--version"],
83
- capture_output=True,
84
- text=True,
85
- timeout=10,
86
- )
87
- if result.returncode == 0:
88
- # Extract version from output like "1.0.60 (Claude Code)"
89
- version_line = result.stdout.strip()
90
- if "/" in version_line:
91
- # Handle "claude-cli/1.0.60" format
92
- version_line = version_line.split("/")[-1]
93
- if "(" in version_line:
94
- # Handle "1.0.60 (Claude Code)" format - extract just the version number
95
- return version_line.split("(")[0].strip()
96
- return version_line
97
- else:
98
- raise RuntimeError(f"Claude version command failed: {result.stderr}")
99
-
100
- except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
101
- logger.warning("claude_version_detection_failed", error=str(e))
102
- return "unknown"
103
-
104
- async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
105
- """Execute Claude CLI with proxy to capture headers and system prompt."""
106
- # Data captured from the request
107
- captured_data: dict[str, Any] = {}
108
-
109
- async def capture_handler(request: Request) -> Response:
110
- """Capture the Claude CLI request."""
111
- captured_data["headers"] = dict(request.headers)
112
- captured_data["body"] = await request.body()
113
- # Return a mock response to satisfy Claude CLI
114
- return Response(
115
- content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
116
- media_type="application/json",
117
- status_code=200,
118
- )
119
-
120
- # Create temporary FastAPI app
121
- temp_app = FastAPI()
122
- temp_app.post("/v1/messages")(capture_handler)
123
-
124
- # Find available port
125
- sock = socket.socket()
126
- sock.bind(("", 0))
127
- port = sock.getsockname()[1]
128
- sock.close()
129
-
130
- # Start server in background
131
- from uvicorn import Config, Server
132
-
133
- config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
134
- server = Server(config)
135
-
136
- server_task = asyncio.create_task(server.serve())
137
-
138
- try:
139
- # Wait for server to start
140
- await asyncio.sleep(0.5)
141
-
142
- # Execute Claude CLI with proxy
143
- env = {**dict(os.environ), "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}"}
144
-
145
- process = await asyncio.create_subprocess_exec(
146
- "claude",
147
- "test",
148
- env=env,
149
- stdout=asyncio.subprocess.PIPE,
150
- stderr=asyncio.subprocess.PIPE,
151
- )
152
-
153
- # Wait for process with timeout
154
- try:
155
- await asyncio.wait_for(process.wait(), timeout=30)
156
- except TimeoutError:
157
- process.kill()
158
- await process.wait()
159
-
160
- # Stop server
161
- server.should_exit = True
162
- await server_task
163
-
164
- if not captured_data:
165
- raise RuntimeError("Failed to capture Claude CLI request")
166
-
167
- # Extract headers and system prompt
168
- headers = self._extract_headers(captured_data["headers"])
169
- system_prompt = self._extract_system_prompt(captured_data["body"])
170
-
171
- return ClaudeCacheData(
172
- claude_version=version, headers=headers, system_prompt=system_prompt
173
- )
174
-
175
- except Exception as e:
176
- # Ensure server is stopped
177
- server.should_exit = True
178
- if not server_task.done():
179
- await server_task
180
- raise
181
-
182
- def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
183
- """Load cached data for specific Claude version."""
184
- cache_file = self.cache_dir / f"claude_headers_{version}.json"
185
-
186
- if not cache_file.exists():
187
- return None
188
-
189
- try:
190
- with cache_file.open("r") as f:
191
- data = json.load(f)
192
- return ClaudeCacheData.model_validate(data)
193
- except Exception:
194
- return None
195
-
196
- def _save_to_cache(self, data: ClaudeCacheData) -> None:
197
- """Save detection data to cache."""
198
- cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
199
-
200
- try:
201
- with cache_file.open("w") as f:
202
- json.dump(data.model_dump(), f, indent=2, default=str)
203
- logger.debug(
204
- "cache_saved", file=str(cache_file), version=data.claude_version
205
- )
206
- except Exception as e:
207
- logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
208
-
209
- def _extract_headers(self, headers: dict[str, str]) -> ClaudeCodeHeaders:
210
- """Extract Claude CLI headers from captured request."""
211
- try:
212
- return ClaudeCodeHeaders.model_validate(headers)
213
- except Exception as e:
214
- logger.error("header_extraction_failed", error=str(e))
215
- raise ValueError(f"Failed to extract required headers: {e}") from e
216
-
217
- def _extract_system_prompt(self, body: bytes) -> SystemPromptData:
218
- """Extract system prompt from captured request body."""
219
- try:
220
- data = json.loads(body.decode("utf-8"))
221
- system_content = data.get("system")
222
-
223
- if system_content is None:
224
- raise ValueError("No system field found in request body")
225
-
226
- return SystemPromptData(system_field=system_content)
227
-
228
- except Exception as e:
229
- logger.error("system_prompt_extraction_failed", error=str(e))
230
- raise ValueError(f"Failed to extract system prompt: {e}") from e
231
-
232
- def _get_fallback_data(self) -> ClaudeCacheData:
233
- """Get fallback data when detection fails."""
234
- logger.warning("using_fallback_claude_data")
235
-
236
- # Use existing hardcoded values as fallback
237
- fallback_headers = ClaudeCodeHeaders(
238
- **{
239
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
240
- "anthropic-version": "2023-06-01",
241
- "anthropic-dangerous-direct-browser-access": "true",
242
- "x-app": "cli",
243
- "User-Agent": "claude-cli/1.0.60 (external, cli)",
244
- "X-Stainless-Lang": "js",
245
- "X-Stainless-Retry-Count": "0",
246
- "X-Stainless-Timeout": "60",
247
- "X-Stainless-Package-Version": "0.55.1",
248
- "X-Stainless-OS": "Linux",
249
- "X-Stainless-Arch": "x64",
250
- "X-Stainless-Runtime": "node",
251
- "X-Stainless-Runtime-Version": "v24.3.0",
252
- }
253
- )
254
-
255
- fallback_prompt = SystemPromptData(
256
- system_field=[
257
- {
258
- "type": "text",
259
- "text": "You are Claude Code, Anthropic's official CLI for Claude.",
260
- "cache_control": {"type": "ephemeral"},
261
- }
262
- ]
263
- )
264
-
265
- return ClaudeCacheData(
266
- claude_version="fallback",
267
- headers=fallback_headers,
268
- system_prompt=fallback_prompt,
269
- )
@@ -1,263 +0,0 @@
1
- """Service for automatically detecting Codex CLI headers at startup."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import json
7
- import os
8
- import socket
9
- import subprocess
10
- from typing import Any
11
-
12
- import structlog
13
- from fastapi import FastAPI, Request, Response
14
-
15
- from ccproxy.config.discovery import get_ccproxy_cache_dir
16
- from ccproxy.config.settings import Settings
17
- from ccproxy.models.detection import (
18
- CodexCacheData,
19
- CodexHeaders,
20
- CodexInstructionsData,
21
- )
22
-
23
-
24
- logger = structlog.get_logger(__name__)
25
-
26
-
27
- class CodexDetectionService:
28
- """Service for automatically detecting Codex CLI headers at startup."""
29
-
30
- def __init__(self, settings: Settings) -> None:
31
- """Initialize Codex detection service."""
32
- self.settings = settings
33
- self.cache_dir = get_ccproxy_cache_dir()
34
- self.cache_dir.mkdir(parents=True, exist_ok=True)
35
- self._cached_data: CodexCacheData | None = None
36
-
37
- async def initialize_detection(self) -> CodexCacheData:
38
- """Initialize Codex detection at startup."""
39
- try:
40
- # Get current Codex version
41
- current_version = await self._get_codex_version()
42
-
43
- # Try to load from cache first
44
- detected_data = self._load_from_cache(current_version)
45
- cached = detected_data is not None
46
- if cached:
47
- logger.debug("detection_codex_headers_debug", version=current_version)
48
- else:
49
- # No cache or version changed - detect fresh
50
- detected_data = await self._detect_codex_headers(current_version)
51
- # Cache the results
52
- self._save_to_cache(detected_data)
53
-
54
- self._cached_data = detected_data
55
-
56
- logger.info(
57
- "detection_codex_headers_completed",
58
- version=current_version,
59
- cached=cached,
60
- )
61
-
62
- # TODO: add proper testing without codex cli installed
63
- if detected_data is None:
64
- raise ValueError("Codex detection failed")
65
- return detected_data
66
-
67
- except Exception as e:
68
- logger.warning("detection_codex_headers_failed", fallback=True, error=e)
69
- # Return fallback data
70
- fallback_data = self._get_fallback_data()
71
- self._cached_data = fallback_data
72
- return fallback_data
73
-
74
- def get_cached_data(self) -> CodexCacheData | None:
75
- """Get currently cached detection data."""
76
- return self._cached_data
77
-
78
- async def _get_codex_version(self) -> str:
79
- """Get Codex CLI version."""
80
- try:
81
- result = subprocess.run(
82
- ["codex", "--version"],
83
- capture_output=True,
84
- text=True,
85
- timeout=10,
86
- )
87
- if result.returncode == 0:
88
- # Extract version from output like "codex 0.21.0"
89
- version_line = result.stdout.strip()
90
- if " " in version_line:
91
- # Handle "codex 0.21.0" format - extract just the version number
92
- return version_line.split()[-1]
93
- return version_line
94
- else:
95
- raise RuntimeError(f"Codex version command failed: {result.stderr}")
96
-
97
- except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
98
- logger.warning("codex_version_detection_failed", error=str(e))
99
- return "unknown"
100
-
101
- async def _detect_codex_headers(self, version: str) -> CodexCacheData:
102
- """Execute Codex CLI with proxy to capture headers and instructions."""
103
- # Data captured from the request
104
- captured_data: dict[str, Any] = {}
105
-
106
- async def capture_handler(request: Request) -> Response:
107
- """Capture the Codex CLI request."""
108
- captured_data["headers"] = dict(request.headers)
109
- captured_data["body"] = await request.body()
110
- # Return a mock response to satisfy Codex CLI
111
- return Response(
112
- content='{"choices": [{"message": {"content": "Test response"}}]}',
113
- media_type="application/json",
114
- status_code=200,
115
- )
116
-
117
- # Create temporary FastAPI app
118
- temp_app = FastAPI()
119
- temp_app.post("/backend-api/codex/responses")(capture_handler)
120
-
121
- # Find available port
122
- sock = socket.socket()
123
- sock.bind(("", 0))
124
- port = sock.getsockname()[1]
125
- sock.close()
126
-
127
- # Start server in background
128
- from uvicorn import Config, Server
129
-
130
- config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
131
- server = Server(config)
132
-
133
- logger.debug("start")
134
- server_task = asyncio.create_task(server.serve())
135
-
136
- try:
137
- # Wait for server to start
138
- await asyncio.sleep(0.5)
139
-
140
- # Execute Codex CLI with proxy
141
- env = {
142
- **dict(os.environ),
143
- "OPENAI_BASE_URL": f"http://127.0.0.1:{port}/backend-api/codex",
144
- }
145
-
146
- process = await asyncio.create_subprocess_exec(
147
- "codex",
148
- "exec",
149
- "test",
150
- env=env,
151
- stdout=asyncio.subprocess.PIPE,
152
- stderr=asyncio.subprocess.PIPE,
153
- )
154
- # stderr = ""
155
- # if process.stderr:
156
- # stderr = await process.stderr.read(128)
157
- # stdout = ""
158
- # if process.stdout:
159
- # stdout = await process.stdout.read(128)
160
- # logger.warning("rcecdy", stderr=stderr, stdout=stdout)
161
-
162
- # Wait for process with timeout
163
- try:
164
- await asyncio.wait_for(process.wait(), timeout=300)
165
- except TimeoutError:
166
- process.kill()
167
- await process.wait()
168
-
169
- # Stop server
170
- server.should_exit = True
171
- await server_task
172
-
173
- if not captured_data:
174
- raise RuntimeError("Failed to capture Codex CLI request")
175
-
176
- # Extract headers and instructions
177
- headers = self._extract_headers(captured_data["headers"])
178
- instructions = self._extract_instructions(captured_data["body"])
179
-
180
- return CodexCacheData(
181
- codex_version=version, headers=headers, instructions=instructions
182
- )
183
-
184
- except Exception as e:
185
- # Ensure server is stopped
186
- server.should_exit = True
187
- if not server_task.done():
188
- await server_task
189
- raise
190
-
191
- def _load_from_cache(self, version: str) -> CodexCacheData | None:
192
- """Load cached data for specific Codex version."""
193
- cache_file = self.cache_dir / f"codex_headers_{version}.json"
194
-
195
- if not cache_file.exists():
196
- return None
197
-
198
- try:
199
- with cache_file.open("r") as f:
200
- data = json.load(f)
201
- return CodexCacheData.model_validate(data)
202
- except Exception:
203
- return None
204
-
205
- def _save_to_cache(self, data: CodexCacheData) -> None:
206
- """Save detection data to cache."""
207
- cache_file = self.cache_dir / f"codex_headers_{data.codex_version}.json"
208
-
209
- try:
210
- with cache_file.open("w") as f:
211
- json.dump(data.model_dump(), f, indent=2, default=str)
212
- logger.debug(
213
- "cache_saved", file=str(cache_file), version=data.codex_version
214
- )
215
- except Exception as e:
216
- logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
217
-
218
- def _extract_headers(self, headers: dict[str, str]) -> CodexHeaders:
219
- """Extract Codex CLI headers from captured request."""
220
- try:
221
- return CodexHeaders.model_validate(headers)
222
- except Exception as e:
223
- logger.error("header_extraction_failed", error=str(e))
224
- raise ValueError(f"Failed to extract required headers: {e}") from e
225
-
226
- def _extract_instructions(self, body: bytes) -> CodexInstructionsData:
227
- """Extract instructions from captured request body."""
228
- try:
229
- data = json.loads(body.decode("utf-8"))
230
- instructions_content = data.get("instructions")
231
-
232
- if instructions_content is None:
233
- raise ValueError("No instructions field found in request body")
234
-
235
- return CodexInstructionsData(instructions_field=instructions_content)
236
-
237
- except Exception as e:
238
- logger.error("instructions_extraction_failed", error=str(e))
239
- raise ValueError(f"Failed to extract instructions: {e}") from e
240
-
241
- def _get_fallback_data(self) -> CodexCacheData:
242
- """Get fallback data when detection fails."""
243
- logger.warning("using_fallback_codex_data")
244
-
245
- # Use hardcoded values as fallback from req.json
246
- fallback_headers = CodexHeaders(
247
- session_id="", # Will be generated per request
248
- originator="codex_cli_rs",
249
- **{"openai-beta": "responses=experimental"},
250
- version="0.21.0",
251
- **{"chatgpt-account-id": ""}, # Will be set from auth
252
- )
253
-
254
- # Use exact instructions from req.json
255
- fallback_instructions = CodexInstructionsData(
256
- instructions_field='You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.\n\nYour capabilities:\n- Receive user prompts and other context provided by the harness, such as files in the workspace.\n- Communicate with the user by streaming thinking & responses, and by making & updating plans.\n- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section.\n\nWithin this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).'
257
- )
258
-
259
- return CodexCacheData(
260
- codex_version="fallback",
261
- headers=fallback_headers,
262
- instructions=fallback_instructions,
263
- )
@@ -1,55 +0,0 @@
1
- """Credentials management package."""
2
-
3
- from ccproxy.auth.exceptions import (
4
- CredentialsError,
5
- CredentialsExpiredError,
6
- CredentialsInvalidError,
7
- CredentialsNotFoundError,
8
- CredentialsStorageError,
9
- OAuthCallbackError,
10
- OAuthError,
11
- OAuthLoginError,
12
- OAuthTokenRefreshError,
13
- )
14
- from ccproxy.auth.models import (
15
- AccountInfo,
16
- ClaudeCredentials,
17
- OAuthToken,
18
- OrganizationInfo,
19
- UserProfile,
20
- )
21
- from ccproxy.auth.storage import JsonFileTokenStorage as JsonFileStorage
22
- from ccproxy.auth.storage import TokenStorage as CredentialsStorageBackend
23
- from ccproxy.services.credentials.config import CredentialsConfig, OAuthConfig
24
- from ccproxy.services.credentials.manager import CredentialsManager
25
- from ccproxy.services.credentials.oauth_client import OAuthClient
26
-
27
-
28
- __all__ = [
29
- # Manager
30
- "CredentialsManager",
31
- # Config
32
- "CredentialsConfig",
33
- "OAuthConfig",
34
- # Models
35
- "ClaudeCredentials",
36
- "OAuthToken",
37
- "OrganizationInfo",
38
- "AccountInfo",
39
- "UserProfile",
40
- # Storage
41
- "CredentialsStorageBackend",
42
- "JsonFileStorage",
43
- # OAuth
44
- "OAuthClient",
45
- # Exceptions
46
- "CredentialsError",
47
- "CredentialsNotFoundError",
48
- "CredentialsInvalidError",
49
- "CredentialsExpiredError",
50
- "CredentialsStorageError",
51
- "OAuthError",
52
- "OAuthLoginError",
53
- "OAuthTokenRefreshError",
54
- "OAuthCallbackError",
55
- ]
@@ -1,105 +0,0 @@
1
- """Configuration for credentials and OAuth."""
2
-
3
- import os
4
-
5
- from pydantic import BaseModel, Field
6
-
7
-
8
- def _get_default_storage_paths() -> list[str]:
9
- """Get default storage paths, with test override support."""
10
- # Allow tests to override credential paths
11
- if os.getenv("CCPROXY_TEST_MODE") == "true":
12
- # Use a test-specific location that won't pollute real credentials
13
- return [
14
- "/tmp/ccproxy-test/.config/claude/.credentials.json",
15
- "/tmp/ccproxy-test/.claude/.credentials.json",
16
- ]
17
-
18
- return [
19
- "~/.config/claude/.credentials.json", # Alternative legacy location
20
- "~/.claude/.credentials.json", # Legacy location
21
- "~/.config/ccproxy/credentials.json", # location in app config
22
- ]
23
-
24
-
25
- class OAuthConfig(BaseModel):
26
- """OAuth configuration settings."""
27
-
28
- base_url: str = Field(
29
- default="https://console.anthropic.com",
30
- description="Base URL for OAuth API endpoints",
31
- )
32
- beta_version: str = Field(
33
- default="oauth-2025-04-20",
34
- description="OAuth beta version header",
35
- )
36
- token_url: str = Field(
37
- default="https://console.anthropic.com/v1/oauth/token",
38
- description="OAuth token endpoint URL",
39
- )
40
- authorize_url: str = Field(
41
- default="https://claude.ai/oauth/authorize",
42
- description="OAuth authorization endpoint URL",
43
- )
44
- profile_url: str = Field(
45
- default="https://api.anthropic.com/api/oauth/profile",
46
- description="OAuth profile endpoint URL",
47
- )
48
- client_id: str = Field(
49
- default="9d1c250a-e61b-44d9-88ed-5944d1962f5e",
50
- description="OAuth client ID",
51
- )
52
- redirect_uri: str = Field(
53
- default="http://localhost:54545/callback",
54
- description="OAuth redirect URI",
55
- )
56
- scopes: list[str] = Field(
57
- default_factory=lambda: [
58
- "org:create_api_key",
59
- "user:profile",
60
- "user:inference",
61
- ],
62
- description="OAuth scopes to request",
63
- )
64
- request_timeout: int = Field(
65
- default=30,
66
- description="Timeout in seconds for OAuth requests",
67
- )
68
- user_agent: str = Field(
69
- default="Claude-Code/1.0.43",
70
- description="User agent string for OAuth requests",
71
- )
72
- callback_timeout: int = Field(
73
- default=300,
74
- description="Timeout in seconds for OAuth callback",
75
- ge=60,
76
- le=600,
77
- )
78
- callback_port: int = Field(
79
- default=54545,
80
- description="Port for OAuth callback server",
81
- ge=1024,
82
- le=65535,
83
- )
84
-
85
-
86
- class CredentialsConfig(BaseModel):
87
- """Configuration for credentials management."""
88
-
89
- storage_paths: list[str] = Field(
90
- default_factory=lambda: _get_default_storage_paths(),
91
- description="Paths to search for credentials files",
92
- )
93
- oauth: OAuthConfig = Field(
94
- default_factory=OAuthConfig,
95
- description="OAuth configuration",
96
- )
97
- auto_refresh: bool = Field(
98
- default=True,
99
- description="Automatically refresh expired tokens",
100
- )
101
- refresh_buffer_seconds: int = Field(
102
- default=300,
103
- description="Refresh token this many seconds before expiry",
104
- ge=0,
105
- )