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
@@ -1,239 +1,80 @@
1
- """OpenAI streaming response formatting.
1
+ """Stream processing utilities for converting between different streaming formats.
2
2
 
3
- This module provides Server-Sent Events (SSE) formatting for OpenAI-compatible
4
- streaming responses.
3
+ This module provides stream processors that convert between different LLM
4
+ streaming response formats (e.g., Anthropic to OpenAI, OpenAI to Anthropic).
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import json
8
+ import os
10
9
  import time
11
10
  from collections.abc import AsyncIterator
12
11
  from typing import Any, Literal
13
12
 
14
- import structlog
15
-
16
- from .models import (
17
- generate_openai_response_id,
18
- )
13
+ from ccproxy.core.logging import get_logger
19
14
 
15
+ from .formatters import AnthropicSSEFormatter, OpenAISSEFormatter
20
16
 
21
- logger = structlog.get_logger(__name__)
22
17
 
18
+ logger = get_logger(__name__)
23
19
 
24
- class OpenAISSEFormatter:
25
- """Formats streaming responses to match OpenAI's SSE format."""
26
20
 
27
- @staticmethod
28
- def format_data_event(data: dict[str, Any]) -> str:
29
- """Format a data event for OpenAI-compatible Server-Sent Events.
30
-
31
- Args:
32
- data: Event data dictionary
33
-
34
- Returns:
35
- Formatted SSE string
36
- """
37
- json_data = json.dumps(data, separators=(",", ":"))
38
- return f"data: {json_data}\n\n"
21
+ class AnthropicStreamProcessor:
22
+ """Processes OpenAI streaming data into Anthropic SSE format."""
39
23
 
40
- @staticmethod
41
- def format_first_chunk(
42
- message_id: str, model: str, created: int, role: str = "assistant"
43
- ) -> str:
44
- """Format the first chunk with role and basic metadata.
24
+ def __init__(self, model: str = "claude-3-5-sonnet-20241022"):
25
+ """Initialize the stream processor.
45
26
 
46
27
  Args:
47
- message_id: Unique identifier for the completion
48
- model: Model name being used
49
- created: Unix timestamp when the completion was created
50
- role: Role of the assistant
51
-
52
- Returns:
53
- Formatted SSE string
28
+ model: Model name for responses
54
29
  """
55
- data = {
56
- "id": message_id,
57
- "object": "chat.completion.chunk",
58
- "created": created,
59
- "model": model,
60
- "choices": [
61
- {
62
- "index": 0,
63
- "delta": {"role": role},
64
- "logprobs": None,
65
- "finish_reason": None,
66
- }
67
- ],
68
- }
69
- return OpenAISSEFormatter.format_data_event(data)
70
-
71
- @staticmethod
72
- def format_content_chunk(
73
- message_id: str, model: str, created: int, content: str, choice_index: int = 0
74
- ) -> str:
75
- """Format a content chunk with text delta.
76
-
77
- Args:
78
- message_id: Unique identifier for the completion
79
- model: Model name being used
80
- created: Unix timestamp when the completion was created
81
- content: Text content to include in the delta
82
- choice_index: Index of the choice (usually 0)
30
+ self.model = model
31
+ self.formatter = AnthropicSSEFormatter()
83
32
 
84
- Returns:
85
- Formatted SSE string
86
- """
87
- data = {
88
- "id": message_id,
89
- "object": "chat.completion.chunk",
90
- "created": created,
91
- "model": model,
92
- "choices": [
93
- {
94
- "index": choice_index,
95
- "delta": {"content": content},
96
- "logprobs": None,
97
- "finish_reason": None,
98
- }
99
- ],
100
- }
101
- return OpenAISSEFormatter.format_data_event(data)
102
-
103
- @staticmethod
104
- def format_tool_call_chunk(
105
- message_id: str,
106
- model: str,
107
- created: int,
108
- tool_call_id: str,
109
- function_name: str | None = None,
110
- function_arguments: str | None = None,
111
- tool_call_index: int = 0,
112
- choice_index: int = 0,
113
- ) -> str:
114
- """Format a tool call chunk.
33
+ async def process_stream(
34
+ self, stream: AsyncIterator[dict[str, Any]]
35
+ ) -> AsyncIterator[str]:
36
+ """Process OpenAI-format streaming data into Anthropic SSE format.
115
37
 
116
38
  Args:
117
- message_id: Unique identifier for the completion
118
- model: Model name being used
119
- created: Unix timestamp when the completion was created
120
- tool_call_id: ID of the tool call
121
- function_name: Name of the function being called
122
- function_arguments: Arguments for the function
123
- tool_call_index: Index of the tool call
124
- choice_index: Index of the choice (usually 0)
39
+ stream: Async iterator of OpenAI-style response chunks
125
40
 
126
- Returns:
127
- Formatted SSE string
41
+ Yields:
42
+ Anthropic-formatted SSE strings with proper event: lines
128
43
  """
129
- tool_call: dict[str, Any] = {
130
- "index": tool_call_index,
131
- "id": tool_call_id,
132
- "type": "function",
133
- "function": {},
134
- }
135
-
136
- if function_name is not None:
137
- tool_call["function"]["name"] = function_name
138
-
139
- if function_arguments is not None:
140
- tool_call["function"]["arguments"] = function_arguments
141
-
142
- data = {
143
- "id": message_id,
144
- "object": "chat.completion.chunk",
145
- "created": created,
146
- "model": model,
147
- "choices": [
148
- {
149
- "index": choice_index,
150
- "delta": {"tool_calls": [tool_call]},
151
- "logprobs": None,
152
- "finish_reason": None,
153
- }
154
- ],
155
- }
156
- return OpenAISSEFormatter.format_data_event(data)
157
-
158
- @staticmethod
159
- def format_final_chunk(
160
- message_id: str,
161
- model: str,
162
- created: int,
163
- finish_reason: str = "stop",
164
- choice_index: int = 0,
165
- usage: dict[str, int] | None = None,
166
- ) -> str:
167
- """Format the final chunk with finish_reason.
44
+ message_started = False
45
+ content_block_started = False
168
46
 
169
- Args:
170
- message_id: Unique identifier for the completion
171
- model: Model name being used
172
- created: Unix timestamp when the completion was created
173
- finish_reason: Reason for completion (stop, length, tool_calls, etc.)
174
- choice_index: Index of the choice (usually 0)
175
- usage: Optional usage information to include
47
+ async for chunk in stream:
48
+ if not isinstance(chunk, dict):
49
+ continue
176
50
 
177
- Returns:
178
- Formatted SSE string
179
- """
180
- data = {
181
- "id": message_id,
182
- "object": "chat.completion.chunk",
183
- "created": created,
184
- "model": model,
185
- "choices": [
186
- {
187
- "index": choice_index,
188
- "delta": {},
189
- "logprobs": None,
190
- "finish_reason": finish_reason,
191
- }
192
- ],
193
- }
51
+ chunk_type = chunk.get("type")
194
52
 
195
- # Add usage if provided
196
- if usage:
197
- data["usage"] = usage
53
+ if chunk_type == "message_start":
54
+ if not message_started:
55
+ yield self.formatter.format_event("message_start", chunk)
56
+ message_started = True
198
57
 
199
- return OpenAISSEFormatter.format_data_event(data)
58
+ elif chunk_type == "content_block_start":
59
+ if not content_block_started:
60
+ yield self.formatter.format_event("content_block_start", chunk)
61
+ content_block_started = True
200
62
 
201
- @staticmethod
202
- def format_error_chunk(
203
- message_id: str, model: str, created: int, error_type: str, error_message: str
204
- ) -> str:
205
- """Format an error chunk.
63
+ elif chunk_type == "content_block_delta":
64
+ yield self.formatter.format_event("content_block_delta", chunk)
206
65
 
207
- Args:
208
- message_id: Unique identifier for the completion
209
- model: Model name being used
210
- created: Unix timestamp when the completion was created
211
- error_type: Type of error
212
- error_message: Error message
66
+ elif chunk_type == "ping":
67
+ yield self.formatter.format_ping()
213
68
 
214
- Returns:
215
- Formatted SSE string
216
- """
217
- data = {
218
- "id": message_id,
219
- "object": "chat.completion.chunk",
220
- "created": created,
221
- "model": model,
222
- "choices": [
223
- {"index": 0, "delta": {}, "logprobs": None, "finish_reason": "error"}
224
- ],
225
- "error": {"type": error_type, "message": error_message},
226
- }
227
- return OpenAISSEFormatter.format_data_event(data)
69
+ elif chunk_type == "content_block_stop":
70
+ yield self.formatter.format_event("content_block_stop", chunk)
228
71
 
229
- @staticmethod
230
- def format_done() -> str:
231
- """Format the final DONE event.
72
+ elif chunk_type == "message_delta":
73
+ yield self.formatter.format_event("message_delta", chunk)
232
74
 
233
- Returns:
234
- Formatted SSE termination string
235
- """
236
- return "data: [DONE]\n\n"
75
+ elif chunk_type == "message_stop":
76
+ yield self.formatter.format_event("message_stop", chunk)
77
+ break
237
78
 
238
79
 
239
80
  class OpenAIStreamProcessor:
@@ -246,6 +87,7 @@ class OpenAIStreamProcessor:
246
87
  created: int | None = None,
247
88
  enable_usage: bool = True,
248
89
  enable_tool_calls: bool = True,
90
+ enable_thinking_serialization: bool | None = None,
249
91
  output_format: Literal["sse", "dict"] = "sse",
250
92
  ):
251
93
  """Initialize the stream processor.
@@ -258,12 +100,45 @@ class OpenAIStreamProcessor:
258
100
  enable_tool_calls: Whether to process tool calls
259
101
  output_format: Output format - "sse" for Server-Sent Events strings, "dict" for dict objects
260
102
  """
261
- self.message_id = message_id or generate_openai_response_id()
103
+ # Import here to avoid circular imports
104
+ from ccproxy.llms.models.openai import generate_responses_id
105
+
106
+ self.message_id = message_id or generate_responses_id()
262
107
  self.model = model
263
108
  self.created = created or int(time.time())
264
109
  self.enable_usage = enable_usage
265
110
  self.enable_tool_calls = enable_tool_calls
266
111
  self.output_format = output_format
112
+ if enable_thinking_serialization is None:
113
+ # Prefer service Settings.llm.openai_thinking_xml if available
114
+ setting_val: bool | None = None
115
+ try:
116
+ from ccproxy.config.settings import Settings
117
+
118
+ cfg = Settings.from_config()
119
+ setting_val = bool(
120
+ getattr(getattr(cfg, "llm", {}), "openai_thinking_xml", True)
121
+ )
122
+ except Exception:
123
+ setting_val = None
124
+
125
+ if setting_val is not None:
126
+ self.enable_thinking_serialization = setting_val
127
+ else:
128
+ # Fallback to env-based toggle
129
+ env_val = (
130
+ os.getenv("LLM__OPENAI_THINKING_XML")
131
+ or os.getenv("OPENAI_STREAM_ENABLE_THINKING_SERIALIZATION")
132
+ or "true"
133
+ ).lower()
134
+ self.enable_thinking_serialization = env_val not in (
135
+ "0",
136
+ "false",
137
+ "no",
138
+ "off",
139
+ )
140
+ else:
141
+ self.enable_thinking_serialization = enable_thinking_serialization
267
142
  self.formatter = OpenAISSEFormatter()
268
143
 
269
144
  # State tracking
@@ -287,31 +162,44 @@ class OpenAIStreamProcessor:
287
162
  Yields:
288
163
  OpenAI-formatted SSE strings or dict objects based on output_format
289
164
  """
165
+ # Get logger with request context at the start of the function
166
+ logger = get_logger(__name__)
167
+
290
168
  try:
291
169
  chunk_count = 0
292
170
  processed_count = 0
171
+ logger.debug(
172
+ "openai_stream_processor_start",
173
+ message_id=self.message_id,
174
+ model=self.model,
175
+ output_format=self.output_format,
176
+ enable_usage=self.enable_usage,
177
+ enable_tool_calls=self.enable_tool_calls,
178
+ category="streaming_conversion",
179
+ enable_thinking_serialization=self.enable_thinking_serialization,
180
+ )
181
+
293
182
  async for chunk in claude_stream:
294
183
  chunk_count += 1
295
- logger.debug(
296
- "openai_stream_chunk_received",
297
- chunk_count=chunk_count,
298
- chunk_type=chunk.get("type"),
299
- chunk=chunk,
184
+ chunk_type = chunk.get("type", "unknown")
185
+
186
+ logger.trace(
187
+ "openai_processor_input_chunk",
188
+ chunk_number=chunk_count,
189
+ chunk_type=chunk_type,
190
+ category="format_detection",
300
191
  )
192
+
301
193
  async for sse_chunk in self._process_chunk(chunk):
302
194
  processed_count += 1
303
- logger.debug(
304
- "openai_stream_chunk_processed",
305
- processed_count=processed_count,
306
- sse_chunk=sse_chunk,
307
- )
308
195
  yield sse_chunk
309
196
 
310
197
  logger.debug(
311
198
  "openai_stream_complete",
312
199
  total_chunks=chunk_count,
313
200
  processed_chunks=processed_count,
314
- usage_info=self.usage_info,
201
+ message_id=self.message_id,
202
+ category="streaming_conversion",
315
203
  )
316
204
 
317
205
  # Send final chunk
@@ -327,7 +215,23 @@ class OpenAIStreamProcessor:
327
215
  if self.output_format == "sse":
328
216
  yield self.formatter.format_done()
329
217
 
218
+ except (OSError, PermissionError) as e:
219
+ logger.error("stream_processing_io_error", error=str(e), exc_info=e)
220
+ # Send error chunk for IO errors
221
+ if self.output_format == "sse":
222
+ yield self.formatter.format_error_chunk(
223
+ self.message_id,
224
+ self.model,
225
+ self.created,
226
+ "error",
227
+ f"IO error: {str(e)}",
228
+ )
229
+ yield self.formatter.format_done()
230
+ else:
231
+ # Dict format error
232
+ yield self._create_chunk_dict(finish_reason="error")
330
233
  except Exception as e:
234
+ logger.error("stream_processing_error", error=str(e), exc_info=e)
331
235
  # Send error chunk
332
236
  if self.output_format == "sse":
333
237
  yield self.formatter.format_error_chunk(
@@ -357,14 +261,29 @@ class OpenAIStreamProcessor:
357
261
  # Claude SDK format
358
262
  chunk_data = chunk.get("data", {})
359
263
  chunk_type = chunk_data.get("type")
264
+ format_source = "claude_sdk"
360
265
  else:
361
266
  # Standard Anthropic API format
362
267
  chunk_data = chunk
363
268
  chunk_type = chunk.get("type")
269
+ format_source = "anthropic_api"
270
+
271
+ logger.trace(
272
+ "openai_processor_chunk_conversion",
273
+ format_source=format_source,
274
+ chunk_type=chunk_type,
275
+ event_type=event_type,
276
+ category="format_detection",
277
+ )
364
278
 
365
279
  if chunk_type == "message_start":
366
280
  # Send initial role chunk
367
281
  if not self.role_sent:
282
+ logger.trace(
283
+ "openai_conversion_message_start",
284
+ action="sending_role_chunk",
285
+ category="streaming_conversion",
286
+ )
368
287
  yield self._format_chunk_output(delta={"role": "assistant"})
369
288
  self.role_sent = True
370
289
 
@@ -378,7 +297,7 @@ class OpenAIStreamProcessor:
378
297
  elif block.get("type") == "system_message":
379
298
  # Handle system message content block
380
299
  system_text = block.get("text", "")
381
- source = block.get("source", "claude_code_sdk")
300
+ source = block.get("source", "ccproxy")
382
301
  # Format as text with clear source attribution
383
302
  formatted_text = f"[{source}]: {system_text}"
384
303
  yield self._format_chunk_output(delta={"content": formatted_text})
@@ -387,7 +306,7 @@ class OpenAIStreamProcessor:
387
306
  tool_id = block.get("id", "")
388
307
  tool_name = block.get("name", "")
389
308
  tool_input = block.get("input", {})
390
- source = block.get("source", "claude_code_sdk")
309
+ source = block.get("source", "ccproxy")
391
310
 
392
311
  # For dict format, immediately yield the tool call
393
312
  if self.output_format == "dict":
@@ -416,7 +335,7 @@ class OpenAIStreamProcessor:
416
335
  }
417
336
  elif block.get("type") == "tool_result_sdk":
418
337
  # Handle custom tool_result_sdk content block
419
- source = block.get("source", "claude_code_sdk")
338
+ source = block.get("source", "ccproxy")
420
339
  tool_use_id = block.get("tool_use_id", "")
421
340
  result_content = block.get("content", "")
422
341
  is_error = block.get("is_error", False)
@@ -425,7 +344,7 @@ class OpenAIStreamProcessor:
425
344
  yield self._format_chunk_output(delta={"content": formatted_text})
426
345
  elif block.get("type") == "result_message":
427
346
  # Handle custom result_message content block
428
- source = block.get("source", "claude_code_sdk")
347
+ source = block.get("source", "ccproxy")
429
348
  result_data = block.get("data", {})
430
349
  session_id = result_data.get("session_id", "")
431
350
  stop_reason = result_data.get("stop_reason", "")
@@ -454,6 +373,11 @@ class OpenAIStreamProcessor:
454
373
  # Text content
455
374
  text = delta.get("text", "")
456
375
  if text:
376
+ logger.trace(
377
+ "openai_conversion_text_delta",
378
+ text_length=len(text),
379
+ category="streaming_conversion",
380
+ )
457
381
  yield self._format_chunk_output(delta={"content": text})
458
382
 
459
383
  elif delta_type == "thinking_delta" and self.thinking_block_active:
@@ -485,24 +409,45 @@ class OpenAIStreamProcessor:
485
409
  self.thinking_block_active = False
486
410
  if self.current_thinking_text:
487
411
  # Format thinking block with signature
488
- thinking_content = f'<thinking signature="{self.current_thinking_signature}">{self.current_thinking_text}</thinking>'
489
- yield self._format_chunk_output(delta={"content": thinking_content})
412
+ if self.enable_thinking_serialization:
413
+ thinking_content = (
414
+ f'<thinking signature="{self.current_thinking_signature}">'
415
+ f"{self.current_thinking_text}</thinking>"
416
+ )
417
+ yield self._format_chunk_output(
418
+ delta={"content": thinking_content}
419
+ )
490
420
  # Reset thinking state
491
421
  self.current_thinking_text = ""
492
422
  self.current_thinking_signature = None
493
423
 
494
- elif (
495
- self.tool_calls
496
- and self.enable_tool_calls
497
- and self.output_format == "sse"
498
- ):
499
- # Send completed tool calls (only for SSE format, dict format sends immediately)
500
- for tool_call in self.tool_calls.values():
424
+ elif self.tool_calls and self.enable_tool_calls:
425
+ # Send completed tool calls for both SSE and dict formats
426
+ # Previous bug: Only sent for SSE format, causing dict format (SDK mode) to miss tool calls
427
+ logger.trace(
428
+ "openai_stream_sending_tool_calls",
429
+ tool_count=len(self.tool_calls),
430
+ output_format=self.output_format,
431
+ category="streaming_conversion",
432
+ )
433
+
434
+ for tool_call_index, (tool_call_id, tool_call) in enumerate(
435
+ self.tool_calls.items()
436
+ ):
437
+ logger.trace(
438
+ "openai_stream_tool_call_yielding",
439
+ tool_call_id=tool_call_id,
440
+ tool_name=tool_call["name"],
441
+ has_arguments=bool(tool_call["arguments"]),
442
+ index=tool_call_index,
443
+ category="streaming_conversion",
444
+ )
445
+
501
446
  yield self._format_chunk_output(
502
447
  delta={
503
448
  "tool_calls": [
504
449
  {
505
- "index": 0,
450
+ "index": tool_call_index,
506
451
  "id": tool_call["id"],
507
452
  "type": "function",
508
453
  "function": {
@@ -516,10 +461,24 @@ class OpenAIStreamProcessor:
516
461
  }
517
462
  )
518
463
 
464
+ # Clear tool_calls after yielding to prevent duplicates
465
+ logger.trace(
466
+ "openai_stream_clearing_tool_calls",
467
+ cleared_count=len(self.tool_calls),
468
+ category="streaming_conversion",
469
+ )
470
+ self.tool_calls.clear()
471
+
519
472
  elif chunk_type == "message_delta":
520
473
  # Usage information
521
474
  usage = chunk_data.get("usage", {})
522
475
  if usage and self.enable_usage:
476
+ logger.trace(
477
+ "openai_conversion_usage_info",
478
+ input_tokens=usage.get("input_tokens", 0),
479
+ output_tokens=usage.get("output_tokens", 0),
480
+ category="streaming_conversion",
481
+ )
523
482
  self.usage_info = {
524
483
  "prompt_tokens": usage.get("input_tokens", 0),
525
484
  "completion_tokens": usage.get("output_tokens", 0),
@@ -620,13 +579,7 @@ class OpenAIStreamProcessor:
620
579
  tool_call.get("function", {}).get("arguments"),
621
580
  )
622
581
  else:
623
- # Empty delta
624
- return self.formatter.format_final_chunk(
625
- self.message_id, self.model, self.created, "stop"
582
+ # Empty delta - send chunk with null finish_reason
583
+ return self.formatter.format_content_chunk(
584
+ self.message_id, self.model, self.created, ""
626
585
  )
627
-
628
-
629
- __all__ = [
630
- "OpenAISSEFormatter",
631
- "OpenAIStreamProcessor",
632
- ]