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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1231
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1546 @@
1
+ """Anthropic→OpenAI streaming conversion entry points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections.abc import AsyncGenerator, AsyncIterator
8
+ from typing import Any, Literal, cast
9
+
10
+ from pydantic import ValidationError
11
+
12
+ import ccproxy.core.logging
13
+ from ccproxy.llms.formatters.common import (
14
+ IndexedToolCallTracker,
15
+ ObfuscationTokenFactory,
16
+ ToolCallState,
17
+ ensure_identifier,
18
+ )
19
+ from ccproxy.llms.formatters.constants import ANTHROPIC_TO_OPENAI_FINISH_REASON
20
+ from ccproxy.llms.formatters.context import (
21
+ get_last_instructions,
22
+ get_last_request,
23
+ register_request,
24
+ )
25
+ from ccproxy.llms.formatters.utils import anthropic_usage_snapshot
26
+ from ccproxy.llms.models import anthropic as anthropic_models
27
+ from ccproxy.llms.models import openai as openai_models
28
+ from ccproxy.llms.streaming.accumulators import ClaudeAccumulator
29
+
30
+ from .requests import _build_responses_payload_from_anthropic_request
31
+ from .responses import (
32
+ convert__anthropic_usage_to_openai_responses__usage,
33
+ )
34
+
35
+
36
+ logger = ccproxy.core.logging.get_logger(__name__)
37
+
38
+ FinishReason = Literal["stop", "length", "tool_calls"]
39
+
40
+
41
+ def _normalize_anthropic_stream_event(
42
+ event: Any,
43
+ ) -> tuple[str | None, dict[str, Any]]:
44
+ """Return a (type, payload) tuple for mixed dict/model stream events."""
45
+
46
+ if isinstance(event, dict):
47
+ event_type = event.get("type") or event.get("event")
48
+ return (cast(str | None, event_type), event)
49
+
50
+ event_type = getattr(event, "type", None)
51
+ if event_type is None:
52
+ return None, {}
53
+
54
+ if hasattr(event, "model_dump"):
55
+ payload = cast(dict[str, Any], event.model_dump(mode="json"))
56
+ elif hasattr(event, "dict"):
57
+ payload = cast(dict[str, Any], event.dict())
58
+ else:
59
+ payload = {}
60
+
61
+ return cast(str | None, event_type), payload
62
+
63
+
64
+ def _anthropic_delta_to_text(
65
+ accumulator: ClaudeAccumulator,
66
+ block_index: int,
67
+ delta: dict[str, Any] | None,
68
+ ) -> str | None:
69
+ if not isinstance(delta, dict):
70
+ return None
71
+
72
+ block_info = accumulator.get_block_info(block_index)
73
+ block_meta = block_info[1] if block_info else {}
74
+ block_type = block_meta.get("type")
75
+
76
+ if block_type == "thinking":
77
+ thinking_text = delta.get("thinking")
78
+ if not isinstance(thinking_text, str) or not thinking_text:
79
+ return None
80
+ signature = block_meta.get("signature")
81
+ if isinstance(signature, str) and signature:
82
+ return f'<thinking signature="{signature}">{thinking_text}</thinking>'
83
+ return f"<thinking>{thinking_text}</thinking>"
84
+
85
+ text_val = delta.get("text")
86
+ if isinstance(text_val, str) and text_val:
87
+ return text_val
88
+
89
+ return None
90
+
91
+
92
+ def _build_openai_tool_call(
93
+ accumulator: ClaudeAccumulator,
94
+ block_index: int,
95
+ ) -> openai_models.ToolCall | None:
96
+ for tool_call in accumulator.get_complete_tool_calls():
97
+ if tool_call.get("index") != block_index:
98
+ continue
99
+
100
+ function_payload = (
101
+ tool_call.get("function", {}) if isinstance(tool_call, dict) else {}
102
+ )
103
+ name = function_payload.get("name") or tool_call.get("name") or "function"
104
+ arguments = function_payload.get("arguments")
105
+ if not isinstance(arguments, str) or not arguments:
106
+ try:
107
+ arguments = json.dumps(tool_call.get("input", {}), ensure_ascii=False)
108
+ except Exception:
109
+ arguments = json.dumps(tool_call.get("input", {}))
110
+
111
+ tool_id = tool_call.get("id") or f"call_{block_index}"
112
+
113
+ return openai_models.ToolCall(
114
+ id=str(tool_id),
115
+ function=openai_models.FunctionCall(
116
+ name=str(name),
117
+ arguments=str(arguments),
118
+ ),
119
+ )
120
+
121
+ return None
122
+
123
+
124
+ class AnthropicToOpenAIResponsesStreamAdapter:
125
+ """Stateful adapter for Anthropic → OpenAI Responses streaming."""
126
+
127
+ async def run(
128
+ self,
129
+ stream: AsyncIterator[anthropic_models.MessageStreamEvent],
130
+ ) -> AsyncGenerator[openai_models.StreamEventType, None]:
131
+ async for event in self._convert_responses_stream(stream):
132
+ yield event
133
+
134
+ async def _convert_responses_stream(
135
+ self,
136
+ stream: AsyncIterator[anthropic_models.MessageStreamEvent],
137
+ ) -> AsyncGenerator[openai_models.StreamEventType, None]:
138
+ """Convert Anthropic MessageStreamEvents into OpenAI Responses stream events."""
139
+
140
+ accumulator = ClaudeAccumulator()
141
+ sequence_counter = -1
142
+ model_id = ""
143
+ response_id = ""
144
+ id_suffix: str | None = None
145
+ message_item_id = ""
146
+ message_output_index: int | None = None
147
+ next_output_index = 0
148
+ content_index = 0
149
+ message_item_added = False
150
+ message_content_part_added = False
151
+ text_buffer: list[str] = []
152
+ message_last_logprobs: Any | None = None
153
+ message_text_done_emitted = False
154
+ message_part_done_emitted = False
155
+ message_item_done_emitted = False
156
+ message_completed_entry: tuple[int, openai_models.MessageOutput] | None = None
157
+ latest_usage_model: openai_models.ResponseUsage | None = None
158
+ final_stop_reason: str | None = None
159
+ stream_completed = False
160
+
161
+ reasoning_item_id = ""
162
+ reasoning_output_index: int | None = None
163
+ reasoning_item_added = False
164
+ reasoning_output_done = False
165
+ reasoning_summary_indices: dict[str, int] = {}
166
+ reasoning_summary_added: set[int] = set()
167
+ reasoning_summary_text_fragments: dict[int, list[str]] = {}
168
+ reasoning_summary_text_done: set[int] = set()
169
+ reasoning_summary_part_done: set[int] = set()
170
+ reasoning_completed_entry: tuple[int, openai_models.ReasoningOutput] | None = (
171
+ None
172
+ )
173
+ next_reasoning_summary_index = 0
174
+ reasoning_summary_signatures: dict[int, str | None] = {}
175
+ created_at_value: int | None = None
176
+
177
+ instructions_text = get_last_instructions()
178
+ if not instructions_text:
179
+ try:
180
+ from ccproxy.core.request_context import RequestContext
181
+
182
+ ctx = RequestContext.get_current()
183
+ if ctx is not None:
184
+ instr = ctx.metadata.get("instructions")
185
+ if isinstance(instr, str) and instr.strip():
186
+ instructions_text = instr.strip()
187
+ except Exception:
188
+ pass
189
+
190
+ instructions_value = instructions_text or None
191
+
192
+ envelope_base_kwargs: dict[str, Any] = {
193
+ "id": "",
194
+ "object": "response",
195
+ "created_at": 0,
196
+ "instructions": instructions_value,
197
+ }
198
+ reasoning_summary_payload: list[dict[str, Any]] | None = None
199
+
200
+ last_request = get_last_request()
201
+ anthropic_request: anthropic_models.CreateMessageRequest | None = None
202
+ if isinstance(last_request, anthropic_models.CreateMessageRequest):
203
+ anthropic_request = last_request
204
+ elif isinstance(last_request, dict):
205
+ try:
206
+ anthropic_request = (
207
+ anthropic_models.CreateMessageRequest.model_validate(last_request)
208
+ )
209
+ except ValidationError:
210
+ anthropic_request = None
211
+
212
+ base_parallel_tool_calls = True
213
+ text_payload: dict[str, Any] | None = None
214
+
215
+ if anthropic_request is not None:
216
+ payload_data, _ = _build_responses_payload_from_anthropic_request(
217
+ anthropic_request
218
+ )
219
+ base_parallel_tool_calls = bool(
220
+ payload_data.get("parallel_tool_calls", True)
221
+ )
222
+ background_value = payload_data.get("background", None)
223
+ envelope_base_kwargs["background"] = (
224
+ bool(background_value) if background_value is not None else None
225
+ )
226
+ for key in (
227
+ "max_output_tokens",
228
+ "tool_choice",
229
+ "tools",
230
+ "service_tier",
231
+ "temperature",
232
+ "prompt_cache_key",
233
+ "top_p",
234
+ "metadata",
235
+ ):
236
+ if key in payload_data:
237
+ envelope_base_kwargs[key] = payload_data[key]
238
+ text_payload = payload_data.get("text")
239
+ else:
240
+ envelope_base_kwargs["background"] = None
241
+
242
+ if text_payload is None:
243
+ text_payload = {"format": {"type": "text"}}
244
+ else:
245
+ text_payload = dict(text_payload)
246
+ text_payload.setdefault("verbosity", "low")
247
+ envelope_base_kwargs["text"] = text_payload
248
+
249
+ if "store" not in envelope_base_kwargs:
250
+ envelope_base_kwargs["store"] = True
251
+
252
+ if "temperature" not in envelope_base_kwargs:
253
+ temp_value = None
254
+ if anthropic_request is not None:
255
+ temp_value = anthropic_request.temperature
256
+ envelope_base_kwargs["temperature"] = (
257
+ temp_value if temp_value is not None else 1.0
258
+ )
259
+
260
+ if "service_tier" not in envelope_base_kwargs:
261
+ service_value = None
262
+ if anthropic_request is not None:
263
+ service_value = anthropic_request.service_tier
264
+ envelope_base_kwargs["service_tier"] = service_value or "auto"
265
+
266
+ if "top_p" not in envelope_base_kwargs:
267
+ top_p_value = None
268
+ if anthropic_request is not None:
269
+ top_p_value = anthropic_request.top_p
270
+ envelope_base_kwargs["top_p"] = (
271
+ top_p_value if top_p_value is not None else 1.0
272
+ )
273
+
274
+ if "metadata" not in envelope_base_kwargs:
275
+ envelope_base_kwargs["metadata"] = {}
276
+
277
+ reasoning_effort = None
278
+ if anthropic_request is not None:
279
+ thinking_cfg = getattr(anthropic_request, "thinking", None)
280
+ if getattr(thinking_cfg, "type", None) == "enabled":
281
+ reasoning_effort = "medium"
282
+ envelope_base_kwargs["reasoning"] = openai_models.Reasoning(
283
+ effort=reasoning_effort,
284
+ summary=None,
285
+ )
286
+
287
+ if "tool_choice" not in envelope_base_kwargs:
288
+ envelope_base_kwargs["tool_choice"] = "auto"
289
+ if "tools" not in envelope_base_kwargs:
290
+ envelope_base_kwargs["tools"] = []
291
+
292
+ parallel_setting_initial = bool(base_parallel_tool_calls)
293
+ envelope_base_kwargs["parallel_tool_calls"] = parallel_setting_initial
294
+
295
+ tool_states = IndexedToolCallTracker()
296
+ obfuscation_factory = ObfuscationTokenFactory(
297
+ lambda: id_suffix or response_id or "stream"
298
+ )
299
+
300
+ def ensure_message_output_item() -> list[openai_models.StreamEventType]:
301
+ nonlocal message_item_added, message_output_index, next_output_index
302
+ events: list[openai_models.StreamEventType] = []
303
+ if message_output_index is None:
304
+ message_output_index = next_output_index
305
+ next_output_index += 1
306
+ if not message_item_added:
307
+ message_item_added = True
308
+ nonlocal sequence_counter
309
+ sequence_counter += 1
310
+ events.append(
311
+ openai_models.ResponseOutputItemAddedEvent(
312
+ type="response.output_item.added",
313
+ sequence_number=sequence_counter,
314
+ output_index=message_output_index,
315
+ item=openai_models.OutputItem(
316
+ id=message_item_id,
317
+ type="message",
318
+ role="assistant",
319
+ status="in_progress",
320
+ content=[],
321
+ ),
322
+ )
323
+ )
324
+ return events
325
+
326
+ def ensure_message_content_part() -> list[openai_models.StreamEventType]:
327
+ events = ensure_message_output_item()
328
+ nonlocal message_content_part_added, sequence_counter
329
+ if not message_content_part_added and message_output_index is not None:
330
+ message_content_part_added = True
331
+ sequence_counter += 1
332
+ events.append(
333
+ openai_models.ResponseContentPartAddedEvent(
334
+ type="response.content_part.added",
335
+ sequence_number=sequence_counter,
336
+ item_id=message_item_id,
337
+ output_index=message_output_index,
338
+ content_index=content_index,
339
+ part=openai_models.ContentPart(
340
+ type="output_text",
341
+ text="",
342
+ annotations=[],
343
+ ),
344
+ )
345
+ )
346
+ return events
347
+
348
+ def emit_message_text_delta(
349
+ text_delta: str,
350
+ *,
351
+ logprobs: Any | None = None,
352
+ obfuscation: str | None = None,
353
+ ) -> list[openai_models.StreamEventType]:
354
+ if not isinstance(text_delta, str) or not text_delta:
355
+ return []
356
+
357
+ nonlocal sequence_counter, message_last_logprobs, message_item_done_emitted
358
+ if message_item_done_emitted:
359
+ return []
360
+
361
+ events = ensure_message_content_part()
362
+ sequence_counter += 1
363
+ event_sequence = sequence_counter
364
+ logprobs_value: Any = [] if logprobs is None else logprobs
365
+ events.append(
366
+ openai_models.ResponseOutputTextDeltaEvent(
367
+ type="response.output_text.delta",
368
+ sequence_number=event_sequence,
369
+ item_id=message_item_id,
370
+ output_index=message_output_index or 0,
371
+ content_index=content_index,
372
+ delta=text_delta,
373
+ logprobs=logprobs_value,
374
+ )
375
+ )
376
+ text_buffer.append(text_delta)
377
+ message_last_logprobs = logprobs_value
378
+ return events
379
+
380
+ def _reasoning_key(signature: str | None) -> str:
381
+ if isinstance(signature, str) and signature.strip():
382
+ return signature.strip()
383
+ return "__default__"
384
+
385
+ def get_reasoning_summary_index(signature: str | None) -> int:
386
+ nonlocal next_reasoning_summary_index
387
+ key = _reasoning_key(signature)
388
+ existing = reasoning_summary_indices.get(key)
389
+ if existing is not None:
390
+ return existing
391
+ reasoning_summary_indices[key] = next_reasoning_summary_index
392
+ reasoning_summary_signatures[next_reasoning_summary_index] = signature
393
+ next_reasoning_summary_index += 1
394
+ return reasoning_summary_indices[key]
395
+
396
+ def ensure_reasoning_output_item() -> (
397
+ openai_models.ResponseOutputItemAddedEvent | None
398
+ ):
399
+ nonlocal reasoning_item_added, reasoning_output_index
400
+ nonlocal sequence_counter, next_output_index
401
+ if reasoning_output_index is None:
402
+ reasoning_output_index = next_output_index
403
+ next_output_index += 1
404
+ if not reasoning_item_added:
405
+ reasoning_item_added = True
406
+ sequence_counter += 1
407
+ return openai_models.ResponseOutputItemAddedEvent(
408
+ type="response.output_item.added",
409
+ sequence_number=sequence_counter,
410
+ output_index=reasoning_output_index,
411
+ item=openai_models.OutputItem(
412
+ id=reasoning_item_id,
413
+ type="reasoning",
414
+ status="in_progress",
415
+ summary=[],
416
+ ),
417
+ )
418
+ return None
419
+
420
+ def ensure_reasoning_summary_part(
421
+ summary_index: int,
422
+ ) -> openai_models.ReasoningSummaryPartAddedEvent | None:
423
+ nonlocal sequence_counter
424
+ if reasoning_output_index is None:
425
+ return None
426
+ if summary_index in reasoning_summary_added:
427
+ return None
428
+ reasoning_summary_added.add(summary_index)
429
+ sequence_counter += 1
430
+ return openai_models.ReasoningSummaryPartAddedEvent(
431
+ type="response.reasoning_summary_part.added",
432
+ sequence_number=sequence_counter,
433
+ item_id=reasoning_item_id,
434
+ output_index=reasoning_output_index,
435
+ summary_index=summary_index,
436
+ part=openai_models.ReasoningSummaryPart(
437
+ type="summary_text",
438
+ text="",
439
+ ),
440
+ )
441
+
442
+ def emit_reasoning_text_delta(
443
+ text_delta: str,
444
+ signature: str | None,
445
+ ) -> list[openai_models.StreamEventType]:
446
+ if not isinstance(text_delta, str) or not text_delta:
447
+ return []
448
+
449
+ events: list[openai_models.StreamEventType] = []
450
+ output_event = ensure_reasoning_output_item()
451
+ if output_event is not None:
452
+ events.append(output_event)
453
+
454
+ summary_index = get_reasoning_summary_index(signature)
455
+ part_event = ensure_reasoning_summary_part(summary_index)
456
+ if part_event is not None:
457
+ events.append(part_event)
458
+
459
+ fragments = reasoning_summary_text_fragments.setdefault(summary_index, [])
460
+ fragments.append(text_delta)
461
+ if summary_index not in reasoning_summary_signatures:
462
+ reasoning_summary_signatures[summary_index] = signature
463
+
464
+ nonlocal sequence_counter
465
+ sequence_counter += 1
466
+ event_sequence = sequence_counter
467
+ events.append(
468
+ openai_models.ReasoningSummaryTextDeltaEvent(
469
+ type="response.reasoning_summary_text.delta",
470
+ sequence_number=event_sequence,
471
+ item_id=reasoning_item_id,
472
+ output_index=reasoning_output_index or 0,
473
+ summary_index=summary_index,
474
+ delta=text_delta,
475
+ )
476
+ )
477
+ return events
478
+
479
+ def finalize_reasoning() -> list[openai_models.StreamEventType]:
480
+ nonlocal reasoning_output_done, reasoning_completed_entry
481
+ nonlocal reasoning_summary_payload, sequence_counter
482
+ if not reasoning_item_added or reasoning_output_index is None:
483
+ return []
484
+
485
+ events: list[openai_models.StreamEventType] = []
486
+ summary_entries: list[dict[str, Any]] = []
487
+
488
+ for summary_index in sorted(reasoning_summary_text_fragments):
489
+ text_value = "".join(
490
+ reasoning_summary_text_fragments.get(summary_index, [])
491
+ )
492
+ if summary_index not in reasoning_summary_text_done:
493
+ sequence_counter += 1
494
+ events.append(
495
+ openai_models.ReasoningSummaryTextDoneEvent(
496
+ type="response.reasoning_summary_text.done",
497
+ sequence_number=sequence_counter,
498
+ item_id=reasoning_item_id,
499
+ output_index=reasoning_output_index,
500
+ summary_index=summary_index,
501
+ text=text_value,
502
+ )
503
+ )
504
+ reasoning_summary_text_done.add(summary_index)
505
+ if summary_index not in reasoning_summary_part_done:
506
+ sequence_counter += 1
507
+ events.append(
508
+ openai_models.ReasoningSummaryPartDoneEvent(
509
+ type="response.reasoning_summary_part.done",
510
+ sequence_number=sequence_counter,
511
+ item_id=reasoning_item_id,
512
+ output_index=reasoning_output_index,
513
+ summary_index=summary_index,
514
+ part=openai_models.ReasoningSummaryPart(
515
+ type="summary_text",
516
+ text=text_value,
517
+ ),
518
+ )
519
+ )
520
+ reasoning_summary_part_done.add(summary_index)
521
+ summary_entry: dict[str, Any] = {
522
+ "type": "summary_text",
523
+ "text": text_value,
524
+ }
525
+ signature_value = reasoning_summary_signatures.get(summary_index)
526
+ if signature_value:
527
+ summary_entry["signature"] = signature_value
528
+ summary_entries.append(summary_entry)
529
+
530
+ reasoning_summary_payload = summary_entries
531
+
532
+ if not reasoning_output_done:
533
+ sequence_counter += 1
534
+ events.append(
535
+ openai_models.ResponseOutputItemDoneEvent(
536
+ type="response.output_item.done",
537
+ sequence_number=sequence_counter,
538
+ output_index=reasoning_output_index,
539
+ item=openai_models.OutputItem(
540
+ id=reasoning_item_id,
541
+ type="reasoning",
542
+ status="completed",
543
+ summary=summary_entries,
544
+ ),
545
+ )
546
+ )
547
+ reasoning_output_done = True
548
+ reasoning_completed_entry = (
549
+ reasoning_output_index,
550
+ openai_models.ReasoningOutput(
551
+ type="reasoning",
552
+ id=reasoning_item_id,
553
+ status="completed",
554
+ summary=summary_entries,
555
+ ),
556
+ )
557
+
558
+ return events
559
+
560
+ def ensure_tool_state(block_index: int) -> ToolCallState:
561
+ nonlocal next_output_index
562
+ state = tool_states.ensure(block_index)
563
+ if state.output_index < 0:
564
+ state.output_index = next_output_index
565
+ next_output_index += 1
566
+ return state
567
+
568
+ def emit_tool_item_added(
569
+ block_index: int, state: ToolCallState
570
+ ) -> list[openai_models.StreamEventType]:
571
+ events: list[openai_models.StreamEventType] = []
572
+ if state.added_emitted:
573
+ return events
574
+
575
+ tool_entry = accumulator.get_tool_entry(block_index)
576
+ if tool_entry:
577
+ if not state.name:
578
+ state.name = tool_entry.get("function", {}).get(
579
+ "name"
580
+ ) or tool_entry.get("name")
581
+ if not state.call_id:
582
+ state.call_id = tool_entry.get("id")
583
+
584
+ item_id = state.item_id or state.call_id or f"call_{state.index}"
585
+ state.item_id = item_id
586
+
587
+ name = state.name or "function"
588
+
589
+ nonlocal sequence_counter
590
+ sequence_counter += 1
591
+ events.append(
592
+ openai_models.ResponseOutputItemAddedEvent(
593
+ type="response.output_item.added",
594
+ sequence_number=sequence_counter,
595
+ output_index=state.output_index,
596
+ item=openai_models.OutputItem(
597
+ id=str(item_id),
598
+ type="function_call",
599
+ status="in_progress",
600
+ name=str(name),
601
+ arguments="",
602
+ call_id=state.call_id,
603
+ ),
604
+ )
605
+ )
606
+ state.added_emitted = True
607
+ return events
608
+
609
+ def emit_tool_arguments_delta(
610
+ state: ToolCallState, delta_text: str
611
+ ) -> openai_models.StreamEventType:
612
+ nonlocal sequence_counter
613
+ sequence_counter += 1
614
+ event_sequence = sequence_counter
615
+ state.add_arguments_part(delta_text)
616
+ item_identifier = str(state.item_id or f"call_{state.index}")
617
+ return openai_models.ResponseFunctionCallArgumentsDeltaEvent(
618
+ type="response.function_call_arguments.delta",
619
+ sequence_number=event_sequence,
620
+ item_id=item_identifier,
621
+ output_index=state.output_index,
622
+ delta=delta_text,
623
+ )
624
+
625
+ def emit_tool_finalize(
626
+ block_index: int, state: ToolCallState
627
+ ) -> list[openai_models.StreamEventType]:
628
+ events: list[openai_models.StreamEventType] = []
629
+ tool_entry = accumulator.get_tool_entry(block_index)
630
+
631
+ if tool_entry:
632
+ if not state.name:
633
+ state.name = tool_entry.get("function", {}).get(
634
+ "name"
635
+ ) or tool_entry.get("name")
636
+ if not state.call_id:
637
+ state.call_id = tool_entry.get("id")
638
+ if not state.item_id:
639
+ state.item_id = tool_entry.get("id")
640
+
641
+ item_id = state.item_id or state.call_id or f"call_{state.index}"
642
+ state.item_id = item_id
643
+ name = state.name or "function"
644
+
645
+ args_str = "".join(state.arguments_parts)
646
+ if not args_str and tool_entry:
647
+ try:
648
+ args_str = json.dumps(
649
+ tool_entry.get("input", {}), ensure_ascii=False
650
+ )
651
+ except Exception:
652
+ args_str = json.dumps(tool_entry.get("input", {}))
653
+
654
+ nonlocal sequence_counter
655
+ if not state.added_emitted:
656
+ events.extend(emit_tool_item_added(block_index, state))
657
+
658
+ if not state.arguments_done_emitted:
659
+ sequence_counter += 1
660
+ events.append(
661
+ openai_models.ResponseFunctionCallArgumentsDoneEvent(
662
+ type="response.function_call_arguments.done",
663
+ sequence_number=sequence_counter,
664
+ item_id=str(item_id),
665
+ output_index=state.output_index,
666
+ arguments=args_str,
667
+ )
668
+ )
669
+ state.arguments_done_emitted = True
670
+
671
+ if not state.item_done_emitted:
672
+ sequence_counter += 1
673
+ events.append(
674
+ openai_models.ResponseOutputItemDoneEvent(
675
+ type="response.output_item.done",
676
+ sequence_number=sequence_counter,
677
+ output_index=state.output_index,
678
+ item=openai_models.OutputItem(
679
+ id=str(item_id),
680
+ type="function_call",
681
+ status="completed",
682
+ name=str(name),
683
+ arguments=args_str,
684
+ call_id=state.call_id,
685
+ ),
686
+ )
687
+ )
688
+ state.item_done_emitted = True
689
+ state.final_arguments = args_str
690
+
691
+ return events
692
+
693
+ def finalize_message() -> list[openai_models.StreamEventType]:
694
+ nonlocal sequence_counter
695
+ nonlocal message_text_done_emitted, message_part_done_emitted
696
+ nonlocal message_item_done_emitted, message_completed_entry
697
+ nonlocal message_last_logprobs
698
+ nonlocal accumulator
699
+
700
+ if not message_item_added or message_output_index is None:
701
+ return []
702
+
703
+ events: list[openai_models.StreamEventType] = []
704
+ final_text = "".join(text_buffer)
705
+ logprobs_value: Any
706
+ if message_last_logprobs is None:
707
+ logprobs_value = []
708
+ else:
709
+ logprobs_value = message_last_logprobs
710
+
711
+ primary_text_part: openai_models.OutputTextContent | None = None
712
+ tool_and_aux_blocks: list[Any] = []
713
+
714
+ if accumulator.content_blocks:
715
+ sorted_blocks = sorted(
716
+ accumulator.content_blocks, key=lambda block: block.get("index", 0)
717
+ )
718
+ for block in sorted_blocks:
719
+ block_type = block.get("type")
720
+ if block_type == "text":
721
+ text_value = block.get("text", "")
722
+ part = openai_models.OutputTextContent(
723
+ type="output_text",
724
+ text=text_value,
725
+ annotations=[],
726
+ logprobs=logprobs_value if text_value else [],
727
+ )
728
+ if primary_text_part is None and text_value:
729
+ primary_text_part = part
730
+ tool_and_aux_blocks.append(part)
731
+ else:
732
+ block_payload = {k: v for k, v in block.items() if k != "index"}
733
+ if block_payload.get("type") == "tool_use":
734
+ tool_input = block_payload.get("input")
735
+ if tool_input is not None:
736
+ block_payload.setdefault("arguments", tool_input)
737
+ tool_and_aux_blocks.append(block_payload)
738
+
739
+ if primary_text_part is None and final_text:
740
+ primary_text_part = openai_models.OutputTextContent(
741
+ type="output_text",
742
+ text=final_text,
743
+ annotations=[],
744
+ logprobs=logprobs_value if final_text else [],
745
+ )
746
+ tool_and_aux_blocks.insert(0, primary_text_part)
747
+
748
+ if message_content_part_added and not message_text_done_emitted:
749
+ sequence_counter += 1
750
+ event_sequence = sequence_counter
751
+ events.append(
752
+ openai_models.ResponseOutputTextDoneEvent(
753
+ type="response.output_text.done",
754
+ sequence_number=event_sequence,
755
+ item_id=message_item_id,
756
+ output_index=message_output_index,
757
+ content_index=content_index,
758
+ text=final_text,
759
+ logprobs=logprobs_value,
760
+ )
761
+ )
762
+ message_text_done_emitted = True
763
+
764
+ if message_content_part_added and not message_part_done_emitted:
765
+ sequence_counter += 1
766
+ event_sequence = sequence_counter
767
+ events.append(
768
+ openai_models.ResponseContentPartDoneEvent(
769
+ type="response.content_part.done",
770
+ sequence_number=event_sequence,
771
+ item_id=message_item_id,
772
+ output_index=message_output_index,
773
+ content_index=content_index,
774
+ part=openai_models.ContentPart(
775
+ type="output_text",
776
+ text=final_text,
777
+ annotations=[],
778
+ ),
779
+ )
780
+ )
781
+ message_part_done_emitted = True
782
+
783
+ if not message_item_done_emitted:
784
+ sequence_counter += 1
785
+ event_sequence = sequence_counter
786
+ if primary_text_part is None:
787
+ primary_text_part = openai_models.OutputTextContent(
788
+ type="output_text",
789
+ text=final_text,
790
+ annotations=[],
791
+ logprobs=logprobs_value if logprobs_value != [] else [],
792
+ )
793
+ tool_and_aux_blocks.insert(0, primary_text_part)
794
+ message_output = openai_models.MessageOutput(
795
+ type="message",
796
+ id=message_item_id,
797
+ status="completed",
798
+ role="assistant",
799
+ content=tool_and_aux_blocks,
800
+ )
801
+ message_completed_entry = (message_output_index, message_output)
802
+ events.append(
803
+ openai_models.ResponseOutputItemDoneEvent(
804
+ type="response.output_item.done",
805
+ sequence_number=event_sequence,
806
+ output_index=message_output_index,
807
+ item=openai_models.OutputItem(
808
+ id=message_item_id,
809
+ type="message",
810
+ role="assistant",
811
+ status="completed",
812
+ content=[
813
+ part.model_dump()
814
+ if hasattr(part, "model_dump")
815
+ else part
816
+ for part in tool_and_aux_blocks
817
+ ],
818
+ text=final_text or None,
819
+ ),
820
+ )
821
+ )
822
+ message_item_done_emitted = True
823
+ else:
824
+ if primary_text_part is None and final_text:
825
+ primary_text_part = openai_models.OutputTextContent(
826
+ type="output_text",
827
+ text=final_text,
828
+ annotations=[],
829
+ logprobs=logprobs_value if logprobs_value != [] else [],
830
+ )
831
+ tool_and_aux_blocks.insert(0, primary_text_part)
832
+ message_completed_entry = (
833
+ message_output_index,
834
+ openai_models.MessageOutput(
835
+ type="message",
836
+ id=message_item_id,
837
+ status="completed",
838
+ role="assistant",
839
+ content=tool_and_aux_blocks,
840
+ ),
841
+ )
842
+
843
+ return events
844
+
845
+ def make_response_object(
846
+ *,
847
+ status: str,
848
+ model: str | None,
849
+ usage: openai_models.ResponseUsage | None = None,
850
+ output: list[Any] | None = None,
851
+ parallel_override: bool | None = None,
852
+ reasoning_summary: list[dict[str, Any]] | None = None,
853
+ extra: dict[str, Any] | None = None,
854
+ ) -> openai_models.ResponseObject:
855
+ payload = dict(envelope_base_kwargs)
856
+ payload["status"] = status
857
+ payload["model"] = model or payload.get("model") or ""
858
+ payload["output"] = output or []
859
+ payload["usage"] = usage
860
+ payload.setdefault("object", "response")
861
+ payload.setdefault("created_at", int(time.time()))
862
+ if parallel_override is not None:
863
+ payload["parallel_tool_calls"] = parallel_override
864
+ if reasoning_summary is not None:
865
+ reasoning_entry = payload.get("reasoning")
866
+ if isinstance(reasoning_entry, openai_models.Reasoning):
867
+ payload["reasoning"] = reasoning_entry.model_copy(
868
+ update={"summary": reasoning_summary}
869
+ )
870
+ elif isinstance(reasoning_entry, dict):
871
+ payload["reasoning"] = openai_models.Reasoning(
872
+ effort=reasoning_entry.get("effort"),
873
+ summary=reasoning_summary,
874
+ )
875
+ else:
876
+ payload["reasoning"] = openai_models.Reasoning(
877
+ effort=None,
878
+ summary=reasoning_summary,
879
+ )
880
+ if extra:
881
+ payload.update(extra)
882
+ return openai_models.ResponseObject(**payload)
883
+
884
+ try:
885
+ async for raw_event in stream:
886
+ event_type, event_payload = _normalize_anthropic_stream_event(raw_event)
887
+ if not event_type:
888
+ continue
889
+
890
+ accumulator.accumulate(event_type, event_payload)
891
+
892
+ if event_type == "ping":
893
+ continue
894
+
895
+ if event_type == "error":
896
+ continue
897
+
898
+ if event_type == "message_start":
899
+ message = (
900
+ event_payload.get("message", {})
901
+ if isinstance(event_payload, dict)
902
+ else {}
903
+ )
904
+ model_id = str(message.get("model", ""))
905
+ response_id, id_suffix = ensure_identifier(
906
+ "resp", message.get("id")
907
+ )
908
+ envelope_base_kwargs["id"] = response_id
909
+ envelope_base_kwargs.setdefault("object", "response")
910
+ if model_id:
911
+ envelope_base_kwargs["model"] = model_id
912
+ if not message_item_id:
913
+ message_item_id = f"msg_{id_suffix}"
914
+ if not reasoning_item_id:
915
+ reasoning_item_id = f"rs_{id_suffix}"
916
+
917
+ created_at_value = (
918
+ message.get("created_at")
919
+ or message.get("created")
920
+ or int(time.time())
921
+ )
922
+ envelope_base_kwargs["created_at"] = int(created_at_value)
923
+
924
+ sequence_counter += 1
925
+ yield openai_models.ResponseCreatedEvent(
926
+ type="response.created",
927
+ sequence_number=sequence_counter,
928
+ response=make_response_object(
929
+ status="in_progress",
930
+ model=model_id,
931
+ usage=None,
932
+ output=[],
933
+ parallel_override=parallel_setting_initial,
934
+ ),
935
+ )
936
+ sequence_counter += 1
937
+ yield openai_models.ResponseInProgressEvent(
938
+ type="response.in_progress",
939
+ sequence_number=sequence_counter,
940
+ response=make_response_object(
941
+ status="in_progress",
942
+ model=model_id,
943
+ usage=latest_usage_model,
944
+ output=[],
945
+ parallel_override=parallel_setting_initial,
946
+ ),
947
+ )
948
+ continue
949
+
950
+ if event_type == "content_block_start":
951
+ block_index = int(event_payload.get("index", 0))
952
+ content_block = (
953
+ event_payload.get("content_block", {})
954
+ if isinstance(event_payload, dict)
955
+ else {}
956
+ )
957
+ if (
958
+ isinstance(content_block, dict)
959
+ and content_block.get("type") == "tool_use"
960
+ ):
961
+ state = ensure_tool_state(block_index)
962
+ name_value = content_block.get("name")
963
+ if isinstance(name_value, str) and name_value:
964
+ state.name = state.name or name_value
965
+ block_id = content_block.get("id")
966
+ if isinstance(block_id, str) and block_id:
967
+ if not state.call_id:
968
+ state.call_id = block_id
969
+ if not state.item_id:
970
+ state.item_id = block_id
971
+ for event in finalize_message():
972
+ yield event
973
+ for event in emit_tool_item_added(block_index, state):
974
+ yield event
975
+ continue
976
+
977
+ if event_type == "content_block_delta":
978
+ block_index = int(event_payload.get("index", 0))
979
+ block_info = accumulator.get_block_info(block_index)
980
+ if not block_info:
981
+ continue
982
+ _, block_meta = block_info
983
+ delta_payload = event_payload.get("delta")
984
+
985
+ block_type = block_meta.get("type")
986
+
987
+ if block_type == "thinking" and isinstance(delta_payload, dict):
988
+ thinking_text = delta_payload.get("thinking")
989
+ if isinstance(thinking_text, str) and thinking_text:
990
+ signature = block_meta.get("signature")
991
+ for event in emit_reasoning_text_delta(
992
+ thinking_text, signature
993
+ ):
994
+ yield event
995
+ continue
996
+
997
+ if block_type == "text" and isinstance(delta_payload, dict):
998
+ text_delta = delta_payload.get("text")
999
+ if isinstance(text_delta, str) and text_delta:
1000
+ for event in emit_message_text_delta(
1001
+ text_delta,
1002
+ logprobs=delta_payload.get("logprobs"),
1003
+ obfuscation=delta_payload.get("obfuscation")
1004
+ or delta_payload.get("obfuscated"),
1005
+ ):
1006
+ yield event
1007
+ continue
1008
+
1009
+ if block_type == "tool_use" and isinstance(delta_payload, dict):
1010
+ partial = delta_payload.get("partial_json") or ""
1011
+ if partial:
1012
+ state = ensure_tool_state(block_index)
1013
+ for event in finalize_message():
1014
+ yield event
1015
+ for event in emit_tool_item_added(block_index, state):
1016
+ yield event
1017
+ yield emit_tool_arguments_delta(
1018
+ state,
1019
+ str(partial),
1020
+ )
1021
+ continue
1022
+
1023
+ if event_type == "content_block_stop":
1024
+ block_index = int(event_payload.get("index", 0))
1025
+ block_info = accumulator.get_block_info(block_index)
1026
+ if block_info and block_info[1].get("type") == "tool_use":
1027
+ state = ensure_tool_state(block_index)
1028
+ for event in emit_tool_finalize(block_index, state):
1029
+ yield event
1030
+ continue
1031
+
1032
+ if event_type == "message_delta":
1033
+ delta_payload = (
1034
+ event_payload.get("delta", {})
1035
+ if isinstance(event_payload, dict)
1036
+ else {}
1037
+ )
1038
+ stop_reason = (
1039
+ delta_payload.get("stop_reason")
1040
+ if isinstance(delta_payload, dict)
1041
+ else None
1042
+ )
1043
+ if isinstance(stop_reason, str):
1044
+ final_stop_reason = stop_reason
1045
+
1046
+ usage_payload = (
1047
+ event_payload.get("usage")
1048
+ if isinstance(event_payload, dict)
1049
+ else None
1050
+ )
1051
+ usage_model: anthropic_models.Usage | None = None
1052
+ if usage_payload:
1053
+ try:
1054
+ usage_model = anthropic_models.Usage.model_validate(
1055
+ usage_payload
1056
+ )
1057
+ except ValidationError:
1058
+ usage_model = anthropic_models.Usage(
1059
+ input_tokens=usage_payload.get("input_tokens", 0),
1060
+ output_tokens=usage_payload.get("output_tokens", 0),
1061
+ )
1062
+ elif hasattr(raw_event, "usage") and raw_event.usage is not None:
1063
+ usage_model = raw_event.usage
1064
+
1065
+ if usage_model is not None:
1066
+ latest_usage_model = (
1067
+ convert__anthropic_usage_to_openai_responses__usage(
1068
+ usage_model
1069
+ )
1070
+ )
1071
+
1072
+ sequence_counter += 1
1073
+ yield openai_models.ResponseInProgressEvent(
1074
+ type="response.in_progress",
1075
+ sequence_number=sequence_counter,
1076
+ response=make_response_object(
1077
+ status="in_progress",
1078
+ model=model_id,
1079
+ usage=latest_usage_model,
1080
+ output=[],
1081
+ parallel_override=parallel_setting_initial,
1082
+ ),
1083
+ )
1084
+ continue
1085
+
1086
+ if event_type == "message_stop":
1087
+ for event in finalize_reasoning():
1088
+ yield event
1089
+
1090
+ for event in finalize_message():
1091
+ yield event
1092
+
1093
+ for index, state in list(tool_states.items()):
1094
+ for event in emit_tool_finalize(index, state):
1095
+ yield event
1096
+
1097
+ first_completed_entries: list[tuple[int, Any]] = []
1098
+ if reasoning_completed_entry is not None:
1099
+ first_completed_entries.append(reasoning_completed_entry)
1100
+ if message_completed_entry is not None:
1101
+ first_completed_entries.append(message_completed_entry)
1102
+
1103
+ for index, state in tool_states.items():
1104
+ tool_entry = accumulator.get_tool_entry(index)
1105
+ if state.name is None and tool_entry is not None:
1106
+ state.name = tool_entry.get("name") or tool_entry.get(
1107
+ "function", {}
1108
+ ).get("name")
1109
+ if state.call_id is None and tool_entry is not None:
1110
+ state.call_id = tool_entry.get("id")
1111
+ if not state.item_id:
1112
+ state.item_id = state.call_id or f"call_{state.index}"
1113
+
1114
+ final_args = state.final_arguments
1115
+ if final_args is None:
1116
+ combined = "".join(state.arguments_parts)
1117
+ if not combined and tool_entry is not None:
1118
+ input_payload = tool_entry.get("input", {}) or {}
1119
+ try:
1120
+ combined = json.dumps(
1121
+ input_payload, ensure_ascii=False
1122
+ )
1123
+ except Exception:
1124
+ combined = json.dumps(input_payload)
1125
+ final_args = combined or ""
1126
+ state.final_arguments = final_args
1127
+
1128
+ first_completed_entries.append(
1129
+ (
1130
+ state.output_index,
1131
+ openai_models.FunctionCallOutput(
1132
+ type="function_call",
1133
+ id=state.item_id,
1134
+ status="completed",
1135
+ name=state.name,
1136
+ call_id=state.call_id,
1137
+ arguments=final_args,
1138
+ ),
1139
+ )
1140
+ )
1141
+
1142
+ first_completed_entries.sort(key=lambda item: item[0])
1143
+ completed_outputs = [entry for _, entry in first_completed_entries]
1144
+
1145
+ complete_tool_calls_payload = accumulator.get_complete_tool_calls()
1146
+ parallel_final = parallel_setting_initial or len(tool_states) > 1
1147
+
1148
+ extra_fields: dict[str, Any] | None = None
1149
+ if complete_tool_calls_payload:
1150
+ extra_fields = {"tool_calls": complete_tool_calls_payload}
1151
+
1152
+ status_value = "completed"
1153
+ if final_stop_reason == "max_tokens":
1154
+ status_value = "incomplete"
1155
+
1156
+ completed_response = make_response_object(
1157
+ status=status_value,
1158
+ model=model_id,
1159
+ usage=latest_usage_model,
1160
+ output=completed_outputs,
1161
+ parallel_override=parallel_final,
1162
+ reasoning_summary=reasoning_summary_payload,
1163
+ extra=extra_fields,
1164
+ )
1165
+
1166
+ sequence_counter += 1
1167
+ yield openai_models.ResponseCompletedEvent(
1168
+ type="response.completed",
1169
+ sequence_number=sequence_counter,
1170
+ response=completed_response,
1171
+ )
1172
+ stream_completed = True
1173
+ break
1174
+
1175
+ if not stream_completed:
1176
+ for event in finalize_reasoning():
1177
+ yield event
1178
+
1179
+ for event in finalize_message():
1180
+ yield event
1181
+
1182
+ for index, state in list(tool_states.items()):
1183
+ for event in emit_tool_finalize(index, state):
1184
+ yield event
1185
+
1186
+ if (
1187
+ message_completed_entry is None
1188
+ and message_item_added
1189
+ and message_output_index is not None
1190
+ ):
1191
+ final_text = "".join(text_buffer)
1192
+ logprobs_value: Any
1193
+ if message_last_logprobs is None:
1194
+ logprobs_value = []
1195
+ else:
1196
+ logprobs_value = message_last_logprobs
1197
+ content_blocks: list[Any] = []
1198
+ if accumulator.content_blocks:
1199
+ sorted_blocks = sorted(
1200
+ accumulator.content_blocks,
1201
+ key=lambda block: block.get("index", 0),
1202
+ )
1203
+ for block in sorted_blocks:
1204
+ block_type = block.get("type")
1205
+ if block_type == "text":
1206
+ text_value = block.get("text", "")
1207
+ content_blocks.append(
1208
+ openai_models.OutputTextContent(
1209
+ type="output_text",
1210
+ text=text_value,
1211
+ annotations=[],
1212
+ logprobs=logprobs_value if text_value else [],
1213
+ )
1214
+ )
1215
+ else:
1216
+ payload = {
1217
+ k: v for k, v in block.items() if k != "index"
1218
+ }
1219
+ if payload.get("type") == "tool_use":
1220
+ tool_input = payload.get("input")
1221
+ if tool_input is not None:
1222
+ payload.setdefault("arguments", tool_input)
1223
+ content_blocks.append(payload)
1224
+ else:
1225
+ if final_text:
1226
+ content_blocks.append(
1227
+ openai_models.OutputTextContent(
1228
+ type="output_text",
1229
+ text=final_text,
1230
+ annotations=[],
1231
+ logprobs=logprobs_value
1232
+ if logprobs_value != []
1233
+ else [],
1234
+ )
1235
+ )
1236
+
1237
+ message_completed_entry = (
1238
+ message_output_index,
1239
+ openai_models.MessageOutput(
1240
+ type="message",
1241
+ id=message_item_id,
1242
+ status="completed",
1243
+ role="assistant",
1244
+ content=content_blocks,
1245
+ ),
1246
+ )
1247
+
1248
+ final_completed_entries: list[tuple[int, Any]] = []
1249
+ if reasoning_completed_entry is not None:
1250
+ final_completed_entries.append(reasoning_completed_entry)
1251
+ if message_completed_entry is not None:
1252
+ final_completed_entries.append(message_completed_entry)
1253
+
1254
+ for index, state in tool_states.items():
1255
+ tool_entry = accumulator.get_tool_entry(index)
1256
+ if state.name is None and tool_entry is not None:
1257
+ state.name = tool_entry.get("name") or tool_entry.get(
1258
+ "function", {}
1259
+ ).get("name")
1260
+ if state.call_id is None and tool_entry is not None:
1261
+ state.call_id = tool_entry.get("id")
1262
+ if not state.item_id:
1263
+ state.item_id = state.call_id or f"call_{state.index}"
1264
+ final_args = state.final_arguments
1265
+ if final_args is None:
1266
+ combined = "".join(state.arguments_parts)
1267
+ if not combined and tool_entry is not None:
1268
+ input_payload = tool_entry.get("input", {}) or {}
1269
+ try:
1270
+ combined = json.dumps(input_payload, ensure_ascii=False)
1271
+ except Exception:
1272
+ combined = json.dumps(input_payload)
1273
+ final_args = combined or ""
1274
+ state.final_arguments = final_args
1275
+ final_completed_entries.append(
1276
+ (
1277
+ state.output_index,
1278
+ openai_models.FunctionCallOutput(
1279
+ type="function_call",
1280
+ id=state.item_id,
1281
+ status="completed",
1282
+ name=state.name,
1283
+ call_id=state.call_id,
1284
+ arguments=final_args,
1285
+ ),
1286
+ )
1287
+ )
1288
+
1289
+ final_completed_entries.sort(key=lambda item: item[0])
1290
+ completed_outputs = [entry for _, entry in final_completed_entries]
1291
+
1292
+ complete_tool_calls_payload = accumulator.get_complete_tool_calls()
1293
+ parallel_final = parallel_setting_initial or len(tool_states) > 1
1294
+
1295
+ final_extra_fields: dict[str, Any] | None = None
1296
+ if complete_tool_calls_payload:
1297
+ final_extra_fields = {"tool_calls": complete_tool_calls_payload}
1298
+
1299
+ fallback_response = make_response_object(
1300
+ status="completed",
1301
+ model=model_id,
1302
+ usage=latest_usage_model,
1303
+ output=completed_outputs,
1304
+ parallel_override=parallel_final,
1305
+ reasoning_summary=reasoning_summary_payload,
1306
+ extra=final_extra_fields,
1307
+ )
1308
+
1309
+ sequence_counter += 1
1310
+ yield openai_models.ResponseCompletedEvent(
1311
+ type="response.completed",
1312
+ sequence_number=sequence_counter,
1313
+ response=fallback_response,
1314
+ )
1315
+
1316
+ finally:
1317
+ register_request(None)
1318
+
1319
+
1320
+ class AnthropicToOpenAIChatStreamAdapter:
1321
+ """Stateful adapter for Anthropic → OpenAI Chat streaming."""
1322
+
1323
+ async def run(
1324
+ self,
1325
+ stream: AsyncIterator[anthropic_models.MessageStreamEvent],
1326
+ ) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
1327
+ async for chunk in self._convert_chat_stream(stream):
1328
+ yield chunk
1329
+
1330
+ def _convert_chat_stream(
1331
+ self,
1332
+ stream: AsyncIterator[anthropic_models.MessageStreamEvent],
1333
+ ) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
1334
+ """Convert Anthropic stream to OpenAI stream using ClaudeAccumulator."""
1335
+
1336
+ async def generator() -> AsyncGenerator[
1337
+ openai_models.ChatCompletionChunk, None
1338
+ ]:
1339
+ accumulator = ClaudeAccumulator()
1340
+ model_id = ""
1341
+ finish_reason: FinishReason = "stop"
1342
+ usage_prompt = 0
1343
+ usage_completion = 0
1344
+ message_started = False
1345
+ emitted_tool_indices: set[int] = set()
1346
+
1347
+ async for raw_event in stream:
1348
+ event_type, event_payload = _normalize_anthropic_stream_event(raw_event)
1349
+ if not event_type:
1350
+ continue
1351
+
1352
+ accumulator.accumulate(event_type, event_payload)
1353
+
1354
+ if event_type == "ping":
1355
+ continue
1356
+
1357
+ if event_type == "error":
1358
+ # Error events are handled elsewhere by callers.
1359
+ continue
1360
+
1361
+ if event_type == "message_start":
1362
+ message_data = (
1363
+ event_payload.get("message", {})
1364
+ if isinstance(event_payload, dict)
1365
+ else {}
1366
+ )
1367
+ model_id = str(message_data.get("model", ""))
1368
+ message_started = True
1369
+ yield openai_models.ChatCompletionChunk(
1370
+ id="chatcmpl-stream",
1371
+ object="chat.completion.chunk",
1372
+ created=0,
1373
+ model=model_id,
1374
+ choices=[
1375
+ openai_models.StreamingChoice(
1376
+ index=0,
1377
+ delta=openai_models.DeltaMessage(
1378
+ role="assistant", content=""
1379
+ ),
1380
+ finish_reason=None,
1381
+ )
1382
+ ],
1383
+ )
1384
+ continue
1385
+
1386
+ if not message_started:
1387
+ continue
1388
+
1389
+ if event_type == "content_block_delta":
1390
+ block_index = int(event_payload.get("index", 0))
1391
+ text_delta = _anthropic_delta_to_text(
1392
+ accumulator,
1393
+ block_index,
1394
+ cast(dict[str, Any] | None, event_payload.get("delta")),
1395
+ )
1396
+ if text_delta:
1397
+ yield openai_models.ChatCompletionChunk(
1398
+ id="chatcmpl-stream",
1399
+ object="chat.completion.chunk",
1400
+ created=0,
1401
+ model=model_id,
1402
+ choices=[
1403
+ openai_models.StreamingChoice(
1404
+ index=0,
1405
+ delta=openai_models.DeltaMessage(
1406
+ role="assistant", content=text_delta
1407
+ ),
1408
+ finish_reason=None,
1409
+ )
1410
+ ],
1411
+ )
1412
+ continue
1413
+
1414
+ if event_type == "content_block_stop":
1415
+ block_index = int(event_payload.get("index", 0))
1416
+ block_info = accumulator.get_block_info(block_index)
1417
+ if not block_info:
1418
+ continue
1419
+ _, block_meta = block_info
1420
+ if block_meta.get("type") != "tool_use":
1421
+ continue
1422
+ if block_index in emitted_tool_indices:
1423
+ continue
1424
+ tool_call = _build_openai_tool_call(accumulator, block_index)
1425
+ if tool_call is None:
1426
+ continue
1427
+ emitted_tool_indices.add(block_index)
1428
+ yield openai_models.ChatCompletionChunk(
1429
+ id="chatcmpl-stream",
1430
+ object="chat.completion.chunk",
1431
+ created=0,
1432
+ model=model_id,
1433
+ choices=[
1434
+ openai_models.StreamingChoice(
1435
+ index=0,
1436
+ delta=openai_models.DeltaMessage(
1437
+ role="assistant", tool_calls=[tool_call]
1438
+ ),
1439
+ finish_reason=None,
1440
+ )
1441
+ ],
1442
+ )
1443
+ continue
1444
+
1445
+ if event_type == "message_delta":
1446
+ delta_payload = (
1447
+ event_payload.get("delta", {})
1448
+ if isinstance(event_payload, dict)
1449
+ else {}
1450
+ )
1451
+ stop_reason = (
1452
+ delta_payload.get("stop_reason")
1453
+ if isinstance(delta_payload, dict)
1454
+ else None
1455
+ )
1456
+ if isinstance(stop_reason, str):
1457
+ finish_reason = cast(
1458
+ FinishReason,
1459
+ ANTHROPIC_TO_OPENAI_FINISH_REASON.get(stop_reason, "stop"),
1460
+ )
1461
+
1462
+ usage_payload = (
1463
+ event_payload.get("usage")
1464
+ if isinstance(event_payload, dict)
1465
+ else None
1466
+ )
1467
+ if usage_payload:
1468
+ snapshot = anthropic_usage_snapshot(usage_payload)
1469
+ usage_prompt = snapshot.input_tokens
1470
+ usage_completion = snapshot.output_tokens
1471
+ elif hasattr(raw_event, "usage") and raw_event.usage is not None:
1472
+ snapshot = anthropic_usage_snapshot(raw_event.usage)
1473
+ usage_prompt = snapshot.input_tokens
1474
+ usage_completion = snapshot.output_tokens
1475
+ continue
1476
+
1477
+ if event_type == "message_stop":
1478
+ usage = None
1479
+ if usage_prompt or usage_completion:
1480
+ usage = openai_models.CompletionUsage(
1481
+ prompt_tokens=usage_prompt,
1482
+ completion_tokens=usage_completion,
1483
+ total_tokens=usage_prompt + usage_completion,
1484
+ )
1485
+
1486
+ yield openai_models.ChatCompletionChunk(
1487
+ id="chatcmpl-stream",
1488
+ object="chat.completion.chunk",
1489
+ created=0,
1490
+ model=model_id,
1491
+ choices=[
1492
+ openai_models.StreamingChoice(
1493
+ index=0,
1494
+ delta=openai_models.DeltaMessage(),
1495
+ finish_reason=finish_reason,
1496
+ )
1497
+ ],
1498
+ usage=usage,
1499
+ )
1500
+ break
1501
+
1502
+ else:
1503
+ if message_started:
1504
+ yield openai_models.ChatCompletionChunk(
1505
+ id="chatcmpl-stream",
1506
+ object="chat.completion.chunk",
1507
+ created=0,
1508
+ model=model_id,
1509
+ choices=[
1510
+ openai_models.StreamingChoice(
1511
+ index=0,
1512
+ delta=openai_models.DeltaMessage(),
1513
+ finish_reason=finish_reason,
1514
+ )
1515
+ ],
1516
+ )
1517
+
1518
+ return generator()
1519
+
1520
+
1521
+ async def convert__anthropic_message_to_openai_responses__stream(
1522
+ stream: AsyncIterator[anthropic_models.MessageStreamEvent],
1523
+ ) -> AsyncGenerator[openai_models.StreamEventType, None]:
1524
+ """Convert Anthropic MessageStreamEvents into OpenAI Responses stream events."""
1525
+
1526
+ adapter = AnthropicToOpenAIResponsesStreamAdapter()
1527
+ async for event in adapter.run(stream):
1528
+ yield event
1529
+
1530
+
1531
+ async def convert__anthropic_message_to_openai_chat__stream(
1532
+ stream: AsyncIterator[anthropic_models.MessageStreamEvent],
1533
+ ) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
1534
+ """Convert Anthropic stream to OpenAI stream using ClaudeAccumulator."""
1535
+
1536
+ adapter = AnthropicToOpenAIChatStreamAdapter()
1537
+ async for chunk in adapter.run(stream):
1538
+ yield chunk
1539
+
1540
+
1541
+ __all__ = [
1542
+ "AnthropicToOpenAIChatStreamAdapter",
1543
+ "AnthropicToOpenAIResponsesStreamAdapter",
1544
+ "convert__anthropic_message_to_openai_chat__stream",
1545
+ "convert__anthropic_message_to_openai_responses__stream",
1546
+ ]