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,135 @@
1
+ """Shared token usage conversion helpers for formatter adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ccproxy.llms.formatters.utils import (
8
+ anthropic_usage_snapshot,
9
+ openai_completion_usage_snapshot,
10
+ openai_response_usage_snapshot,
11
+ )
12
+ from ccproxy.llms.models import anthropic as anthropic_models
13
+ from ccproxy.llms.models import openai as openai_models
14
+
15
+
16
+ def convert_openai_responses_usage_to_completion_usage(
17
+ usage: Any,
18
+ ) -> openai_models.CompletionUsage:
19
+ """Normalize Responses usage into the legacy CompletionUsage envelope."""
20
+
21
+ snapshot = openai_response_usage_snapshot(usage)
22
+
23
+ prompt_tokens_details = openai_models.PromptTokensDetails(
24
+ cached_tokens=snapshot.cache_read_tokens,
25
+ audio_tokens=0,
26
+ )
27
+ completion_tokens_details = openai_models.CompletionTokensDetails(
28
+ reasoning_tokens=snapshot.reasoning_tokens,
29
+ audio_tokens=0,
30
+ accepted_prediction_tokens=0,
31
+ rejected_prediction_tokens=0,
32
+ )
33
+
34
+ return openai_models.CompletionUsage(
35
+ prompt_tokens=snapshot.input_tokens,
36
+ completion_tokens=snapshot.output_tokens,
37
+ total_tokens=snapshot.input_tokens + snapshot.output_tokens,
38
+ prompt_tokens_details=prompt_tokens_details,
39
+ completion_tokens_details=completion_tokens_details,
40
+ )
41
+
42
+
43
+ def convert_openai_completion_usage_to_responses_usage(
44
+ usage: Any,
45
+ ) -> openai_models.ResponseUsage:
46
+ """Map Completion usage payloads into Responses Usage structures."""
47
+
48
+ snapshot = openai_completion_usage_snapshot(usage)
49
+
50
+ input_tokens_details = openai_models.InputTokensDetails(
51
+ cached_tokens=snapshot.cache_read_tokens
52
+ )
53
+ output_tokens_details = openai_models.OutputTokensDetails(
54
+ reasoning_tokens=snapshot.reasoning_tokens
55
+ )
56
+
57
+ return openai_models.ResponseUsage(
58
+ input_tokens=snapshot.input_tokens,
59
+ input_tokens_details=input_tokens_details,
60
+ output_tokens=snapshot.output_tokens,
61
+ output_tokens_details=output_tokens_details,
62
+ total_tokens=snapshot.input_tokens + snapshot.output_tokens,
63
+ )
64
+
65
+
66
+ def convert_openai_responses_usage_to_anthropic_usage(
67
+ usage: Any,
68
+ ) -> anthropic_models.Usage:
69
+ """Translate OpenAI Responses usage into Anthropic Usage models."""
70
+
71
+ snapshot = openai_response_usage_snapshot(usage)
72
+
73
+ return anthropic_models.Usage(
74
+ input_tokens=snapshot.input_tokens,
75
+ output_tokens=snapshot.output_tokens,
76
+ cache_read_input_tokens=snapshot.cache_read_tokens,
77
+ cache_creation_input_tokens=snapshot.cache_creation_tokens,
78
+ )
79
+
80
+
81
+ def convert_anthropic_usage_to_openai_completion_usage(
82
+ usage: Any,
83
+ ) -> openai_models.CompletionUsage:
84
+ """Translate Anthropic Usage values into OpenAI Completion usage."""
85
+
86
+ snapshot = anthropic_usage_snapshot(usage)
87
+ cached_tokens = snapshot.cache_read_tokens or snapshot.cache_creation_tokens
88
+
89
+ prompt_tokens_details = openai_models.PromptTokensDetails(
90
+ cached_tokens=cached_tokens,
91
+ audio_tokens=0,
92
+ )
93
+ completion_tokens_details = openai_models.CompletionTokensDetails(
94
+ reasoning_tokens=0,
95
+ audio_tokens=0,
96
+ accepted_prediction_tokens=0,
97
+ rejected_prediction_tokens=0,
98
+ )
99
+
100
+ return openai_models.CompletionUsage(
101
+ prompt_tokens=snapshot.input_tokens,
102
+ completion_tokens=snapshot.output_tokens,
103
+ total_tokens=snapshot.input_tokens + snapshot.output_tokens,
104
+ prompt_tokens_details=prompt_tokens_details,
105
+ completion_tokens_details=completion_tokens_details,
106
+ )
107
+
108
+
109
+ def convert_anthropic_usage_to_openai_responses_usage(
110
+ usage: Any,
111
+ ) -> openai_models.ResponseUsage:
112
+ """Translate Anthropic Usage values into OpenAI Responses usage."""
113
+
114
+ snapshot = anthropic_usage_snapshot(usage)
115
+ cached_tokens = snapshot.cache_read_tokens or snapshot.cache_creation_tokens
116
+
117
+ input_tokens_details = openai_models.InputTokensDetails(cached_tokens=cached_tokens)
118
+ output_tokens_details = openai_models.OutputTokensDetails(reasoning_tokens=0)
119
+
120
+ return openai_models.ResponseUsage(
121
+ input_tokens=snapshot.input_tokens,
122
+ input_tokens_details=input_tokens_details,
123
+ output_tokens=snapshot.output_tokens,
124
+ output_tokens_details=output_tokens_details,
125
+ total_tokens=snapshot.input_tokens + snapshot.output_tokens,
126
+ )
127
+
128
+
129
+ __all__ = [
130
+ "convert_anthropic_usage_to_openai_completion_usage",
131
+ "convert_anthropic_usage_to_openai_responses_usage",
132
+ "convert_openai_completion_usage_to_responses_usage",
133
+ "convert_openai_responses_usage_to_anthropic_usage",
134
+ "convert_openai_responses_usage_to_completion_usage",
135
+ ]
@@ -0,0 +1,55 @@
1
+ """Shared constant mappings for LLM adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+
8
+ ANTHROPIC_TO_OPENAI_FINISH_REASON: Final[dict[str, str]] = {
9
+ "end_turn": "stop",
10
+ "max_tokens": "length",
11
+ "stop_sequence": "stop",
12
+ "tool_use": "tool_calls",
13
+ # Anthropic-specific values mapped to closest reasonable OpenAI value
14
+ "pause_turn": "stop",
15
+ "refusal": "stop",
16
+ }
17
+
18
+ OPENAI_TO_ANTHROPIC_STOP_REASON: Final[dict[str, str]] = {
19
+ "stop": "end_turn",
20
+ "length": "max_tokens",
21
+ "tool_calls": "tool_use",
22
+ }
23
+
24
+ OPENAI_TO_ANTHROPIC_ERROR_TYPE: Final[dict[str, str]] = {
25
+ "invalid_request_error": "invalid_request_error",
26
+ "authentication_error": "invalid_request_error",
27
+ "permission_error": "invalid_request_error",
28
+ "not_found_error": "invalid_request_error",
29
+ "rate_limit_error": "rate_limit_error",
30
+ "internal_server_error": "api_error",
31
+ "overloaded_error": "api_error",
32
+ }
33
+
34
+ ANTHROPIC_TO_OPENAI_ERROR_TYPE: Final[dict[str, str]] = {
35
+ "invalid_request_error": "invalid_request_error",
36
+ "authentication_error": "authentication_error",
37
+ "permission_error": "permission_error",
38
+ "not_found_error": "invalid_request_error", # OpenAI doesn't expose not_found
39
+ "rate_limit_error": "rate_limit_error",
40
+ "api_error": "api_error",
41
+ "overloaded_error": "api_error",
42
+ "billing_error": "invalid_request_error",
43
+ "timeout_error": "api_error",
44
+ }
45
+
46
+ DEFAULT_MAX_TOKENS: Final[int] = 1024
47
+
48
+
49
+ __all__ = [
50
+ "ANTHROPIC_TO_OPENAI_FINISH_REASON",
51
+ "OPENAI_TO_ANTHROPIC_STOP_REASON",
52
+ "OPENAI_TO_ANTHROPIC_ERROR_TYPE",
53
+ "ANTHROPIC_TO_OPENAI_ERROR_TYPE",
54
+ "DEFAULT_MAX_TOKENS",
55
+ ]
@@ -0,0 +1,116 @@
1
+ """Context helpers for formatter conversions using async contextvars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+ from typing import Any
7
+
8
+
9
+ _REQUEST_VAR: ContextVar[Any | None] = ContextVar("formatter_request", default=None)
10
+ _INSTRUCTIONS_VAR: ContextVar[str | None] = ContextVar(
11
+ "formatter_instructions", default=None
12
+ )
13
+ _TOOLS_VAR: ContextVar[list[Any] | None] = ContextVar("formatter_tools", default=None)
14
+
15
+
16
+ def register_request(request: Any | None, instructions: str | None = None) -> None:
17
+ """Record the most recent upstream request for streaming conversions."""
18
+
19
+ normalized = instructions.strip() if isinstance(instructions, str) else None
20
+
21
+ _REQUEST_VAR.set(request)
22
+ _INSTRUCTIONS_VAR.set(normalized)
23
+
24
+ try:
25
+ from ccproxy.core.request_context import RequestContext
26
+
27
+ ctx = RequestContext.get_current()
28
+ if ctx is not None:
29
+ formatter_state = ctx.metadata.setdefault("formatter_state", {})
30
+ if request is None:
31
+ formatter_state.pop("request", None)
32
+ else:
33
+ formatter_state["request"] = request
34
+
35
+ if normalized:
36
+ formatter_state["instructions"] = normalized
37
+ elif instructions is None:
38
+ formatter_state.pop("instructions", None)
39
+ except Exception:
40
+ # Request context propagation is best-effort; proceed even when
41
+ # request context is unavailable (e.g., during unit tests).
42
+ pass
43
+
44
+
45
+ def get_last_request() -> Any | None:
46
+ """Return the cached upstream request for the active conversion, if any."""
47
+
48
+ try:
49
+ from ccproxy.core.request_context import RequestContext
50
+
51
+ ctx = RequestContext.get_current()
52
+ if ctx is not None:
53
+ formatter_state = ctx.metadata.get("formatter_state", {})
54
+ if "request" in formatter_state:
55
+ return formatter_state["request"]
56
+ except Exception:
57
+ pass
58
+
59
+ return _REQUEST_VAR.get()
60
+
61
+
62
+ def get_last_instructions() -> str | None:
63
+ """Return the cached instruction string from the last registered request."""
64
+
65
+ try:
66
+ from ccproxy.core.request_context import RequestContext
67
+
68
+ ctx = RequestContext.get_current()
69
+ if ctx is not None:
70
+ formatter_state = ctx.metadata.get("formatter_state", {})
71
+ instructions = formatter_state.get("instructions")
72
+ if isinstance(instructions, str) and instructions.strip():
73
+ return instructions.strip()
74
+ except Exception:
75
+ pass
76
+
77
+ return _INSTRUCTIONS_VAR.get()
78
+
79
+
80
+ def register_request_tools(tools: list[Any] | None) -> None:
81
+ """Cache request tool definitions for downstream streaming responses."""
82
+
83
+ normalized = list(tools) if tools else None
84
+ _TOOLS_VAR.set(normalized)
85
+
86
+ try:
87
+ from ccproxy.core.request_context import RequestContext
88
+
89
+ ctx = RequestContext.get_current()
90
+ if ctx is not None:
91
+ formatter_state = ctx.metadata.setdefault("formatter_state", {})
92
+ if normalized is None:
93
+ formatter_state.pop("tools", None)
94
+ else:
95
+ formatter_state["tools"] = normalized
96
+ except Exception:
97
+ pass
98
+
99
+
100
+ def get_last_request_tools() -> list[Any] | None:
101
+ """Return cached request tool definitions, if any."""
102
+
103
+ try:
104
+ from ccproxy.core.request_context import RequestContext
105
+
106
+ ctx = RequestContext.get_current()
107
+ if ctx is not None:
108
+ formatter_state = ctx.metadata.get("formatter_state", {})
109
+ tools = formatter_state.get("tools")
110
+ if isinstance(tools, list):
111
+ return list(tools)
112
+ except Exception:
113
+ pass
114
+
115
+ cached = _TOOLS_VAR.get()
116
+ return list(cached) if cached else None
@@ -0,0 +1,33 @@
1
+ """Compatibility layer for adapter mapping utilities.
2
+
3
+ This shim was previously used to re-export usage converters that have now been
4
+ inlined into their respective adapter helpers. It now only re-exports constants
5
+ and error conversion utilities that remain shared.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ccproxy.llms.formatters.anthropic_to_openai import (
11
+ convert__anthropic_to_openai__error,
12
+ )
13
+ from ccproxy.llms.formatters.constants import (
14
+ ANTHROPIC_TO_OPENAI_ERROR_TYPE,
15
+ ANTHROPIC_TO_OPENAI_FINISH_REASON,
16
+ DEFAULT_MAX_TOKENS,
17
+ OPENAI_TO_ANTHROPIC_ERROR_TYPE,
18
+ OPENAI_TO_ANTHROPIC_STOP_REASON,
19
+ )
20
+ from ccproxy.llms.formatters.openai_to_anthropic import (
21
+ convert__openai_to_anthropic__error,
22
+ )
23
+
24
+
25
+ __all__ = [
26
+ "ANTHROPIC_TO_OPENAI_ERROR_TYPE",
27
+ "ANTHROPIC_TO_OPENAI_FINISH_REASON",
28
+ "DEFAULT_MAX_TOKENS",
29
+ "OPENAI_TO_ANTHROPIC_ERROR_TYPE",
30
+ "OPENAI_TO_ANTHROPIC_STOP_REASON",
31
+ "convert__anthropic_to_openai__error",
32
+ "convert__openai_to_anthropic__error",
33
+ ]
@@ -0,0 +1,55 @@
1
+ """Facade module exposing OpenAI→Anthropic formatter entry points."""
2
+
3
+ import sys
4
+ from types import ModuleType
5
+
6
+ from . import streams as _streams
7
+ from .errors import convert__openai_to_anthropic__error
8
+ from .requests import (
9
+ convert__openai_chat_to_anthropic_message__request,
10
+ convert__openai_responses_to_anthropic_message__request,
11
+ )
12
+ from .responses import (
13
+ convert__openai_chat_to_anthropic_messages__response,
14
+ convert__openai_responses_to_anthropic_message__response,
15
+ convert__openai_responses_usage_to_anthropic__usage,
16
+ convert__openai_responses_usage_to_openai_completion__usage,
17
+ )
18
+ from .streams import (
19
+ OpenAIChatToAnthropicStreamAdapter,
20
+ OpenAIResponsesToAnthropicStreamAdapter,
21
+ convert__openai_chat_to_anthropic_messages__stream,
22
+ convert__openai_responses_to_anthropic_messages__stream,
23
+ )
24
+
25
+
26
+ __all__ = [
27
+ "convert__openai_to_anthropic__error",
28
+ "convert__openai_chat_to_anthropic_message__request",
29
+ "convert__openai_responses_to_anthropic_message__request",
30
+ "convert__openai_chat_to_anthropic_messages__response",
31
+ "convert__openai_responses_to_anthropic_message__response",
32
+ "convert__openai_responses_usage_to_anthropic__usage",
33
+ "convert__openai_responses_usage_to_openai_completion__usage",
34
+ "OpenAIChatToAnthropicStreamAdapter",
35
+ "OpenAIResponsesToAnthropicStreamAdapter",
36
+ "convert__openai_chat_to_anthropic_messages__stream",
37
+ "convert__openai_responses_to_anthropic_messages__stream",
38
+ ]
39
+
40
+
41
+ class _OpenAIToAnthropicModule(ModuleType):
42
+ _propagated_names = {
43
+ "OpenAIChatToAnthropicStreamAdapter",
44
+ "OpenAIResponsesToAnthropicStreamAdapter",
45
+ }
46
+
47
+ def __setattr__(self, name: str, value: object) -> None:
48
+ super().__setattr__(name, value)
49
+ if name in self._propagated_names and hasattr(_streams, name):
50
+ setattr(_streams, name, value)
51
+
52
+
53
+ _module = sys.modules[__name__]
54
+ if not isinstance(_module, _OpenAIToAnthropicModule):
55
+ _module.__class__ = _OpenAIToAnthropicModule
@@ -0,0 +1,141 @@
1
+ """Shared helper utilities for OpenAI→Anthropic formatters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from ccproxy.llms.formatters.utils import strict_parse_tool_arguments
9
+ from ccproxy.llms.models import openai as openai_models
10
+
11
+
12
+ def _to_mapping(value: Any) -> Mapping[str, Any] | None:
13
+ if isinstance(value, Mapping):
14
+ return value
15
+ if hasattr(value, "model_dump"):
16
+ dumped = value.model_dump()
17
+ if isinstance(dumped, Mapping):
18
+ return dumped
19
+ return None
20
+
21
+
22
+ def _normalize_text_and_images(
23
+ content: Any,
24
+ ) -> tuple[list[str], list[dict[str, Any]]]:
25
+ text_parts: list[str] = []
26
+ image_blocks: list[dict[str, Any]] = []
27
+
28
+ if not isinstance(content, list):
29
+ return text_parts, image_blocks
30
+
31
+ for part in content:
32
+ mapping = _to_mapping(part)
33
+ if not mapping:
34
+ continue
35
+ part_type = str(mapping.get("type", "")).lower()
36
+ if part_type in {"text", "input_text"}:
37
+ text_val = mapping.get("text")
38
+ if isinstance(text_val, str) and text_val:
39
+ text_parts.append(text_val)
40
+ elif part_type == "image_url":
41
+ image_info = mapping.get("image_url")
42
+ image_map = _to_mapping(image_info)
43
+ if not image_map:
44
+ continue
45
+ url = image_map.get("url")
46
+ if isinstance(url, str) and url.startswith("data:"):
47
+ try:
48
+ header, b64data = url.split(",", 1)
49
+ mediatype = header.split(";")[0].split(":", 1)[1]
50
+ except (ValueError, IndexError):
51
+ continue
52
+ image_blocks.append(
53
+ {
54
+ "type": "image",
55
+ "source": {
56
+ "type": "base64",
57
+ "media_type": mediatype,
58
+ "data": b64data,
59
+ },
60
+ }
61
+ )
62
+
63
+ return text_parts, image_blocks
64
+
65
+
66
+ def _stringify_content(content: Any) -> str:
67
+ if content is None:
68
+ return ""
69
+ if isinstance(content, str):
70
+ return content
71
+ if isinstance(content, list):
72
+ text_parts, _ = _normalize_text_and_images(content)
73
+ return " ".join(text_parts)
74
+ return str(content)
75
+
76
+
77
+ def _coerce_system_content(content: Any) -> str | None:
78
+ if isinstance(content, str):
79
+ return content
80
+ if isinstance(content, list):
81
+ text_parts, _ = _normalize_text_and_images(content)
82
+ return " ".join(text_parts) if text_parts else None
83
+ return None
84
+
85
+
86
+ def _build_user_blocks(content: Any) -> str | list[dict[str, Any]] | None:
87
+ if content is None:
88
+ return None
89
+ if isinstance(content, str):
90
+ return content
91
+
92
+ text_parts, image_blocks = _normalize_text_and_images(content)
93
+
94
+ if not text_parts and not image_blocks:
95
+ return ""
96
+
97
+ blocks: list[dict[str, Any]] = []
98
+ if text_parts:
99
+ blocks.append({"type": "text", "text": " ".join(text_parts)})
100
+ blocks.extend(image_blocks)
101
+
102
+ if len(blocks) == 1 and blocks[0]["type"] == "text":
103
+ return str(blocks[0]["text"])
104
+ return blocks
105
+
106
+
107
+ def _build_assistant_blocks(
108
+ content: Any, tool_calls: list[openai_models.ToolCall] | None
109
+ ) -> list[dict[str, Any]]:
110
+ blocks: list[dict[str, Any]] = []
111
+
112
+ normalized_content = _build_user_blocks(content)
113
+ if isinstance(normalized_content, str):
114
+ text = normalized_content.strip()
115
+ if text:
116
+ blocks.append({"type": "text", "text": text})
117
+ elif isinstance(normalized_content, list):
118
+ blocks.extend(normalized_content)
119
+
120
+ for call in tool_calls or []:
121
+ args_dict = strict_parse_tool_arguments(call.function.arguments)
122
+ blocks.append(
123
+ {
124
+ "type": "tool_use",
125
+ "id": call.id,
126
+ "name": call.function.name,
127
+ "input": args_dict,
128
+ }
129
+ )
130
+
131
+ return blocks
132
+
133
+
134
+ __all__ = [
135
+ "_to_mapping",
136
+ "_normalize_text_and_images",
137
+ "_stringify_content",
138
+ "_coerce_system_content",
139
+ "_build_user_blocks",
140
+ "_build_assistant_blocks",
141
+ ]
@@ -0,0 +1,53 @@
1
+ """OpenAI→Anthropic error conversion entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from ccproxy.llms.formatters.constants import OPENAI_TO_ANTHROPIC_ERROR_TYPE
8
+ from ccproxy.llms.models import anthropic as anthropic_models
9
+ from ccproxy.llms.models import openai as openai_models
10
+
11
+
12
+ def convert__openai_to_anthropic__error(error: BaseModel) -> BaseModel:
13
+ """Convert an OpenAI error payload to the Anthropic envelope."""
14
+ if isinstance(error, openai_models.ErrorResponse):
15
+ openai_error = error.error
16
+ error_message = openai_error.message
17
+ openai_error_type = openai_error.type or "api_error"
18
+ anthropic_error_type = OPENAI_TO_ANTHROPIC_ERROR_TYPE.get(
19
+ openai_error_type, "api_error"
20
+ )
21
+
22
+ if anthropic_error_type == "invalid_request_error":
23
+ anthropic_error: anthropic_models.ErrorType = (
24
+ anthropic_models.InvalidRequestError(message=error_message)
25
+ )
26
+ elif anthropic_error_type == "rate_limit_error":
27
+ anthropic_error = anthropic_models.RateLimitError(message=error_message)
28
+ else:
29
+ anthropic_error = anthropic_models.APIError(message=error_message)
30
+
31
+ return anthropic_models.ErrorResponse(error=anthropic_error)
32
+
33
+ if hasattr(error, "error") and hasattr(error.error, "message"):
34
+ error_message = error.error.message
35
+ fallback_error: anthropic_models.ErrorType = anthropic_models.APIError(
36
+ message=error_message
37
+ )
38
+ return anthropic_models.ErrorResponse(error=fallback_error)
39
+
40
+ error_message = "Unknown error occurred"
41
+ if hasattr(error, "message"):
42
+ error_message = error.message
43
+ elif hasattr(error, "model_dump"):
44
+ error_dict = error.model_dump()
45
+ error_message = str(error_dict.get("message", error_dict))
46
+
47
+ generic_error: anthropic_models.ErrorType = anthropic_models.APIError(
48
+ message=error_message
49
+ )
50
+ return anthropic_models.ErrorResponse(error=generic_error)
51
+
52
+
53
+ __all__ = ["convert__openai_to_anthropic__error"]