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,571 @@
1
+ """Direct dict-based conversion functions for use with DictFormatAdapter.
2
+
3
+ This module provides simple wrapper functions around the existing formatter functions
4
+ that operate directly on dictionaries instead of typed Pydantic models.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import AsyncIterator
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from .protocols import StreamAccumulatorProtocol
15
+
16
+ from pydantic import TypeAdapter, ValidationError
17
+
18
+ from ccproxy.core import logging
19
+ from ccproxy.core.constants import (
20
+ FORMAT_ANTHROPIC_MESSAGES as ANTHROPIC_MESSAGES,
21
+ )
22
+ from ccproxy.core.constants import (
23
+ FORMAT_OPENAI_CHAT as OPENAI_CHAT,
24
+ )
25
+ from ccproxy.core.constants import (
26
+ FORMAT_OPENAI_RESPONSES as OPENAI_RESPONSES,
27
+ )
28
+ from ccproxy.llms.formatters import (
29
+ anthropic_to_openai,
30
+ openai_to_anthropic,
31
+ openai_to_openai,
32
+ )
33
+ from ccproxy.llms.formatters import anthropic_to_openai as a2o
34
+ from ccproxy.llms.models import anthropic as anthropic_models
35
+ from ccproxy.llms.models import openai as openai_models
36
+ from ccproxy.llms.models.anthropic import MessageStreamEvent
37
+
38
+ from .format_adapter import DictFormatAdapter
39
+ from .format_registry import FormatRegistry
40
+
41
+
42
+ FormatDict = dict[str, Any]
43
+
44
+ logger = logging.get_logger(__name__)
45
+
46
+
47
+ _type_adapter_cache: dict[Any, TypeAdapter[Any]] = {}
48
+
49
+
50
+ def _validate_stream_event(model: Any, data: dict[str, Any]) -> Any:
51
+ """Validate a streaming event against the provided model.
52
+
53
+ Raises ValidationError when the payload does not conform so callers can fail fast.
54
+ """
55
+
56
+ adapter = _type_adapter_cache.get(model)
57
+ if adapter is None:
58
+ adapter = TypeAdapter(model)
59
+ _type_adapter_cache[model] = adapter
60
+ return adapter.validate_python(data)
61
+
62
+
63
+ # Generic stream mapper to DRY conversion loops
64
+ async def map_stream(
65
+ stream: AsyncIterator[FormatDict],
66
+ *,
67
+ validator_model: Any,
68
+ converter: Any,
69
+ accumulator: StreamAccumulatorProtocol | None = None,
70
+ ) -> AsyncIterator[FormatDict]:
71
+ """Map stream with optional accumulation before validation.
72
+
73
+ Args:
74
+ stream: Input stream of format dictionaries
75
+ validator_model: Pydantic model for validation
76
+ converter: Converter function to apply to validated stream
77
+ accumulator: Optional accumulator for handling partial chunks
78
+
79
+ Returns:
80
+ Converted stream
81
+ """
82
+
83
+ async def _typed_stream() -> AsyncIterator[Any]:
84
+ async for chunk_data in stream:
85
+ if accumulator:
86
+ # Accumulate chunk and get complete object if ready
87
+ complete_object = accumulator.accumulate_chunk(chunk_data)
88
+
89
+ if complete_object is None:
90
+ # Still accumulating, skip this chunk
91
+ continue
92
+
93
+ # Have complete object, validate it
94
+ try:
95
+ yield _validate_stream_event(validator_model, complete_object)
96
+ except ValidationError as exc:
97
+ logger.debug(
98
+ "stream_chunk_validation_failed",
99
+ model=str(validator_model),
100
+ error=str(exc),
101
+ action="raise",
102
+ )
103
+ raise
104
+ else:
105
+ # No accumulator, validate directly
106
+ try:
107
+ yield _validate_stream_event(validator_model, chunk_data)
108
+ except ValidationError as exc:
109
+ logger.debug(
110
+ "stream_chunk_validation_failed",
111
+ model=str(validator_model),
112
+ error=str(exc),
113
+ action="raise",
114
+ )
115
+ raise
116
+
117
+ converted_chunks = converter(_typed_stream())
118
+ async for converted_chunk in converted_chunks:
119
+ if hasattr(converted_chunk, "model_dump"):
120
+ yield converted_chunk.model_dump(exclude_unset=True)
121
+ else:
122
+ yield converted_chunk
123
+
124
+
125
+ # OpenAI to Anthropic converters (for plugins that target Anthropic APIs)
126
+ async def convert_openai_to_anthropic_request(data: FormatDict) -> FormatDict:
127
+ """Convert OpenAI ChatCompletion request to Anthropic CreateMessage request."""
128
+ # Convert dict to typed model
129
+ request = openai_models.ChatCompletionRequest.model_validate(data)
130
+
131
+ # Use existing formatter function
132
+ result = (
133
+ await openai_to_anthropic.convert__openai_chat_to_anthropic_message__request(
134
+ request
135
+ )
136
+ )
137
+
138
+ # Convert back to dict
139
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
140
+ return result_dict
141
+
142
+
143
+ async def convert_anthropic_to_openai_response(data: FormatDict) -> FormatDict:
144
+ """Convert Anthropic MessageResponse to OpenAI ChatCompletion response."""
145
+ # Convert dict to typed model
146
+ response = anthropic_models.MessageResponse.model_validate(data)
147
+
148
+ # Use existing formatter function
149
+ result = anthropic_to_openai.convert__anthropic_message_to_openai_chat__response(
150
+ response
151
+ )
152
+
153
+ # Convert back to dict
154
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
155
+ return result_dict
156
+
157
+
158
+ async def convert_anthropic_to_openai_stream(
159
+ stream: AsyncIterator[FormatDict],
160
+ ) -> AsyncIterator[FormatDict]:
161
+ """Convert Anthropic MessageStream to OpenAI ChatCompletion stream."""
162
+
163
+ async for out_chunk in map_stream(
164
+ stream,
165
+ validator_model=anthropic_models.MessageStreamEvent,
166
+ converter=anthropic_to_openai.convert__anthropic_message_to_openai_chat__stream,
167
+ ):
168
+ yield out_chunk
169
+
170
+
171
+ async def convert_openai_to_anthropic_error(data: FormatDict) -> FormatDict:
172
+ """Convert OpenAI error to Anthropic error."""
173
+ # Convert dict to typed model
174
+ error = openai_models.ErrorResponse.model_validate(data)
175
+
176
+ # Use existing formatter function
177
+ result = openai_to_anthropic.convert__openai_to_anthropic__error(error)
178
+
179
+ # Convert back to dict
180
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
181
+ return result_dict
182
+
183
+
184
+ # Anthropic to OpenAI converters (reverse direction, if needed)
185
+ async def convert_anthropic_to_openai_request(data: FormatDict) -> FormatDict:
186
+ """Convert Anthropic CreateMessage request to OpenAI ChatCompletion request."""
187
+ # Convert dict to typed model
188
+ request = anthropic_models.CreateMessageRequest.model_validate(data)
189
+
190
+ # Use existing formatter function
191
+ result = anthropic_to_openai.convert__anthropic_message_to_openai_chat__request(
192
+ request
193
+ )
194
+
195
+ # Convert back to dict
196
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
197
+ return result_dict
198
+
199
+
200
+ async def convert_openai_to_anthropic_response(data: FormatDict) -> FormatDict:
201
+ """Convert OpenAI ChatCompletion response to Anthropic MessageResponse."""
202
+ # Convert dict to typed model
203
+ response = openai_models.ChatCompletionResponse.model_validate(data)
204
+
205
+ # Use existing formatter function
206
+ result = openai_to_anthropic.convert__openai_chat_to_anthropic_messages__response(
207
+ response
208
+ )
209
+
210
+ # Convert back to dict
211
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
212
+ return result_dict
213
+
214
+
215
+ async def convert_openai_to_anthropic_stream(
216
+ stream: AsyncIterator[FormatDict],
217
+ ) -> AsyncIterator[FormatDict]:
218
+ """Convert OpenAI ChatCompletion stream to Anthropic MessageStream."""
219
+ from .chat_accumulator import ChatCompletionAccumulator
220
+
221
+ # Use accumulator to handle partial tool calls
222
+ accumulator = ChatCompletionAccumulator()
223
+
224
+ async for out_chunk in map_stream(
225
+ stream,
226
+ validator_model=openai_models.ChatCompletionChunk,
227
+ converter=openai_to_anthropic.convert__openai_chat_to_anthropic_messages__stream,
228
+ accumulator=accumulator,
229
+ ):
230
+ yield out_chunk
231
+
232
+
233
+ async def convert_anthropic_to_openai_error(data: FormatDict) -> FormatDict:
234
+ """Convert Anthropic error to OpenAI error."""
235
+ # Convert dict to typed model
236
+ error = anthropic_models.ErrorResponse.model_validate(data)
237
+
238
+ # Use existing formatter function
239
+ result = anthropic_to_openai.convert__anthropic_to_openai__error(error)
240
+
241
+ # Convert back to dict
242
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
243
+ return result_dict
244
+
245
+
246
+ # OpenAI Responses format converters (for Codex plugin)
247
+ async def convert_openai_responses_to_anthropic_request(data: FormatDict) -> FormatDict:
248
+ """Convert OpenAI Responses request to Anthropic CreateMessage request."""
249
+ # Convert dict to typed model
250
+ request = openai_models.ResponseRequest.model_validate(data)
251
+
252
+ # Use existing formatter function
253
+ result = (
254
+ openai_to_anthropic.convert__openai_responses_to_anthropic_message__request(
255
+ request
256
+ )
257
+ )
258
+
259
+ # Convert back to dict
260
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
261
+ return result_dict
262
+
263
+
264
+ async def convert_openai_responses_to_anthropic_response(
265
+ data: FormatDict,
266
+ ) -> FormatDict:
267
+ """Convert OpenAI Responses response to Anthropic MessageResponse."""
268
+ # Convert dict to typed model
269
+ response = openai_models.ResponseObject.model_validate(data)
270
+
271
+ # Use existing formatter function
272
+ result = (
273
+ openai_to_anthropic.convert__openai_responses_to_anthropic_message__response(
274
+ response
275
+ )
276
+ )
277
+
278
+ # Convert back to dict
279
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
280
+ return result_dict
281
+
282
+
283
+ async def convert_anthropic_to_openai_responses_request(data: FormatDict) -> FormatDict:
284
+ """Convert Anthropic CreateMessage request to OpenAI Responses request."""
285
+ # Convert dict to typed model
286
+ request = anthropic_models.CreateMessageRequest.model_validate(data)
287
+
288
+ # Use existing formatter function
289
+ result = (
290
+ anthropic_to_openai.convert__anthropic_message_to_openai_responses__request(
291
+ request
292
+ )
293
+ )
294
+
295
+ # Convert back to dict
296
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
297
+ return result_dict
298
+
299
+
300
+ async def convert_anthropic_to_openai_responses_response(
301
+ data: FormatDict,
302
+ ) -> FormatDict:
303
+ """Convert Anthropic MessageResponse to OpenAI Responses response."""
304
+ # Convert dict to typed model
305
+ response = anthropic_models.MessageResponse.model_validate(data)
306
+
307
+ # Use existing formatter function
308
+ result = (
309
+ anthropic_to_openai.convert__anthropic_message_to_openai_responses__response(
310
+ response
311
+ )
312
+ )
313
+
314
+ # Convert back to dict
315
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
316
+ return result_dict
317
+
318
+
319
+ # OpenAI Chat ↔ OpenAI Responses converters (for Codex plugin)
320
+ async def convert_openai_chat_to_openai_responses_request(
321
+ data: FormatDict,
322
+ ) -> FormatDict:
323
+ """Convert OpenAI ChatCompletion request to OpenAI Responses request."""
324
+ # Convert dict to typed model
325
+ request = openai_models.ChatCompletionRequest.model_validate(data)
326
+
327
+ # Use existing formatter function
328
+ result = await openai_to_openai.convert__openai_chat_to_openai_responses__request(
329
+ request
330
+ )
331
+
332
+ # Convert back to dict
333
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
334
+ return result_dict
335
+
336
+
337
+ async def convert_openai_responses_to_openai_chat_response(
338
+ data: FormatDict,
339
+ ) -> FormatDict:
340
+ """Convert OpenAI Responses response to OpenAI ChatCompletion response."""
341
+ if isinstance(data, dict):
342
+ if data.get("object") == "chat.completion" or (
343
+ "choices" in data and "response" not in data and "model" in data
344
+ ):
345
+ return data
346
+
347
+ # Convert dict to typed model
348
+ response = openai_models.ResponseObject.model_validate(data)
349
+
350
+ # Use existing formatter function
351
+ result = openai_to_openai.convert__openai_responses_to_openai_chat__response(
352
+ response
353
+ )
354
+
355
+ # Convert back to dict
356
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
357
+ return result_dict
358
+
359
+
360
+ async def convert_openai_chat_to_openai_responses_response(
361
+ data: FormatDict,
362
+ ) -> FormatDict:
363
+ """Convert OpenAI ChatCompletion response to OpenAI Responses response."""
364
+ # Convert dict to typed model
365
+ response = openai_models.ChatCompletionResponse.model_validate(data)
366
+
367
+ # Use existing formatter function
368
+ result = await openai_to_openai.convert__openai_chat_to_openai_responses__response(
369
+ response
370
+ )
371
+
372
+ # Convert back to dict
373
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
374
+ return result_dict
375
+
376
+
377
+ async def convert_openai_responses_to_openai_chat_stream(
378
+ stream: AsyncIterator[FormatDict],
379
+ ) -> AsyncIterator[FormatDict]:
380
+ """Convert OpenAI Responses stream to OpenAI ChatCompletion stream."""
381
+ from ccproxy.llms.models.openai import AnyStreamEvent
382
+
383
+ async for out_chunk in map_stream(
384
+ stream,
385
+ validator_model=AnyStreamEvent,
386
+ converter=openai_to_openai.convert__openai_responses_to_openai_chat__stream,
387
+ ):
388
+ yield out_chunk
389
+
390
+
391
+ async def convert_openai_chat_to_openai_responses_stream(
392
+ stream: AsyncIterator[FormatDict],
393
+ ) -> AsyncIterator[FormatDict]:
394
+ """Convert OpenAI ChatCompletion stream to OpenAI Responses stream."""
395
+ from .chat_accumulator import ChatCompletionAccumulator
396
+
397
+ # Use accumulator to handle partial tool calls
398
+ accumulator = ChatCompletionAccumulator()
399
+
400
+ async for out_chunk in map_stream(
401
+ stream,
402
+ validator_model=openai_models.ChatCompletionChunk,
403
+ converter=openai_to_openai.convert__openai_chat_to_openai_responses__stream,
404
+ accumulator=accumulator,
405
+ ):
406
+ yield out_chunk
407
+
408
+
409
+ async def convert_anthropic_to_openai_responses_stream(
410
+ stream: AsyncIterator[FormatDict],
411
+ ) -> AsyncIterator[FormatDict]:
412
+ """Convert Anthropic MessageStream to OpenAI Responses stream.
413
+
414
+ Avoid dict→model→dict churn by using the shared map_stream helper.
415
+ """
416
+
417
+ async for out_chunk in map_stream(
418
+ stream,
419
+ validator_model=MessageStreamEvent,
420
+ converter=a2o.convert__anthropic_message_to_openai_responses__stream,
421
+ ):
422
+ yield out_chunk
423
+
424
+
425
+ async def convert_openai_responses_to_anthropic_stream(
426
+ stream: AsyncIterator[FormatDict],
427
+ ) -> AsyncIterator[FormatDict]:
428
+ """Convert OpenAI Responses stream to Anthropic MessageStream."""
429
+ # Since there's no direct openai.responses -> anthropic stream converter,
430
+ # we'll convert responses -> chat -> anthropic
431
+ chat_stream = convert_openai_responses_to_openai_chat_stream(stream)
432
+ anthropic_stream = convert_openai_to_anthropic_stream(chat_stream)
433
+ async for chunk in anthropic_stream:
434
+ yield chunk
435
+
436
+
437
+ async def convert_openai_responses_to_openai_chat_request(
438
+ data: FormatDict,
439
+ ) -> FormatDict:
440
+ """Convert OpenAI Responses request to OpenAI ChatCompletion request."""
441
+ # Convert dict to typed model
442
+ request = openai_models.ResponseRequest.model_validate(data)
443
+
444
+ # Use existing formatter function
445
+ result = await openai_to_openai.convert__openai_responses_to_openaichat__request(
446
+ request
447
+ )
448
+
449
+ # Convert back to dict
450
+ result_dict: FormatDict = result.model_dump(exclude_unset=True)
451
+ return result_dict
452
+
453
+
454
+ # Passthrough and additional error conversion functions
455
+ # OpenAI↔OpenAI error formats are identical; return input unchanged.
456
+ async def convert_openai_responses_to_anthropic_error(data: FormatDict) -> FormatDict:
457
+ """Convert OpenAI Responses error to Anthropic error."""
458
+ # OpenAI errors are similar across formats - use existing converter
459
+ return await convert_openai_to_anthropic_error(data)
460
+
461
+
462
+ async def convert_anthropic_to_openai_responses_error(data: FormatDict) -> FormatDict:
463
+ """Convert Anthropic error to OpenAI Responses error."""
464
+ # Use existing anthropic -> openai error converter (errors are same format)
465
+ return await convert_anthropic_to_openai_error(data)
466
+
467
+
468
+ async def convert_openai_responses_to_openai_chat_error(data: FormatDict) -> FormatDict:
469
+ """Convert OpenAI Responses error to OpenAI ChatCompletion error."""
470
+ # Errors have the same format between OpenAI endpoints - passthrough
471
+ return data
472
+
473
+
474
+ async def convert_openai_chat_to_openai_responses_error(data: FormatDict) -> FormatDict:
475
+ """Convert OpenAI ChatCompletion error to OpenAI Responses error."""
476
+ # Errors have the same format between OpenAI endpoints - passthrough
477
+ return data
478
+
479
+
480
+ __all__ = [
481
+ "convert_openai_to_anthropic_request",
482
+ "convert_anthropic_to_openai_response",
483
+ "convert_anthropic_to_openai_stream",
484
+ "convert_openai_to_anthropic_error",
485
+ "convert_anthropic_to_openai_request",
486
+ "convert_openai_to_anthropic_response",
487
+ "convert_openai_to_anthropic_stream",
488
+ "convert_anthropic_to_openai_error",
489
+ "convert_openai_responses_to_anthropic_request",
490
+ "convert_openai_responses_to_anthropic_response",
491
+ "convert_openai_responses_to_anthropic_error",
492
+ "convert_anthropic_to_openai_responses_request",
493
+ "convert_anthropic_to_openai_responses_response",
494
+ "convert_anthropic_to_openai_responses_error",
495
+ "convert_anthropic_to_openai_responses_stream",
496
+ "convert_openai_responses_to_anthropic_stream",
497
+ "convert_openai_chat_to_openai_responses_request",
498
+ "convert_openai_responses_to_openai_chat_response",
499
+ "convert_openai_responses_to_openai_chat_error",
500
+ "convert_openai_chat_to_openai_responses_response",
501
+ "convert_openai_chat_to_openai_responses_error",
502
+ "convert_openai_chat_to_openai_responses_stream",
503
+ "convert_openai_responses_to_openai_chat_stream",
504
+ "convert_openai_responses_to_openai_chat_request",
505
+ ]
506
+
507
+ # Centralized pair→stage mapping and registration helpers
508
+
509
+
510
+ def get_converter_map() -> dict[tuple[str, str], dict[str, Any]]:
511
+ """Return a mapping of (from, to) → {request, response, error, stream} callables.
512
+
513
+ Missing stages are allowed (e.g., error), and will default to passthrough in composition.
514
+ """
515
+ return {
516
+ # OpenAI Chat → Anthropic Messages
517
+ (OPENAI_CHAT, ANTHROPIC_MESSAGES): {
518
+ "request": convert_openai_to_anthropic_request,
519
+ "response": convert_anthropic_to_openai_response,
520
+ "error": convert_anthropic_to_openai_error,
521
+ "stream": convert_anthropic_to_openai_stream,
522
+ },
523
+ # Anthropic Messages → OpenAI Chat
524
+ (ANTHROPIC_MESSAGES, OPENAI_CHAT): {
525
+ "request": convert_anthropic_to_openai_request,
526
+ "response": convert_openai_to_anthropic_response,
527
+ "error": convert_openai_to_anthropic_error,
528
+ "stream": convert_openai_to_anthropic_stream,
529
+ },
530
+ # OpenAI Chat ↔ OpenAI Responses
531
+ (OPENAI_CHAT, OPENAI_RESPONSES): {
532
+ "request": convert_openai_chat_to_openai_responses_request,
533
+ "response": convert_openai_chat_to_openai_responses_response,
534
+ "error": convert_openai_chat_to_openai_responses_error,
535
+ "stream": convert_openai_chat_to_openai_responses_stream,
536
+ },
537
+ (OPENAI_RESPONSES, OPENAI_CHAT): {
538
+ "request": convert_openai_responses_to_openai_chat_request,
539
+ "response": convert_openai_responses_to_openai_chat_response,
540
+ "error": convert_openai_responses_to_openai_chat_error,
541
+ "stream": convert_openai_responses_to_openai_chat_stream,
542
+ },
543
+ # OpenAI Responses ↔ Anthropic Messages
544
+ (OPENAI_RESPONSES, ANTHROPIC_MESSAGES): {
545
+ "request": convert_openai_responses_to_anthropic_request,
546
+ "response": convert_openai_responses_to_anthropic_response,
547
+ "error": convert_openai_responses_to_anthropic_error,
548
+ "stream": convert_openai_responses_to_anthropic_stream,
549
+ },
550
+ (ANTHROPIC_MESSAGES, OPENAI_RESPONSES): {
551
+ "request": convert_anthropic_to_openai_responses_request,
552
+ "response": convert_anthropic_to_openai_responses_response,
553
+ "error": convert_anthropic_to_openai_responses_error,
554
+ "stream": convert_anthropic_to_openai_responses_stream,
555
+ },
556
+ }
557
+
558
+
559
+ def register_converters(registry: FormatRegistry, *, plugin_name: str = "core") -> None:
560
+ """Register DictFormatAdapter instances for all known pairs into the registry."""
561
+ for (src, dst), stages in get_converter_map().items():
562
+ adapter = DictFormatAdapter(
563
+ request=stages.get("request"),
564
+ response=stages.get("response"),
565
+ error=stages.get("error"),
566
+ stream=stages.get("stream"),
567
+ name=f"{src}->{dst}",
568
+ )
569
+ registry.register(
570
+ from_format=src, to_format=dst, adapter=adapter, plugin_name=plugin_name
571
+ )