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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,356 @@
1
+ """Request conversion entry points for Anthropic→OpenAI adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from ccproxy.llms.formatters.context import register_request
9
+ from ccproxy.llms.models import anthropic as anthropic_models
10
+ from ccproxy.llms.models import openai as openai_models
11
+
12
+
13
+ def _build_responses_payload_from_anthropic_request(
14
+ request: anthropic_models.CreateMessageRequest,
15
+ ) -> tuple[dict[str, Any], str | None]:
16
+ """Project an Anthropic message request into Responses payload fields."""
17
+
18
+ payload_data: dict[str, Any] = {"model": request.model}
19
+ instructions_text: str | None = None
20
+
21
+ if request.max_tokens is not None:
22
+ payload_data["max_output_tokens"] = int(request.max_tokens)
23
+ if request.stream:
24
+ payload_data["stream"] = True
25
+
26
+ if request.service_tier is not None:
27
+ payload_data["service_tier"] = request.service_tier
28
+ if request.temperature is not None:
29
+ payload_data["temperature"] = request.temperature
30
+ if request.top_p is not None:
31
+ payload_data["top_p"] = request.top_p
32
+
33
+ if request.metadata is not None and hasattr(request.metadata, "model_dump"):
34
+ meta_dump = request.metadata.model_dump()
35
+ payload_data["metadata"] = meta_dump
36
+
37
+ if request.system:
38
+ if isinstance(request.system, str):
39
+ instructions_text = request.system
40
+ payload_data["instructions"] = request.system
41
+ else:
42
+ joined = "".join(block.text for block in request.system if block.text)
43
+ instructions_text = joined or None
44
+ if joined:
45
+ payload_data["instructions"] = joined
46
+
47
+ last_user_text: str | None = None
48
+ for msg in reversed(request.messages):
49
+ if msg.role != "user":
50
+ continue
51
+ if isinstance(msg.content, str):
52
+ last_user_text = msg.content
53
+ elif isinstance(msg.content, list):
54
+ texts: list[str] = []
55
+ for block in msg.content:
56
+ if isinstance(block, dict):
57
+ if block.get("type") == "text" and isinstance(
58
+ block.get("text"), str
59
+ ):
60
+ texts.append(block.get("text") or "")
61
+ elif (
62
+ getattr(block, "type", None) == "text"
63
+ and hasattr(block, "text")
64
+ and isinstance(getattr(block, "text", None), str)
65
+ ):
66
+ texts.append(block.text or "")
67
+ if texts:
68
+ last_user_text = " ".join(texts)
69
+ break
70
+
71
+ if last_user_text:
72
+ payload_data["input"] = [
73
+ {
74
+ "type": "message",
75
+ "role": "user",
76
+ "content": [
77
+ {"type": "input_text", "text": last_user_text},
78
+ ],
79
+ }
80
+ ]
81
+ else:
82
+ payload_data["input"] = []
83
+
84
+ if request.tools:
85
+ tools: list[dict[str, Any]] = []
86
+ for tool in request.tools:
87
+ if isinstance(tool, anthropic_models.Tool):
88
+ tools.append(
89
+ {
90
+ "type": "function",
91
+ "name": tool.name,
92
+ "description": tool.description,
93
+ "parameters": tool.input_schema,
94
+ }
95
+ )
96
+ if tools:
97
+ payload_data["tools"] = tools
98
+
99
+ tc = request.tool_choice
100
+ if tc is not None:
101
+ tc_type = getattr(tc, "type", None)
102
+ if tc_type == "none":
103
+ payload_data["tool_choice"] = "none"
104
+ elif tc_type == "auto":
105
+ payload_data["tool_choice"] = "auto"
106
+ elif tc_type == "any":
107
+ payload_data["tool_choice"] = "required"
108
+ elif tc_type == "tool":
109
+ name = getattr(tc, "name", None)
110
+ if name:
111
+ payload_data["tool_choice"] = {
112
+ "type": "function",
113
+ "function": {"name": name},
114
+ }
115
+ disable_parallel = getattr(tc, "disable_parallel_tool_use", None)
116
+ if isinstance(disable_parallel, bool):
117
+ payload_data["parallel_tool_calls"] = not disable_parallel
118
+
119
+ payload_data.setdefault("background", None)
120
+
121
+ return payload_data, instructions_text
122
+
123
+
124
+ def convert__anthropic_message_to_openai_responses__request(
125
+ request: anthropic_models.CreateMessageRequest,
126
+ ) -> openai_models.ResponseRequest:
127
+ """Convert Anthropic CreateMessageRequest to OpenAI ResponseRequest using typed models."""
128
+ payload_data, instructions_text = _build_responses_payload_from_anthropic_request(
129
+ request
130
+ )
131
+
132
+ response_request = openai_models.ResponseRequest.model_validate(payload_data)
133
+
134
+ register_request(request, instructions_text)
135
+
136
+ return response_request
137
+
138
+
139
+ def convert__anthropic_message_to_openai_chat__request(
140
+ request: anthropic_models.CreateMessageRequest,
141
+ ) -> openai_models.ChatCompletionRequest:
142
+ """Convert Anthropic CreateMessageRequest to OpenAI ChatCompletionRequest using typed models."""
143
+ openai_messages: list[dict[str, Any]] = []
144
+ # System prompt
145
+ if request.system:
146
+ if isinstance(request.system, str):
147
+ sys_content = request.system
148
+ else:
149
+ sys_content = "".join(block.text for block in request.system)
150
+ if sys_content:
151
+ openai_messages.append({"role": "system", "content": sys_content})
152
+
153
+ # User/assistant messages with text + data-url images
154
+ for msg in request.messages:
155
+ role = msg.role
156
+ content = msg.content
157
+
158
+ # Handle tool usage and results
159
+ if role == "assistant" and isinstance(content, list):
160
+ tool_calls = []
161
+ text_parts = []
162
+ for block in content:
163
+ block_type = getattr(block, "type", None)
164
+ if block_type == "tool_use":
165
+ # Type guard for ToolUseBlock
166
+ if hasattr(block, "id") and hasattr(block, "name"):
167
+ # Safely get input with fallback to empty dict
168
+ tool_input = getattr(block, "input", {}) or {}
169
+
170
+ # Ensure input is properly serialized as JSON
171
+ try:
172
+ args_str = json.dumps(tool_input)
173
+ except Exception:
174
+ args_str = json.dumps({"arguments": str(tool_input)})
175
+
176
+ tool_calls.append(
177
+ {
178
+ "id": block.id,
179
+ "type": "function",
180
+ "function": {
181
+ "name": block.name,
182
+ "arguments": args_str,
183
+ },
184
+ }
185
+ )
186
+ elif block_type == "text":
187
+ # Type guard for TextBlock
188
+ if hasattr(block, "text"):
189
+ text_parts.append(block.text)
190
+ if tool_calls:
191
+ assistant_msg: dict[str, Any] = {
192
+ "role": "assistant",
193
+ "tool_calls": tool_calls,
194
+ }
195
+ assistant_msg["content"] = " ".join(text_parts) if text_parts else None
196
+ openai_messages.append(assistant_msg)
197
+ continue
198
+ elif role == "user" and isinstance(content, list):
199
+ is_tool_result = any(
200
+ getattr(b, "type", None) == "tool_result" for b in content
201
+ )
202
+ if is_tool_result:
203
+ for block in content:
204
+ if getattr(block, "type", None) == "tool_result":
205
+ # Type guard for ToolResultBlock
206
+ if hasattr(block, "tool_use_id"):
207
+ # Get content with an empty string fallback
208
+ result_content = getattr(block, "content", "")
209
+
210
+ # Convert complex content to string representation
211
+ if not isinstance(result_content, str):
212
+ try:
213
+ if isinstance(result_content, list):
214
+ # Handle list of text blocks
215
+ text_parts = []
216
+ for part in result_content:
217
+ if (
218
+ hasattr(part, "text")
219
+ and hasattr(part, "type")
220
+ and part.type == "text"
221
+ ):
222
+ text_parts.append(part.text)
223
+ if text_parts:
224
+ result_content = " ".join(text_parts)
225
+ else:
226
+ result_content = json.dumps(result_content)
227
+ else:
228
+ # Convert other non-string content to JSON
229
+ result_content = json.dumps(result_content)
230
+ except Exception:
231
+ # Fallback to string representation
232
+ result_content = str(result_content)
233
+
234
+ openai_messages.append(
235
+ {
236
+ "role": "tool",
237
+ "tool_call_id": block.tool_use_id,
238
+ "content": result_content,
239
+ }
240
+ )
241
+ continue
242
+
243
+ if isinstance(content, list):
244
+ parts: list[dict[str, Any]] = []
245
+ text_accum: list[str] = []
246
+ for block in content:
247
+ # Support both raw dicts and Anthropic model instances
248
+ if isinstance(block, dict):
249
+ btype = block.get("type")
250
+ if btype == "text" and isinstance(block.get("text"), str):
251
+ text_accum.append(block.get("text") or "")
252
+ elif btype == "image":
253
+ source = block.get("source") or {}
254
+ if (
255
+ isinstance(source, dict)
256
+ and source.get("type") == "base64"
257
+ and isinstance(source.get("media_type"), str)
258
+ and isinstance(source.get("data"), str)
259
+ ):
260
+ url = f"data:{source['media_type']};base64,{source['data']}"
261
+ parts.append(
262
+ {
263
+ "type": "image_url",
264
+ "image_url": {"url": url},
265
+ }
266
+ )
267
+ else:
268
+ # Pydantic models
269
+ btype = getattr(block, "type", None)
270
+ if (
271
+ btype == "text"
272
+ and hasattr(block, "text")
273
+ and isinstance(getattr(block, "text", None), str)
274
+ ):
275
+ text_accum.append(block.text or "")
276
+ elif btype == "image":
277
+ source = getattr(block, "source", None)
278
+ if (
279
+ source is not None
280
+ and getattr(source, "type", None) == "base64"
281
+ and isinstance(getattr(source, "media_type", None), str)
282
+ and isinstance(getattr(source, "data", None), str)
283
+ ):
284
+ url = f"data:{source.media_type};base64,{source.data}"
285
+ parts.append(
286
+ {
287
+ "type": "image_url",
288
+ "image_url": {"url": url},
289
+ }
290
+ )
291
+ if parts or len(text_accum) > 1:
292
+ if text_accum:
293
+ parts.insert(0, {"type": "text", "text": " ".join(text_accum)})
294
+ openai_messages.append({"role": role, "content": parts})
295
+ else:
296
+ openai_messages.append(
297
+ {"role": role, "content": (text_accum[0] if text_accum else "")}
298
+ )
299
+ else:
300
+ openai_messages.append({"role": role, "content": content})
301
+
302
+ # Tools mapping (custom tools -> function tools)
303
+ tools: list[dict[str, Any]] = []
304
+ if request.tools:
305
+ for tool in request.tools:
306
+ if isinstance(tool, anthropic_models.Tool):
307
+ tools.append(
308
+ {
309
+ "type": "function",
310
+ "function": {
311
+ "name": tool.name,
312
+ "description": tool.description,
313
+ "parameters": tool.input_schema,
314
+ },
315
+ }
316
+ )
317
+
318
+ params: dict[str, Any] = {
319
+ "model": request.model,
320
+ "messages": openai_messages,
321
+ "max_completion_tokens": request.max_tokens,
322
+ "stream": request.stream or None,
323
+ }
324
+ if tools:
325
+ params["tools"] = tools
326
+
327
+ # tool_choice mapping
328
+ tc = request.tool_choice
329
+ if tc is not None:
330
+ tc_type = getattr(tc, "type", None)
331
+ if tc_type == "none":
332
+ params["tool_choice"] = "none"
333
+ elif tc_type == "auto":
334
+ params["tool_choice"] = "auto"
335
+ elif tc_type == "any":
336
+ params["tool_choice"] = "required"
337
+ elif tc_type == "tool":
338
+ name = getattr(tc, "name", None)
339
+ if name:
340
+ params["tool_choice"] = {
341
+ "type": "function",
342
+ "function": {"name": name},
343
+ }
344
+ # parallel_tool_calls from disable_parallel_tool_use
345
+ disable_parallel = getattr(tc, "disable_parallel_tool_use", None)
346
+ if isinstance(disable_parallel, bool):
347
+ params["parallel_tool_calls"] = not disable_parallel
348
+
349
+ # Validate against OpenAI model
350
+ return openai_models.ChatCompletionRequest.model_validate(params)
351
+
352
+
353
+ __all__ = [
354
+ "convert__anthropic_message_to_openai_chat__request",
355
+ "convert__anthropic_message_to_openai_responses__request",
356
+ ]
@@ -0,0 +1,153 @@
1
+ """Response conversion entry points for Anthropic→OpenAI adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ import ccproxy.core.logging
9
+ from ccproxy.llms.formatters.common import (
10
+ convert_anthropic_usage_to_openai_completion_usage,
11
+ convert_anthropic_usage_to_openai_responses_usage,
12
+ )
13
+ from ccproxy.llms.formatters.constants import ANTHROPIC_TO_OPENAI_FINISH_REASON
14
+ from ccproxy.llms.models import anthropic as anthropic_models
15
+ from ccproxy.llms.models import openai as openai_models
16
+
17
+
18
+ logger = ccproxy.core.logging.get_logger(__name__)
19
+
20
+
21
+ def convert__anthropic_usage_to_openai_completion__usage(
22
+ usage: anthropic_models.Usage,
23
+ ) -> openai_models.CompletionUsage:
24
+ return convert_anthropic_usage_to_openai_completion_usage(usage)
25
+
26
+
27
+ def convert__anthropic_usage_to_openai_responses__usage(
28
+ usage: anthropic_models.Usage,
29
+ ) -> openai_models.ResponseUsage:
30
+ return convert_anthropic_usage_to_openai_responses_usage(usage)
31
+
32
+
33
+ def convert__anthropic_message_to_openai_responses__response(
34
+ response: anthropic_models.MessageResponse,
35
+ ) -> openai_models.ResponseObject:
36
+ """Convert Anthropic MessageResponse to an OpenAI ResponseObject."""
37
+ text_parts: list[str] = []
38
+ tool_contents: list[dict[str, Any]] = []
39
+ for block in response.content:
40
+ block_type = getattr(block, "type", None)
41
+ if block_type == "text":
42
+ text_parts.append(getattr(block, "text", ""))
43
+ elif block_type == "thinking":
44
+ thinking = getattr(block, "thinking", None) or ""
45
+ signature = getattr(block, "signature", None)
46
+ sig_attr = (
47
+ f' signature="{signature}"'
48
+ if isinstance(signature, str) and signature
49
+ else ""
50
+ )
51
+ text_parts.append(f"<thinking{sig_attr}>{thinking}</thinking>")
52
+ elif block_type == "tool_use":
53
+ tool_contents.append(
54
+ {
55
+ "type": "tool_use",
56
+ "id": getattr(block, "id", "tool_1"),
57
+ "name": getattr(block, "name", "function"),
58
+ "arguments": getattr(block, "input", {}) or {},
59
+ }
60
+ )
61
+
62
+ message_content: list[dict[str, Any]] = []
63
+ if text_parts:
64
+ message_content.append(
65
+ openai_models.OutputTextContent(
66
+ type="output_text",
67
+ text="".join(text_parts),
68
+ ).model_dump()
69
+ )
70
+ message_content.extend(tool_contents)
71
+
72
+ usage_model = None
73
+ if response.usage is not None:
74
+ usage_model = convert__anthropic_usage_to_openai_responses__usage(
75
+ response.usage
76
+ )
77
+
78
+ return openai_models.ResponseObject(
79
+ id=response.id,
80
+ object="response",
81
+ created_at=0,
82
+ status="completed",
83
+ model=response.model,
84
+ output=[
85
+ openai_models.MessageOutput(
86
+ type="message",
87
+ id=f"{response.id}_msg_0",
88
+ status="completed",
89
+ role="assistant",
90
+ content=message_content, # type: ignore[arg-type]
91
+ )
92
+ ],
93
+ parallel_tool_calls=False,
94
+ usage=usage_model,
95
+ )
96
+
97
+
98
+ def convert__anthropic_message_to_openai_chat__response(
99
+ response: anthropic_models.MessageResponse,
100
+ ) -> openai_models.ChatCompletionResponse:
101
+ """Convert Anthropic MessageResponse to an OpenAI ChatCompletionResponse."""
102
+ content_blocks = response.content
103
+ parts: list[str] = []
104
+ for block in content_blocks:
105
+ btype = getattr(block, "type", None)
106
+ if btype == "text":
107
+ text = getattr(block, "text", None)
108
+ if isinstance(text, str):
109
+ parts.append(text)
110
+ elif btype == "thinking":
111
+ thinking = getattr(block, "thinking", None)
112
+ signature = getattr(block, "signature", None)
113
+ if isinstance(thinking, str):
114
+ sig_attr = (
115
+ f' signature="{signature}"'
116
+ if isinstance(signature, str) and signature
117
+ else ""
118
+ )
119
+ parts.append(f"<thinking{sig_attr}>{thinking}</thinking>")
120
+
121
+ content_text = "".join(parts)
122
+
123
+ stop_reason = response.stop_reason
124
+ finish_reason = ANTHROPIC_TO_OPENAI_FINISH_REASON.get(
125
+ stop_reason or "end_turn", "stop"
126
+ )
127
+
128
+ usage_model = convert__anthropic_usage_to_openai_completion__usage(response.usage)
129
+
130
+ payload = {
131
+ "id": response.id,
132
+ "choices": [
133
+ {
134
+ "index": 0,
135
+ "message": {"role": "assistant", "content": content_text},
136
+ "finish_reason": finish_reason,
137
+ }
138
+ ],
139
+ "created": int(time.time()),
140
+ "model": response.model,
141
+ "object": "chat.completion",
142
+ "usage": usage_model.model_dump(),
143
+ }
144
+
145
+ return openai_models.ChatCompletionResponse.model_validate(payload)
146
+
147
+
148
+ __all__ = [
149
+ "convert__anthropic_message_to_openai_chat__response",
150
+ "convert__anthropic_message_to_openai_responses__response",
151
+ "convert__anthropic_usage_to_openai_completion__usage",
152
+ "convert__anthropic_usage_to_openai_responses__usage",
153
+ ]