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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,594 @@
1
+ """Response conversion entry points for OpenAI↔OpenAI adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import time
8
+ from typing import Any, Literal
9
+
10
+ import ccproxy.core.logging
11
+ from ccproxy.llms.formatters.common import (
12
+ THINKING_PATTERN,
13
+ ThinkingSegment,
14
+ convert_openai_completion_usage_to_responses_usage,
15
+ convert_openai_responses_usage_to_completion_usage,
16
+ merge_thinking_segments,
17
+ )
18
+ from ccproxy.llms.models import openai as openai_models
19
+
20
+ from ._helpers import (
21
+ _get_attr,
22
+ )
23
+
24
+
25
+ logger = ccproxy.core.logging.get_logger(__name__)
26
+
27
+
28
+ def convert__openai_responses_usage_to_openai_completion__usage(
29
+ usage: openai_models.ResponseUsage,
30
+ ) -> openai_models.CompletionUsage:
31
+ return convert_openai_responses_usage_to_completion_usage(usage)
32
+
33
+
34
+ def convert__openai_completion_usage_to_openai_responses__usage(
35
+ usage: openai_models.CompletionUsage,
36
+ ) -> openai_models.ResponseUsage:
37
+ return convert_openai_completion_usage_to_responses_usage(usage)
38
+
39
+
40
+ def _adopt_summary_entry(entry: Any) -> dict[str, Any] | None:
41
+ """Conversion of arbitrary summary nodes to dicts."""
42
+
43
+ if isinstance(entry, dict):
44
+ return entry
45
+
46
+ if hasattr(entry, "model_dump"):
47
+ with contextlib.suppress(Exception):
48
+ data = entry.model_dump(mode="json", exclude_none=True)
49
+ if isinstance(data, dict):
50
+ return data
51
+ with contextlib.suppress(Exception):
52
+ data = entry.model_dump()
53
+ if isinstance(data, dict):
54
+ return data
55
+
56
+ if hasattr(entry, "__dict__"):
57
+ dict_data: dict[str, Any] = {}
58
+ for key in (
59
+ "type",
60
+ "text",
61
+ "content",
62
+ "signature",
63
+ "summary",
64
+ "value",
65
+ "delta",
66
+ "reasoning",
67
+ ):
68
+ if hasattr(entry, key):
69
+ value = getattr(entry, key)
70
+ if value is not None:
71
+ dict_data[key] = value
72
+ if dict_data:
73
+ return dict_data
74
+ return None
75
+
76
+
77
+ def _collect_reasoning_segments(source: Any) -> list[ThinkingSegment]:
78
+ if source is None:
79
+ return []
80
+
81
+ segments: list[ThinkingSegment] = []
82
+ visited: set[int] = set()
83
+
84
+ def _walk(node: Any, inherited_signature: str | None) -> None:
85
+ if node is None:
86
+ return
87
+
88
+ # if isinstance(node, str):
89
+ # if node:
90
+ # normalized = node.strip().lower()
91
+ # if normalized and normalized not in _REASONING_SUMMARY_MODES:
92
+ # segments.append(
93
+ # ThinkingSegment(thinking=node, signature=inherited_signature)
94
+ # )
95
+ # return
96
+
97
+ if isinstance(node, bytes | bytearray):
98
+ try:
99
+ decoded = node.decode()
100
+ except UnicodeDecodeError:
101
+ return
102
+ if decoded:
103
+ segments.append(
104
+ ThinkingSegment(thinking=decoded, signature=inherited_signature)
105
+ )
106
+ return
107
+
108
+ if isinstance(node, list | tuple | set):
109
+ node_id = id(node)
110
+ if node_id in visited:
111
+ return
112
+ visited.add(node_id)
113
+ start_idx = len(segments)
114
+ current_signature = inherited_signature
115
+ for child in node:
116
+ child_data = _adopt_summary_entry(child)
117
+ child_type = (
118
+ child_data.get("type")
119
+ if isinstance(child_data, dict)
120
+ else _get_attr(child, "type")
121
+ )
122
+ if child_type == "signature":
123
+ candidate = None
124
+ if isinstance(child_data, dict):
125
+ candidate = child_data.get("text") or child_data.get(
126
+ "signature"
127
+ )
128
+ else:
129
+ candidate = _get_attr(child, "text") or _get_attr(
130
+ child, "signature"
131
+ )
132
+ if isinstance(candidate, str) and candidate:
133
+ current_signature = candidate
134
+ for idx in range(start_idx, len(segments)):
135
+ segments[idx] = ThinkingSegment(
136
+ thinking=segments[idx].thinking,
137
+ signature=current_signature,
138
+ )
139
+ start_idx = len(segments)
140
+ _walk(child, current_signature)
141
+ return
142
+
143
+ if isinstance(node, dict) or hasattr(node, "__dict__"):
144
+ current_node_id = id(node)
145
+ if current_node_id in visited:
146
+ return
147
+ visited.add(current_node_id)
148
+
149
+ data = _adopt_summary_entry(node)
150
+ if data is None:
151
+ text_attr = _get_attr(node, "text")
152
+ signature_attr = _get_attr(node, "signature")
153
+ type_attr = _get_attr(node, "type")
154
+ next_signature = inherited_signature
155
+ if isinstance(signature_attr, str) and signature_attr:
156
+ next_signature = signature_attr
157
+ if type_attr == "signature" and isinstance(text_attr, str) and text_attr:
158
+ next_signature = text_attr
159
+ text_attr = None
160
+ if isinstance(text_attr, str) and text_attr:
161
+ segments.append(
162
+ ThinkingSegment(thinking=text_attr, signature=next_signature)
163
+ )
164
+
165
+ for key in ("summary", "content"):
166
+ nested = _get_attr(node, key)
167
+ if isinstance(nested, list | tuple | set):
168
+ for child in nested:
169
+ _walk(child, next_signature)
170
+ elif isinstance(nested, dict):
171
+ _walk(nested, next_signature)
172
+ return
173
+
174
+ node_type = data.get("type")
175
+ text_value = data.get("text")
176
+ signature_value = data.get("signature")
177
+ content_value = data.get("content")
178
+ summary_value = data.get("summary")
179
+ reasoning_value = data.get("reasoning")
180
+
181
+ next_signature = inherited_signature
182
+ if isinstance(signature_value, str) and signature_value:
183
+ next_signature = signature_value
184
+
185
+ if node_type == "signature":
186
+ if isinstance(text_value, str) and text_value:
187
+ next_signature = text_value
188
+ if isinstance(content_value, list | tuple | set):
189
+ for child in content_value:
190
+ _walk(child, next_signature)
191
+ return
192
+
193
+ if node_type in {"summary_group", "group"}:
194
+ if isinstance(content_value, list | tuple | set):
195
+ start_idx = len(segments)
196
+ current_signature = next_signature
197
+ for child in content_value:
198
+ child_data = _adopt_summary_entry(child)
199
+ child_type = (
200
+ child_data.get("type")
201
+ if isinstance(child_data, dict)
202
+ else _get_attr(child, "type")
203
+ )
204
+ if child_type == "signature":
205
+ candidate = None
206
+ if isinstance(child_data, dict):
207
+ candidate = child_data.get("text") or child_data.get(
208
+ "signature"
209
+ )
210
+ else:
211
+ candidate = _get_attr(child, "text") or _get_attr(
212
+ child, "signature"
213
+ )
214
+ if isinstance(candidate, str) and candidate:
215
+ current_signature = candidate
216
+ for idx in range(start_idx, len(segments)):
217
+ segments[idx] = ThinkingSegment(
218
+ thinking=segments[idx].thinking,
219
+ signature=current_signature,
220
+ )
221
+ start_idx = len(segments)
222
+ _walk(child, current_signature)
223
+ return
224
+
225
+ emitted = False
226
+ if node_type in {"summary_text", "text", "reasoning_text"}:
227
+ if isinstance(text_value, str) and text_value:
228
+ segments.append(
229
+ ThinkingSegment(thinking=text_value, signature=next_signature)
230
+ )
231
+ emitted = True
232
+ elif (
233
+ isinstance(text_value, str)
234
+ and text_value
235
+ and node_type not in {"signature"}
236
+ ):
237
+ segments.append(
238
+ ThinkingSegment(thinking=text_value, signature=next_signature)
239
+ )
240
+ emitted = True
241
+
242
+ value_value = data.get("value")
243
+ if not emitted and isinstance(value_value, str) and value_value:
244
+ segments.append(
245
+ ThinkingSegment(thinking=value_value, signature=next_signature)
246
+ )
247
+ emitted = True
248
+
249
+ if isinstance(summary_value, list | tuple | set):
250
+ for child in summary_value:
251
+ _walk(child, next_signature)
252
+ elif isinstance(summary_value, dict):
253
+ _walk(summary_value, next_signature)
254
+
255
+ if isinstance(content_value, list | tuple | set):
256
+ for child in content_value:
257
+ _walk(child, next_signature)
258
+ elif isinstance(content_value, dict):
259
+ _walk(content_value, next_signature)
260
+
261
+ if isinstance(reasoning_value, list | tuple | set | dict):
262
+ _walk(reasoning_value, next_signature)
263
+
264
+ _walk(source, None)
265
+ return merge_thinking_segments(segments)
266
+
267
+
268
+ def _wrap_thinking(signature: str | None, text: str) -> str:
269
+ """Serialize a reasoning block into <thinking> XML."""
270
+ return ThinkingSegment(thinking=text, signature=signature).to_xml()
271
+
272
+
273
+ def _extract_reasoning_blocks(payload: Any) -> list[ThinkingSegment]:
274
+ """Extract reasoning blocks from a response output payload."""
275
+
276
+ if not payload:
277
+ return []
278
+
279
+ summary = _get_attr(payload, "summary")
280
+ segments = _collect_reasoning_segments(summary)
281
+ if segments:
282
+ return segments
283
+
284
+ if isinstance(payload, list | tuple | set):
285
+ segments = _collect_reasoning_segments(payload)
286
+ if segments:
287
+ return segments
288
+
289
+ text_value = _get_attr(payload, "text")
290
+ if isinstance(text_value, str) and text_value:
291
+ return [ThinkingSegment(thinking=text_value)]
292
+
293
+ if isinstance(payload, dict):
294
+ raw = payload.get("reasoning")
295
+ if raw:
296
+ return _extract_reasoning_blocks(raw)
297
+
298
+ return []
299
+
300
+
301
+ def _split_content_segments(content: str) -> list[tuple[str, Any]]:
302
+ """Split mixed assistant content into ordered text vs thinking blocks."""
303
+
304
+ if not content:
305
+ return []
306
+
307
+ segments: list[tuple[str, Any]] = []
308
+ last_idx = 0
309
+ for match in THINKING_PATTERN.finditer(content):
310
+ start, end = match.span()
311
+ if start > last_idx:
312
+ text_segment = content[last_idx:start]
313
+ if text_segment:
314
+ segments.append(("text", text_segment))
315
+ signature = match.group(1) or None
316
+ thinking_text = match.group(2) or ""
317
+ segments.append(
318
+ ("thinking", ThinkingSegment.from_xml(signature, thinking_text))
319
+ )
320
+ last_idx = end
321
+
322
+ if last_idx < len(content):
323
+ tail = content[last_idx:]
324
+ if tail:
325
+ segments.append(("text", tail))
326
+
327
+ if not segments:
328
+ segments.append(("text", content))
329
+ return segments
330
+
331
+
332
+ def convert__openai_responses_to_openai_chat__response(
333
+ response: openai_models.ResponseObject,
334
+ ) -> openai_models.ChatCompletionResponse:
335
+ """Convert an OpenAI ResponseObject to a ChatCompletionResponse."""
336
+ text_segments: list[str] = []
337
+ added_reasoning: set[tuple[str, str]] = set()
338
+ tool_calls: list[openai_models.ToolCall] = []
339
+
340
+ for item in response.output or []:
341
+ logger.debug(
342
+ "convert_responses_to_chat_response_item", item_type=_get_attr(item, "type")
343
+ )
344
+ item_type = _get_attr(item, "type")
345
+ if item_type == "reasoning":
346
+ for segment in _extract_reasoning_blocks(item):
347
+ signature = segment.signature
348
+ thinking_text = segment.thinking
349
+ logger.debug(
350
+ "convert_responses_to_chat_reasoning_block",
351
+ signature=signature,
352
+ text_snippet=(thinking_text[:30] + "...")
353
+ if thinking_text and len(thinking_text) > 30
354
+ else thinking_text,
355
+ )
356
+ if thinking_text:
357
+ key = (signature or "", thinking_text)
358
+ if key not in added_reasoning:
359
+ text_segments.append(_wrap_thinking(signature, thinking_text))
360
+ added_reasoning.add(key)
361
+ elif item_type == "message":
362
+ parts: list[str] = []
363
+ content_list = _get_attr(item, "content")
364
+ if isinstance(content_list, list):
365
+ for part in content_list:
366
+ part_type = _get_attr(part, "type")
367
+ if part_type == "output_text":
368
+ text_val = _get_attr(part, "text")
369
+ if isinstance(text_val, str):
370
+ parts.append(text_val)
371
+ elif isinstance(part, str):
372
+ parts.append(part)
373
+ elif isinstance(content_list, str):
374
+ parts.append(content_list)
375
+ if parts:
376
+ text_segments.append("".join(parts))
377
+ elif item_type == "function_call":
378
+ function_block = _get_attr(item, "function")
379
+ name = _get_attr(function_block, "name") or _get_attr(item, "name")
380
+ arguments_value: Any = _get_attr(item, "arguments")
381
+ if arguments_value is None and isinstance(function_block, dict):
382
+ arguments_value = function_block.get("arguments")
383
+
384
+ if not isinstance(name, str) or not name:
385
+ continue
386
+
387
+ if isinstance(arguments_value, dict):
388
+ arguments_str = json.dumps(arguments_value)
389
+ elif isinstance(arguments_value, str):
390
+ arguments_str = arguments_value
391
+ else:
392
+ arguments_str = json.dumps(arguments_value or {})
393
+
394
+ tool_calls.append(
395
+ openai_models.ToolCall(
396
+ id=_get_attr(item, "id")
397
+ or _get_attr(item, "call_id")
398
+ or f"call_{len(tool_calls)}",
399
+ type="function",
400
+ function=openai_models.FunctionCall(
401
+ name=name,
402
+ arguments=arguments_str,
403
+ ),
404
+ )
405
+ )
406
+
407
+ text_content = "".join(text_segments)
408
+
409
+ usage = None
410
+ if response.usage:
411
+ usage = convert__openai_responses_usage_to_openai_completion__usage(
412
+ response.usage
413
+ )
414
+
415
+ finish_reason: Literal["stop", "length", "tool_calls", "content_filter"] = (
416
+ "tool_calls" if tool_calls else "stop"
417
+ )
418
+
419
+ return openai_models.ChatCompletionResponse(
420
+ id=response.id or "chatcmpl-resp",
421
+ choices=[
422
+ openai_models.Choice(
423
+ index=0,
424
+ message=openai_models.ResponseMessage(
425
+ role="assistant",
426
+ content=text_content,
427
+ tool_calls=tool_calls or None,
428
+ ),
429
+ finish_reason=finish_reason,
430
+ )
431
+ ],
432
+ created=0,
433
+ model=response.model or "",
434
+ object="chat.completion",
435
+ usage=usage
436
+ or openai_models.CompletionUsage(
437
+ prompt_tokens=0, completion_tokens=0, total_tokens=0
438
+ ),
439
+ )
440
+
441
+
442
+ async def convert__openai_chat_to_openai_responses__response(
443
+ chat_response: openai_models.ChatCompletionResponse,
444
+ ) -> openai_models.ResponseObject:
445
+ content_text = ""
446
+ tool_calls: list[Any] = []
447
+ if chat_response.choices:
448
+ first_choice = chat_response.choices[0]
449
+ if first_choice.message:
450
+ content = first_choice.message.content
451
+ if content:
452
+ if isinstance(content, str):
453
+ content_text = content
454
+ elif isinstance(content, list):
455
+ # Handle list content - convert to string
456
+ content_text = str(content)
457
+ else:
458
+ content_text = str(content)
459
+ if first_choice.message.tool_calls:
460
+ tool_calls = list(first_choice.message.tool_calls)
461
+
462
+ segments = _split_content_segments(content_text)
463
+
464
+ outputs: list[Any] = []
465
+ reasoning_entries: list[Any] = []
466
+ message_buffer: list[str] = []
467
+ message_counter = 0
468
+
469
+ def flush_message() -> None:
470
+ nonlocal message_buffer, message_counter
471
+ if not message_buffer:
472
+ return
473
+ message_text = "".join(message_buffer)
474
+ message_buffer = []
475
+ message_id = f"msg_{chat_response.id or 'unknown'}_{message_counter}"
476
+ message_counter += 1
477
+ outputs.append(
478
+ openai_models.MessageOutput(
479
+ type="message",
480
+ role="assistant",
481
+ id=message_id,
482
+ status="completed",
483
+ content=[
484
+ openai_models.OutputTextContent(
485
+ type="output_text", text=message_text
486
+ )
487
+ ],
488
+ )
489
+ )
490
+
491
+ for segment in segments or [("text", "")]:
492
+ if not segment:
493
+ continue
494
+ kind = segment[0]
495
+ if kind == "text":
496
+ text_part = segment[1]
497
+ if isinstance(text_part, str) and text_part:
498
+ message_buffer.append(text_part)
499
+ elif kind == "thinking":
500
+ segment_value = segment[1]
501
+ if isinstance(segment_value, ThinkingSegment):
502
+ signature = segment_value.signature
503
+ thinking_text = segment_value.thinking
504
+ else:
505
+ signature = None
506
+ thinking_text = ""
507
+ flush_message()
508
+ summary_entry: dict[str, Any] = {
509
+ "type": "summary_text",
510
+ "text": thinking_text,
511
+ }
512
+ if signature:
513
+ summary_entry["signature"] = signature
514
+ reasoning_id = (
515
+ f"reasoning_{chat_response.id or 'unknown'}_{len(reasoning_entries)}"
516
+ )
517
+ reasoning_output = openai_models.ReasoningOutput(
518
+ type="reasoning",
519
+ id=reasoning_id,
520
+ status="completed",
521
+ summary=[summary_entry],
522
+ )
523
+ outputs.append(reasoning_output)
524
+ reasoning_entries.append(reasoning_output)
525
+
526
+ # Flush any remaining assistant text
527
+ flush_message()
528
+
529
+ if not outputs:
530
+ outputs.append(
531
+ openai_models.MessageOutput(
532
+ type="message",
533
+ role="assistant",
534
+ id=f"msg_{chat_response.id or 'unknown'}_0",
535
+ status="completed",
536
+ content=[openai_models.OutputTextContent(type="output_text", text="")],
537
+ )
538
+ )
539
+
540
+ if tool_calls:
541
+ for idx, tool_call in enumerate(tool_calls):
542
+ fn = getattr(tool_call, "function", None)
543
+ name = _get_attr(fn, "name") or _get_attr(tool_call, "name") or ""
544
+ arguments = _get_attr(fn, "arguments") or _get_attr(tool_call, "arguments")
545
+ if isinstance(arguments, dict):
546
+ arguments_value: str | dict[str, Any] | None = arguments
547
+ else:
548
+ arguments_value = str(arguments) if arguments is not None else None
549
+ outputs.append(
550
+ openai_models.FunctionCallOutput(
551
+ type="function_call",
552
+ id=getattr(tool_call, "id", f"call_{idx}"),
553
+ status="completed",
554
+ name=name,
555
+ call_id=getattr(tool_call, "id", None),
556
+ arguments=arguments_value,
557
+ )
558
+ )
559
+
560
+ reasoning_summary = []
561
+ for entry in reasoning_entries:
562
+ summary_list = _get_attr(entry, "summary")
563
+ if isinstance(summary_list, list):
564
+ reasoning_summary.extend(summary_list)
565
+
566
+ usage: openai_models.ResponseUsage | None = None
567
+ if chat_response.usage:
568
+ usage = convert__openai_completion_usage_to_openai_responses__usage(
569
+ chat_response.usage
570
+ )
571
+
572
+ return openai_models.ResponseObject(
573
+ id=chat_response.id or "resp-unknown",
574
+ object="response",
575
+ created_at=int(time.time()),
576
+ model=chat_response.model or "",
577
+ status="completed",
578
+ output=outputs,
579
+ parallel_tool_calls=False,
580
+ usage=usage,
581
+ reasoning=(
582
+ openai_models.Reasoning(summary=reasoning_summary)
583
+ if reasoning_summary
584
+ else None
585
+ ),
586
+ )
587
+
588
+
589
+ __all__ = [
590
+ "convert__openai_chat_to_openai_responses__response",
591
+ "convert__openai_completion_usage_to_openai_responses__usage",
592
+ "convert__openai_responses_to_openai_chat__response",
593
+ "convert__openai_responses_usage_to_openai_completion__usage",
594
+ ]