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,749 @@
1
+ """Claude SDK adapter implementation using delegation pattern."""
2
+
3
+ import asyncio
4
+ import json
5
+ import uuid
6
+ from collections.abc import AsyncIterator
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ import httpx
10
+ from fastapi import HTTPException, Request
11
+ from starlette.requests import Request as StarletteRequest
12
+ from starlette.responses import Response, StreamingResponse
13
+
14
+ from ccproxy.config.utils import OPENAI_CHAT_COMPLETIONS_PATH
15
+ from ccproxy.core.logging import get_plugin_logger
16
+ from ccproxy.core.request_context import RequestContext
17
+ from ccproxy.llms.streaming import OpenAIStreamProcessor
18
+ from ccproxy.services.adapters.chain_composer import compose_from_chain
19
+ from ccproxy.services.adapters.format_adapter import FormatAdapterProtocol
20
+ from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
21
+ from ccproxy.streaming import DeferredStreaming
22
+ from ccproxy.streaming.sse import serialize_json_to_sse_stream
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from ccproxy.services.interfaces import IMetricsCollector
27
+
28
+ from .auth import NoOpAuthManager
29
+ from .config import ClaudeSDKSettings
30
+ from .handler import ClaudeSDKHandler
31
+ from .manager import SessionManager
32
+ from .models import MessageResponse
33
+
34
+
35
+ logger = get_plugin_logger()
36
+
37
+
38
+ class ClaudeSDKAdapter(BaseHTTPAdapter):
39
+ """Claude SDK adapter implementation using delegation pattern.
40
+
41
+ This adapter integrates with the application request lifecycle,
42
+ following the same pattern as claude_api and codex plugins.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ config: ClaudeSDKSettings,
48
+ # Optional dependencies
49
+ session_manager: SessionManager | None = None,
50
+ metrics: "IMetricsCollector | None" = None,
51
+ hook_manager: Any | None = None,
52
+ **kwargs: Any,
53
+ ) -> None:
54
+ """Initialize the Claude SDK adapter with explicit dependencies.
55
+
56
+ Args:
57
+ config: SDK configuration settings
58
+ session_manager: Optional session manager for session handling
59
+ metrics: Optional metrics collector
60
+ hook_manager: Optional hook manager for emitting events
61
+ """
62
+ # Initialize BaseHTTPAdapter with dummy auth_manager and http_pool_manager
63
+ # since ClaudeSDK doesn't use external HTTP
64
+ super().__init__(
65
+ config=config, auth_manager=None, http_pool_manager=None, **kwargs
66
+ )
67
+ self.metrics = metrics
68
+ self.hook_manager = hook_manager
69
+
70
+ # Generate or set default session ID
71
+ self._runtime_default_session_id = None
72
+ if (
73
+ config.auto_generate_default_session
74
+ and config.sdk_session_pool
75
+ and config.sdk_session_pool.enabled
76
+ ):
77
+ # Generate a random session ID for this runtime
78
+ self._runtime_default_session_id = f"auto-{uuid.uuid4().hex[:12]}"
79
+ logger.debug(
80
+ "auto_generated_session",
81
+ session_id=self._runtime_default_session_id,
82
+ lifetime="runtime",
83
+ )
84
+ elif config.default_session_id:
85
+ self._runtime_default_session_id = config.default_session_id
86
+ logger.debug(
87
+ "using_configured_default_session",
88
+ session_id=self._runtime_default_session_id,
89
+ )
90
+
91
+ # Use provided session_manager or create if needed and enabled
92
+ if (
93
+ session_manager is None
94
+ and config.sdk_session_pool
95
+ and config.sdk_session_pool.enabled
96
+ ):
97
+ session_manager = SessionManager(config=config)
98
+ logger.debug(
99
+ "adapter_session_pool_enabled",
100
+ session_ttl=config.sdk_session_pool.session_ttl,
101
+ max_sessions=config.sdk_session_pool.max_sessions,
102
+ has_default_session=bool(self._runtime_default_session_id),
103
+ auto_generated=config.auto_generate_default_session,
104
+ )
105
+
106
+ self.session_manager = session_manager
107
+ self.handler: ClaudeSDKHandler | None = ClaudeSDKHandler(
108
+ config=config,
109
+ session_manager=session_manager,
110
+ hook_manager=hook_manager,
111
+ )
112
+ self.auth_manager = NoOpAuthManager()
113
+ self._detection_service: Any | None = None
114
+ self._initialized = False
115
+ self._format_adapter_cache: dict[tuple[str, ...], FormatAdapterProtocol] = {}
116
+
117
+ async def initialize(self) -> None:
118
+ """Initialize the adapter and start session manager if needed."""
119
+ if not self._initialized:
120
+ if self.session_manager:
121
+ await self.session_manager.start()
122
+ logger.debug("session_manager_started")
123
+ self._initialized = True
124
+
125
+ def set_detection_service(self, detection_service: Any) -> None:
126
+ """Set the detection service.
127
+
128
+ Args:
129
+ detection_service: Claude CLI detection service
130
+ """
131
+ self._detection_service = detection_service
132
+
133
+ def _resolve_format_adapter(
134
+ self, format_chain: list[str]
135
+ ) -> FormatAdapterProtocol | None:
136
+ """Return a composed format adapter for the provided chain."""
137
+
138
+ if not self.format_registry or len(format_chain) < 2:
139
+ return None
140
+
141
+ key = tuple(format_chain)
142
+ adapter = self._format_adapter_cache.get(key)
143
+ if adapter is not None:
144
+ return adapter
145
+
146
+ adapter = compose_from_chain(
147
+ registry=self.format_registry,
148
+ chain=format_chain,
149
+ name=f"claude_sdk_adapter_{'__'.join(format_chain)}",
150
+ )
151
+ self._format_adapter_cache[key] = adapter
152
+ return adapter
153
+
154
+ @staticmethod
155
+ async def _single_payload_stream(
156
+ payload: dict[str, Any],
157
+ ) -> AsyncIterator[dict[str, Any]]:
158
+ yield payload
159
+
160
+ async def handle_request(
161
+ self, request: Request
162
+ ) -> Response | StreamingResponse | DeferredStreaming:
163
+ # Ensure adapter is initialized
164
+ await self.initialize()
165
+
166
+ # Extract endpoint from request URL
167
+ endpoint = request.url.path
168
+ method = request.method
169
+
170
+ # Parse request body
171
+ body = await request.body()
172
+ if not body:
173
+ raise HTTPException(status_code=400, detail="Request body is required")
174
+
175
+ try:
176
+ request_data = json.loads(body)
177
+ except json.JSONDecodeError as e:
178
+ raise HTTPException(
179
+ status_code=400, detail=f"Invalid JSON: {str(e)}"
180
+ ) from e
181
+
182
+ request_context: RequestContext | None = RequestContext.get_current()
183
+ if not request_context:
184
+ raise HTTPException(
185
+ status_code=500,
186
+ detail=(
187
+ "RequestContext not available - plugin must be invoked within the "
188
+ "application request lifecycle"
189
+ ),
190
+ )
191
+
192
+ self._ensure_tool_accumulator(request_context)
193
+
194
+ format_chain = list(getattr(request_context, "format_chain", []) or [])
195
+ try:
196
+ format_adapter = self._resolve_format_adapter(format_chain)
197
+ except Exception as exc: # pragma: no cover - defensive logging in production
198
+ logger.error(
199
+ "format_adapter_resolution_failed",
200
+ error=str(exc),
201
+ format_chain=format_chain,
202
+ endpoint=endpoint,
203
+ category="format",
204
+ exc_info=exc,
205
+ )
206
+ raise HTTPException(
207
+ status_code=500,
208
+ detail="Failed to prepare format adapter for Claude SDK request",
209
+ ) from exc
210
+
211
+ if format_adapter:
212
+ try:
213
+ request_data = await format_adapter.convert_request(request_data)
214
+ except Exception as exc:
215
+ logger.error(
216
+ "format_request_conversion_failed",
217
+ error=str(exc),
218
+ format_chain=format_chain,
219
+ endpoint=endpoint,
220
+ category="format",
221
+ exc_info=exc,
222
+ )
223
+ raise HTTPException(
224
+ status_code=400,
225
+ detail="Failed to convert request payload for Claude SDK",
226
+ ) from exc
227
+
228
+ # Check if format conversion is needed (OpenAI to Anthropic)
229
+ # The endpoint will contain the path after the prefix, e.g., "/v1/chat/completions"
230
+ needs_conversion = bool(format_adapter) or endpoint.endswith(
231
+ OPENAI_CHAT_COMPLETIONS_PATH
232
+ )
233
+
234
+ # Extract parameters for SDK handler
235
+ messages = request_data.get("messages", [])
236
+ model = request_data.get("model", "claude-3-opus-20240229")
237
+ temperature = request_data.get("temperature")
238
+ max_tokens = request_data.get("max_tokens")
239
+ stream = request_data.get("stream", False)
240
+
241
+ # Get session_id from multiple sources (in priority order):
242
+ # 1. URL path (stored in request.state by the route handler)
243
+ # 2. Query parameters
244
+ # 3. Request body
245
+ # 4. Default from config (if session pool is enabled)
246
+ session_id = getattr(request.state, "session_id", None)
247
+ source = "path" if session_id else None
248
+
249
+ if not session_id and request.query_params:
250
+ session_id = request.query_params.get("session_id")
251
+ source = "query" if session_id else None
252
+
253
+ if not session_id:
254
+ session_id = request_data.get("session_id")
255
+ source = "body" if session_id else None
256
+
257
+ if (
258
+ not session_id
259
+ and self._runtime_default_session_id
260
+ and self.config.sdk_session_pool
261
+ and self.config.sdk_session_pool.enabled
262
+ ):
263
+ # Use runtime default session_id (either configured or auto-generated)
264
+ session_id = self._runtime_default_session_id
265
+ source = (
266
+ "default"
267
+ if not self.config.auto_generate_default_session
268
+ else "auto-generated"
269
+ )
270
+
271
+ # Log session_id source for debugging
272
+ if session_id:
273
+ logger.debug(
274
+ "session_id_extracted",
275
+ session_id=session_id,
276
+ source=source,
277
+ has_default_configured=bool(self.config.default_session_id),
278
+ auto_generate_enabled=self.config.auto_generate_default_session,
279
+ runtime_default=self._runtime_default_session_id,
280
+ session_pool_enabled=bool(
281
+ self.config.sdk_session_pool
282
+ and self.config.sdk_session_pool.enabled
283
+ ),
284
+ )
285
+
286
+ # Update context with claude_sdk specific metadata
287
+ request_context.metadata.update(
288
+ {
289
+ "provider": "claude_sdk",
290
+ "service_type": "claude_sdk",
291
+ "endpoint": endpoint.rstrip("/").split("/")[-1]
292
+ if endpoint
293
+ else "messages",
294
+ "model": model,
295
+ "stream": stream,
296
+ }
297
+ )
298
+
299
+ logger.info(
300
+ "plugin_request",
301
+ plugin="claude_sdk",
302
+ endpoint=endpoint,
303
+ model=model,
304
+ is_streaming=stream,
305
+ needs_conversion=needs_conversion,
306
+ session_id=session_id,
307
+ target_url=f"claude-sdk://{session_id}"
308
+ if session_id
309
+ else "claude-sdk://direct",
310
+ )
311
+
312
+ try:
313
+ # Call handler directly to create completion
314
+ if not self.handler:
315
+ raise HTTPException(status_code=503, detail="Handler not initialized")
316
+
317
+ result = await self.handler.create_completion(
318
+ request_context=request_context,
319
+ messages=messages,
320
+ model=model,
321
+ temperature=temperature,
322
+ max_tokens=max_tokens,
323
+ stream=stream,
324
+ session_id=session_id,
325
+ **{
326
+ k: v
327
+ for k, v in request_data.items()
328
+ if k
329
+ not in [
330
+ "messages",
331
+ "model",
332
+ "temperature",
333
+ "max_tokens",
334
+ "stream",
335
+ "session_id",
336
+ ]
337
+ },
338
+ )
339
+
340
+ if stream:
341
+ # Return streaming response
342
+ stream_result = cast(AsyncIterator[dict[str, Any]], result)
343
+
344
+ if format_adapter:
345
+ logger.debug(
346
+ "format_stream_adapter_applied",
347
+ format_chain=format_chain,
348
+ endpoint=endpoint,
349
+ category="format",
350
+ )
351
+ try:
352
+ converted_stream = format_adapter.convert_stream(stream_result)
353
+ except Exception as exc:
354
+ logger.error(
355
+ "format_stream_conversion_failed",
356
+ error=str(exc),
357
+ format_chain=format_chain,
358
+ endpoint=endpoint,
359
+ category="format",
360
+ exc_info=exc,
361
+ )
362
+ raise HTTPException(
363
+ status_code=500,
364
+ detail="Failed to convert Claude SDK streaming payload",
365
+ ) from exc
366
+
367
+ async def adapted_stream_generator() -> AsyncIterator[bytes]:
368
+ """Generate SSE stream from converted OpenAI-style chunks."""
369
+
370
+ try:
371
+ async for sse_chunk in serialize_json_to_sse_stream(
372
+ converted_stream,
373
+ include_done=bool(
374
+ format_chain
375
+ and format_chain[0].startswith("openai.")
376
+ ),
377
+ request_context=request_context,
378
+ ):
379
+ yield sse_chunk
380
+ except asyncio.CancelledError as exc:
381
+ logger.warning(
382
+ "streaming_cancelled",
383
+ error=str(exc),
384
+ exc_info=exc,
385
+ category="streaming",
386
+ )
387
+ raise
388
+ except httpx.TimeoutException as exc:
389
+ logger.error(
390
+ "streaming_timeout",
391
+ error=str(exc),
392
+ exc_info=exc,
393
+ category="streaming",
394
+ )
395
+ error_stream = serialize_json_to_sse_stream(
396
+ self._single_payload_stream(
397
+ {"error": "Request timed out"}
398
+ ),
399
+ include_done=False,
400
+ request_context=request_context,
401
+ )
402
+ async for error_chunk in error_stream:
403
+ yield error_chunk
404
+ except httpx.HTTPError as exc:
405
+ logger.error(
406
+ "streaming_http_error",
407
+ error=str(exc),
408
+ status_code=getattr(exc.response, "status_code", None)
409
+ if hasattr(exc, "response")
410
+ else None,
411
+ exc_info=exc,
412
+ category="streaming",
413
+ )
414
+ error_stream = serialize_json_to_sse_stream(
415
+ self._single_payload_stream(
416
+ {"error": f"HTTP error: {exc}"}
417
+ ),
418
+ include_done=False,
419
+ request_context=request_context,
420
+ )
421
+ async for error_chunk in error_stream:
422
+ yield error_chunk
423
+ except Exception as exc: # pragma: no cover - defensive
424
+ logger.error(
425
+ "streaming_unexpected_error",
426
+ error=str(exc),
427
+ exc_info=exc,
428
+ category="streaming",
429
+ )
430
+ error_stream = serialize_json_to_sse_stream(
431
+ self._single_payload_stream({"error": str(exc)}),
432
+ include_done=False,
433
+ request_context=request_context,
434
+ )
435
+ async for error_chunk in error_stream:
436
+ yield error_chunk
437
+
438
+ return StreamingResponse(
439
+ content=adapted_stream_generator(),
440
+ media_type="text/event-stream",
441
+ headers={
442
+ "Cache-Control": "no-cache",
443
+ "Connection": "keep-alive",
444
+ "X-Claude-SDK-Response": "true",
445
+ },
446
+ )
447
+
448
+ logger.debug(
449
+ "format_stream_adapter_not_used",
450
+ reason="no_format_adapter" if not format_adapter else "fallback",
451
+ format_chain=format_chain,
452
+ endpoint=endpoint,
453
+ category="format",
454
+ )
455
+
456
+ async def stream_generator() -> AsyncIterator[bytes]:
457
+ """Handle passthrough or OpenAI-format streaming."""
458
+
459
+ try:
460
+ if needs_conversion:
461
+ processor = OpenAIStreamProcessor(
462
+ model=model,
463
+ enable_usage=True,
464
+ enable_tool_calls=True,
465
+ output_format="sse",
466
+ )
467
+
468
+ async for sse_chunk in processor.process_stream(
469
+ stream_result
470
+ ):
471
+ if isinstance(sse_chunk, bytes):
472
+ yield sse_chunk
473
+ else:
474
+ yield str(sse_chunk).encode()
475
+ else:
476
+ async for chunk in serialize_json_to_sse_stream(
477
+ stream_result,
478
+ include_done=True,
479
+ request_context=request_context,
480
+ ):
481
+ if isinstance(chunk, bytes):
482
+ yield chunk
483
+ else:
484
+ yield str(chunk).encode()
485
+ except asyncio.CancelledError as exc:
486
+ logger.warning(
487
+ "streaming_cancelled",
488
+ error=str(exc),
489
+ exc_info=exc,
490
+ category="streaming",
491
+ )
492
+ raise
493
+ except httpx.TimeoutException as exc:
494
+ logger.error(
495
+ "streaming_timeout",
496
+ error=str(exc),
497
+ exc_info=exc,
498
+ category="streaming",
499
+ )
500
+ async for error_chunk in serialize_json_to_sse_stream(
501
+ self._single_payload_stream({"error": "Request timed out"}),
502
+ include_done=False,
503
+ request_context=request_context,
504
+ ):
505
+ yield error_chunk
506
+ except httpx.HTTPError as exc:
507
+ logger.error(
508
+ "streaming_http_error",
509
+ error=str(exc),
510
+ status_code=getattr(exc.response, "status_code", None)
511
+ if hasattr(exc, "response")
512
+ else None,
513
+ exc_info=exc,
514
+ category="streaming",
515
+ )
516
+ async for error_chunk in serialize_json_to_sse_stream(
517
+ self._single_payload_stream(
518
+ {"error": f"HTTP error: {exc}"}
519
+ ),
520
+ include_done=False,
521
+ request_context=request_context,
522
+ ):
523
+ yield error_chunk
524
+ except Exception as exc:
525
+ logger.error(
526
+ "streaming_unexpected_error",
527
+ error=str(exc),
528
+ exc_info=exc,
529
+ category="streaming",
530
+ )
531
+ async for error_chunk in serialize_json_to_sse_stream(
532
+ self._single_payload_stream({"error": str(exc)}),
533
+ include_done=False,
534
+ request_context=request_context,
535
+ ):
536
+ yield error_chunk
537
+
538
+ return StreamingResponse(
539
+ content=stream_generator(),
540
+ media_type="text/event-stream",
541
+ headers={
542
+ "Cache-Control": "no-cache",
543
+ "Connection": "keep-alive",
544
+ "X-Claude-SDK-Response": "true",
545
+ },
546
+ )
547
+ else:
548
+ # Convert MessageResponse to dict for JSON response
549
+ if isinstance(result, MessageResponse):
550
+ response_data = result.model_dump()
551
+ else:
552
+ # This shouldn't happen when stream=False, but handle it
553
+ response_data = cast(dict[str, Any], result)
554
+
555
+ # Convert to OpenAI format if needed
556
+ if format_adapter:
557
+ try:
558
+ response_data = await format_adapter.convert_response(
559
+ response_data
560
+ )
561
+ except Exception as exc:
562
+ logger.error(
563
+ "format_response_conversion_failed",
564
+ error=str(exc),
565
+ format_chain=format_chain,
566
+ endpoint=endpoint,
567
+ category="format",
568
+ exc_info=exc,
569
+ )
570
+ raise HTTPException(
571
+ status_code=500,
572
+ detail="Failed to convert Claude SDK response payload",
573
+ ) from exc
574
+
575
+ return Response(
576
+ content=json.dumps(response_data),
577
+ media_type="application/json",
578
+ headers={
579
+ "X-Claude-SDK-Response": "true",
580
+ },
581
+ )
582
+
583
+ except httpx.TimeoutException as e:
584
+ logger.error(
585
+ "request_timeout",
586
+ error=str(e),
587
+ exc_info=e,
588
+ category="http",
589
+ )
590
+ raise HTTPException(status_code=408, detail="Request timed out") from e
591
+ except httpx.HTTPError as e:
592
+ logger.error(
593
+ "http_error",
594
+ error=str(e),
595
+ status_code=getattr(e.response, "status_code", None)
596
+ if hasattr(e, "response")
597
+ else None,
598
+ exc_info=e,
599
+ category="http",
600
+ )
601
+ raise HTTPException(status_code=502, detail=f"HTTP error: {e}") from e
602
+ except asyncio.CancelledError as e:
603
+ logger.warning(
604
+ "request_cancelled",
605
+ error=str(e),
606
+ exc_info=e,
607
+ )
608
+ raise
609
+ except Exception as e:
610
+ logger.error(
611
+ "request_handling_failed",
612
+ error=str(e),
613
+ exc_info=e,
614
+ )
615
+ raise HTTPException(
616
+ status_code=500, detail=f"SDK request failed: {str(e)}"
617
+ ) from e
618
+
619
+ async def handle_streaming(
620
+ self, request: Request, endpoint: str, **kwargs: Any
621
+ ) -> StreamingResponse:
622
+ """Handle a streaming request through Claude SDK.
623
+
624
+ This is a convenience method that ensures stream=true and delegates
625
+ to handle_request which handles both streaming and non-streaming.
626
+
627
+ Args:
628
+ request: FastAPI request object
629
+ endpoint: Target endpoint path
630
+ **kwargs: Additional arguments
631
+
632
+ Returns:
633
+ Streaming response from Claude SDK
634
+ """
635
+ if not self._initialized:
636
+ await self.initialize()
637
+
638
+ # Parse and modify request to ensure stream=true
639
+ body = await request.body()
640
+ if not body:
641
+ request_data = {"stream": True}
642
+ else:
643
+ try:
644
+ request_data = json.loads(body)
645
+ except json.JSONDecodeError:
646
+ request_data = {"stream": True}
647
+
648
+ # Force streaming
649
+ request_data["stream"] = True
650
+ modified_body = json.dumps(request_data).encode()
651
+
652
+ # Create modified request with stream=true
653
+ modified_scope = {
654
+ **request.scope,
655
+ "_body": modified_body,
656
+ }
657
+
658
+ modified_request = StarletteRequest(
659
+ scope=modified_scope,
660
+ receive=request.receive,
661
+ )
662
+ modified_request._body = modified_body
663
+
664
+ # Delegate to handle_request which will handle streaming
665
+ result = await self.handle_request(modified_request)
666
+
667
+ # Ensure we return a streaming response
668
+ if not isinstance(result, StreamingResponse):
669
+ # This shouldn't happen since we forced stream=true, but handle it gracefully
670
+ logger.warning(
671
+ "unexpected_response_type",
672
+ expected="StreamingResponse",
673
+ actual=type(result).__name__,
674
+ )
675
+ return StreamingResponse(
676
+ iter([result.body if hasattr(result, "body") else b""]),
677
+ media_type="text/event-stream",
678
+ headers={"X-Claude-SDK-Response": "true"},
679
+ )
680
+
681
+ return result
682
+
683
+ async def cleanup(self) -> None:
684
+ """Cleanup resources when shutting down."""
685
+ try:
686
+ # Shutdown session manager first
687
+ if self.session_manager:
688
+ await self.session_manager.shutdown()
689
+ self.session_manager = None
690
+
691
+ # Close handler
692
+ if self.handler:
693
+ await self.handler.close()
694
+ self.handler = None
695
+
696
+ # Clear references to prevent memory leaks
697
+ self._detection_service = None
698
+
699
+ # Mark as not initialized
700
+ self._initialized = False
701
+
702
+ logger.debug("adapter_cleanup_completed")
703
+
704
+ except Exception as e:
705
+ logger.error(
706
+ "adapter_cleanup_failed",
707
+ error=str(e),
708
+ exc_info=e,
709
+ )
710
+
711
+ async def close(self) -> None:
712
+ """Compatibility method - delegates to cleanup()."""
713
+ await self.cleanup()
714
+
715
+ # BaseHTTPAdapter abstract method implementations
716
+ # Note: ClaudeSDK doesn't use external HTTP, so these methods are minimal implementations
717
+
718
+ async def prepare_provider_request(
719
+ self, body: bytes, headers: dict[str, str], endpoint: str
720
+ ) -> tuple[bytes, dict[str, str]]:
721
+ """Prepare request for ClaudeSDK (minimal implementation).
722
+
723
+ ClaudeSDK uses the local Claude SDK rather than making HTTP requests,
724
+ so this just passes through the body and headers.
725
+ """
726
+ return body, headers
727
+
728
+ async def process_provider_response(
729
+ self, response: "httpx.Response", endpoint: str
730
+ ) -> Response | StreamingResponse:
731
+ """Process response from ClaudeSDK (minimal implementation).
732
+
733
+ ClaudeSDK handles response processing in handle_request method,
734
+ so this should not be called in normal operation.
735
+ """
736
+ # This shouldn't be called for ClaudeSDK, but provide a fallback
737
+ return Response(
738
+ content=response.content,
739
+ status_code=response.status_code,
740
+ headers=dict(response.headers),
741
+ )
742
+
743
+ async def get_target_url(self, endpoint: str) -> str:
744
+ """Get target URL for ClaudeSDK (minimal implementation).
745
+
746
+ ClaudeSDK uses local SDK rather than HTTP URLs,
747
+ so this returns a placeholder URL.
748
+ """
749
+ return f"claude-sdk://local/{endpoint.lstrip('/')}"