ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -1,70 +0,0 @@
1
- """
2
- SQLModel schema definitions for observability storage.
3
-
4
- This module provides the centralized schema definitions for access logs and metrics
5
- using SQLModel to ensure type safety and eliminate column name repetition.
6
- """
7
-
8
- from datetime import datetime
9
-
10
- from sqlmodel import Field, SQLModel
11
-
12
-
13
- class AccessLog(SQLModel, table=True):
14
- """Access log model for storing request/response data."""
15
-
16
- __tablename__ = "access_logs"
17
-
18
- # Core request identification
19
- request_id: str = Field(primary_key=True)
20
- timestamp: datetime = Field(default_factory=datetime.now, index=True)
21
-
22
- # Request details
23
- method: str
24
- endpoint: str
25
- path: str
26
- query: str = Field(default="")
27
- client_ip: str
28
- user_agent: str
29
-
30
- # Service and model info
31
- service_type: str
32
- model: str
33
- streaming: bool = Field(default=False)
34
-
35
- # Response details
36
- status_code: int
37
- duration_ms: float
38
- duration_seconds: float
39
-
40
- # Token and cost tracking
41
- tokens_input: int = Field(default=0)
42
- tokens_output: int = Field(default=0)
43
- cache_read_tokens: int = Field(default=0)
44
- cache_write_tokens: int = Field(default=0)
45
- cost_usd: float = Field(default=0.0)
46
- cost_sdk_usd: float = Field(default=0.0)
47
- num_turns: int = Field(default=0) # number of conversation turns
48
-
49
- # Session context metadata
50
- session_type: str = Field(default="") # "session_pool" or "direct"
51
- session_status: str = Field(default="") # active, idle, connecting, etc.
52
- session_age_seconds: float = Field(default=0.0) # how long session has been alive
53
- session_message_count: int = Field(default=0) # number of messages in session
54
- session_client_id: str = Field(default="") # unique session client identifier
55
- session_pool_enabled: bool = Field(
56
- default=False
57
- ) # whether session pooling is enabled
58
- session_idle_seconds: float = Field(default=0.0) # how long since last activity
59
- session_error_count: int = Field(default=0) # number of errors in this session
60
- session_is_new: bool = Field(
61
- default=True
62
- ) # whether this is a newly created session
63
-
64
- class Config:
65
- """SQLModel configuration."""
66
-
67
- # Enable automatic conversion from dict
68
- from_attributes = True
69
- # Use enum values
70
- use_enum_values = True
@@ -1,107 +0,0 @@
1
- """FastAPI StreamingResponse with automatic access logging on completion.
2
-
3
- This module provides a reusable StreamingResponseWithLogging class that wraps
4
- any async generator and handles access logging when the stream completes,
5
- eliminating code duplication between different streaming endpoints.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from collections.abc import AsyncGenerator, AsyncIterator
11
- from typing import TYPE_CHECKING, Any
12
-
13
- import structlog
14
- from fastapi.responses import StreamingResponse
15
-
16
- from ccproxy.observability.access_logger import log_request_access
17
-
18
-
19
- if TYPE_CHECKING:
20
- from ccproxy.observability.context import RequestContext
21
- from ccproxy.observability.metrics import PrometheusMetrics
22
-
23
- logger = structlog.get_logger(__name__)
24
-
25
-
26
- class StreamingResponseWithLogging(StreamingResponse):
27
- """FastAPI StreamingResponse that triggers access logging on completion.
28
-
29
- This class wraps a streaming response generator to automatically trigger
30
- access logging when the stream completes (either successfully or with an error).
31
- This eliminates the need for manual access logging in individual stream processors.
32
- """
33
-
34
- def __init__(
35
- self,
36
- content: AsyncGenerator[bytes, None] | AsyncIterator[bytes],
37
- request_context: RequestContext,
38
- metrics: PrometheusMetrics | None = None,
39
- status_code: int = 200,
40
- **kwargs: Any,
41
- ) -> None:
42
- """Initialize streaming response with logging capability.
43
-
44
- Args:
45
- content: The async generator producing streaming content
46
- request_context: The request context for access logging
47
- metrics: Optional PrometheusMetrics instance for recording metrics
48
- status_code: HTTP status code for the response
49
- **kwargs: Additional arguments passed to StreamingResponse
50
- """
51
- # Wrap the content generator to add logging
52
- logged_content = self._wrap_with_logging(
53
- content, request_context, metrics, status_code
54
- )
55
- super().__init__(logged_content, status_code=status_code, **kwargs)
56
-
57
- async def _wrap_with_logging(
58
- self,
59
- content: AsyncGenerator[bytes, None] | AsyncIterator[bytes],
60
- context: RequestContext,
61
- metrics: PrometheusMetrics | None,
62
- status_code: int,
63
- ) -> AsyncGenerator[bytes, None]:
64
- """Wrap content generator with access logging on completion.
65
-
66
- Args:
67
- content: The original content generator
68
- context: Request context for logging
69
- metrics: Optional metrics instance
70
- status_code: HTTP status code
71
-
72
- Yields:
73
- bytes: Content chunks from the original generator
74
- """
75
- try:
76
- # Stream all content from the original generator
77
- async for chunk in content:
78
- yield chunk
79
- except GeneratorExit:
80
- # Client disconnected - log this and re-raise to propagate to underlying generators
81
- logger.info(
82
- "streaming_response_client_disconnected",
83
- request_id=context.request_id,
84
- message="Client disconnected from streaming response, propagating GeneratorExit",
85
- )
86
- # CRITICAL: Re-raise GeneratorExit to propagate disconnect to create_listener()
87
- raise
88
- finally:
89
- # Log access when stream completes (success or error)
90
- try:
91
- # Add streaming completion event type to context
92
- context.add_metadata(event_type="streaming_complete")
93
-
94
- # Check if status_code was updated in context metadata (e.g., due to error)
95
- final_status_code = context.metadata.get("status_code", status_code)
96
-
97
- await log_request_access(
98
- context=context,
99
- status_code=final_status_code,
100
- metrics=metrics,
101
- )
102
- except Exception as e:
103
- logger.warning(
104
- "streaming_access_log_failed",
105
- error=str(e),
106
- request_id=context.request_id,
107
- )
@@ -1,19 +0,0 @@
1
- """Dynamic pricing system for Claude models.
2
-
3
- This module provides dynamic pricing capabilities by downloading and caching
4
- pricing information from external sources like LiteLLM.
5
- """
6
-
7
- from .cache import PricingCache
8
- from .loader import PricingLoader
9
- from .models import ModelPricing, PricingData
10
- from .updater import PricingUpdater
11
-
12
-
13
- __all__ = [
14
- "PricingCache",
15
- "PricingLoader",
16
- "PricingUpdater",
17
- "ModelPricing",
18
- "PricingData",
19
- ]
ccproxy/pricing/loader.py DELETED
@@ -1,251 +0,0 @@
1
- """Pricing data loader and format converter for LiteLLM pricing data."""
2
-
3
- from decimal import Decimal
4
- from typing import Any
5
-
6
- from pydantic import ValidationError
7
- from structlog import get_logger
8
-
9
- from ccproxy.utils.model_mapping import get_claude_aliases_mapping, map_model_to_claude
10
-
11
- from .models import PricingData
12
-
13
-
14
- logger = get_logger(__name__)
15
-
16
-
17
- class PricingLoader:
18
- """Loads and converts pricing data from LiteLLM format to internal format."""
19
-
20
- @staticmethod
21
- def extract_claude_models(
22
- litellm_data: dict[str, Any], verbose: bool = True
23
- ) -> dict[str, Any]:
24
- """Extract Claude model entries from LiteLLM data.
25
-
26
- Args:
27
- litellm_data: Raw LiteLLM pricing data
28
- verbose: Whether to log individual model discoveries
29
-
30
- Returns:
31
- Dictionary with only Claude models
32
- """
33
- claude_models = {}
34
-
35
- for model_name, model_data in litellm_data.items():
36
- # Check if this is a Claude model
37
- if (
38
- isinstance(model_data, dict)
39
- and model_data.get("litellm_provider") == "anthropic"
40
- and "claude" in model_name.lower()
41
- ):
42
- claude_models[model_name] = model_data
43
- if verbose:
44
- logger.debug("claude_model_found", model_name=model_name)
45
-
46
- if verbose:
47
- logger.info(
48
- "claude_models_extracted",
49
- model_count=len(claude_models),
50
- source="LiteLLM",
51
- )
52
- return claude_models
53
-
54
- @staticmethod
55
- def convert_to_internal_format(
56
- claude_models: dict[str, Any], verbose: bool = True
57
- ) -> dict[str, dict[str, Decimal]]:
58
- """Convert LiteLLM pricing format to internal format.
59
-
60
- LiteLLM format uses cost per token, we use cost per 1M tokens as Decimal.
61
-
62
- Args:
63
- claude_models: Claude models in LiteLLM format
64
- verbose: Whether to log individual model conversions
65
-
66
- Returns:
67
- Dictionary in internal pricing format
68
- """
69
- internal_format = {}
70
-
71
- for model_name, model_data in claude_models.items():
72
- try:
73
- # Extract pricing fields
74
- input_cost_per_token = model_data.get("input_cost_per_token")
75
- output_cost_per_token = model_data.get("output_cost_per_token")
76
- cache_creation_cost = model_data.get("cache_creation_input_token_cost")
77
- cache_read_cost = model_data.get("cache_read_input_token_cost")
78
-
79
- # Skip models without pricing info
80
- if input_cost_per_token is None or output_cost_per_token is None:
81
- if verbose:
82
- logger.warning("model_pricing_missing", model_name=model_name)
83
- continue
84
-
85
- # Convert to per-1M-token pricing (multiply by 1,000,000)
86
- pricing = {
87
- "input": Decimal(str(input_cost_per_token * 1_000_000)),
88
- "output": Decimal(str(output_cost_per_token * 1_000_000)),
89
- }
90
-
91
- # Add cache pricing if available
92
- if cache_creation_cost is not None:
93
- pricing["cache_write"] = Decimal(
94
- str(cache_creation_cost * 1_000_000)
95
- )
96
-
97
- if cache_read_cost is not None:
98
- pricing["cache_read"] = Decimal(str(cache_read_cost * 1_000_000))
99
-
100
- # Map to canonical model name if needed
101
- canonical_name = map_model_to_claude(model_name)
102
- internal_format[canonical_name] = pricing
103
-
104
- if verbose:
105
- logger.debug(
106
- "model_pricing_converted",
107
- original_name=model_name,
108
- canonical_name=canonical_name,
109
- input_cost=str(pricing["input"]),
110
- output_cost=str(pricing["output"]),
111
- )
112
-
113
- except (ValueError, TypeError) as e:
114
- if verbose:
115
- logger.error(
116
- "pricing_conversion_failed", model_name=model_name, error=str(e)
117
- )
118
- continue
119
-
120
- if verbose:
121
- logger.info("models_converted", model_count=len(internal_format))
122
- return internal_format
123
-
124
- @staticmethod
125
- def load_pricing_from_data(
126
- litellm_data: dict[str, Any], verbose: bool = True
127
- ) -> PricingData | None:
128
- """Load and convert pricing data from LiteLLM format.
129
-
130
- Args:
131
- litellm_data: Raw LiteLLM pricing data
132
- verbose: Whether to enable verbose logging
133
-
134
- Returns:
135
- Validated pricing data as PricingData model, or None if invalid
136
- """
137
- try:
138
- # Extract Claude models
139
- claude_models = PricingLoader.extract_claude_models(
140
- litellm_data, verbose=verbose
141
- )
142
-
143
- if not claude_models:
144
- if verbose:
145
- logger.warning("claude_models_not_found", source="LiteLLM")
146
- return None
147
-
148
- # Convert to internal format
149
- internal_pricing = PricingLoader.convert_to_internal_format(
150
- claude_models, verbose=verbose
151
- )
152
-
153
- if not internal_pricing:
154
- if verbose:
155
- logger.warning("pricing_data_invalid")
156
- return None
157
-
158
- # Validate and create PricingData model
159
- pricing_data = PricingData.from_dict(internal_pricing)
160
-
161
- if verbose:
162
- logger.info("pricing_data_loaded", model_count=len(pricing_data))
163
-
164
- return pricing_data
165
-
166
- except ValidationError as e:
167
- if verbose:
168
- logger.error("pricing_validation_failed", error=str(e))
169
- return None
170
- except Exception as e:
171
- if verbose:
172
- logger.error("pricing_load_failed", source="LiteLLM", error=str(e))
173
- return None
174
-
175
- @staticmethod
176
- def validate_pricing_data(
177
- pricing_data: Any, verbose: bool = True
178
- ) -> PricingData | None:
179
- """Validate pricing data using Pydantic models.
180
-
181
- Args:
182
- pricing_data: Pricing data to validate (dict or PricingData)
183
- verbose: Whether to enable verbose logging
184
-
185
- Returns:
186
- Valid PricingData model or None if validation fails
187
- """
188
- try:
189
- # If already a PricingData instance, return it
190
- if isinstance(pricing_data, PricingData):
191
- if verbose:
192
- logger.debug(
193
- "pricing_already_validated", model_count=len(pricing_data)
194
- )
195
- return pricing_data
196
-
197
- # If it's a dict, try to create PricingData from it
198
- if isinstance(pricing_data, dict):
199
- if not pricing_data:
200
- if verbose:
201
- logger.warning("pricing_data_empty")
202
- return None
203
-
204
- # Try to create PricingData model
205
- validated_data = PricingData.from_dict(pricing_data)
206
-
207
- if verbose:
208
- logger.debug(
209
- "pricing_data_validated", model_count=len(validated_data)
210
- )
211
-
212
- return validated_data
213
-
214
- # Invalid type
215
- if verbose:
216
- logger.error(
217
- "pricing_data_invalid_type",
218
- actual_type=type(pricing_data).__name__,
219
- expected_types=["dict", "PricingData"],
220
- )
221
- return None
222
-
223
- except ValidationError as e:
224
- if verbose:
225
- logger.error("pricing_validation_failed", error=str(e))
226
- return None
227
- except Exception as e:
228
- if verbose:
229
- logger.error("pricing_validation_unexpected_error", error=str(e))
230
- return None
231
-
232
- @staticmethod
233
- def get_model_aliases() -> dict[str, str]:
234
- """Get mapping of model aliases to canonical names.
235
-
236
- Returns:
237
- Dictionary mapping aliases to canonical model names
238
- """
239
- return get_claude_aliases_mapping()
240
-
241
- @staticmethod
242
- def get_canonical_model_name(model_name: str) -> str:
243
- """Get canonical model name for a given model name.
244
-
245
- Args:
246
- model_name: Model name (possibly an alias)
247
-
248
- Returns:
249
- Canonical model name
250
- """
251
- return map_model_to_claude(model_name)
@@ -1,243 +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 pathlib import Path
11
- from typing import Any
12
-
13
- import structlog
14
- from fastapi import FastAPI, Request, Response
15
-
16
- from ccproxy.config.discovery import get_ccproxy_cache_dir
17
- from ccproxy.config.settings import Settings
18
- from ccproxy.models.detection import (
19
- ClaudeCacheData,
20
- ClaudeCodeHeaders,
21
- SystemPromptData,
22
- )
23
-
24
-
25
- logger = structlog.get_logger(__name__)
26
-
27
-
28
- class ClaudeDetectionService:
29
- """Service for automatically detecting Claude CLI headers at startup."""
30
-
31
- def __init__(self, settings: Settings) -> None:
32
- """Initialize Claude detection service."""
33
- self.settings = settings
34
- self.cache_dir = get_ccproxy_cache_dir()
35
- self.cache_dir.mkdir(parents=True, exist_ok=True)
36
- self._cached_data: ClaudeCacheData | None = None
37
-
38
- async def initialize_detection(self) -> ClaudeCacheData:
39
- """Initialize Claude detection at startup."""
40
- try:
41
- # Get current Claude version
42
- current_version = await self._get_claude_version()
43
-
44
- # Try to load from cache first
45
- detected_data = self._load_from_cache(current_version)
46
- cached = detected_data is not None
47
- if cached:
48
- logger.debug("detection_claude_headers_debug", version=current_version)
49
- else:
50
- # No cache or version changed - detect fresh
51
- detected_data = await self._detect_claude_headers(current_version)
52
- # Cache the results
53
- self._save_to_cache(detected_data)
54
-
55
- self._cached_data = detected_data
56
-
57
- logger.info(
58
- "detection_claude_headers_completed",
59
- version=current_version,
60
- cached=cached,
61
- )
62
-
63
- # TODO: add proper testing without claude cli installed
64
- if detected_data is None:
65
- raise ValueError("Claude detection failed")
66
- return detected_data
67
-
68
- except Exception as e:
69
- logger.warning("detection_claude_headers_failed", fallback=True, error=e)
70
- # Return fallback data
71
- fallback_data = self._get_fallback_data()
72
- self._cached_data = fallback_data
73
- return fallback_data
74
-
75
- def get_cached_data(self) -> ClaudeCacheData | None:
76
- """Get currently cached detection data."""
77
- return self._cached_data
78
-
79
- async def _get_claude_version(self) -> str:
80
- """Get Claude CLI version."""
81
- try:
82
- result = subprocess.run(
83
- ["claude", "--version"],
84
- capture_output=True,
85
- text=True,
86
- timeout=10,
87
- )
88
- if result.returncode == 0:
89
- # Extract version from output like "1.0.60 (Claude Code)"
90
- version_line = result.stdout.strip()
91
- if "/" in version_line:
92
- # Handle "claude-cli/1.0.60" format
93
- version_line = version_line.split("/")[-1]
94
- if "(" in version_line:
95
- # Handle "1.0.60 (Claude Code)" format - extract just the version number
96
- return version_line.split("(")[0].strip()
97
- return version_line
98
- else:
99
- raise RuntimeError(f"Claude version command failed: {result.stderr}")
100
-
101
- except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
102
- logger.warning("claude_version_detection_failed", error=str(e))
103
- return "unknown"
104
-
105
- async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
106
- """Execute Claude CLI with proxy to capture headers and system prompt."""
107
- # Data captured from the request
108
- captured_data: dict[str, Any] = {}
109
-
110
- async def capture_handler(request: Request) -> Response:
111
- """Capture the Claude CLI request."""
112
- captured_data["headers"] = dict(request.headers)
113
- captured_data["body"] = await request.body()
114
- # Return a mock response to satisfy Claude CLI
115
- return Response(
116
- content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
117
- media_type="application/json",
118
- status_code=200,
119
- )
120
-
121
- # Create temporary FastAPI app
122
- temp_app = FastAPI()
123
- temp_app.post("/v1/messages")(capture_handler)
124
-
125
- # Find available port
126
- sock = socket.socket()
127
- sock.bind(("", 0))
128
- port = sock.getsockname()[1]
129
- sock.close()
130
-
131
- # Start server in background
132
- from uvicorn import Config, Server
133
-
134
- config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
135
- server = Server(config)
136
-
137
- server_task = asyncio.create_task(server.serve())
138
-
139
- try:
140
- # Wait for server to start
141
- await asyncio.sleep(0.5)
142
-
143
- # Execute Claude CLI with proxy
144
- env = {**dict(os.environ), "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}"}
145
-
146
- process = await asyncio.create_subprocess_exec(
147
- "claude",
148
- "test",
149
- env=env,
150
- stdout=asyncio.subprocess.PIPE,
151
- stderr=asyncio.subprocess.PIPE,
152
- )
153
-
154
- # Wait for process with timeout
155
- try:
156
- await asyncio.wait_for(process.wait(), timeout=30)
157
- except TimeoutError:
158
- process.kill()
159
- await process.wait()
160
-
161
- # Stop server
162
- server.should_exit = True
163
- await server_task
164
-
165
- if not captured_data:
166
- raise RuntimeError("Failed to capture Claude CLI request")
167
-
168
- # Extract headers and system prompt
169
- headers = self._extract_headers(captured_data["headers"])
170
- system_prompt = self._extract_system_prompt(captured_data["body"])
171
-
172
- return ClaudeCacheData(
173
- claude_version=version, headers=headers, system_prompt=system_prompt
174
- )
175
-
176
- except Exception as e:
177
- # Ensure server is stopped
178
- server.should_exit = True
179
- if not server_task.done():
180
- await server_task
181
- raise
182
-
183
- def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
184
- """Load cached data for specific Claude version."""
185
- cache_file = self.cache_dir / f"claude_headers_{version}.json"
186
-
187
- if not cache_file.exists():
188
- return None
189
-
190
- try:
191
- with cache_file.open("r") as f:
192
- data = json.load(f)
193
- return ClaudeCacheData.model_validate(data)
194
- except Exception:
195
- return None
196
-
197
- def _save_to_cache(self, data: ClaudeCacheData) -> None:
198
- """Save detection data to cache."""
199
- cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
200
-
201
- try:
202
- with cache_file.open("w") as f:
203
- json.dump(data.model_dump(), f, indent=2, default=str)
204
- logger.debug(
205
- "cache_saved", file=str(cache_file), version=data.claude_version
206
- )
207
- except Exception as e:
208
- logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
209
-
210
- def _extract_headers(self, headers: dict[str, str]) -> ClaudeCodeHeaders:
211
- """Extract Claude CLI headers from captured request."""
212
- try:
213
- return ClaudeCodeHeaders.model_validate(headers)
214
- except Exception as e:
215
- logger.error("header_extraction_failed", error=str(e))
216
- raise ValueError(f"Failed to extract required headers: {e}") from e
217
-
218
- def _extract_system_prompt(self, body: bytes) -> SystemPromptData:
219
- """Extract system prompt from captured request body."""
220
- try:
221
- data = json.loads(body.decode("utf-8"))
222
- system_content = data.get("system")
223
-
224
- if system_content is None:
225
- raise ValueError("No system field found in request body")
226
-
227
- return SystemPromptData(system_field=system_content)
228
-
229
- except Exception as e:
230
- logger.error("system_prompt_extraction_failed", error=str(e))
231
- raise ValueError(f"Failed to extract system prompt: {e}") from e
232
-
233
- def _get_fallback_data(self) -> ClaudeCacheData:
234
- """Get fallback data when detection fails."""
235
- logger.warning("using_fallback_claude_data")
236
-
237
- # Load fallback data from package data file
238
- package_data_file = (
239
- Path(__file__).parent.parent / "data" / "claude_headers_fallback.json"
240
- )
241
- with package_data_file.open("r") as f:
242
- fallback_data_dict = json.load(f)
243
- return ClaudeCacheData.model_validate(fallback_data_dict)