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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1832 @@
1
+ """Streaming conversion entry points for OpenAI↔OpenAI adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import time
8
+ import uuid
9
+ from collections.abc import AsyncGenerator, AsyncIterator
10
+ from typing import Any, Literal
11
+
12
+ from pydantic import ValidationError
13
+
14
+ import ccproxy.core.logging
15
+ from ccproxy.llms.formatters.common import (
16
+ THINKING_CLOSE_PATTERN,
17
+ THINKING_OPEN_PATTERN,
18
+ IndexedToolCallTracker,
19
+ ObfuscationTokenFactory,
20
+ ReasoningBuffer,
21
+ ThinkingSegment,
22
+ ToolCallState,
23
+ ToolCallTracker,
24
+ ensure_identifier,
25
+ )
26
+ from ccproxy.llms.formatters.context import (
27
+ get_last_instructions,
28
+ get_last_request,
29
+ get_last_request_tools,
30
+ register_request,
31
+ register_request_tools,
32
+ )
33
+ from ccproxy.llms.models import openai as openai_models
34
+ from ccproxy.llms.streaming.accumulators import OpenAIAccumulator
35
+
36
+ from ._helpers import (
37
+ _convert_tools_chat_to_responses,
38
+ _get_attr,
39
+ )
40
+ from .requests import _build_responses_payload_from_chat_request
41
+ from .responses import (
42
+ _collect_reasoning_segments,
43
+ _wrap_thinking,
44
+ convert__openai_completion_usage_to_openai_responses__usage,
45
+ convert__openai_responses_usage_to_openai_completion__usage,
46
+ )
47
+
48
+
49
+ logger = ccproxy.core.logging.get_logger(__name__)
50
+
51
+
52
+ class OpenAIResponsesToChatStreamAdapter:
53
+ """Stateful adapter for Responses -> Chat streaming conversions."""
54
+
55
+ def run(
56
+ self,
57
+ stream: AsyncIterator[openai_models.AnyStreamEvent],
58
+ ) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
59
+ """Convert Response API stream events to ChatCompletionChunk events."""
60
+
61
+ async def generator() -> AsyncGenerator[
62
+ openai_models.ChatCompletionChunk, None
63
+ ]:
64
+ model_id = ""
65
+ role_sent = False
66
+
67
+ # Track tool call state keyed by response item id
68
+ tool_tracker = ToolCallTracker()
69
+ tool_delta_emitted = False
70
+ saw_tool_event = False
71
+ tool_candidates: list[tuple[str | None, set[str]]] = []
72
+ reasoning_buffer = ReasoningBuffer()
73
+
74
+ def _extract_tool_signature(tool_entry: Any) -> tuple[str | None, set[str]]:
75
+ name: str | None = None
76
+ param_keys: set[str] = set()
77
+
78
+ if hasattr(tool_entry, "function"):
79
+ fn = getattr(tool_entry, "function", None)
80
+ if fn is not None:
81
+ name = getattr(fn, "name", None)
82
+ parameters = getattr(fn, "parameters", None)
83
+ if isinstance(parameters, dict):
84
+ props = parameters.get("properties")
85
+ if isinstance(props, dict):
86
+ param_keys = {str(key) for key in props}
87
+ if name is None and isinstance(tool_entry, dict):
88
+ fn_dict = tool_entry.get("function")
89
+ if isinstance(fn_dict, dict):
90
+ name = fn_dict.get("name", name)
91
+ parameters = fn_dict.get("parameters")
92
+ if isinstance(parameters, dict):
93
+ props = parameters.get("properties")
94
+ if isinstance(props, dict):
95
+ param_keys = {str(key) for key in props}
96
+ if name is None:
97
+ name = tool_entry.get("name")
98
+
99
+ return name, param_keys
100
+
101
+ def _guess_tool_name(arguments: str | None) -> str | None:
102
+ if not arguments:
103
+ return None
104
+ try:
105
+ parsed = json.loads(arguments)
106
+ except Exception:
107
+ return None
108
+ if not isinstance(parsed, dict):
109
+ return None
110
+ keys = {str(k) for k in parsed}
111
+ if not keys:
112
+ return None
113
+
114
+ candidates = [
115
+ tool_name
116
+ for tool_name, param_keys in tool_candidates
117
+ if tool_name
118
+ and ((param_keys and keys.issubset(param_keys)) or not param_keys)
119
+ ]
120
+
121
+ if len(candidates) == 1:
122
+ return candidates[0]
123
+
124
+ exact = [
125
+ tool_name
126
+ for tool_name, param_keys in tool_candidates
127
+ if tool_name and param_keys == keys
128
+ ]
129
+ if len(exact) == 1:
130
+ return exact[0]
131
+
132
+ return None
133
+
134
+ def _ensure_tool_state(item_id: str) -> ToolCallState:
135
+ return tool_tracker.ensure(item_id)
136
+
137
+ item_id = "msg_stream"
138
+ output_index = 0
139
+ content_index = 0
140
+ sequence_counter = 0
141
+ first_logged = False
142
+
143
+ inline_reasoning_id = "__inline_reasoning__"
144
+ inline_summary_index = "__inline__"
145
+
146
+ async for event_wrapper in stream:
147
+ evt = getattr(event_wrapper, "root", event_wrapper)
148
+ if not hasattr(evt, "type"):
149
+ continue
150
+
151
+ logger.debug("stream_event", event_type=getattr(evt, "type", None))
152
+ evt_type = getattr(evt, "type", "")
153
+
154
+ if evt_type == "response.reasoning_summary_part.added":
155
+ item_id = _get_attr(evt, "item_id")
156
+ part = _get_attr(evt, "part")
157
+ if isinstance(item_id, str) and item_id and part is not None:
158
+ summary_index = _get_attr(evt, "summary_index")
159
+ part_signature = _get_attr(part, "signature")
160
+ if isinstance(part_signature, str) and part_signature:
161
+ reasoning_buffer.set_signature(
162
+ item_id, summary_index, part_signature
163
+ )
164
+ else:
165
+ part_type = _get_attr(part, "type")
166
+ part_text = _get_attr(part, "text")
167
+ if (
168
+ part_type == "signature"
169
+ and isinstance(part_text, str)
170
+ and part_text
171
+ ):
172
+ reasoning_buffer.set_signature(
173
+ item_id, summary_index, part_text
174
+ )
175
+ reasoning_buffer.reset_buffer(item_id, summary_index)
176
+ continue
177
+
178
+ if evt_type in {
179
+ "response.reasoning_summary_text.delta",
180
+ "response.reasoning_text.delta",
181
+ }:
182
+ item_id = _get_attr(evt, "item_id")
183
+ delta_text = _get_attr(evt, "delta")
184
+ if isinstance(item_id, str):
185
+ summary_index = _get_attr(evt, "summary_index")
186
+ reasoning_buffer.append_text(item_id, summary_index, delta_text)
187
+ continue
188
+
189
+ if evt_type in {
190
+ "response.reasoning_summary_text.done",
191
+ "response.reasoning_text.done",
192
+ }:
193
+ item_id = _get_attr(evt, "item_id")
194
+ text_value = _get_attr(evt, "text")
195
+ if isinstance(item_id, str):
196
+ summary_index = _get_attr(evt, "summary_index")
197
+ for chunk_text in reasoning_buffer.emit(
198
+ item_id, summary_index, text_value
199
+ ):
200
+ sequence_counter += 1
201
+ yield openai_models.ChatCompletionChunk(
202
+ id="chatcmpl-stream",
203
+ created=0,
204
+ model=model_id,
205
+ choices=[
206
+ openai_models.StreamingChoice(
207
+ index=0,
208
+ delta=openai_models.DeltaMessage(
209
+ role="assistant" if not role_sent else None,
210
+ content=chunk_text,
211
+ ),
212
+ finish_reason=None,
213
+ )
214
+ ],
215
+ )
216
+ role_sent = True
217
+ continue
218
+
219
+ if evt_type == "response.created":
220
+ response_obj = getattr(evt, "response", None)
221
+ model_id = getattr(response_obj, "model", model_id) or model_id
222
+ tools_metadata = getattr(response_obj, "tools", None)
223
+ if not tools_metadata:
224
+ tools_metadata = get_last_request_tools() or []
225
+ if tools_metadata:
226
+ tool_candidates = [
227
+ _extract_tool_signature(entry) for entry in tools_metadata
228
+ ]
229
+ continue
230
+
231
+ if evt_type == "response.output_text.delta":
232
+ delta_text = getattr(evt, "delta", None) or ""
233
+ if not delta_text:
234
+ continue
235
+
236
+ remaining = delta_text
237
+
238
+ # Directly create chunks and yield them instead of using a nested function
239
+ # which has closure binding issues
240
+ chunks_to_yield: list[openai_models.ChatCompletionChunk] = []
241
+
242
+ def create_text_chunk(
243
+ current_model_id: str, text_segment: str, is_role_sent: bool
244
+ ) -> tuple[openai_models.ChatCompletionChunk | None, bool]:
245
+ if not text_segment:
246
+ return None, is_role_sent
247
+ delta_msg = openai_models.DeltaMessage(
248
+ role="assistant" if not is_role_sent else None,
249
+ content=text_segment,
250
+ )
251
+ new_role_sent = True
252
+ chunk = openai_models.ChatCompletionChunk(
253
+ id="chatcmpl-stream",
254
+ created=0,
255
+ model=current_model_id,
256
+ choices=[
257
+ openai_models.StreamingChoice(
258
+ index=0,
259
+ delta=delta_msg,
260
+ finish_reason=None,
261
+ )
262
+ ],
263
+ )
264
+ return chunk, new_role_sent
265
+
266
+ while remaining:
267
+ if reasoning_buffer.is_open(
268
+ inline_reasoning_id, inline_summary_index
269
+ ):
270
+ close_match = THINKING_CLOSE_PATTERN.search(remaining)
271
+ if close_match:
272
+ inside_text = remaining[: close_match.start()]
273
+ if inside_text:
274
+ reasoning_buffer.append_text(
275
+ inline_reasoning_id,
276
+ inline_summary_index,
277
+ inside_text,
278
+ )
279
+ for chunk_text in reasoning_buffer.emit(
280
+ inline_reasoning_id, inline_summary_index
281
+ ):
282
+ chunk, role_sent = create_text_chunk(
283
+ model_id, chunk_text, role_sent
284
+ )
285
+ if chunk:
286
+ sequence_counter += 1
287
+ chunks_to_yield.append(chunk)
288
+ reasoning_buffer.close_part(
289
+ inline_reasoning_id, inline_summary_index
290
+ )
291
+ remaining = remaining[close_match.end() :]
292
+ continue
293
+ reasoning_buffer.append_text(
294
+ inline_reasoning_id,
295
+ inline_summary_index,
296
+ remaining,
297
+ )
298
+ remaining = ""
299
+ break
300
+
301
+ open_match = THINKING_OPEN_PATTERN.search(remaining)
302
+ if open_match:
303
+ prefix_text = remaining[: open_match.start()]
304
+ if prefix_text:
305
+ chunk, role_sent = create_text_chunk(
306
+ model_id, prefix_text, role_sent
307
+ )
308
+ if chunk:
309
+ sequence_counter += 1
310
+ chunks_to_yield.append(chunk)
311
+
312
+ signature = open_match.group(1) or None
313
+ part_state = reasoning_buffer.ensure_part(
314
+ inline_reasoning_id, inline_summary_index
315
+ )
316
+ if signature:
317
+ part_state.signature = signature
318
+ remaining = remaining[open_match.end() :]
319
+
320
+ if part_state.open:
321
+ # Already inside a reasoning block; ignore duplicate tag
322
+ continue
323
+
324
+ reasoning_buffer.open_part(
325
+ inline_reasoning_id, inline_summary_index
326
+ )
327
+ continue
328
+
329
+ # No reasoning markers in the rest of the chunk
330
+ if reasoning_buffer.is_open(
331
+ inline_reasoning_id, inline_summary_index
332
+ ):
333
+ reasoning_buffer.append_text(
334
+ inline_reasoning_id, inline_summary_index, remaining
335
+ )
336
+ else:
337
+ chunk, role_sent = create_text_chunk(
338
+ model_id, remaining, role_sent
339
+ )
340
+ if chunk:
341
+ sequence_counter += 1
342
+ chunks_to_yield.append(chunk)
343
+ remaining = ""
344
+
345
+ for chunk in chunks_to_yield:
346
+ yield chunk
347
+ continue
348
+
349
+ if evt_type == "response.output_item.added":
350
+ item = getattr(evt, "item", None)
351
+ if not item:
352
+ continue
353
+
354
+ item_type = getattr(item, "type", None)
355
+ if item_type != "function_call":
356
+ continue
357
+
358
+ saw_tool_event = True
359
+
360
+ item_id_value = getattr(item, "id", None) or getattr(
361
+ item, "call_id", None
362
+ )
363
+ if not item_id_value:
364
+ item_id_value = f"call_{uuid.uuid4().hex}"
365
+ item_id = item_id_value
366
+
367
+ state = _ensure_tool_state(item_id)
368
+ state.id = getattr(item, "id", state.id) or state.id
369
+ state.call_id = getattr(item, "call_id", None) or state.call_id
370
+
371
+ if not state.name and state.index < len(tool_candidates):
372
+ candidate_name = tool_candidates[state.index][0]
373
+ if candidate_name:
374
+ state.name = candidate_name
375
+
376
+ name = getattr(item, "name", None)
377
+ if name:
378
+ state.name = name
379
+
380
+ arguments = getattr(item, "arguments", None)
381
+ if isinstance(arguments, str) and arguments:
382
+ state.arguments += arguments
383
+ if not state.name:
384
+ guessed = _guess_tool_name(state.arguments)
385
+ if guessed:
386
+ state.name = guessed
387
+
388
+ # Emit initial tool call chunk to surface id/name information
389
+ if not state.initial_emitted:
390
+ tool_call = openai_models.ToolCall(
391
+ id=state.id,
392
+ type="function",
393
+ function=openai_models.FunctionCall(
394
+ name=state.name or "",
395
+ arguments=arguments or "",
396
+ ),
397
+ )
398
+ state.emitted = True
399
+ state.initial_emitted = True
400
+ if state.name:
401
+ state.name_emitted = True
402
+ if arguments:
403
+ state.arguments_emitted = True
404
+
405
+ tool_delta_emitted = True
406
+
407
+ yield openai_models.ChatCompletionChunk(
408
+ id="chatcmpl-stream",
409
+ created=0,
410
+ model=model_id,
411
+ choices=[
412
+ openai_models.StreamingChoice(
413
+ index=0,
414
+ delta=openai_models.DeltaMessage(
415
+ role="assistant" if not role_sent else None,
416
+ tool_calls=[tool_call],
417
+ ),
418
+ finish_reason=None,
419
+ )
420
+ ],
421
+ )
422
+ role_sent = True
423
+ continue
424
+
425
+ if evt_type == "response.function_call_arguments.delta":
426
+ saw_tool_event = True
427
+ item_id_val = getattr(evt, "item_id", None)
428
+ if not isinstance(item_id_val, str):
429
+ continue
430
+ item_id = item_id_val
431
+ delta_segment = getattr(evt, "delta", None)
432
+ if not isinstance(delta_segment, str):
433
+ continue
434
+
435
+ state = _ensure_tool_state(item_id)
436
+ state.arguments += delta_segment
437
+ if not state.name:
438
+ guessed = _guess_tool_name(state.arguments)
439
+ if guessed:
440
+ state.name = guessed
441
+
442
+ if state.initial_emitted:
443
+ tool_call = openai_models.ToolCall(
444
+ id=state.id,
445
+ type="function",
446
+ function=openai_models.FunctionCall(
447
+ name=state.name or "",
448
+ arguments=delta_segment,
449
+ ),
450
+ )
451
+
452
+ state.emitted = True
453
+ if delta_segment:
454
+ state.arguments_emitted = True
455
+
456
+ tool_delta_emitted = True
457
+
458
+ yield openai_models.ChatCompletionChunk(
459
+ id="chatcmpl-stream",
460
+ created=0,
461
+ model=model_id,
462
+ choices=[
463
+ openai_models.StreamingChoice(
464
+ index=0,
465
+ delta=openai_models.DeltaMessage(
466
+ role="assistant" if not role_sent else None,
467
+ tool_calls=[tool_call],
468
+ ),
469
+ finish_reason=None,
470
+ )
471
+ ],
472
+ )
473
+ role_sent = True
474
+ continue
475
+
476
+ if evt_type == "response.function_call_arguments.done":
477
+ saw_tool_event = True
478
+ item_id_val = getattr(evt, "item_id", None)
479
+ if not isinstance(item_id_val, str):
480
+ continue
481
+ item_id = item_id_val
482
+ arguments = getattr(evt, "arguments", None)
483
+ if not isinstance(arguments, str) or not arguments:
484
+ continue
485
+
486
+ state = _ensure_tool_state(item_id)
487
+ # Only emit a chunk if we never emitted arguments earlier
488
+ if not state.arguments_emitted:
489
+ state.arguments = arguments
490
+ if not state.name:
491
+ guessed = _guess_tool_name(arguments)
492
+ if guessed:
493
+ state.name = guessed
494
+
495
+ tool_call = openai_models.ToolCall(
496
+ id=state.id,
497
+ type="function",
498
+ function=openai_models.FunctionCall(
499
+ name=state.name or "",
500
+ arguments=arguments,
501
+ ),
502
+ )
503
+
504
+ state.emitted = True
505
+ state.arguments_emitted = True
506
+
507
+ tool_delta_emitted = True
508
+
509
+ yield openai_models.ChatCompletionChunk(
510
+ id="chatcmpl-stream",
511
+ created=0,
512
+ model=model_id,
513
+ choices=[
514
+ openai_models.StreamingChoice(
515
+ index=0,
516
+ delta=openai_models.DeltaMessage(
517
+ role="assistant" if not role_sent else None,
518
+ tool_calls=[tool_call],
519
+ ),
520
+ finish_reason=None,
521
+ )
522
+ ],
523
+ )
524
+ role_sent = True
525
+ continue
526
+
527
+ if evt_type == "response.output_item.done":
528
+ item = getattr(evt, "item", None)
529
+ if not item:
530
+ continue
531
+
532
+ item_type = getattr(item, "type", None)
533
+
534
+ if item_type == "reasoning":
535
+ summary_list = getattr(item, "summary", None)
536
+ if isinstance(summary_list, list):
537
+ for entry in summary_list:
538
+ text = _get_attr(entry, "text")
539
+ signature = _get_attr(entry, "signature")
540
+ if isinstance(text, str) and text:
541
+ chunk_text = _wrap_thinking(signature, text)
542
+ sequence_counter += 1
543
+ yield openai_models.ChatCompletionChunk(
544
+ id="chatcmpl-stream",
545
+ created=0,
546
+ model=model_id,
547
+ choices=[
548
+ openai_models.StreamingChoice(
549
+ index=0,
550
+ delta=openai_models.DeltaMessage(
551
+ role="assistant"
552
+ if not role_sent
553
+ else None,
554
+ content=chunk_text,
555
+ ),
556
+ finish_reason=None,
557
+ )
558
+ ],
559
+ )
560
+ role_sent = True
561
+ continue
562
+
563
+ if item_type != "function_call":
564
+ continue
565
+
566
+ saw_tool_event = True
567
+
568
+ item_id_value = getattr(item, "id", None) or getattr(
569
+ item, "call_id", None
570
+ )
571
+ if not isinstance(item_id_value, str) or not item_id_value:
572
+ continue
573
+ item_id = item_id_value
574
+
575
+ state = _ensure_tool_state(item_id)
576
+ name = getattr(item, "name", None)
577
+ if name:
578
+ state.name = name
579
+ arguments = getattr(item, "arguments", None)
580
+ if isinstance(arguments, str) and arguments:
581
+ state.arguments = arguments
582
+ if not state.name:
583
+ guessed = _guess_tool_name(arguments)
584
+ if guessed:
585
+ state.name = guessed
586
+ if not state.arguments_emitted:
587
+ tool_call = openai_models.ToolCall(
588
+ id=state.id,
589
+ type="function",
590
+ function=openai_models.FunctionCall(
591
+ name=state.name or "",
592
+ arguments=arguments,
593
+ ),
594
+ )
595
+ state.emitted = True
596
+ state.arguments_emitted = True
597
+
598
+ yield openai_models.ChatCompletionChunk(
599
+ id="chatcmpl-stream",
600
+ created=0,
601
+ model=model_id,
602
+ choices=[
603
+ openai_models.StreamingChoice(
604
+ index=0,
605
+ delta=openai_models.DeltaMessage(
606
+ role="assistant" if not role_sent else None,
607
+ tool_calls=[tool_call],
608
+ ),
609
+ finish_reason=None,
610
+ )
611
+ ],
612
+ )
613
+ role_sent = True
614
+
615
+ # Emit a patch chunk if the name was never surfaced earlier
616
+ if state.name and not state.name_emitted:
617
+ tool_call = openai_models.ToolCall(
618
+ id=state.id,
619
+ type="function",
620
+ function=openai_models.FunctionCall(
621
+ name=state.name or "",
622
+ arguments="",
623
+ ),
624
+ )
625
+ state.name_emitted = True
626
+
627
+ tool_delta_emitted = True
628
+
629
+ yield openai_models.ChatCompletionChunk(
630
+ id="chatcmpl-stream",
631
+ created=0,
632
+ model=model_id,
633
+ choices=[
634
+ openai_models.StreamingChoice(
635
+ index=0,
636
+ delta=openai_models.DeltaMessage(
637
+ role="assistant" if not role_sent else None,
638
+ tool_calls=[tool_call],
639
+ ),
640
+ finish_reason=None,
641
+ )
642
+ ],
643
+ )
644
+ role_sent = True
645
+
646
+ state.completed = True
647
+ continue
648
+
649
+ if evt_type in {
650
+ "response.completed",
651
+ "response.incomplete",
652
+ "response.failed",
653
+ }:
654
+ usage = None
655
+ response_obj = getattr(evt, "response", None)
656
+ if response_obj and getattr(response_obj, "usage", None):
657
+ usage = (
658
+ convert__openai_responses_usage_to_openai_completion__usage(
659
+ response_obj.usage
660
+ )
661
+ )
662
+
663
+ finish_reason: Literal["stop", "length", "tool_calls"] = "stop"
664
+ if (
665
+ tool_delta_emitted
666
+ or saw_tool_event
667
+ or len(tool_tracker)
668
+ or tool_tracker.any_completed()
669
+ ):
670
+ finish_reason = "tool_calls"
671
+
672
+ yield openai_models.ChatCompletionChunk(
673
+ id="chatcmpl-stream",
674
+ created=0,
675
+ model=model_id,
676
+ choices=[
677
+ openai_models.StreamingChoice(
678
+ index=0,
679
+ delta=openai_models.DeltaMessage(),
680
+ finish_reason=finish_reason,
681
+ )
682
+ ],
683
+ usage=usage,
684
+ )
685
+
686
+ # Cleanup request tool cache context when stream completes
687
+ register_request_tools(None)
688
+
689
+ return generator()
690
+
691
+
692
+ def convert__openai_responses_to_openai_chat__stream(
693
+ stream: AsyncIterator[openai_models.AnyStreamEvent],
694
+ ) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
695
+ """Convert Response API stream events to ChatCompletionChunk events."""
696
+ adapter = OpenAIResponsesToChatStreamAdapter()
697
+ return adapter.run(stream)
698
+
699
+
700
+ class OpenAIChatToResponsesStreamAdapter:
701
+ """Stateful adapter for Chat -> Responses streaming conversions."""
702
+
703
+ def run(
704
+ self,
705
+ stream: AsyncIterator[openai_models.ChatCompletionChunk | dict[str, Any]],
706
+ ) -> AsyncGenerator[openai_models.StreamEventType, None]:
707
+ """Convert OpenAI ChatCompletionChunk stream to Responses API events.
708
+
709
+ Replays chat deltas as Responses events, including function-call output items
710
+ and argument deltas so partial tool calls stream correctly.
711
+ """
712
+
713
+ async def generator() -> AsyncGenerator[openai_models.StreamEventType, None]:
714
+ log = logger.bind(
715
+ category="formatter", converter="chat_to_responses_stream"
716
+ )
717
+
718
+ created_sent = False
719
+ response_id = ""
720
+ id_suffix: str | None = None
721
+ last_model = ""
722
+ sequence_counter = -1
723
+ first_logged = False
724
+
725
+ openai_accumulator = OpenAIAccumulator()
726
+ latest_usage_model: openai_models.ResponseUsage | None = None
727
+ convert_usage = convert__openai_completion_usage_to_openai_responses__usage
728
+ delta_event_cls = openai_models.ResponseFunctionCallArgumentsDeltaEvent
729
+
730
+ instructions_text = get_last_instructions()
731
+ if not instructions_text:
732
+ try:
733
+ from ccproxy.core.request_context import RequestContext
734
+
735
+ ctx = RequestContext.get_current()
736
+ if ctx is not None:
737
+ raw_instr = ctx.metadata.get("instructions")
738
+ if isinstance(raw_instr, str) and raw_instr.strip():
739
+ instructions_text = raw_instr.strip()
740
+ except Exception:
741
+ pass
742
+ instructions_value = instructions_text or None
743
+
744
+ envelope_base_kwargs: dict[str, Any] = {
745
+ "id": response_id,
746
+ "object": "response",
747
+ "created_at": 0,
748
+ "instructions": instructions_value,
749
+ }
750
+ reasoning_summary_payload: list[dict[str, Any]] | None = None
751
+
752
+ last_request = get_last_request()
753
+ chat_request: openai_models.ChatCompletionRequest | None = None
754
+ if isinstance(last_request, openai_models.ChatCompletionRequest):
755
+ chat_request = last_request
756
+ elif isinstance(last_request, dict):
757
+ try:
758
+ chat_request = openai_models.ChatCompletionRequest.model_validate(
759
+ last_request
760
+ )
761
+ except ValidationError:
762
+ chat_request = None
763
+
764
+ base_parallel_tool_calls = True
765
+ text_payload: dict[str, Any] | None = None
766
+
767
+ if chat_request is not None:
768
+ request_payload, _ = _build_responses_payload_from_chat_request(
769
+ chat_request
770
+ )
771
+ base_parallel_tool_calls = bool(
772
+ request_payload.get("parallel_tool_calls", True)
773
+ )
774
+ background_value = request_payload.get("background", None)
775
+ envelope_base_kwargs["background"] = (
776
+ bool(background_value) if background_value is not None else None
777
+ )
778
+ for key in (
779
+ "max_output_tokens",
780
+ "tool_choice",
781
+ "tools",
782
+ "store",
783
+ "service_tier",
784
+ "temperature",
785
+ "prompt_cache_key",
786
+ "top_p",
787
+ "top_logprobs",
788
+ "truncation",
789
+ "metadata",
790
+ "user",
791
+ ):
792
+ if key in request_payload:
793
+ envelope_base_kwargs[key] = request_payload[key]
794
+ text_payload = request_payload.get("text")
795
+ reasoning_source = request_payload.get("reasoning")
796
+ reasoning_effort = None
797
+ if isinstance(reasoning_source, dict):
798
+ reasoning_effort = reasoning_source.get("effort")
799
+ if reasoning_effort is None:
800
+ reasoning_effort = getattr(chat_request, "reasoning_effort", None)
801
+ envelope_base_kwargs["reasoning"] = openai_models.Reasoning(
802
+ effort=reasoning_effort,
803
+ summary=None,
804
+ )
805
+ if envelope_base_kwargs.get("tool_choice") is None:
806
+ envelope_base_kwargs["tool_choice"] = (
807
+ chat_request.tool_choice or "auto"
808
+ )
809
+ if envelope_base_kwargs.get("tools") is None and chat_request.tools:
810
+ envelope_base_kwargs["tools"] = _convert_tools_chat_to_responses(
811
+ chat_request.tools
812
+ )
813
+ if envelope_base_kwargs.get("store") is None:
814
+ store_value = getattr(chat_request, "store", None)
815
+ if store_value is not None:
816
+ envelope_base_kwargs["store"] = store_value
817
+ if envelope_base_kwargs.get("temperature") is None:
818
+ temperature_value = getattr(chat_request, "temperature", None)
819
+ if temperature_value is not None:
820
+ envelope_base_kwargs["temperature"] = temperature_value
821
+ if envelope_base_kwargs.get("service_tier") is None:
822
+ service_tier_value = getattr(chat_request, "service_tier", None)
823
+ envelope_base_kwargs["service_tier"] = service_tier_value or "auto"
824
+ if "metadata" not in envelope_base_kwargs:
825
+ envelope_base_kwargs["metadata"] = {}
826
+ register_request_tools(chat_request.tools)
827
+ else:
828
+ envelope_base_kwargs["background"] = envelope_base_kwargs.get(
829
+ "background"
830
+ )
831
+ envelope_base_kwargs["reasoning"] = openai_models.Reasoning(
832
+ effort=None, summary=None
833
+ )
834
+ envelope_base_kwargs.setdefault("metadata", {})
835
+
836
+ if text_payload is None:
837
+ text_payload = {"format": {"type": "text"}}
838
+ else:
839
+ text_payload = dict(text_payload)
840
+
841
+ verbosity_value = None
842
+ if chat_request is not None:
843
+ verbosity_value = getattr(chat_request, "verbosity", None)
844
+ if verbosity_value is not None:
845
+ text_payload["verbosity"] = verbosity_value
846
+ else:
847
+ text_payload.setdefault("verbosity", "low")
848
+ envelope_base_kwargs["text"] = text_payload
849
+
850
+ if "store" not in envelope_base_kwargs:
851
+ envelope_base_kwargs["store"] = True
852
+ if "temperature" not in envelope_base_kwargs:
853
+ envelope_base_kwargs["temperature"] = 1.0
854
+ if "service_tier" not in envelope_base_kwargs:
855
+ envelope_base_kwargs["service_tier"] = "auto"
856
+ if "tool_choice" not in envelope_base_kwargs:
857
+ envelope_base_kwargs["tool_choice"] = "auto"
858
+ if "prompt_cache_key" not in envelope_base_kwargs:
859
+ envelope_base_kwargs["prompt_cache_key"] = None
860
+ if "top_p" not in envelope_base_kwargs:
861
+ envelope_base_kwargs["top_p"] = 1.0
862
+ if "top_logprobs" not in envelope_base_kwargs:
863
+ envelope_base_kwargs["top_logprobs"] = None
864
+ if "truncation" not in envelope_base_kwargs:
865
+ envelope_base_kwargs["truncation"] = None
866
+ if "user" not in envelope_base_kwargs:
867
+ envelope_base_kwargs["user"] = None
868
+
869
+ parallel_setting_initial = bool(base_parallel_tool_calls)
870
+ envelope_base_kwargs["parallel_tool_calls"] = parallel_setting_initial
871
+
872
+ message_item_id = ""
873
+ message_output_index: int | None = None
874
+ content_index = 0
875
+ message_item_added = False
876
+ message_content_part_added = False
877
+ message_text_buffer: list[str] = []
878
+ message_last_logprobs: Any | None = None
879
+ message_text_done_emitted = False
880
+ message_part_done_emitted = False
881
+ message_item_done_emitted = False
882
+ message_completed_entry: tuple[int, openai_models.MessageOutput] | None = (
883
+ None
884
+ )
885
+
886
+ reasoning_item_id = ""
887
+ reasoning_output_index: int | None = None
888
+ reasoning_item_added = False
889
+ reasoning_output_done = False
890
+ reasoning_summary_indices: dict[str, int] = {}
891
+ reasoning_summary_added: set[int] = set()
892
+ reasoning_summary_text_fragments: dict[int, list[str]] = {}
893
+ reasoning_summary_text_done: set[int] = set()
894
+ reasoning_summary_part_done: set[int] = set()
895
+ reasoning_completed_entry: (
896
+ tuple[int, openai_models.ReasoningOutput] | None
897
+ ) = None
898
+ next_summary_index = 0
899
+ reasoning_summary_signatures: dict[int, str | None] = {}
900
+
901
+ created_at_value: int | None = None
902
+
903
+ next_output_index = 0
904
+ tool_call_states = IndexedToolCallTracker()
905
+
906
+ obfuscation_factory = ObfuscationTokenFactory(
907
+ lambda: id_suffix or response_id or "stream"
908
+ )
909
+
910
+ def ensure_message_output_item() -> (
911
+ openai_models.ResponseOutputItemAddedEvent | None
912
+ ):
913
+ nonlocal message_item_added, message_output_index, next_output_index
914
+ nonlocal sequence_counter
915
+ if message_output_index is None:
916
+ message_output_index = next_output_index
917
+ next_output_index += 1
918
+ if not message_item_added:
919
+ message_item_added = True
920
+ sequence_counter += 1
921
+ return openai_models.ResponseOutputItemAddedEvent(
922
+ type="response.output_item.added",
923
+ sequence_number=sequence_counter,
924
+ output_index=message_output_index,
925
+ item=openai_models.OutputItem(
926
+ id=message_item_id,
927
+ type="message",
928
+ role="assistant",
929
+ status="in_progress",
930
+ content=[],
931
+ ),
932
+ )
933
+ return None
934
+
935
+ def ensure_message_content_part() -> (
936
+ openai_models.ResponseContentPartAddedEvent | None
937
+ ):
938
+ nonlocal message_content_part_added, sequence_counter
939
+ if message_output_index is None:
940
+ return None
941
+ if not message_content_part_added:
942
+ message_content_part_added = True
943
+ sequence_counter += 1
944
+ return openai_models.ResponseContentPartAddedEvent(
945
+ type="response.content_part.added",
946
+ sequence_number=sequence_counter,
947
+ item_id=message_item_id,
948
+ output_index=message_output_index,
949
+ content_index=content_index,
950
+ part=openai_models.ContentPart(
951
+ type="output_text",
952
+ text="",
953
+ annotations=[],
954
+ ),
955
+ )
956
+ return None
957
+
958
+ def emit_message_text_delta(
959
+ delta_text: str,
960
+ *,
961
+ logprobs: Any | None = None,
962
+ obfuscation: str | None = None,
963
+ ) -> list[openai_models.StreamEventType]:
964
+ if not isinstance(delta_text, str) or not delta_text:
965
+ return []
966
+
967
+ nonlocal \
968
+ message_last_logprobs, \
969
+ sequence_counter, \
970
+ message_item_done_emitted
971
+ if message_item_done_emitted:
972
+ return []
973
+
974
+ events: list[openai_models.StreamEventType] = []
975
+
976
+ message_event = ensure_message_output_item()
977
+ if message_event is not None:
978
+ events.append(message_event)
979
+
980
+ content_event = ensure_message_content_part()
981
+ if content_event is not None:
982
+ events.append(content_event)
983
+
984
+ sequence_counter += 1
985
+ event_sequence = sequence_counter
986
+ logprobs_value: Any
987
+ if logprobs is None:
988
+ logprobs_value = []
989
+ else:
990
+ logprobs_value = logprobs
991
+ obfuscation_value = obfuscation or obfuscation_factory.make(
992
+ "message.delta",
993
+ sequence=event_sequence,
994
+ item_id=message_item_id,
995
+ payload=delta_text,
996
+ )
997
+ events.append(
998
+ openai_models.ResponseOutputTextDeltaEvent(
999
+ type="response.output_text.delta",
1000
+ sequence_number=event_sequence,
1001
+ item_id=message_item_id,
1002
+ output_index=message_output_index or 0,
1003
+ content_index=content_index,
1004
+ delta=delta_text,
1005
+ logprobs=logprobs_value,
1006
+ )
1007
+ )
1008
+ message_text_buffer.append(delta_text)
1009
+ message_last_logprobs = logprobs_value
1010
+ return events
1011
+
1012
+ def _reasoning_key(signature: str | None) -> str:
1013
+ if isinstance(signature, str) and signature.strip():
1014
+ return signature.strip()
1015
+ return "__default__"
1016
+
1017
+ def get_summary_index(signature: str | None) -> int:
1018
+ nonlocal next_summary_index
1019
+ key = _reasoning_key(signature)
1020
+ maybe_index = reasoning_summary_indices.get(key)
1021
+ if maybe_index is not None:
1022
+ return maybe_index
1023
+ reasoning_summary_indices[key] = next_summary_index
1024
+ next_summary_index += 1
1025
+ return reasoning_summary_indices[key]
1026
+
1027
+ def ensure_reasoning_output_item() -> (
1028
+ openai_models.ResponseOutputItemAddedEvent | None
1029
+ ):
1030
+ nonlocal reasoning_item_added, reasoning_output_index
1031
+ nonlocal next_output_index, sequence_counter
1032
+ if reasoning_output_index is None:
1033
+ reasoning_output_index = next_output_index
1034
+ next_output_index += 1
1035
+ if not reasoning_item_added:
1036
+ reasoning_item_added = True
1037
+ sequence_counter += 1
1038
+ return openai_models.ResponseOutputItemAddedEvent(
1039
+ type="response.output_item.added",
1040
+ sequence_number=sequence_counter,
1041
+ output_index=reasoning_output_index,
1042
+ item=openai_models.OutputItem(
1043
+ id=reasoning_item_id,
1044
+ type="reasoning",
1045
+ status="in_progress",
1046
+ summary=[],
1047
+ ),
1048
+ )
1049
+ return None
1050
+
1051
+ def ensure_reasoning_summary_part(
1052
+ summary_index: int,
1053
+ ) -> openai_models.ReasoningSummaryPartAddedEvent | None:
1054
+ nonlocal sequence_counter
1055
+ if reasoning_output_index is None:
1056
+ return None
1057
+ if summary_index in reasoning_summary_added:
1058
+ return None
1059
+ reasoning_summary_added.add(summary_index)
1060
+ sequence_counter += 1
1061
+ return openai_models.ReasoningSummaryPartAddedEvent(
1062
+ type="response.reasoning_summary_part.added",
1063
+ sequence_number=sequence_counter,
1064
+ item_id=reasoning_item_id,
1065
+ output_index=reasoning_output_index,
1066
+ summary_index=summary_index,
1067
+ part=openai_models.ReasoningSummaryPart(
1068
+ type="summary_text",
1069
+ text="",
1070
+ ),
1071
+ )
1072
+
1073
+ def emit_reasoning_segments(
1074
+ segments: list[ThinkingSegment],
1075
+ ) -> list[openai_models.StreamEventType]:
1076
+ events: list[openai_models.StreamEventType] = []
1077
+ if not segments:
1078
+ return events
1079
+
1080
+ output_event = ensure_reasoning_output_item()
1081
+ if output_event is not None:
1082
+ events.append(output_event)
1083
+
1084
+ nonlocal sequence_counter
1085
+ for segment in segments:
1086
+ text_value = getattr(segment, "thinking", "")
1087
+ if not isinstance(text_value, str) or not text_value:
1088
+ continue
1089
+ summary_index = get_summary_index(
1090
+ getattr(segment, "signature", None)
1091
+ )
1092
+ signature_value = getattr(segment, "signature", None)
1093
+ if summary_index not in reasoning_summary_signatures:
1094
+ reasoning_summary_signatures[summary_index] = signature_value
1095
+ part_event = ensure_reasoning_summary_part(summary_index)
1096
+ if part_event is not None:
1097
+ events.append(part_event)
1098
+ fragments = reasoning_summary_text_fragments.setdefault(
1099
+ summary_index, []
1100
+ )
1101
+ fragments.append(text_value)
1102
+ sequence_counter += 1
1103
+ event_sequence = sequence_counter
1104
+ events.append(
1105
+ openai_models.ReasoningSummaryTextDeltaEvent(
1106
+ type="response.reasoning_summary_text.delta",
1107
+ sequence_number=event_sequence,
1108
+ item_id=reasoning_item_id,
1109
+ output_index=reasoning_output_index or 0,
1110
+ summary_index=summary_index,
1111
+ delta=text_value,
1112
+ )
1113
+ )
1114
+ return events
1115
+
1116
+ def finalize_reasoning() -> list[openai_models.StreamEventType]:
1117
+ nonlocal reasoning_output_done, reasoning_completed_entry
1118
+ nonlocal reasoning_summary_payload, sequence_counter
1119
+ if not reasoning_item_added or reasoning_output_index is None:
1120
+ return []
1121
+
1122
+ events: list[openai_models.StreamEventType] = []
1123
+ summary_entries: list[dict[str, Any]] = []
1124
+
1125
+ for summary_index in sorted(reasoning_summary_text_fragments):
1126
+ text_value = "".join(
1127
+ reasoning_summary_text_fragments.get(summary_index, [])
1128
+ )
1129
+ if summary_index not in reasoning_summary_text_done:
1130
+ sequence_counter += 1
1131
+ events.append(
1132
+ openai_models.ReasoningSummaryTextDoneEvent(
1133
+ type="response.reasoning_summary_text.done",
1134
+ sequence_number=sequence_counter,
1135
+ item_id=reasoning_item_id,
1136
+ output_index=reasoning_output_index,
1137
+ summary_index=summary_index,
1138
+ text=text_value,
1139
+ )
1140
+ )
1141
+ reasoning_summary_text_done.add(summary_index)
1142
+ if summary_index not in reasoning_summary_part_done:
1143
+ sequence_counter += 1
1144
+ events.append(
1145
+ openai_models.ReasoningSummaryPartDoneEvent(
1146
+ type="response.reasoning_summary_part.done",
1147
+ sequence_number=sequence_counter,
1148
+ item_id=reasoning_item_id,
1149
+ output_index=reasoning_output_index,
1150
+ summary_index=summary_index,
1151
+ part=openai_models.ReasoningSummaryPart(
1152
+ type="summary_text",
1153
+ text=text_value,
1154
+ ),
1155
+ )
1156
+ )
1157
+ reasoning_summary_part_done.add(summary_index)
1158
+ summary_entry: dict[str, Any] = {
1159
+ "type": "summary_text",
1160
+ "text": text_value,
1161
+ }
1162
+ signature_value = reasoning_summary_signatures.get(summary_index)
1163
+ if signature_value:
1164
+ summary_entry["signature"] = signature_value
1165
+ summary_entries.append(summary_entry)
1166
+
1167
+ reasoning_summary_payload = summary_entries
1168
+
1169
+ if not reasoning_output_done:
1170
+ sequence_counter += 1
1171
+ events.append(
1172
+ openai_models.ResponseOutputItemDoneEvent(
1173
+ type="response.output_item.done",
1174
+ sequence_number=sequence_counter,
1175
+ output_index=reasoning_output_index,
1176
+ item=openai_models.OutputItem(
1177
+ id=reasoning_item_id,
1178
+ type="reasoning",
1179
+ status="completed",
1180
+ summary=summary_entries,
1181
+ ),
1182
+ )
1183
+ )
1184
+ reasoning_output_done = True
1185
+ reasoning_completed_entry = (
1186
+ reasoning_output_index,
1187
+ openai_models.ReasoningOutput(
1188
+ type="reasoning",
1189
+ id=reasoning_item_id,
1190
+ status="completed",
1191
+ summary=summary_entries,
1192
+ ),
1193
+ )
1194
+
1195
+ return events
1196
+
1197
+ def finalize_message() -> list[openai_models.StreamEventType]:
1198
+ nonlocal sequence_counter
1199
+ nonlocal message_text_done_emitted, message_part_done_emitted
1200
+ nonlocal message_item_done_emitted, message_completed_entry
1201
+ nonlocal message_last_logprobs
1202
+
1203
+ if not message_item_added:
1204
+ return []
1205
+
1206
+ events: list[openai_models.StreamEventType] = []
1207
+ final_text = "".join(message_text_buffer)
1208
+ logprobs_value: Any
1209
+ if message_last_logprobs is None:
1210
+ logprobs_value = []
1211
+ else:
1212
+ logprobs_value = message_last_logprobs
1213
+
1214
+ if message_content_part_added and not message_text_done_emitted:
1215
+ sequence_counter += 1
1216
+ event_sequence = sequence_counter
1217
+ events.append(
1218
+ openai_models.ResponseOutputTextDoneEvent(
1219
+ type="response.output_text.done",
1220
+ sequence_number=event_sequence,
1221
+ item_id=message_item_id,
1222
+ output_index=message_output_index or 0,
1223
+ content_index=content_index,
1224
+ text=final_text,
1225
+ logprobs=logprobs_value,
1226
+ )
1227
+ )
1228
+ message_text_done_emitted = True
1229
+
1230
+ if message_content_part_added and not message_part_done_emitted:
1231
+ sequence_counter += 1
1232
+ event_sequence = sequence_counter
1233
+ events.append(
1234
+ openai_models.ResponseContentPartDoneEvent(
1235
+ type="response.content_part.done",
1236
+ sequence_number=event_sequence,
1237
+ item_id=message_item_id,
1238
+ output_index=message_output_index or 0,
1239
+ content_index=content_index,
1240
+ part=openai_models.ContentPart(
1241
+ type="output_text",
1242
+ text=final_text,
1243
+ annotations=[],
1244
+ ),
1245
+ )
1246
+ )
1247
+ message_part_done_emitted = True
1248
+
1249
+ if not message_item_done_emitted:
1250
+ sequence_counter += 1
1251
+ event_sequence = sequence_counter
1252
+ output_text_part = openai_models.OutputTextContent(
1253
+ type="output_text",
1254
+ text=final_text,
1255
+ annotations=[],
1256
+ logprobs=logprobs_value if logprobs_value != [] else [],
1257
+ )
1258
+ message_output = openai_models.MessageOutput(
1259
+ type="message",
1260
+ id=message_item_id,
1261
+ status="completed",
1262
+ role="assistant",
1263
+ content=[output_text_part] if final_text else [],
1264
+ )
1265
+ message_completed_entry = (
1266
+ message_output_index or 0,
1267
+ message_output,
1268
+ )
1269
+ events.append(
1270
+ openai_models.ResponseOutputItemDoneEvent(
1271
+ type="response.output_item.done",
1272
+ sequence_number=event_sequence,
1273
+ output_index=message_output_index or 0,
1274
+ item=openai_models.OutputItem(
1275
+ id=message_item_id,
1276
+ type="message",
1277
+ role="assistant",
1278
+ status="completed",
1279
+ content=[output_text_part.model_dump()]
1280
+ if final_text
1281
+ else [],
1282
+ text=final_text or None,
1283
+ ),
1284
+ )
1285
+ )
1286
+ message_item_done_emitted = True
1287
+ elif message_completed_entry is None:
1288
+ output_text_part = openai_models.OutputTextContent(
1289
+ type="output_text",
1290
+ text=final_text,
1291
+ annotations=[],
1292
+ logprobs=logprobs_value if logprobs_value != [] else [],
1293
+ )
1294
+ message_completed_entry = (
1295
+ message_output_index or 0,
1296
+ openai_models.MessageOutput(
1297
+ type="message",
1298
+ id=message_item_id,
1299
+ status="completed",
1300
+ role="assistant",
1301
+ content=[output_text_part] if final_text else [],
1302
+ ),
1303
+ )
1304
+
1305
+ return events
1306
+
1307
+ def get_tool_state(index: int) -> ToolCallState:
1308
+ nonlocal next_output_index
1309
+ state = tool_call_states.ensure(index)
1310
+ if state.output_index < 0:
1311
+ state.output_index = next_output_index
1312
+ next_output_index += 1
1313
+ return state
1314
+
1315
+ def get_accumulator_entry(idx: int) -> dict[str, Any] | None:
1316
+ for entry in openai_accumulator.tools.values():
1317
+ if entry.get("index") == idx:
1318
+ return entry
1319
+ return None
1320
+
1321
+ def emit_tool_item_added(
1322
+ state: ToolCallState,
1323
+ ) -> list[openai_models.StreamEventType]:
1324
+ nonlocal sequence_counter
1325
+ if state.added_emitted:
1326
+ return []
1327
+ if state.name is None:
1328
+ return []
1329
+ if not state.item_id:
1330
+ item_identifier = state.call_id
1331
+ if not item_identifier:
1332
+ item_identifier = f"call_{state.index}"
1333
+ state.item_id = item_identifier
1334
+ sequence_counter += 1
1335
+ state.added_emitted = True
1336
+ return [
1337
+ openai_models.ResponseOutputItemAddedEvent(
1338
+ type="response.output_item.added",
1339
+ sequence_number=sequence_counter,
1340
+ output_index=state.output_index,
1341
+ item=openai_models.OutputItem(
1342
+ id=state.item_id,
1343
+ type="function_call",
1344
+ status="in_progress",
1345
+ name=state.name,
1346
+ arguments="",
1347
+ call_id=state.call_id,
1348
+ ),
1349
+ )
1350
+ ]
1351
+
1352
+ def finalize_tool_calls() -> list[openai_models.StreamEventType]:
1353
+ nonlocal sequence_counter
1354
+ events: list[openai_models.StreamEventType] = []
1355
+ for idx, state in tool_call_states.items():
1356
+ accumulator_entry = get_accumulator_entry(idx)
1357
+ if state.name is None and accumulator_entry is not None:
1358
+ fn_name = accumulator_entry.get("function", {}).get("name")
1359
+ if isinstance(fn_name, str) and fn_name:
1360
+ state.name = fn_name
1361
+ if state.call_id is None and accumulator_entry is not None:
1362
+ call_identifier = accumulator_entry.get("id")
1363
+ if isinstance(call_identifier, str) and call_identifier:
1364
+ state.call_id = call_identifier
1365
+ if not state.item_id:
1366
+ candidate_id = None
1367
+ if accumulator_entry is not None:
1368
+ candidate_id = accumulator_entry.get("id")
1369
+ state.item_id = (
1370
+ candidate_id or state.call_id or f"call_{state.index}"
1371
+ )
1372
+ if not state.added_emitted:
1373
+ events.extend(emit_tool_item_added(state))
1374
+ final_args = state.final_arguments
1375
+ if final_args is None:
1376
+ combined = "".join(state.arguments_parts or [])
1377
+ if not combined and accumulator_entry is not None:
1378
+ combined = (
1379
+ accumulator_entry.get("function", {}).get("arguments")
1380
+ or ""
1381
+ )
1382
+ final_args = combined or ""
1383
+ state.final_arguments = final_args
1384
+ if not state.arguments_done_emitted:
1385
+ sequence_counter += 1
1386
+ events.append(
1387
+ openai_models.ResponseFunctionCallArgumentsDoneEvent(
1388
+ type="response.function_call_arguments.done",
1389
+ sequence_number=sequence_counter,
1390
+ item_id=state.item_id,
1391
+ output_index=state.output_index,
1392
+ arguments=final_args,
1393
+ )
1394
+ )
1395
+ state.arguments_done_emitted = True
1396
+ if not state.item_done_emitted:
1397
+ sequence_counter += 1
1398
+ events.append(
1399
+ openai_models.ResponseOutputItemDoneEvent(
1400
+ type="response.output_item.done",
1401
+ sequence_number=sequence_counter,
1402
+ output_index=state.output_index,
1403
+ item=openai_models.OutputItem(
1404
+ id=state.item_id,
1405
+ type="function_call",
1406
+ status="completed",
1407
+ name=state.name,
1408
+ arguments=final_args,
1409
+ call_id=state.call_id,
1410
+ ),
1411
+ )
1412
+ )
1413
+ state.item_done_emitted = True
1414
+ return events
1415
+
1416
+ def make_response_object(
1417
+ *,
1418
+ status: str,
1419
+ model: str | None,
1420
+ usage: openai_models.ResponseUsage | None = None,
1421
+ output: list[Any] | None = None,
1422
+ parallel_override: bool | None = None,
1423
+ reasoning_summary: list[dict[str, Any]] | None = None,
1424
+ extra: dict[str, Any] | None = None,
1425
+ ) -> openai_models.ResponseObject:
1426
+ payload = dict(envelope_base_kwargs)
1427
+ payload["status"] = status
1428
+ payload["model"] = model or payload.get("model") or ""
1429
+ payload["output"] = output or []
1430
+ payload["usage"] = usage
1431
+ payload.setdefault("object", "response")
1432
+ payload.setdefault("created_at", int(time.time()))
1433
+ if parallel_override is not None:
1434
+ payload["parallel_tool_calls"] = parallel_override
1435
+ if reasoning_summary is not None:
1436
+ reasoning_entry = payload.get("reasoning")
1437
+ if isinstance(reasoning_entry, openai_models.Reasoning):
1438
+ payload["reasoning"] = reasoning_entry.model_copy(
1439
+ update={"summary": reasoning_summary}
1440
+ )
1441
+ elif isinstance(reasoning_entry, dict):
1442
+ payload["reasoning"] = openai_models.Reasoning(
1443
+ effort=reasoning_entry.get("effort"),
1444
+ summary=reasoning_summary,
1445
+ )
1446
+ else:
1447
+ payload["reasoning"] = openai_models.Reasoning(
1448
+ effort=None,
1449
+ summary=reasoning_summary,
1450
+ )
1451
+ if extra:
1452
+ payload.update(extra)
1453
+ return openai_models.ResponseObject(**payload)
1454
+
1455
+ try:
1456
+ async for chunk in stream:
1457
+ if isinstance(chunk, dict):
1458
+ chunk_payload = chunk
1459
+ else:
1460
+ chunk_payload = chunk.model_dump(exclude_none=True)
1461
+
1462
+ openai_accumulator.accumulate("", chunk_payload)
1463
+
1464
+ model = chunk_payload.get("model") or last_model
1465
+ choices = chunk_payload.get("choices") or []
1466
+ usage_obj = chunk_payload.get("usage")
1467
+
1468
+ finish_reasons: list[str | None] = []
1469
+ deltas: list[dict[str, Any]] = []
1470
+ for choice in choices:
1471
+ if not isinstance(choice, dict):
1472
+ continue
1473
+ finish_reasons.append(choice.get("finish_reason"))
1474
+ delta_obj = choice.get("delta") or {}
1475
+ if isinstance(delta_obj, dict):
1476
+ deltas.append(delta_obj)
1477
+
1478
+ last_model = model
1479
+ if model:
1480
+ envelope_base_kwargs["model"] = model
1481
+
1482
+ first_delta_text = deltas[0].get("content") if deltas else None
1483
+
1484
+ if not first_logged:
1485
+ first_logged = True
1486
+ with contextlib.suppress(Exception):
1487
+ log.debug(
1488
+ "chat_stream_first_chunk",
1489
+ typed=isinstance(chunk, dict) is False,
1490
+ keys=(
1491
+ list(chunk.keys())
1492
+ if isinstance(chunk, dict)
1493
+ else None
1494
+ ),
1495
+ has_delta=bool(first_delta_text),
1496
+ model=model,
1497
+ )
1498
+ if len(choices) == 0 and not model:
1499
+ log.debug("chat_stream_ignoring_first_chunk")
1500
+ continue
1501
+
1502
+ if not created_sent:
1503
+ created_sent = True
1504
+ response_id, id_suffix = ensure_identifier(
1505
+ "resp", chunk_payload.get("id")
1506
+ )
1507
+ envelope_base_kwargs["id"] = response_id
1508
+ envelope_base_kwargs.setdefault("object", "response")
1509
+ if not message_item_id:
1510
+ message_item_id = f"msg_{id_suffix}"
1511
+ if not reasoning_item_id:
1512
+ reasoning_item_id = f"rs_{id_suffix}"
1513
+
1514
+ created_at_value = chunk_payload.get(
1515
+ "created"
1516
+ ) or chunk_payload.get("created_at")
1517
+ if created_at_value is None:
1518
+ created_at_value = int(time.time())
1519
+ envelope_base_kwargs["created_at"] = int(created_at_value)
1520
+
1521
+ if model:
1522
+ envelope_base_kwargs["model"] = model
1523
+ elif last_model:
1524
+ envelope_base_kwargs.setdefault("model", last_model)
1525
+
1526
+ sequence_counter += 1
1527
+ response_created = make_response_object(
1528
+ status="in_progress",
1529
+ model=model or last_model,
1530
+ usage=None,
1531
+ output=[],
1532
+ parallel_override=parallel_setting_initial,
1533
+ )
1534
+ yield openai_models.ResponseCreatedEvent(
1535
+ type="response.created",
1536
+ sequence_number=sequence_counter,
1537
+ response=response_created,
1538
+ )
1539
+ sequence_counter += 1
1540
+ yield openai_models.ResponseInProgressEvent(
1541
+ type="response.in_progress",
1542
+ sequence_number=sequence_counter,
1543
+ response=make_response_object(
1544
+ status="in_progress",
1545
+ model=model or last_model,
1546
+ usage=latest_usage_model,
1547
+ output=[],
1548
+ parallel_override=parallel_setting_initial,
1549
+ ),
1550
+ )
1551
+
1552
+ for delta in deltas:
1553
+ reasoning_payload = delta.get("reasoning")
1554
+ if reasoning_payload is not None:
1555
+ segments = _collect_reasoning_segments(reasoning_payload)
1556
+ for event in emit_reasoning_segments(segments):
1557
+ yield event
1558
+
1559
+ content_value = delta.get("content")
1560
+ if isinstance(content_value, str) and content_value:
1561
+ for event in emit_message_text_delta(content_value):
1562
+ yield event
1563
+ elif isinstance(content_value, dict):
1564
+ part_type = content_value.get("type")
1565
+ if part_type in {"reasoning", "thinking"}:
1566
+ segments = _collect_reasoning_segments(content_value)
1567
+ for event in emit_reasoning_segments(segments):
1568
+ yield event
1569
+ else:
1570
+ text_value = content_value.get("text")
1571
+ if not isinstance(text_value, str) or not text_value:
1572
+ delta_text = content_value.get("delta")
1573
+ if isinstance(delta_text, str) and delta_text:
1574
+ text_value = delta_text
1575
+ if isinstance(text_value, str) and text_value:
1576
+ for event in emit_message_text_delta(
1577
+ text_value,
1578
+ logprobs=content_value.get("logprobs"),
1579
+ obfuscation=content_value.get("obfuscation")
1580
+ or content_value.get("obfuscated"),
1581
+ ):
1582
+ yield event
1583
+ elif isinstance(content_value, list):
1584
+ for part in content_value:
1585
+ if not isinstance(part, dict):
1586
+ continue
1587
+ part_type = part.get("type")
1588
+ if part_type in {"reasoning", "thinking"}:
1589
+ segments = _collect_reasoning_segments(part)
1590
+ for event in emit_reasoning_segments(segments):
1591
+ yield event
1592
+ continue
1593
+ text_value = part.get("text")
1594
+ if not isinstance(text_value, str) or not text_value:
1595
+ delta_text = part.get("delta")
1596
+ if isinstance(delta_text, str) and delta_text:
1597
+ text_value = delta_text
1598
+ if (
1599
+ part_type
1600
+ in {"text", "output_text", "output_text_delta"}
1601
+ and isinstance(text_value, str)
1602
+ and text_value
1603
+ ):
1604
+ for event in emit_message_text_delta(
1605
+ text_value,
1606
+ logprobs=part.get("logprobs"),
1607
+ obfuscation=part.get("obfuscation")
1608
+ or part.get("obfuscated"),
1609
+ ):
1610
+ yield event
1611
+
1612
+ tool_calls = delta.get("tool_calls") or []
1613
+ if isinstance(tool_calls, list):
1614
+ if tool_calls:
1615
+ for event in finalize_message():
1616
+ yield event
1617
+ for tool_call in tool_calls:
1618
+ if not isinstance(tool_call, dict):
1619
+ continue
1620
+ index_value = int(tool_call.get("index", 0))
1621
+ state = get_tool_state(index_value)
1622
+ tool_id = tool_call.get("id")
1623
+ if isinstance(tool_id, str) and tool_id:
1624
+ state.call_id = tool_id
1625
+ if not state.added_emitted or state.item_id is None:
1626
+ state.item_id = tool_id
1627
+ function_obj = tool_call.get("function") or {}
1628
+ if isinstance(function_obj, dict):
1629
+ name_value = function_obj.get("name")
1630
+ if isinstance(name_value, str) and name_value:
1631
+ state.name = name_value
1632
+ for event in emit_tool_item_added(state):
1633
+ yield event
1634
+ arguments_payload = function_obj.get("arguments")
1635
+ obfuscation_hint = None
1636
+ arguments_delta = ""
1637
+ if isinstance(arguments_payload, str):
1638
+ arguments_delta = arguments_payload
1639
+ elif isinstance(arguments_payload, dict):
1640
+ maybe_delta = arguments_payload.get("delta")
1641
+ if isinstance(maybe_delta, str):
1642
+ arguments_delta = maybe_delta
1643
+ obfuscation_hint = arguments_payload.get(
1644
+ "obfuscation"
1645
+ ) or arguments_payload.get("obfuscated")
1646
+ if arguments_delta:
1647
+ state.add_arguments_part(arguments_delta)
1648
+ sequence_counter += 1
1649
+ event_sequence = sequence_counter
1650
+ yield (
1651
+ delta_event_cls(
1652
+ type="response.function_call_arguments.delta",
1653
+ sequence_number=event_sequence,
1654
+ item_id=state.item_id
1655
+ or f"call_{state.index}",
1656
+ output_index=state.output_index,
1657
+ delta=arguments_delta,
1658
+ )
1659
+ )
1660
+ for tool_call in tool_calls:
1661
+ if not isinstance(tool_call, dict):
1662
+ continue
1663
+ index_value = int(tool_call.get("index", 0))
1664
+ state = get_tool_state(index_value)
1665
+ if state.name:
1666
+ for event in emit_tool_item_added(state):
1667
+ yield event
1668
+
1669
+ usage_model: openai_models.ResponseUsage | None = None
1670
+ if usage_obj is not None:
1671
+ try:
1672
+ if isinstance(usage_obj, openai_models.ResponseUsage):
1673
+ usage_model = usage_obj
1674
+ elif isinstance(usage_obj, dict):
1675
+ usage_model = convert_usage(
1676
+ openai_models.CompletionUsage.model_validate(
1677
+ usage_obj
1678
+ )
1679
+ )
1680
+ else:
1681
+ usage_model = convert_usage(usage_obj)
1682
+ except Exception:
1683
+ usage_model = None
1684
+
1685
+ if usage_model is not None:
1686
+ latest_usage_model = usage_model
1687
+ if all(reason is None for reason in finish_reasons):
1688
+ sequence_counter += 1
1689
+ yield openai_models.ResponseInProgressEvent(
1690
+ type="response.in_progress",
1691
+ sequence_number=sequence_counter,
1692
+ response=make_response_object(
1693
+ status="in_progress",
1694
+ model=model or last_model,
1695
+ usage=usage_model,
1696
+ output=[],
1697
+ parallel_override=parallel_setting_initial,
1698
+ ),
1699
+ )
1700
+
1701
+ if any(reason == "tool_calls" for reason in finish_reasons):
1702
+ for event in finalize_message():
1703
+ yield event
1704
+ for event in finalize_tool_calls():
1705
+ yield event
1706
+
1707
+ finally:
1708
+ register_request(None)
1709
+ register_request_tools(None)
1710
+
1711
+ for event in finalize_reasoning():
1712
+ yield event
1713
+
1714
+ for event in finalize_message():
1715
+ yield event
1716
+
1717
+ for event in finalize_tool_calls():
1718
+ yield event
1719
+
1720
+ if message_completed_entry is None and message_item_added:
1721
+ final_text = "".join(message_text_buffer)
1722
+ logprobs_value: Any
1723
+ if message_last_logprobs is None:
1724
+ logprobs_value = []
1725
+ else:
1726
+ logprobs_value = message_last_logprobs
1727
+ output_text_part = openai_models.OutputTextContent(
1728
+ type="output_text",
1729
+ text=final_text,
1730
+ annotations=[],
1731
+ logprobs=logprobs_value if logprobs_value != [] else [],
1732
+ )
1733
+ message_completed_entry = (
1734
+ message_output_index or 0,
1735
+ openai_models.MessageOutput(
1736
+ type="message",
1737
+ id=message_item_id,
1738
+ status="completed",
1739
+ role="assistant",
1740
+ content=[output_text_part] if final_text else [],
1741
+ ),
1742
+ )
1743
+
1744
+ completed_entries: list[tuple[int, Any]] = []
1745
+ if reasoning_completed_entry is not None:
1746
+ completed_entries.append(reasoning_completed_entry)
1747
+ if message_completed_entry is not None:
1748
+ completed_entries.append(message_completed_entry)
1749
+
1750
+ for idx, state in tool_call_states.items():
1751
+ accumulator_entry = get_accumulator_entry(idx)
1752
+ if state.final_arguments is None:
1753
+ aggregated = ""
1754
+ if accumulator_entry is not None:
1755
+ aggregated = (
1756
+ accumulator_entry.get("function", {}).get("arguments") or ""
1757
+ )
1758
+ if not aggregated:
1759
+ aggregated = "".join(state.arguments_parts or [])
1760
+ state.final_arguments = aggregated or ""
1761
+ if state.name is None and accumulator_entry is not None:
1762
+ fn_name = accumulator_entry.get("function", {}).get("name")
1763
+ if isinstance(fn_name, str) and fn_name:
1764
+ state.name = fn_name
1765
+ if not state.item_id:
1766
+ candidate_id = None
1767
+ if accumulator_entry is not None:
1768
+ candidate_id = accumulator_entry.get("id")
1769
+ state.item_id = candidate_id or f"call_{state.index}"
1770
+ completed_entries.append(
1771
+ (
1772
+ state.output_index,
1773
+ openai_models.FunctionCallOutput(
1774
+ type="function_call",
1775
+ id=state.item_id,
1776
+ status="completed",
1777
+ name=state.name,
1778
+ call_id=state.call_id,
1779
+ arguments=state.final_arguments or "",
1780
+ ),
1781
+ )
1782
+ )
1783
+
1784
+ completed_entries.sort(key=lambda item: item[0])
1785
+ completed_outputs = [entry for _, entry in completed_entries]
1786
+
1787
+ complete_tool_calls_payload = openai_accumulator.get_complete_tool_calls()
1788
+ parallel_tool_calls = len(tool_call_states) > 1
1789
+ parallel_final = parallel_tool_calls or parallel_setting_initial
1790
+
1791
+ extra_fields: dict[str, Any] | None = None
1792
+ if complete_tool_calls_payload:
1793
+ extra_fields = {"tool_calls": complete_tool_calls_payload}
1794
+
1795
+ response_completed = make_response_object(
1796
+ status="completed",
1797
+ model=last_model,
1798
+ usage=latest_usage_model,
1799
+ output=completed_outputs,
1800
+ parallel_override=parallel_final,
1801
+ reasoning_summary=reasoning_summary_payload,
1802
+ extra=extra_fields,
1803
+ )
1804
+
1805
+ sequence_counter += 1
1806
+ yield openai_models.ResponseCompletedEvent(
1807
+ type="response.completed",
1808
+ sequence_number=sequence_counter,
1809
+ response=response_completed,
1810
+ )
1811
+
1812
+ return generator()
1813
+
1814
+
1815
+ def convert__openai_chat_to_openai_responses__stream(
1816
+ stream: AsyncIterator[openai_models.ChatCompletionChunk | dict[str, Any]],
1817
+ ) -> AsyncGenerator[openai_models.StreamEventType, None]:
1818
+ """Convert OpenAI ChatCompletionChunk stream to Responses API events.
1819
+
1820
+ Replays chat deltas as Responses events, including function-call output items
1821
+ and argument deltas so partial tool calls stream correctly.
1822
+ """
1823
+ adapter = OpenAIChatToResponsesStreamAdapter()
1824
+ return adapter.run(stream)
1825
+
1826
+
1827
+ __all__ = [
1828
+ "OpenAIChatToResponsesStreamAdapter",
1829
+ "OpenAIResponsesToChatStreamAdapter",
1830
+ "convert__openai_chat_to_openai_responses__stream",
1831
+ "convert__openai_responses_to_openai_chat__stream",
1832
+ ]