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
@@ -1,6 +1,7 @@
1
1
  """Pricing configuration settings."""
2
2
 
3
3
  from pathlib import Path
4
+ from typing import Literal
4
5
 
5
6
  from pydantic import Field, field_validator
6
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -8,7 +9,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
8
9
  from ccproxy.core.system import get_xdg_cache_home
9
10
 
10
11
 
11
- class PricingSettings(BaseSettings):
12
+ class PricingConfig(BaseSettings):
12
13
  """
13
14
  Configuration settings for the pricing system.
14
15
 
@@ -16,6 +17,11 @@ class PricingSettings(BaseSettings):
16
17
  Settings can be configured via environment variables with PRICING__ prefix.
17
18
  """
18
19
 
20
+ enabled: bool = Field(
21
+ default=True,
22
+ description="Whether the pricing plugin is enabled",
23
+ )
24
+
19
25
  # Cache settings
20
26
  cache_dir: Path = Field(
21
27
  default_factory=lambda: get_xdg_cache_home() / "ccproxy",
@@ -48,11 +54,6 @@ class PricingSettings(BaseSettings):
48
54
  description="Whether to automatically update stale cache",
49
55
  )
50
56
 
51
- fallback_to_embedded: bool = Field(
52
- default=True,
53
- description="Whether to fallback to embedded pricing on failure",
54
- )
55
-
56
57
  # Memory cache settings
57
58
  memory_cache_ttl: int = Field(
58
59
  default=300,
@@ -61,6 +62,31 @@ class PricingSettings(BaseSettings):
61
62
  description="Time to live for in-memory pricing cache in seconds",
62
63
  )
63
64
 
65
+ # Task scheduling settings
66
+ update_interval_hours: float = Field(
67
+ default=6.0,
68
+ ge=0.1,
69
+ le=168.0, # Max 1 week
70
+ description="Hours between scheduled pricing updates",
71
+ )
72
+
73
+ force_refresh_on_startup: bool = Field(
74
+ default=False,
75
+ description="Whether to force pricing refresh on plugin startup",
76
+ )
77
+
78
+ # Backward-compat flag used by older tests; embedded pricing has been removed.
79
+ # Keeping this flag allows type checking and test configuration without effect.
80
+ fallback_to_embedded: bool = Field(
81
+ default=False,
82
+ description="(Deprecated) If true, fall back to embedded pricing when external data is unavailable",
83
+ )
84
+
85
+ pricing_provider: Literal["claude", "anthropic", "openai", "all"] = Field(
86
+ default="all",
87
+ description="Which provider pricing to load: 'claude', 'anthropic', 'openai', or 'all'",
88
+ )
89
+
64
90
  @field_validator("cache_dir", mode="before")
65
91
  @classmethod
66
92
  def validate_cache_dir(cls, v: str | Path | None) -> Path:
@@ -0,0 +1,35 @@
1
+ """Pricing service exceptions."""
2
+
3
+
4
+ class PricingError(Exception):
5
+ """Base exception for pricing-related errors."""
6
+
7
+ pass
8
+
9
+
10
+ class PricingDataNotLoadedError(PricingError):
11
+ """Raised when pricing data has not been loaded yet."""
12
+
13
+ def __init__(
14
+ self,
15
+ message: str = "Pricing data not loaded yet - cost calculation unavailable",
16
+ ):
17
+ self.message = message
18
+ super().__init__(self.message)
19
+
20
+
21
+ class ModelPricingNotFoundError(PricingError):
22
+ """Raised when pricing for a specific model is not found."""
23
+
24
+ def __init__(self, model: str, message: str | None = None):
25
+ self.model = model
26
+ self.message = message or f"No pricing data available for model '{model}'"
27
+ super().__init__(self.message)
28
+
29
+
30
+ class PricingServiceDisabledError(PricingError):
31
+ """Raised when pricing service is disabled."""
32
+
33
+ def __init__(self, message: str = "Pricing service is disabled"):
34
+ self.message = message
35
+ super().__init__(self.message)
@@ -0,0 +1,440 @@
1
+ """Pricing data loader and format converter for LiteLLM pricing data."""
2
+
3
+ import json
4
+ from decimal import Decimal
5
+ from typing import Any, Literal
6
+
7
+ import httpx
8
+ from pydantic import ValidationError
9
+
10
+ from ccproxy.core.logging import get_plugin_logger
11
+ from ccproxy.plugins.claude_shared.model_defaults import (
12
+ DEFAULT_CLAUDE_MODEL_MAPPINGS,
13
+ )
14
+ from ccproxy.utils.model_mapper import ModelMapper
15
+
16
+ from .models import PricingData
17
+
18
+
19
+ logger = get_plugin_logger(__name__)
20
+
21
+ _CLAUDE_MODEL_MAPPER = ModelMapper(DEFAULT_CLAUDE_MODEL_MAPPINGS)
22
+ _CLAUDE_ALIAS_MAP: dict[str, str] = {
23
+ rule.match: rule.target
24
+ for rule in DEFAULT_CLAUDE_MODEL_MAPPINGS
25
+ if rule.match.startswith("claude-")
26
+ }
27
+
28
+
29
+ def _is_openai_model(model_name: str) -> bool:
30
+ lowered = model_name.lower()
31
+ return lowered.startswith(("gpt-", "o1", "o3", "text-davinci"))
32
+
33
+
34
+ class PricingLoader:
35
+ """Loads and converts pricing data from LiteLLM format to internal format."""
36
+
37
+ @staticmethod
38
+ def extract_claude_models(
39
+ litellm_data: dict[str, Any], verbose: bool = True
40
+ ) -> dict[str, Any]:
41
+ """Extract Claude model entries from LiteLLM data.
42
+
43
+ Args:
44
+ litellm_data: Raw LiteLLM pricing data
45
+ verbose: Whether to log individual model discoveries
46
+
47
+ Returns:
48
+ Dictionary with only Claude models
49
+ """
50
+ claude_models = {}
51
+
52
+ for model_name, model_data in litellm_data.items():
53
+ # Check if this is a Claude model
54
+ if (
55
+ isinstance(model_data, dict)
56
+ and model_data.get("litellm_provider") == "anthropic"
57
+ and "claude" in model_name.lower()
58
+ ):
59
+ claude_models[model_name] = model_data
60
+ if verbose:
61
+ logger.debug("claude_model_found", model_name=model_name)
62
+
63
+ if verbose:
64
+ logger.info(
65
+ "claude_models_extracted",
66
+ model_count=len(claude_models),
67
+ source="LiteLLM",
68
+ )
69
+ return claude_models
70
+
71
+ @staticmethod
72
+ def extract_openai_models(
73
+ litellm_data: dict[str, Any], verbose: bool = True
74
+ ) -> dict[str, Any]:
75
+ """Extract OpenAI model entries from LiteLLM data.
76
+
77
+ Args:
78
+ litellm_data: Raw LiteLLM pricing data
79
+ verbose: Whether to log individual model discoveries
80
+
81
+ Returns:
82
+ Dictionary with only OpenAI models
83
+ """
84
+ openai_models = {}
85
+
86
+ for model_name, model_data in litellm_data.items():
87
+ # Check if this is an OpenAI model
88
+ if isinstance(model_data, dict) and (
89
+ model_data.get("litellm_provider") == "openai"
90
+ or _is_openai_model(model_name)
91
+ ):
92
+ openai_models[model_name] = model_data
93
+ if verbose:
94
+ logger.debug("openai_model_found", model_name=model_name)
95
+
96
+ if verbose:
97
+ logger.info(
98
+ "openai_models_extracted",
99
+ model_count=len(openai_models),
100
+ source="LiteLLM",
101
+ )
102
+ return openai_models
103
+
104
+ @staticmethod
105
+ def extract_anthropic_models(
106
+ litellm_data: dict[str, Any], verbose: bool = True
107
+ ) -> dict[str, Any]:
108
+ """Extract all Anthropic model entries from LiteLLM data.
109
+
110
+ This includes Claude models and any other Anthropic models.
111
+
112
+ Args:
113
+ litellm_data: Raw LiteLLM pricing data
114
+ verbose: Whether to log individual model discoveries
115
+
116
+ Returns:
117
+ Dictionary with all Anthropic models
118
+ """
119
+ anthropic_models = {}
120
+
121
+ for model_name, model_data in litellm_data.items():
122
+ # Check if this is an Anthropic model
123
+ if (
124
+ isinstance(model_data, dict)
125
+ and model_data.get("litellm_provider") == "anthropic"
126
+ ):
127
+ anthropic_models[model_name] = model_data
128
+ if verbose:
129
+ logger.debug("anthropic_model_found", model_name=model_name)
130
+
131
+ if verbose:
132
+ logger.info(
133
+ "anthropic_models_extracted",
134
+ model_count=len(anthropic_models),
135
+ source="LiteLLM",
136
+ )
137
+ return anthropic_models
138
+
139
+ @staticmethod
140
+ def extract_models_by_provider(
141
+ litellm_data: dict[str, Any],
142
+ provider: Literal["anthropic", "openai", "all", "claude"] = "all",
143
+ verbose: bool = True,
144
+ ) -> dict[str, Any]:
145
+ """Extract models by provider from LiteLLM data.
146
+
147
+ Args:
148
+ litellm_data: Raw LiteLLM pricing data
149
+ provider: Provider to extract models for ("anthropic", "openai", "claude", or "all")
150
+ verbose: Whether to log individual model discoveries
151
+
152
+ Returns:
153
+ Dictionary with models from specified provider(s)
154
+ """
155
+ if provider == "claude":
156
+ return PricingLoader.extract_claude_models(litellm_data, verbose)
157
+ elif provider == "anthropic":
158
+ return PricingLoader.extract_anthropic_models(litellm_data, verbose)
159
+ elif provider == "openai":
160
+ return PricingLoader.extract_openai_models(litellm_data, verbose)
161
+ elif provider == "all":
162
+ # Extract all models that have pricing data
163
+ all_models = {}
164
+ for model_name, model_data in litellm_data.items():
165
+ if isinstance(model_data, dict):
166
+ all_models[model_name] = model_data
167
+ if verbose:
168
+ provider_name = model_data.get("litellm_provider", "unknown")
169
+ logger.debug(
170
+ "model_found",
171
+ model_name=model_name,
172
+ provider=provider_name,
173
+ )
174
+
175
+ if verbose:
176
+ logger.info(
177
+ "all_models_extracted",
178
+ model_count=len(all_models),
179
+ source="LiteLLM",
180
+ )
181
+ return all_models
182
+ else:
183
+ raise ValueError(
184
+ f"Invalid provider: {provider}. Use 'anthropic', 'openai', 'claude', or 'all'"
185
+ )
186
+
187
+ @staticmethod
188
+ def convert_to_internal_format(
189
+ models: dict[str, Any], map_to_claude: bool = True, verbose: bool = True
190
+ ) -> dict[str, dict[str, Decimal]]:
191
+ """Convert LiteLLM pricing format to internal format.
192
+
193
+ LiteLLM format uses cost per token, we use cost per 1M tokens as Decimal.
194
+
195
+ Args:
196
+ models: Models in LiteLLM format
197
+ map_to_claude: Whether to map model names to Claude equivalents
198
+ verbose: Whether to log individual model conversions
199
+
200
+ Returns:
201
+ Dictionary in internal pricing format
202
+ """
203
+ internal_format = {}
204
+
205
+ for model_name, model_data in models.items():
206
+ try:
207
+ # Extract pricing fields
208
+ input_cost_per_token = model_data.get("input_cost_per_token")
209
+ output_cost_per_token = model_data.get("output_cost_per_token")
210
+ cache_creation_cost = model_data.get("cache_creation_input_token_cost")
211
+ cache_read_cost = model_data.get("cache_read_input_token_cost")
212
+
213
+ # Skip models without pricing info
214
+ if input_cost_per_token is None or output_cost_per_token is None:
215
+ if verbose:
216
+ logger.warning("model_pricing_missing", model_name=model_name)
217
+ continue
218
+
219
+ # Convert to per-1M-token pricing (multiply by 1,000,000)
220
+ pricing = {
221
+ "input": Decimal(str(input_cost_per_token * 1_000_000)),
222
+ "output": Decimal(str(output_cost_per_token * 1_000_000)),
223
+ }
224
+
225
+ # Add cache pricing if available
226
+ if cache_creation_cost is not None:
227
+ pricing["cache_write"] = Decimal(
228
+ str(cache_creation_cost * 1_000_000)
229
+ )
230
+
231
+ if cache_read_cost is not None:
232
+ pricing["cache_read"] = Decimal(str(cache_read_cost * 1_000_000))
233
+
234
+ # Optionally map to canonical model name
235
+ if map_to_claude:
236
+ canonical_name = _CLAUDE_MODEL_MAPPER.map(model_name).mapped
237
+ else:
238
+ canonical_name = model_name
239
+
240
+ internal_format[canonical_name] = pricing
241
+
242
+ if verbose:
243
+ logger.debug(
244
+ "model_pricing_converted",
245
+ original_name=model_name,
246
+ canonical_name=canonical_name,
247
+ input_cost=str(pricing["input"]),
248
+ output_cost=str(pricing["output"]),
249
+ )
250
+
251
+ except (ValueError, TypeError) as e:
252
+ if verbose:
253
+ logger.error(
254
+ "pricing_conversion_failed", model_name=model_name, error=str(e)
255
+ )
256
+ continue
257
+
258
+ if verbose:
259
+ logger.info("models_converted", model_count=len(internal_format))
260
+ return internal_format
261
+
262
+ @staticmethod
263
+ def load_pricing_from_data(
264
+ litellm_data: dict[str, Any],
265
+ provider: Literal["anthropic", "openai", "all", "claude"] = "claude",
266
+ map_to_claude: bool = True,
267
+ verbose: bool = True,
268
+ ) -> PricingData | None:
269
+ """Load and convert pricing data from LiteLLM format.
270
+
271
+ Args:
272
+ litellm_data: Raw LiteLLM pricing data
273
+ provider: Provider to load pricing for ("anthropic", "openai", "all", or "claude")
274
+ "claude" is kept for backward compatibility and extracts only Claude models
275
+ map_to_claude: Whether to map model names to Claude equivalents
276
+ verbose: Whether to enable verbose logging
277
+
278
+ Returns:
279
+ Validated pricing data as PricingData model, or None if invalid
280
+ """
281
+ try:
282
+ # Extract models based on provider
283
+ if provider == "claude":
284
+ # Backward compatibility - extract only Claude models
285
+ models = PricingLoader.extract_claude_models(
286
+ litellm_data, verbose=verbose
287
+ )
288
+ else:
289
+ models = PricingLoader.extract_models_by_provider(
290
+ litellm_data, provider=provider, verbose=verbose
291
+ )
292
+
293
+ if not models:
294
+ if verbose:
295
+ logger.warning(
296
+ "models_not_found", provider=provider, source="LiteLLM"
297
+ )
298
+ return None
299
+
300
+ # Convert to internal format
301
+ internal_pricing = PricingLoader.convert_to_internal_format(
302
+ models, map_to_claude=map_to_claude, verbose=verbose
303
+ )
304
+
305
+ if not internal_pricing:
306
+ if verbose:
307
+ logger.warning("pricing_data_invalid")
308
+ return None
309
+
310
+ # Validate and create PricingData model
311
+ pricing_data = PricingData.model_validate(internal_pricing)
312
+
313
+ if verbose:
314
+ logger.info(
315
+ "pricing_data_loaded",
316
+ model_count=len(pricing_data),
317
+ provider=provider,
318
+ )
319
+
320
+ return pricing_data
321
+
322
+ except ValidationError as e:
323
+ if verbose:
324
+ logger.error("pricing_validation_failed", error=str(e), exc_info=e)
325
+ return None
326
+ except json.JSONDecodeError as e:
327
+ if verbose:
328
+ logger.error(
329
+ "pricing_json_decode_failed",
330
+ source="LiteLLM",
331
+ error=str(e),
332
+ exc_info=e,
333
+ )
334
+ return None
335
+ except httpx.HTTPError as e:
336
+ if verbose:
337
+ logger.error(
338
+ "pricing_http_error", source="LiteLLM", error=str(e), exc_info=e
339
+ )
340
+ return None
341
+ except OSError as e:
342
+ if verbose:
343
+ logger.error(
344
+ "pricing_io_error", source="LiteLLM", error=str(e), exc_info=e
345
+ )
346
+ return None
347
+ except Exception as e:
348
+ if verbose:
349
+ logger.error(
350
+ "pricing_load_failed", source="LiteLLM", error=str(e), exc_info=e
351
+ )
352
+ return None
353
+
354
+ @staticmethod
355
+ def validate_pricing_data(
356
+ pricing_data: Any, verbose: bool = True
357
+ ) -> PricingData | None:
358
+ """Validate pricing data using Pydantic models.
359
+
360
+ Args:
361
+ pricing_data: Pricing data to validate (dict or PricingData)
362
+ verbose: Whether to enable verbose logging
363
+
364
+ Returns:
365
+ Valid PricingData model or None if validation fails
366
+ """
367
+ try:
368
+ # If already a PricingData instance, return it
369
+ if isinstance(pricing_data, PricingData):
370
+ if verbose:
371
+ logger.debug(
372
+ "pricing_already_validated", model_count=len(pricing_data)
373
+ )
374
+ return pricing_data
375
+
376
+ # If it's a dict, try to create PricingData from it
377
+ if isinstance(pricing_data, dict):
378
+ if not pricing_data:
379
+ if verbose:
380
+ logger.warning("pricing_data_empty")
381
+ return None
382
+
383
+ # Try to create PricingData model
384
+ validated_data = PricingData.model_validate(pricing_data)
385
+
386
+ if verbose:
387
+ logger.debug(
388
+ "pricing_data_validated", model_count=len(validated_data)
389
+ )
390
+
391
+ return validated_data
392
+
393
+ # Invalid type
394
+ if verbose:
395
+ logger.error(
396
+ "pricing_data_invalid_type",
397
+ actual_type=type(pricing_data).__name__,
398
+ expected_types=["dict", "PricingData"],
399
+ )
400
+ return None
401
+
402
+ except ValidationError as e:
403
+ if verbose:
404
+ logger.error("pricing_validation_failed", error=str(e), exc_info=e)
405
+ return None
406
+ except json.JSONDecodeError as e:
407
+ if verbose:
408
+ logger.error("pricing_validation_json_error", error=str(e), exc_info=e)
409
+ return None
410
+ except OSError as e:
411
+ if verbose:
412
+ logger.error("pricing_validation_io_error", error=str(e), exc_info=e)
413
+ return None
414
+ except Exception as e:
415
+ if verbose:
416
+ logger.error(
417
+ "pricing_validation_unexpected_error", error=str(e), exc_info=e
418
+ )
419
+ return None
420
+
421
+ @staticmethod
422
+ def get_model_aliases() -> dict[str, str]:
423
+ """Get mapping of model aliases to canonical names.
424
+
425
+ Returns:
426
+ Dictionary mapping aliases to canonical model names
427
+ """
428
+ return _CLAUDE_ALIAS_MAP.copy()
429
+
430
+ @staticmethod
431
+ def get_canonical_model_name(model_name: str) -> str:
432
+ """Get canonical model name for a given model name.
433
+
434
+ Args:
435
+ model_name: Model name (possibly an alias)
436
+
437
+ Returns:
438
+ Canonical model name
439
+ """
440
+ return _CLAUDE_MODEL_MAPPER.map(model_name).mapped
@@ -4,7 +4,14 @@ from collections.abc import Iterator
4
4
  from decimal import Decimal
5
5
  from typing import Any
6
6
 
7
- from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator
7
+ from pydantic import (
8
+ BaseModel,
9
+ ConfigDict,
10
+ Field,
11
+ RootModel,
12
+ field_serializer,
13
+ field_validator,
14
+ )
8
15
 
9
16
 
10
17
  class ModelPricing(BaseModel):
@@ -32,9 +39,13 @@ class ModelPricing(BaseModel):
32
39
  return v
33
40
  raise TypeError(f"Cannot convert {type(v)} to Decimal")
34
41
 
42
+ @field_serializer("input", "output", "cache_read", "cache_write")
43
+ def serialize_decimal(self, value: Decimal) -> float:
44
+ """Serialize Decimal fields as float for JSON compatibility."""
45
+ return float(value)
46
+
35
47
  model_config = ConfigDict(
36
48
  arbitrary_types_allowed=True,
37
- json_encoders={Decimal: lambda v: float(v)},
38
49
  )
39
50
 
40
51
 
@@ -82,24 +93,3 @@ class PricingData(RootModel[dict[str, ModelPricing]]):
82
93
  def model_names(self) -> list[str]:
83
94
  """Get list of all model names."""
84
95
  return list(self.root.keys())
85
-
86
- def to_dict(self) -> dict[str, dict[str, Decimal]]:
87
- """Convert to legacy dict format for backward compatibility."""
88
- return {
89
- model_name: {
90
- "input": pricing.input,
91
- "output": pricing.output,
92
- "cache_read": pricing.cache_read,
93
- "cache_write": pricing.cache_write,
94
- }
95
- for model_name, pricing in self.root.items()
96
- }
97
-
98
- @classmethod
99
- def from_dict(cls, data: dict[str, dict[str, Any]]) -> "PricingData":
100
- """Create PricingData from legacy dict format."""
101
- models = {
102
- model_name: ModelPricing(**pricing_dict)
103
- for model_name, pricing_dict in data.items()
104
- }
105
- return cls(root=models)