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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +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.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -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.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,140 @@
1
+ """Base adapter interface for API format conversion."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncGenerator, AsyncIterator
5
+ from typing import Generic, TypeVar
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from ccproxy.core.interfaces import StreamingConfigurable
10
+
11
+
12
+ RequestType = TypeVar("RequestType", bound=BaseModel)
13
+ ResponseType = TypeVar("ResponseType", bound=BaseModel)
14
+ StreamEventType = TypeVar("StreamEventType", bound=BaseModel)
15
+
16
+
17
+ class APIAdapter(ABC, Generic[RequestType, ResponseType, StreamEventType]):
18
+ """Abstract base class for API format adapters.
19
+
20
+ Provides strongly-typed interface for converting between different API formats
21
+ with full type safety and validation.
22
+ """
23
+
24
+ @abstractmethod
25
+ async def adapt_request(self, request: RequestType) -> BaseModel:
26
+ """Convert a request using strongly-typed Pydantic models.
27
+
28
+ Args:
29
+ request: The typed request model to convert
30
+
31
+ Returns:
32
+ The converted typed request model
33
+
34
+ Raises:
35
+ ValueError: If the request format is invalid or unsupported
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ async def adapt_response(self, response: ResponseType) -> BaseModel:
41
+ """Convert a response using strongly-typed Pydantic models.
42
+
43
+ Args:
44
+ response: The typed response model to convert
45
+
46
+ Returns:
47
+ The converted typed response model
48
+
49
+ Raises:
50
+ ValueError: If the response format is invalid or unsupported
51
+ """
52
+ pass
53
+
54
+ @abstractmethod
55
+ def adapt_stream(
56
+ self, stream: AsyncIterator[StreamEventType]
57
+ ) -> AsyncGenerator[BaseModel, None]:
58
+ """Convert a streaming response using strongly-typed Pydantic models.
59
+
60
+ Args:
61
+ stream: The typed streaming response data to convert
62
+
63
+ Yields:
64
+ The converted typed streaming response chunks
65
+
66
+ Raises:
67
+ ValueError: If the stream format is invalid or unsupported
68
+ """
69
+ # This should be implemented as an async generator
70
+ # Subclasses must override this method
71
+ ...
72
+
73
+ @abstractmethod
74
+ async def adapt_error(self, error: BaseModel) -> BaseModel:
75
+ """Convert an error response using strongly-typed Pydantic models.
76
+
77
+ Args:
78
+ error: The typed error response model to convert
79
+
80
+ Returns:
81
+ The converted typed error response model
82
+
83
+ Raises:
84
+ ValueError: If the error format is invalid or unsupported
85
+ """
86
+ pass
87
+
88
+
89
+ class BaseAPIAdapter(
90
+ APIAdapter[RequestType, ResponseType, StreamEventType],
91
+ StreamingConfigurable,
92
+ ):
93
+ """Base implementation with common functionality.
94
+
95
+ Provides strongly-typed interface for API format conversion with
96
+ better type safety and validation.
97
+ """
98
+
99
+ def __init__(self, name: str):
100
+ self.name = name
101
+ # Optional streaming flags that subclasses may use
102
+ self._openai_thinking_xml: bool | None = None
103
+
104
+ def __str__(self) -> str:
105
+ return f"{self.__class__.__name__}({self.name})"
106
+
107
+ def __repr__(self) -> str:
108
+ return self.__str__()
109
+
110
+ # StreamingConfigurable
111
+ def configure_streaming(self, *, openai_thinking_xml: bool | None = None) -> None:
112
+ self._openai_thinking_xml = openai_thinking_xml
113
+
114
+ # Strongly-typed interface - subclasses implement these
115
+ @abstractmethod
116
+ async def adapt_request(self, request: RequestType) -> BaseModel:
117
+ """Convert a request using strongly-typed Pydantic models."""
118
+ pass
119
+
120
+ @abstractmethod
121
+ async def adapt_response(self, response: ResponseType) -> BaseModel:
122
+ """Convert a response using strongly-typed Pydantic models."""
123
+ pass
124
+
125
+ @abstractmethod
126
+ def adapt_stream(
127
+ self, stream: AsyncIterator[StreamEventType]
128
+ ) -> AsyncGenerator[BaseModel, None]:
129
+ """Convert a streaming response using strongly-typed Pydantic models."""
130
+ # This should be implemented as an async generator
131
+ # Subclasses must override this method
132
+ ...
133
+
134
+ @abstractmethod
135
+ async def adapt_error(self, error: BaseModel) -> BaseModel:
136
+ """Convert an error response using strongly-typed Pydantic models."""
137
+ pass
138
+
139
+
140
+ __all__ = ["APIAdapter", "BaseAPIAdapter"]
@@ -0,0 +1,33 @@
1
+ """Shared base model for all LLM API models."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class LlmBaseModel(BaseModel):
9
+ """Base model for all LLM API models with proper JSON serialization.
10
+
11
+ Excludes None values and empty collections to match API conventions.
12
+ """
13
+
14
+ model_config = ConfigDict(
15
+ extra="allow", # Allow extra fields
16
+ )
17
+
18
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
19
+ """Override to exclude empty collections as well as None values."""
20
+ # Extract exclude_none from kwargs, defaulting to True for our convention
21
+ exclude_none = kwargs.pop("exclude_none", True)
22
+ # First get the data with None values excluded
23
+ data = super().model_dump(exclude_none=exclude_none, **kwargs)
24
+
25
+ # Filter out empty collections (lists, dicts, sets)
26
+ filtered_data = {}
27
+ for key, value in data.items():
28
+ if isinstance(value, list | dict | set) and len(value) == 0:
29
+ # Skip empty collections
30
+ continue
31
+ filtered_data[key] = value
32
+
33
+ return filtered_data
@@ -0,0 +1,51 @@
1
+ """Shared helpers used by formatter adapters."""
2
+
3
+ from .identifiers import ensure_identifier, normalize_suffix
4
+ from .streams import (
5
+ IndexedToolCallTracker,
6
+ ObfuscationTokenFactory,
7
+ ReasoningBuffer,
8
+ ReasoningPartState,
9
+ ToolCallState,
10
+ ToolCallTracker,
11
+ build_anthropic_tool_use_block,
12
+ emit_anthropic_tool_use_events,
13
+ )
14
+ from .thinking import (
15
+ THINKING_CLOSE_PATTERN,
16
+ THINKING_OPEN_PATTERN,
17
+ THINKING_PATTERN,
18
+ ThinkingSegment,
19
+ merge_thinking_segments,
20
+ )
21
+ from .usage import (
22
+ convert_anthropic_usage_to_openai_completion_usage,
23
+ convert_anthropic_usage_to_openai_responses_usage,
24
+ convert_openai_completion_usage_to_responses_usage,
25
+ convert_openai_responses_usage_to_anthropic_usage,
26
+ convert_openai_responses_usage_to_completion_usage,
27
+ )
28
+
29
+
30
+ __all__ = [
31
+ "ensure_identifier",
32
+ "normalize_suffix",
33
+ "THINKING_PATTERN",
34
+ "THINKING_OPEN_PATTERN",
35
+ "THINKING_CLOSE_PATTERN",
36
+ "ThinkingSegment",
37
+ "merge_thinking_segments",
38
+ "ReasoningBuffer",
39
+ "ReasoningPartState",
40
+ "ToolCallState",
41
+ "ToolCallTracker",
42
+ "IndexedToolCallTracker",
43
+ "ObfuscationTokenFactory",
44
+ "build_anthropic_tool_use_block",
45
+ "emit_anthropic_tool_use_events",
46
+ "convert_anthropic_usage_to_openai_completion_usage",
47
+ "convert_anthropic_usage_to_openai_responses_usage",
48
+ "convert_openai_completion_usage_to_responses_usage",
49
+ "convert_openai_responses_usage_to_anthropic_usage",
50
+ "convert_openai_responses_usage_to_completion_usage",
51
+ ]
@@ -0,0 +1,48 @@
1
+ """Identifier helpers shared across formatter adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+
8
+ def normalize_suffix(identifier: str) -> str:
9
+ """Return the suffix part of an identifier split on the first underscore."""
10
+
11
+ if "_" in identifier:
12
+ return identifier.split("_", 1)[1]
13
+ return identifier
14
+
15
+
16
+ def ensure_identifier(prefix: str, existing: str | None = None) -> tuple[str, str]:
17
+ """Return a stable identifier and suffix for the given prefix.
18
+
19
+ If an existing identifier already matches the prefix we reuse its suffix.
20
+ Existing identifiers that begin with ``resp_`` are also understood so both
21
+ ``resp`` and alternate prefixes can build consistent derived identifiers.
22
+ """
23
+
24
+ if isinstance(existing, str) and existing.startswith(f"{prefix}_"):
25
+ return existing, normalize_suffix(existing)
26
+
27
+ if (
28
+ isinstance(existing, str)
29
+ and existing
30
+ and prefix == "resp"
31
+ and existing.startswith("resp_")
32
+ ):
33
+ return existing, normalize_suffix(existing)
34
+
35
+ if (
36
+ isinstance(existing, str)
37
+ and existing
38
+ and existing.startswith("resp_")
39
+ and prefix != "resp"
40
+ ):
41
+ suffix = normalize_suffix(existing)
42
+ return f"{prefix}_{suffix}", suffix
43
+
44
+ suffix = uuid.uuid4().hex
45
+ return f"{prefix}_{suffix}", suffix
46
+
47
+
48
+ __all__ = ["ensure_identifier", "normalize_suffix"]
@@ -0,0 +1,254 @@
1
+ """Shared streaming helpers for formatter adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from ccproxy.llms.formatters.utils import build_obfuscation_token
10
+ from ccproxy.llms.models import anthropic as anthropic_models
11
+
12
+ from .thinking import ThinkingSegment
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class ReasoningPartState:
17
+ """Mutable reasoning buffer for a specific summary segment."""
18
+
19
+ buffer: list[str] = field(default_factory=list)
20
+ signature: str | None = None
21
+ open: bool = False
22
+
23
+
24
+ class ReasoningBuffer:
25
+ """Utility to manage reasoning text buffers keyed by item/summary ids."""
26
+
27
+ def __init__(self) -> None:
28
+ self._states: dict[str, dict[Any, ReasoningPartState]] = {}
29
+
30
+ def ensure_part(self, item_id: str, summary_index: Any) -> ReasoningPartState:
31
+ item_states = self._states.setdefault(item_id, {})
32
+ part_state = item_states.get(summary_index)
33
+ if part_state is None:
34
+ part_state = ReasoningPartState()
35
+ item_states[summary_index] = part_state
36
+ return part_state
37
+
38
+ def set_signature(
39
+ self, item_id: str, summary_index: Any, signature: str | None
40
+ ) -> None:
41
+ if not signature:
42
+ return
43
+ part_state = self.ensure_part(item_id, summary_index)
44
+ part_state.signature = signature
45
+
46
+ def reset_buffer(self, item_id: str, summary_index: Any) -> None:
47
+ part_state = self.ensure_part(item_id, summary_index)
48
+ part_state.buffer.clear()
49
+
50
+ def open_part(
51
+ self, item_id: str, summary_index: Any, signature: str | None = None
52
+ ) -> ReasoningPartState:
53
+ part_state = self.ensure_part(item_id, summary_index)
54
+ if signature:
55
+ part_state.signature = signature
56
+ part_state.buffer.clear()
57
+ part_state.open = True
58
+ return part_state
59
+
60
+ def close_part(self, item_id: str, summary_index: Any) -> None:
61
+ part_state = self.ensure_part(item_id, summary_index)
62
+ part_state.open = False
63
+
64
+ def is_open(self, item_id: str, summary_index: Any) -> bool:
65
+ return self.ensure_part(item_id, summary_index).open
66
+
67
+ def append_text(self, item_id: str, summary_index: Any, text: str | None) -> None:
68
+ if not isinstance(text, str) or not text:
69
+ return
70
+ part_state = self.ensure_part(item_id, summary_index)
71
+ part_state.buffer.append(text)
72
+
73
+ def emit(
74
+ self, item_id: str, summary_index: Any, final_text: str | None = None
75
+ ) -> list[str]:
76
+ part_state = self.ensure_part(item_id, summary_index)
77
+ text = (
78
+ final_text
79
+ if isinstance(final_text, str) and final_text
80
+ else "".join(part_state.buffer)
81
+ )
82
+ part_state.buffer.clear()
83
+ part_state.open = False
84
+ if not text:
85
+ return []
86
+ segment = ThinkingSegment(thinking=text, signature=part_state.signature)
87
+ xml = segment.to_xml()
88
+ closing = "</thinking>"
89
+ body = xml[: -len(closing)] if xml.endswith(closing) else xml
90
+ return [body, closing]
91
+
92
+
93
+ @dataclass(slots=True)
94
+ class ToolCallState:
95
+ """Mutable state for a single streaming tool call."""
96
+
97
+ id: str
98
+ index: int
99
+ call_id: str | None = None
100
+ item_id: str | None = None
101
+ name: str | None = None
102
+ arguments: str = ""
103
+ arguments_parts: list[str] = field(default_factory=list)
104
+ output_index: int = -1
105
+ emitted: bool = False
106
+ initial_emitted: bool = False
107
+ name_emitted: bool = False
108
+ arguments_emitted: bool = False
109
+ arguments_done_emitted: bool = False
110
+ item_done_emitted: bool = False
111
+ added_emitted: bool = False
112
+ completed: bool = False
113
+ final_arguments: str | None = None
114
+
115
+ def append_arguments(self, segment: str) -> None:
116
+ if segment:
117
+ self.arguments += segment
118
+
119
+ def add_arguments_part(self, segment: str) -> None:
120
+ if segment:
121
+ self.arguments_parts.append(segment)
122
+
123
+
124
+ class ToolCallTracker:
125
+ """Registry tracking streaming tool calls by item identifier."""
126
+
127
+ def __init__(self) -> None:
128
+ self._states: dict[str, ToolCallState] = {}
129
+ self._order: list[str] = []
130
+
131
+ def ensure(self, item_id: str) -> ToolCallState:
132
+ state = self._states.get(item_id)
133
+ if state is None:
134
+ state = ToolCallState(
135
+ id=item_id,
136
+ index=len(self._order),
137
+ )
138
+ state.output_index = len(self._order)
139
+ self._states[item_id] = state
140
+ self._order.append(item_id)
141
+ return state
142
+
143
+ def values(self) -> list[ToolCallState]:
144
+ return [self._states[item_id] for item_id in self._order]
145
+
146
+ def any_completed(self) -> bool:
147
+ return any(state.completed for state in self._states.values())
148
+
149
+ def __len__(self) -> int: # noqa: D401
150
+ return len(self._states)
151
+
152
+
153
+ class IndexedToolCallTracker:
154
+ """Registry tracking streaming tool calls keyed by integer index."""
155
+
156
+ def __init__(self) -> None:
157
+ self._states: dict[int, ToolCallState] = {}
158
+
159
+ def ensure(self, index: int) -> ToolCallState:
160
+ state = self._states.get(index)
161
+ if state is None:
162
+ state = ToolCallState(id=f"call_{index}", index=index)
163
+ self._states[index] = state
164
+ return state
165
+
166
+ def items(self) -> list[tuple[int, ToolCallState]]:
167
+ return [(idx, self._states[idx]) for idx in sorted(self._states)]
168
+
169
+ def values(self) -> list[ToolCallState]:
170
+ return [state for _, state in self.items()]
171
+
172
+ def __contains__(self, index: int) -> bool: # noqa: D401
173
+ return index in self._states
174
+
175
+ def __len__(self) -> int: # noqa: D401
176
+ return len(self._states)
177
+
178
+
179
+ class ObfuscationTokenFactory:
180
+ """Utility for building deterministic obfuscation tokens."""
181
+
182
+ def __init__(self, fallback_identifier: Callable[[], str]) -> None:
183
+ self._fallback_identifier = fallback_identifier
184
+
185
+ def make(
186
+ self,
187
+ kind: str,
188
+ *,
189
+ sequence: int,
190
+ item_id: str | None = None,
191
+ payload: str | None = None,
192
+ ) -> str:
193
+ base_identifier = item_id or self._fallback_identifier()
194
+ return build_obfuscation_token(
195
+ seed=f"{kind}:{base_identifier}",
196
+ sequence=sequence,
197
+ payload=payload or "",
198
+ )
199
+
200
+
201
+ def build_anthropic_tool_use_block(
202
+ state: ToolCallState,
203
+ *,
204
+ default_id: str | None = None,
205
+ parser: Callable[[str], dict[str, Any]] | None = None,
206
+ ) -> anthropic_models.ToolUseBlock:
207
+ """Create an Anthropic ToolUseBlock from a tracked tool-call state."""
208
+
209
+ tool_id = state.item_id or state.call_id or default_id or f"call_{state.index}"
210
+ arguments_text = (
211
+ state.final_arguments or state.arguments or "".join(state.arguments_parts)
212
+ )
213
+ parse_input = parser or (lambda text: {"arguments": text} if text else {})
214
+ input_payload = parse_input(arguments_text)
215
+
216
+ return anthropic_models.ToolUseBlock(
217
+ type="tool_use",
218
+ id=str(tool_id),
219
+ name=str(state.name or "tool"),
220
+ input=input_payload,
221
+ )
222
+
223
+
224
+ def emit_anthropic_tool_use_events(
225
+ index: int,
226
+ state: ToolCallState,
227
+ *,
228
+ parser: Callable[[str], dict[str, Any]] | None = None,
229
+ ) -> list[anthropic_models.MessageStreamEvent]:
230
+ """Build start/stop events for a tool-use block at the given index."""
231
+
232
+ block = build_anthropic_tool_use_block(
233
+ state,
234
+ default_id=f"call_{state.index}",
235
+ parser=parser,
236
+ )
237
+ return [
238
+ anthropic_models.ContentBlockStartEvent(
239
+ type="content_block_start", index=index, content_block=block
240
+ ),
241
+ anthropic_models.ContentBlockStopEvent(type="content_block_stop", index=index),
242
+ ]
243
+
244
+
245
+ __all__ = [
246
+ "ReasoningBuffer",
247
+ "ReasoningPartState",
248
+ "ToolCallState",
249
+ "ToolCallTracker",
250
+ "IndexedToolCallTracker",
251
+ "ObfuscationTokenFactory",
252
+ "build_anthropic_tool_use_block",
253
+ "emit_anthropic_tool_use_events",
254
+ ]
@@ -0,0 +1,74 @@
1
+ """Shared helpers for reasoning/thinking segment handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+
9
+ from ccproxy.llms.models import anthropic as anthropic_models
10
+
11
+
12
+ THINKING_PATTERN = re.compile(
13
+ r"<thinking(?:\s+signature=\"([^\"]*)\")?>(.*?)</thinking>",
14
+ re.DOTALL,
15
+ )
16
+ THINKING_OPEN_PATTERN = re.compile(
17
+ r"<thinking(?:\s+signature=\"([^\"]*)\")?\s*>",
18
+ re.IGNORECASE,
19
+ )
20
+ THINKING_CLOSE_PATTERN = re.compile(r"</thinking>", re.IGNORECASE)
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class ThinkingSegment:
25
+ """Lightweight reasoning segment mirroring Anthropic's ThinkingBlock."""
26
+
27
+ thinking: str
28
+ signature: str | None = None
29
+
30
+ def to_block(self) -> anthropic_models.ThinkingBlock:
31
+ return anthropic_models.ThinkingBlock(
32
+ type="thinking",
33
+ thinking=self.thinking,
34
+ signature=self.signature or "",
35
+ )
36
+
37
+ def to_xml(self) -> str:
38
+ signature = (self.signature or "").strip()
39
+ signature_attr = f' signature="{signature}"' if signature else ""
40
+ return f"<thinking{signature_attr}>{self.thinking}</thinking>"
41
+
42
+ @classmethod
43
+ def from_xml(cls, signature: str | None, text: str) -> ThinkingSegment:
44
+ return cls(thinking=text, signature=signature or None)
45
+
46
+
47
+ def merge_thinking_segments(
48
+ segments: Iterable[ThinkingSegment],
49
+ ) -> list[ThinkingSegment]:
50
+ """Collapse adjacent segments that share the same signature."""
51
+
52
+ merged: list[ThinkingSegment] = []
53
+ for segment in segments:
54
+ text = segment.thinking if isinstance(segment.thinking, str) else None
55
+ if not text:
56
+ continue
57
+ signature = segment.signature or None
58
+ if merged and merged[-1].signature == signature:
59
+ merged[-1] = ThinkingSegment(
60
+ thinking=f"{merged[-1].thinking}{text}",
61
+ signature=signature,
62
+ )
63
+ else:
64
+ merged.append(ThinkingSegment(thinking=text, signature=signature))
65
+ return merged
66
+
67
+
68
+ __all__ = [
69
+ "THINKING_PATTERN",
70
+ "THINKING_OPEN_PATTERN",
71
+ "THINKING_CLOSE_PATTERN",
72
+ "ThinkingSegment",
73
+ "merge_thinking_segments",
74
+ ]