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
@@ -0,0 +1,298 @@
1
+ """Claude API plugin implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins import (
7
+ BaseProviderPluginFactory,
8
+ FormatAdapterSpec,
9
+ FormatPair,
10
+ PluginContext,
11
+ PluginManifest,
12
+ ProviderPluginRuntime,
13
+ TaskSpec,
14
+ )
15
+ from ccproxy.core.plugins.declaration import RouterSpec
16
+ from ccproxy.llms.streaming.accumulators import ClaudeAccumulator
17
+ from ccproxy.plugins.oauth_claude.manager import ClaudeApiTokenManager
18
+
19
+ from .adapter import ClaudeAPIAdapter
20
+ from .config import ClaudeAPISettings
21
+ from .detection_service import ClaudeAPIDetectionService
22
+ from .health import claude_api_health_check
23
+ from .routes import router as claude_api_router
24
+ from .tasks import ClaudeAPIDetectionRefreshTask
25
+
26
+
27
+ logger = get_plugin_logger()
28
+
29
+
30
+ class ClaudeAPIRuntime(ProviderPluginRuntime):
31
+ """Runtime for Claude API plugin."""
32
+
33
+ def __init__(self, manifest: PluginManifest):
34
+ """Initialize runtime."""
35
+ self.credential_manager: ClaudeApiTokenManager | None = None
36
+ super().__init__(manifest)
37
+ self.config: ClaudeAPISettings | None = None
38
+
39
+ async def _on_initialize(self) -> None:
40
+ """Initialize the Claude API plugin."""
41
+ if not self.context:
42
+ raise RuntimeError("Context not set")
43
+
44
+ # Call parent initialization first
45
+ await super()._on_initialize()
46
+
47
+ # Get configuration
48
+ try:
49
+ config = self.context.get(ClaudeAPISettings)
50
+ except ValueError:
51
+ logger.warning("plugin_no_config")
52
+ # Use default config if none provided
53
+ config = ClaudeAPISettings()
54
+ logger.debug("plugin_using_default_config", category="plugin")
55
+ self.config = config
56
+
57
+ # Register streaming metrics hook
58
+ await self._register_streaming_metrics_hook()
59
+
60
+ # Initialize detection service to populate cached data
61
+ if self.detection_service:
62
+ try:
63
+ # This will detect headers and system prompt
64
+ await self.detection_service.initialize_detection()
65
+ version = self.detection_service.get_version()
66
+ cli_path = self.detection_service.get_cli_path()
67
+
68
+ if not cli_path:
69
+ logger.warning(
70
+ "cli_detection_completed",
71
+ cli_available=False,
72
+ version=None,
73
+ cli_path=None,
74
+ source="unknown",
75
+ )
76
+ except Exception as e:
77
+ logger.error(
78
+ "claude_detection_initialization_failed",
79
+ error=str(e),
80
+ exc_info=e,
81
+ )
82
+
83
+ # Get CLI info for consolidated logging (only for successful detection)
84
+ cli_info = {}
85
+ if self.detection_service and self.detection_service.get_cli_path():
86
+ cli_info.update(
87
+ {
88
+ "cli_available": True,
89
+ "cli_version": self.detection_service.get_version(),
90
+ "cli_path": self.detection_service.get_cli_path(),
91
+ "cli_source": "package_manager",
92
+ }
93
+ )
94
+
95
+ logger.debug(
96
+ "plugin_initialized",
97
+ plugin="claude_api",
98
+ version=self.manifest.version,
99
+ status="initialized",
100
+ has_credentials=self.credentials_manager is not None,
101
+ base_url=self.config.base_url,
102
+ models_count=len(self.config.models_endpoint)
103
+ if self.config.models_endpoint
104
+ else 0,
105
+ has_adapter=self.adapter is not None,
106
+ **cli_info,
107
+ )
108
+
109
+ async def _get_health_details(self) -> dict[str, Any]:
110
+ """Get health check details."""
111
+ details = await super()._get_health_details()
112
+
113
+ # Add claude-api specific health check
114
+ if self.config and self.detection_service and self.credentials_manager:
115
+ try:
116
+ health_result = await claude_api_health_check(
117
+ self.config,
118
+ self.detection_service,
119
+ self.credentials_manager,
120
+ version=self.manifest.version,
121
+ )
122
+ details.update(
123
+ {
124
+ "health_check_status": health_result.status,
125
+ "health_check_detail": health_result.details,
126
+ }
127
+ )
128
+ except Exception as e:
129
+ details["health_check_error"] = str(e)
130
+
131
+ return details
132
+
133
+ async def _register_streaming_metrics_hook(self) -> None:
134
+ """Register the streaming metrics extraction hook."""
135
+ try:
136
+ if not self.context:
137
+ logger.warning(
138
+ "streaming_metrics_hook_not_registered",
139
+ reason="no_context",
140
+ plugin="claude_api",
141
+ )
142
+ return
143
+ # Debug: Log context details
144
+ logger.debug(
145
+ "streaming_metrics_hook_context_check",
146
+ plugin="claude_api",
147
+ has_context=self.context is not None,
148
+ context_type=type(self.context).__name__ if self.context else None,
149
+ context_keys=list(self.context.keys()) if self.context else [],
150
+ has_hook_registry="hook_registry" in (self.context or {}),
151
+ has_plugin_registry="plugin_registry" in (self.context or {}),
152
+ )
153
+
154
+ # Get hook registry from context
155
+ from ccproxy.core.plugins.hooks.registry import HookRegistry
156
+
157
+ try:
158
+ hook_registry = self.context.get(HookRegistry)
159
+ except ValueError:
160
+ logger.warning(
161
+ "streaming_metrics_hook_not_registered",
162
+ reason="no_hook_registry",
163
+ plugin="claude_api",
164
+ context_keys=list(self.context.keys()) if self.context else [],
165
+ )
166
+ return
167
+
168
+ # Get pricing service from plugin registry if available
169
+ pricing_service = None
170
+ if "plugin_registry" in self.context:
171
+ try:
172
+ from ccproxy.plugins.pricing.service import PricingService
173
+
174
+ plugin_registry = self.context["plugin_registry"]
175
+ logger.debug(
176
+ "getting_pricing_service",
177
+ plugin="claude_api",
178
+ registry_type=type(plugin_registry).__name__,
179
+ )
180
+ pricing_service = plugin_registry.get_service(
181
+ "pricing", PricingService
182
+ )
183
+ logger.debug(
184
+ "pricing_service_obtained",
185
+ plugin="claude_api",
186
+ service_type=type(pricing_service).__name__
187
+ if pricing_service
188
+ else None,
189
+ is_none=pricing_service is None,
190
+ )
191
+ except Exception as e:
192
+ logger.debug(
193
+ "pricing_service_not_available_for_hook",
194
+ plugin="claude_api",
195
+ error=str(e),
196
+ error_type=type(e).__name__,
197
+ )
198
+ else:
199
+ logger.debug(
200
+ "plugin_registry_not_in_context",
201
+ plugin="claude_api",
202
+ context_keys=list(self.context.keys()) if self.context else [],
203
+ )
204
+
205
+ # Create and register the hook
206
+ from .hooks import ClaudeAPIStreamingMetricsHook
207
+
208
+ # Pass both pricing_service (if available now) and plugin_registry (for lazy loading)
209
+ metrics_hook = ClaudeAPIStreamingMetricsHook(
210
+ pricing_service=pricing_service,
211
+ plugin_registry=self.context.get("plugin_registry"),
212
+ )
213
+ hook_registry.register(metrics_hook)
214
+
215
+ logger.debug(
216
+ "streaming_metrics_hook_registered",
217
+ plugin="claude_api",
218
+ hook_name=metrics_hook.name,
219
+ priority=metrics_hook.priority,
220
+ has_pricing=pricing_service is not None,
221
+ pricing_service_type=type(pricing_service).__name__
222
+ if pricing_service
223
+ else "None",
224
+ )
225
+
226
+ except Exception as e:
227
+ logger.error(
228
+ "streaming_metrics_hook_registration_failed",
229
+ plugin="claude_api",
230
+ error=str(e),
231
+ exc_info=e,
232
+ )
233
+
234
+
235
+ class ClaudeAPIFactory(BaseProviderPluginFactory):
236
+ """Factory for Claude API plugin."""
237
+
238
+ cli_safe = False # Heavy provider plugin - not safe for CLI
239
+
240
+ # Plugin configuration via class attributes
241
+ plugin_name = "claude_api"
242
+ plugin_description = "Claude API provider plugin with support for both native Anthropic format and OpenAI-compatible format"
243
+ runtime_class = ClaudeAPIRuntime
244
+ adapter_class = ClaudeAPIAdapter
245
+ detection_service_class = ClaudeAPIDetectionService
246
+ config_class = ClaudeAPISettings
247
+ # String-based auth manager reference
248
+ auth_manager_name = "oauth_claude"
249
+ credentials_manager_class = ClaudeApiTokenManager
250
+ routers = [
251
+ RouterSpec(router=claude_api_router, prefix="/claude", tags=["claude-api"]),
252
+ ]
253
+ # OAuth provider is optional because the token manager can operate
254
+ # without a globally-registered auth provider. When present, it enables
255
+ # first-class OAuth flows in the UI.
256
+ dependencies = ["oauth_claude"]
257
+ optional_requires = ["pricing"]
258
+
259
+ # No format adapters needed - core provides all required conversions
260
+ format_adapters: list[FormatAdapterSpec] = []
261
+
262
+ # Define requirements for adapters this plugin needs
263
+ requires_format_adapters: list[FormatPair] = [
264
+ # Core-provided adapters handle remaining dependencies
265
+ ]
266
+ tasks = [
267
+ TaskSpec(
268
+ task_name="claude_api_detection_refresh",
269
+ task_type="claude_api_detection_refresh",
270
+ task_class=ClaudeAPIDetectionRefreshTask,
271
+ interval_seconds=3600,
272
+ enabled=True,
273
+ kwargs={"skip_initial_run": True},
274
+ )
275
+ ]
276
+ tool_accumulator_class = ClaudeAccumulator
277
+
278
+ def create_detection_service(self, context: PluginContext) -> Any:
279
+ """Create detection service and inject it into task kwargs.
280
+
281
+ Ensures the scheduled detection-refresh task uses the same instance
282
+ that the runtime receives via context.
283
+ """
284
+ detection_service = super().create_detection_service(context)
285
+
286
+ if self.manifest.tasks and detection_service is not None:
287
+ for task_spec in self.manifest.tasks:
288
+ if task_spec.task_name == "claude_api_detection_refresh":
289
+ task_spec.kwargs["detection_service"] = detection_service
290
+
291
+ return detection_service
292
+
293
+
294
+ # Create factory instance for plugin discovery
295
+ # Note: This follows the existing pattern but creates a singleton
296
+ factory = ClaudeAPIFactory()
297
+
298
+ __all__ = ["ClaudeAPIFactory", "ClaudeAPIRuntime", "factory"]
@@ -0,0 +1,118 @@
1
+ """API routes for Claude API plugin."""
2
+
3
+ import uuid
4
+ from typing import TYPE_CHECKING, Annotated, Any, cast
5
+
6
+ from fastapi import APIRouter, Depends, Request
7
+ from fastapi.responses import Response, StreamingResponse
8
+
9
+ from ccproxy.api.decorators import with_format_chain
10
+ from ccproxy.api.dependencies import (
11
+ get_plugin_adapter,
12
+ get_provider_config_dependency,
13
+ )
14
+ from ccproxy.auth.dependencies import ConditionalAuthDep
15
+ from ccproxy.core.constants import (
16
+ FORMAT_ANTHROPIC_MESSAGES,
17
+ FORMAT_OPENAI_CHAT,
18
+ FORMAT_OPENAI_RESPONSES,
19
+ UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
20
+ )
21
+ from ccproxy.core.logging import get_plugin_logger
22
+ from ccproxy.llms.models import anthropic as anthropic_models
23
+ from ccproxy.llms.models import openai as openai_models
24
+ from ccproxy.streaming import DeferredStreaming
25
+
26
+ from .config import ClaudeAPISettings
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ pass
31
+
32
+ logger = get_plugin_logger()
33
+
34
+ ClaudeAPIAdapterDep = Annotated[Any, Depends(get_plugin_adapter("claude_api"))]
35
+ ClaudeAPIConfigDep = Annotated[
36
+ ClaudeAPISettings,
37
+ Depends(get_provider_config_dependency("claude_api", ClaudeAPISettings)),
38
+ ]
39
+
40
+ APIResponse = Response | StreamingResponse | DeferredStreaming
41
+
42
+ # Main API Router - Core Claude API endpoints
43
+ router = APIRouter()
44
+
45
+
46
+ def _cast_result(result: object) -> APIResponse:
47
+ return cast(APIResponse, result)
48
+
49
+
50
+ async def _handle_adapter_request(
51
+ request: Request,
52
+ adapter: Any,
53
+ ) -> APIResponse:
54
+ result = await adapter.handle_request(request)
55
+ return _cast_result(result)
56
+
57
+
58
+ @router.post(
59
+ "/v1/messages",
60
+ response_model=anthropic_models.MessageResponse | anthropic_models.APIError,
61
+ )
62
+ @with_format_chain(
63
+ [FORMAT_ANTHROPIC_MESSAGES], endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES
64
+ )
65
+ async def create_anthropic_message(
66
+ request: Request,
67
+ _: anthropic_models.CreateMessageRequest,
68
+ auth: ConditionalAuthDep,
69
+ adapter: ClaudeAPIAdapterDep,
70
+ ) -> APIResponse:
71
+ """Create a message using Claude AI with native Anthropic format."""
72
+ return await _handle_adapter_request(request, adapter)
73
+
74
+
75
+ @router.post(
76
+ "/v1/chat/completions",
77
+ response_model=openai_models.ChatCompletionResponse | openai_models.ErrorResponse,
78
+ )
79
+ @with_format_chain(
80
+ [FORMAT_OPENAI_CHAT, FORMAT_ANTHROPIC_MESSAGES],
81
+ endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
82
+ )
83
+ async def create_openai_chat_completion(
84
+ request: Request,
85
+ _: openai_models.ChatCompletionRequest,
86
+ auth: ConditionalAuthDep,
87
+ adapter: ClaudeAPIAdapterDep,
88
+ ) -> APIResponse:
89
+ """Create a chat completion using Claude AI with OpenAI-compatible format."""
90
+ return await _handle_adapter_request(request, adapter)
91
+
92
+
93
+ @router.post("/v1/responses", response_model=None)
94
+ @with_format_chain(
95
+ [FORMAT_OPENAI_RESPONSES, FORMAT_ANTHROPIC_MESSAGES],
96
+ endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
97
+ )
98
+ async def claude_v1_responses(
99
+ request: Request,
100
+ auth: ConditionalAuthDep,
101
+ adapter: ClaudeAPIAdapterDep,
102
+ ) -> APIResponse:
103
+ """Response API compatible endpoint using Claude backend."""
104
+ # Ensure format chain is present for request/response conversion
105
+ # format chain and endpoint set by decorator
106
+ session_id = request.headers.get("session_id") or str(uuid.uuid4())
107
+ return await _handle_adapter_request(request, adapter)
108
+
109
+
110
+ @router.get("/v1/models", response_model=openai_models.ModelList)
111
+ async def list_models(
112
+ request: Request,
113
+ auth: ConditionalAuthDep,
114
+ config: ClaudeAPIConfigDep,
115
+ ) -> dict[str, Any]:
116
+ """List available Claude models from configuration."""
117
+ models = [card.model_dump(mode="json") for card in config.models_endpoint]
118
+ return {"object": "list", "data": models}
@@ -0,0 +1,68 @@
1
+ """Claude API streaming metrics extraction utilities.
2
+
3
+ This module provides utilities for extracting token usage from
4
+ Anthropic streaming responses.
5
+ """
6
+
7
+ from typing import Any, TypedDict
8
+
9
+
10
+ class UsageData(TypedDict, total=False):
11
+ """Token usage data extracted from streaming or non-streaming responses."""
12
+
13
+ input_tokens: int | None
14
+ output_tokens: int | None
15
+ cache_read_input_tokens: int | None
16
+ cache_creation_input_tokens: int | None
17
+ event_type: str | None # Extra field for tracking event source
18
+ model: str | None # Extra field for model information
19
+
20
+
21
+ def extract_usage_from_streaming_chunk(chunk_data: Any) -> UsageData | None:
22
+ """Extract usage information from Anthropic streaming response chunk.
23
+
24
+ This function looks for usage information in both message_start and message_delta events
25
+ from Anthropic's streaming API responses. message_start contains initial input tokens,
26
+ message_delta contains final output tokens.
27
+
28
+ Args:
29
+ chunk_data: Streaming response chunk dictionary
30
+
31
+ Returns:
32
+ UsageData with token counts or None if no usage found
33
+ """
34
+ if not isinstance(chunk_data, dict):
35
+ return None
36
+
37
+ chunk_type = chunk_data.get("type")
38
+
39
+ # Look for message_start events with initial usage (input tokens)
40
+ if chunk_type == "message_start" and "message" in chunk_data:
41
+ message = chunk_data["message"]
42
+ # Extract model name if present
43
+ model = message.get("model")
44
+ if "usage" in message:
45
+ usage = message["usage"]
46
+ return UsageData(
47
+ input_tokens=usage.get("input_tokens"),
48
+ output_tokens=usage.get(
49
+ "output_tokens"
50
+ ), # Initial output tokens (usually small)
51
+ cache_read_input_tokens=usage.get("cache_read_input_tokens"),
52
+ cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
53
+ event_type="message_start",
54
+ model=model, # Include model in usage data
55
+ )
56
+
57
+ # Look for message_delta events with final usage (output tokens)
58
+ elif chunk_type == "message_delta" and "usage" in chunk_data:
59
+ usage = chunk_data["usage"]
60
+ return UsageData(
61
+ input_tokens=usage.get("input_tokens"), # Usually None in delta
62
+ output_tokens=usage.get("output_tokens"), # Final output token count
63
+ cache_read_input_tokens=usage.get("cache_read_input_tokens"),
64
+ cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
65
+ event_type="message_delta",
66
+ )
67
+
68
+ return None
@@ -0,0 +1,84 @@
1
+ """Scheduled tasks for Claude API plugin."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.scheduler.tasks import BaseScheduledTask
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from .detection_service import ClaudeAPIDetectionService
11
+
12
+
13
+ logger = get_plugin_logger()
14
+
15
+
16
+ class ClaudeAPIDetectionRefreshTask(BaseScheduledTask):
17
+ """Task to periodically refresh Claude CLI detection headers."""
18
+
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ interval_seconds: float,
23
+ detection_service: "ClaudeAPIDetectionService",
24
+ enabled: bool = True,
25
+ skip_initial_run: bool = True,
26
+ **kwargs: Any,
27
+ ):
28
+ super().__init__(
29
+ name=name,
30
+ interval_seconds=interval_seconds,
31
+ enabled=enabled,
32
+ **kwargs,
33
+ )
34
+ self.detection_service = detection_service
35
+ self.skip_initial_run = skip_initial_run
36
+ self._first_run = True
37
+
38
+ async def run(self) -> bool:
39
+ """Execute the detection refresh."""
40
+ if self._first_run and self.skip_initial_run:
41
+ self._first_run = False
42
+ logger.debug(
43
+ "claude_api_detection_refresh_skipped_initial",
44
+ task_name=self.name,
45
+ )
46
+ return True
47
+
48
+ self._first_run = False
49
+
50
+ try:
51
+ logger.info(
52
+ "claude_api_detection_refresh_starting",
53
+ task_name=self.name,
54
+ )
55
+ detection_data = await self.detection_service.initialize_detection()
56
+
57
+ logger.info(
58
+ "claude_api_detection_refresh_completed",
59
+ task_name=self.name,
60
+ version=detection_data.claude_version if detection_data else "unknown",
61
+ )
62
+ return True
63
+
64
+ except Exception as e:
65
+ logger.error(
66
+ "claude_api_detection_refresh_failed",
67
+ task_name=self.name,
68
+ error=str(e),
69
+ )
70
+ return False
71
+
72
+ async def setup(self) -> None:
73
+ """Setup before task execution starts."""
74
+ logger.debug(
75
+ "claude_api_detection_refresh_setup",
76
+ task_name=self.name,
77
+ )
78
+
79
+ async def cleanup(self) -> None:
80
+ """Cleanup after task execution stops."""
81
+ logger.info(
82
+ "claude_api_detection_refresh_cleanup",
83
+ task_name=self.name,
84
+ )
@@ -0,0 +1,35 @@
1
+ # Claude SDK Plugin
2
+
3
+ Runs Claude through the local Claude Code SDK and CLI with session management.
4
+
5
+ ## Highlights
6
+ - Wraps the SDK adapter with reusable session and streaming support
7
+ - Validates CLI availability via the detection service and refresh task
8
+ - Shares the Claude accumulator to emit streaming metrics comparable to the API
9
+
10
+ ## Configuration
11
+ - `ClaudeSDKSettings` covers CLI discovery, auth, and session pooling options
12
+ - Requires the Claude CLI to be installed and reachable on `PATH`
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin claude_sdk --config-class ClaudeSDKSettings`
15
+
16
+ ```toml
17
+ [plugins.claude_sdk]
18
+ # enabled = true
19
+ # base_url = "claude-sdk://local"
20
+ # session_pool_enabled = false
21
+ # session_pool_size = 5
22
+ # include_system_messages_in_stream = true
23
+ # sdk_message_mode = "formatted"
24
+
25
+ [plugins.claude_sdk.sdk_session_pool]
26
+ # enabled = true
27
+ # session_ttl = 3600
28
+ # max_sessions = 1000
29
+ # cleanup_interval = 300
30
+ ```
31
+
32
+ ## Related Components
33
+ - `adapter.py`: bridge between CCProxy requests and the SDK client
34
+ - `tasks.py`: periodic detection refresh for CLI state
35
+ - `routes.py`: FastAPI router served under `/claude/sdk`
@@ -0,0 +1,80 @@
1
+ """Claude SDK integration module."""
2
+
3
+ from .client import ClaudeSDKClient
4
+ from .exceptions import ClaudeSDKError, StreamTimeoutError
5
+ from .models import (
6
+ AssistantMessage,
7
+ ContentBlock,
8
+ ExtendedContentBlock,
9
+ ResultMessage,
10
+ ResultMessageBlock,
11
+ SDKContentBlock,
12
+ SDKMessage,
13
+ SDKMessageContent,
14
+ SDKMessageMode,
15
+ SystemMessage,
16
+ TextBlock,
17
+ ThinkingBlock,
18
+ ToolResultBlock,
19
+ ToolResultSDKBlock,
20
+ ToolUseBlock,
21
+ ToolUseSDKBlock,
22
+ UserMessage,
23
+ convert_sdk_result_message,
24
+ convert_sdk_system_message,
25
+ convert_sdk_text_block,
26
+ convert_sdk_tool_result_block,
27
+ convert_sdk_tool_use_block,
28
+ create_sdk_message,
29
+ to_sdk_variant,
30
+ )
31
+ from .options import OptionsHandler
32
+
33
+
34
+ # Lazy import to avoid circular dependency
35
+ def __getattr__(name: str) -> object:
36
+ if name == "MessageConverter":
37
+ from .converter import MessageConverter
38
+
39
+ return MessageConverter
40
+ if name == "parse_formatted_sdk_content":
41
+ from .parser import parse_formatted_sdk_content
42
+
43
+ return parse_formatted_sdk_content
44
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
45
+
46
+
47
+ __all__ = [
48
+ # Session Context will be imported here once created
49
+ "ClaudeSDKClient",
50
+ "ClaudeSDKError",
51
+ "StreamTimeoutError",
52
+ "MessageConverter", # Lazy loaded
53
+ "OptionsHandler",
54
+ "parse_formatted_sdk_content", # Lazy loaded
55
+ # Re-export SDK models from core adapter
56
+ "AssistantMessage",
57
+ "ContentBlock",
58
+ "ExtendedContentBlock",
59
+ "ResultMessage",
60
+ "ResultMessageBlock",
61
+ "SDKContentBlock",
62
+ "SDKMessage",
63
+ "SDKMessageContent",
64
+ "SDKMessageMode",
65
+ "SystemMessage",
66
+ "TextBlock",
67
+ "ThinkingBlock",
68
+ "ToolResultBlock",
69
+ "ToolResultSDKBlock",
70
+ "ToolUseBlock",
71
+ "ToolUseSDKBlock",
72
+ "UserMessage",
73
+ "convert_sdk_result_message",
74
+ "convert_sdk_system_message",
75
+ "convert_sdk_text_block",
76
+ "convert_sdk_tool_result_block",
77
+ "convert_sdk_tool_use_block",
78
+ "create_sdk_message",
79
+ "to_sdk_variant",
80
+ ]