llmshim 0.6.0__tar.gz → 0.7.0__tar.gz

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 (189) hide show
  1. {llmshim-0.6.0 → llmshim-0.7.0}/Cargo.lock +1 -1
  2. {llmshim-0.6.0 → llmshim-0.7.0}/Cargo.toml +1 -1
  3. {llmshim-0.6.0 → llmshim-0.7.0}/PKG-INFO +1 -1
  4. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/proxy/scaling.md +12 -5
  5. {llmshim-0.6.0 → llmshim-0.7.0}/src/cost.rs +57 -11
  6. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/types.rs +6 -1
  7. {llmshim-0.6.0 → llmshim-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  8. {llmshim-0.6.0 → llmshim-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  9. {llmshim-0.6.0 → llmshim-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  10. {llmshim-0.6.0 → llmshim-0.7.0}/.github/workflows/catalog-refresh.yml +0 -0
  11. {llmshim-0.6.0 → llmshim-0.7.0}/.github/workflows/pages.yml +0 -0
  12. {llmshim-0.6.0 → llmshim-0.7.0}/.github/workflows/release.yml +0 -0
  13. {llmshim-0.6.0 → llmshim-0.7.0}/.gitignore +0 -0
  14. {llmshim-0.6.0 → llmshim-0.7.0}/CLAUDE.md +0 -0
  15. {llmshim-0.6.0 → llmshim-0.7.0}/CODE_OF_CONDUCT.md +0 -0
  16. {llmshim-0.6.0 → llmshim-0.7.0}/CONTRIBUTING.md +0 -0
  17. {llmshim-0.6.0 → llmshim-0.7.0}/LICENSE-APACHE +0 -0
  18. {llmshim-0.6.0 → llmshim-0.7.0}/LICENSE-MIT +0 -0
  19. {llmshim-0.6.0 → llmshim-0.7.0}/NOTICE +0 -0
  20. {llmshim-0.6.0 → llmshim-0.7.0}/README.md +0 -0
  21. {llmshim-0.6.0 → llmshim-0.7.0}/SECURITY.md +0 -0
  22. {llmshim-0.6.0 → llmshim-0.7.0}/benchmarks/bench.rs +0 -0
  23. {llmshim-0.6.0 → llmshim-0.7.0}/benchmarks/bench_python.py +0 -0
  24. {llmshim-0.6.0 → llmshim-0.7.0}/benchmarks/gateway_loadtest.rs +0 -0
  25. {llmshim-0.6.0 → llmshim-0.7.0}/benchmarks/loadtest.rs +0 -0
  26. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/Cargo.toml +0 -0
  27. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/LICENSE-APACHE +0 -0
  28. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/LICENSE-MIT +0 -0
  29. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/README.md +0 -0
  30. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/data/LICENSE.models.dev +0 -0
  31. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/data/README.md +0 -0
  32. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/data/models.dev.json +0 -0
  33. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/aliases.rs +0 -0
  34. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/builtin.rs +0 -0
  35. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/capabilities.rs +0 -0
  36. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/lib.rs +0 -0
  37. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/merge.rs +0 -0
  38. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/parse.rs +0 -0
  39. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/refresh.rs +0 -0
  40. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/src/types.rs +0 -0
  41. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/tests/catalog.rs +0 -0
  42. {llmshim-0.6.0 → llmshim-0.7.0}/crates/llmshim-catalog/tests/refresh.rs +0 -0
  43. {llmshim-0.6.0 → llmshim-0.7.0}/docs/.gitignore +0 -0
  44. {llmshim-0.6.0 → llmshim-0.7.0}/docs/book.toml +0 -0
  45. {llmshim-0.6.0 → llmshim-0.7.0}/docs/mermaid-init.js +0 -0
  46. {llmshim-0.6.0 → llmshim-0.7.0}/docs/mermaid.min.js +0 -0
  47. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/SUMMARY.md +0 -0
  48. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/concepts/contracts.md +0 -0
  49. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/concepts/conversations.md +0 -0
  50. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/concepts/portability.md +0 -0
  51. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/concepts/routing.md +0 -0
  52. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/concepts/translation-flow.md +0 -0
  53. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/caching.md +0 -0
  54. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/capabilities.md +0 -0
  55. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/fallbacks.md +0 -0
  56. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/images.md +0 -0
  57. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/native-controls.md +0 -0
  58. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/reasoning.md +0 -0
  59. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/schemas.md +0 -0
  60. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/streaming.md +0 -0
  61. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/guides/tools.md +0 -0
  62. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/introduction.md +0 -0
  63. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/proxy/deployment.md +0 -0
  64. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/proxy/http-api.md +0 -0
  65. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/proxy/native-apis.md +0 -0
  66. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/api.md +0 -0
  67. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/cli.md +0 -0
  68. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/configuration.md +0 -0
  69. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/errors.md +0 -0
  70. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/models.md +0 -0
  71. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/providers.md +0 -0
  72. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/request-fields.md +0 -0
  73. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/reference/surfaces.md +0 -0
  74. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/start/choose.md +0 -0
  75. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/start/cli.md +0 -0
  76. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/start/clients.md +0 -0
  77. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/start/configure.md +0 -0
  78. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/start/proxy.md +0 -0
  79. {llmshim-0.6.0 → llmshim-0.7.0}/docs/src/start/rust.md +0 -0
  80. {llmshim-0.6.0 → llmshim-0.7.0}/examples/chat.rs +0 -0
  81. {llmshim-0.6.0 → llmshim-0.7.0}/examples/stream.rs +0 -0
  82. {llmshim-0.6.0 → llmshim-0.7.0}/llmshim/__init__.py +0 -0
  83. {llmshim-0.6.0 → llmshim-0.7.0}/llmshim/_client.py +0 -0
  84. {llmshim-0.6.0 → llmshim-0.7.0}/llmshim/_server.py +0 -0
  85. {llmshim-0.6.0 → llmshim-0.7.0}/llmshim/types.py +0 -0
  86. {llmshim-0.6.0 → llmshim-0.7.0}/pyproject.toml +0 -0
  87. {llmshim-0.6.0 → llmshim-0.7.0}/src/breaker.rs +0 -0
  88. {llmshim-0.6.0 → llmshim-0.7.0}/src/cache.rs +0 -0
  89. {llmshim-0.6.0 → llmshim-0.7.0}/src/cli.rs +0 -0
  90. {llmshim-0.6.0 → llmshim-0.7.0}/src/client.rs +0 -0
  91. {llmshim-0.6.0 → llmshim-0.7.0}/src/config.rs +0 -0
  92. {llmshim-0.6.0 → llmshim-0.7.0}/src/env.rs +0 -0
  93. {llmshim-0.6.0 → llmshim-0.7.0}/src/error/normalize.rs +0 -0
  94. {llmshim-0.6.0 → llmshim-0.7.0}/src/error.rs +0 -0
  95. {llmshim-0.6.0 → llmshim-0.7.0}/src/fallback.rs +0 -0
  96. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/auth.rs +0 -0
  97. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/distributed.rs +0 -0
  98. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/http.rs +0 -0
  99. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/idempotency.rs +0 -0
  100. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/metrics.rs +0 -0
  101. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/mod.rs +0 -0
  102. {llmshim-0.6.0 → llmshim-0.7.0}/src/gateway/quota.rs +0 -0
  103. {llmshim-0.6.0 → llmshim-0.7.0}/src/lib.rs +0 -0
  104. {llmshim-0.6.0 → llmshim-0.7.0}/src/log.rs +0 -0
  105. {llmshim-0.6.0 → llmshim-0.7.0}/src/main.rs +0 -0
  106. {llmshim-0.6.0 → llmshim-0.7.0}/src/models.rs +0 -0
  107. {llmshim-0.6.0 → llmshim-0.7.0}/src/provider.rs +0 -0
  108. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/anthropic.rs +0 -0
  109. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/anthropic_reasoning.rs +0 -0
  110. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/anthropic_signature.rs +0 -0
  111. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/chatgpt/auth.rs +0 -0
  112. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/chatgpt/mod.rs +0 -0
  113. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/chatgpt/streaming.rs +0 -0
  114. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/gemini.rs +0 -0
  115. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/mod.rs +0 -0
  116. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/openai.rs +0 -0
  117. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/openai_compat.rs +0 -0
  118. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/openrouter.rs +0 -0
  119. {llmshim-0.6.0 → llmshim-0.7.0}/src/providers/xai.rs +0 -0
  120. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/convert.rs +0 -0
  121. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/error.rs +0 -0
  122. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/handlers.rs +0 -0
  123. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/health.rs +0 -0
  124. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/mod.rs +0 -0
  125. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/ratelimit.rs +0 -0
  126. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/wire/mod.rs +0 -0
  127. {llmshim-0.6.0 → llmshim-0.7.0}/src/proxy/wire/receipts.rs +0 -0
  128. {llmshim-0.6.0 → llmshim-0.7.0}/src/reasoning/normalize.rs +0 -0
  129. {llmshim-0.6.0 → llmshim-0.7.0}/src/reasoning.rs +0 -0
  130. {llmshim-0.6.0 → llmshim-0.7.0}/src/router.rs +0 -0
  131. {llmshim-0.6.0 → llmshim-0.7.0}/src/schema/memo.rs +0 -0
  132. {llmshim-0.6.0 → llmshim-0.7.0}/src/schema/mod.rs +0 -0
  133. {llmshim-0.6.0 → llmshim-0.7.0}/src/schema/validate.rs +0 -0
  134. {llmshim-0.6.0 → llmshim-0.7.0}/src/schema/walk.rs +0 -0
  135. {llmshim-0.6.0 → llmshim-0.7.0}/src/shim.rs +0 -0
  136. {llmshim-0.6.0 → llmshim-0.7.0}/src/streaming.rs +0 -0
  137. {llmshim-0.6.0 → llmshim-0.7.0}/src/toolcall/streaming.rs +0 -0
  138. {llmshim-0.6.0 → llmshim-0.7.0}/src/toolcall.rs +0 -0
  139. {llmshim-0.6.0 → llmshim-0.7.0}/src/usage.rs +0 -0
  140. {llmshim-0.6.0 → llmshim-0.7.0}/src/vision.rs +0 -0
  141. {llmshim-0.6.0 → llmshim-0.7.0}/tests/fixtures/chatgpt-red.png +0 -0
  142. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration.rs +0 -0
  143. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_chatgpt.rs +0 -0
  144. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_chatgpt_proxy.rs +0 -0
  145. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_current_models.rs +0 -0
  146. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_fallback.rs +0 -0
  147. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_gemini.rs +0 -0
  148. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_gemini_tools.rs +0 -0
  149. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_long_context.rs +0 -0
  150. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_multimodel.rs +0 -0
  151. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_openrouter.rs +0 -0
  152. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_proxy.rs +0 -0
  153. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_sglang.rs +0 -0
  154. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_thinking.rs +0 -0
  155. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_tool_roundtrip.rs +0 -0
  156. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_vision.rs +0 -0
  157. {llmshim-0.6.0 → llmshim-0.7.0}/tests/integration_xai.rs +0 -0
  158. {llmshim-0.6.0 → llmshim-0.7.0}/tests/support/completion_status.rs +0 -0
  159. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_advertised_models.rs +0 -0
  160. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_anthropic.rs +0 -0
  161. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_cache.rs +0 -0
  162. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_chatgpt.rs +0 -0
  163. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_cli.rs +0 -0
  164. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_fable.rs +0 -0
  165. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_fallback.rs +0 -0
  166. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_fast_mode.rs +0 -0
  167. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_gemini.rs +0 -0
  168. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_log.rs +0 -0
  169. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_models.rs +0 -0
  170. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_multimodel.rs +0 -0
  171. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_openai.rs +0 -0
  172. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_openai_compat.rs +0 -0
  173. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_openrouter.rs +0 -0
  174. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_provider_contracts.rs +0 -0
  175. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_proxy.rs +0 -0
  176. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_proxy_convert.rs +0 -0
  177. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_reasoning.rs +0 -0
  178. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_reasoning_profile.rs +0 -0
  179. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_router.rs +0 -0
  180. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_schema.rs +0 -0
  181. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_shim.rs +0 -0
  182. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_signature.rs +0 -0
  183. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_sse.rs +0 -0
  184. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_toolcall.rs +0 -0
  185. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_tools.rs +0 -0
  186. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_usage.rs +0 -0
  187. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_vision.rs +0 -0
  188. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_wire.rs +0 -0
  189. {llmshim-0.6.0 → llmshim-0.7.0}/tests/unit_xai.rs +0 -0
@@ -1180,7 +1180,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
1180
1180
 
1181
1181
  [[package]]
1182
1182
  name = "llmshim"
1183
- version = "0.6.0"
1183
+ version = "0.7.0"
1184
1184
  dependencies = [
1185
1185
  "async-stream",
1186
1186
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "llmshim"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  edition = "2021"
5
5
  description = "Blazing fast LLM API translation layer in pure Rust"
6
6
  license = "MIT OR Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llmshim
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -106,11 +106,18 @@ buckets. A gateway key's identity may carry `budget_usd` and an optional
106
106
  {"sk-example": {"tenant": "acme", "tier": 1, "budget_usd": 100, "budget_window_secs": 86400}}
107
107
  ```
108
108
 
109
- Cost is only knowable after a response, so the cap is checked before dispatch
110
- and charged after: one in-flight request can overshoot.
111
-
112
- A response the catalog cannot price is **not** charged — recording zero would let
113
- an unpriced model run forever under a budget. So that a cap cannot silently stop
109
+ Cost is only knowable after a response, so the cap is checked before dispatch and
110
+ charged after. Everything admitted between the last charge and the next check
111
+ passes, so the overshoot bound is **admitted concurrency × the most expensive
112
+ request**, multiplied again across replicas that have not yet shared their
113
+ ledger. Size a cap with that headroom in mind rather than as a hard ceiling.
114
+
115
+ A response the catalog cannot price at all is **not** charged — recording zero
116
+ would let an unpriced model run forever under a budget. A model that prices only
117
+ *some* token classes is charged at its highest published rate for the rest, so a
118
+ partial price bounds the charge from above instead of voiding it: 2,537 of the
119
+ 7,461 priced models in the catalog publish no `cache_read` rate, and voiding
120
+ those would have reopened this same hole one layer down. So that a cap cannot silently stop
114
121
  binding, a request whose target has **no catalog price is refused before it runs**
115
122
  when a budget is set:
116
123
 
@@ -29,6 +29,27 @@ fn charge(tokens: u64, rate: Option<f64>) -> Option<f64> {
29
29
  Some(tokens as f64 * rate? / PER_MILLION)
30
30
  }
31
31
 
32
+ /// The highest rate this model publishes, used where a class has none of its own.
33
+ ///
34
+ /// Catalogs price partially and often: **2,537 of 7,461 priced models in the
35
+ /// vendored snapshot carry no `cache_read` rate and 5,892 no `cache_write`**,
36
+ /// including `gpt-5-pro` and `o3-pro`, priced `{input, output}` only. Returning
37
+ /// `None` for those made the whole response unpriceable the moment it reported a
38
+ /// cached token — and a discarded charge is a spend cap that silently stops
39
+ /// binding, which is the failure this crate exists to prevent.
40
+ ///
41
+ /// Substituting the highest published rate can only over-estimate, never under.
42
+ /// For a cap that is the safe direction: it spends a budget slightly early, where
43
+ /// under-estimating spends it forever.
44
+ fn highest_published_rate(cost: &Cost) -> Option<f64> {
45
+ [cost.input, cost.output, cost.cache_read, cost.cache_write]
46
+ .into_iter()
47
+ .flatten()
48
+ .fold(None, |best: Option<f64>, rate| {
49
+ Some(best.map_or(rate, |b| b.max(rate)))
50
+ })
51
+ }
52
+
32
53
  /// Price one normalized usage object.
33
54
  ///
34
55
  /// Input is charged on `uncached_input_tokens` — the prompt minus whatever
@@ -53,11 +74,17 @@ pub fn price(usage: &Value, cost: &Cost) -> Option<f64> {
53
74
  return None;
54
75
  }
55
76
 
77
+ // A class the model does not price falls back to its highest published rate
78
+ // rather than voiding the total. See `highest_published_rate`: the result is
79
+ // an upper bound, which is the only safe direction for a budget.
80
+ let ceiling = highest_published_rate(cost);
81
+ let rate_for = |own: Option<f64>| own.or(ceiling);
82
+
56
83
  Some(
57
- charge(input, cost.input)?
58
- + charge(count(usage, "completion_tokens"), cost.output)?
59
- + charge(cache_read, cost.cache_read)?
60
- + charge(cache_write, cost.cache_write)?,
84
+ charge(input, rate_for(cost.input))?
85
+ + charge(count(usage, "completion_tokens"), rate_for(cost.output))?
86
+ + charge(cache_read, rate_for(cost.cache_read))?
87
+ + charge(cache_write, rate_for(cost.cache_write))?,
61
88
  )
62
89
  }
63
90
 
@@ -221,14 +248,21 @@ mod tests {
221
248
  }
222
249
 
223
250
  #[test]
224
- fn a_missing_cache_rate_poisons_the_total_only_when_it_is_used() {
251
+ fn a_missing_cache_rate_is_charged_at_the_models_highest_rate() {
252
+ // This replaced an assertion that a used-but-unpriced class voids the
253
+ // total. That instinct was right — never under-report — but the remedy
254
+ // was wrong: `None` is discarded by `SpendCap::record`, so a spend cap
255
+ // silently stopped binding for the 2,537 catalogued models that publish
256
+ // no `cache_read` rate. An upper bound honours the same principle
257
+ // without the hole.
225
258
  let partial = Cost {
226
259
  input: Some(3.0),
227
260
  output: Some(15.0),
228
261
  cache_read: None,
229
262
  cache_write: None,
230
263
  };
231
- // No cache tokens: the missing rates are irrelevant.
264
+
265
+ // No cache tokens: the missing rates never come into play.
232
266
  close(
233
267
  price(
234
268
  &json!({"uncached_input_tokens": 1_000_000, "completion_tokens": 0, "cache_read_tokens": 0}),
@@ -236,14 +270,26 @@ mod tests {
236
270
  ),
237
271
  3.0,
238
272
  );
239
- // Cache tokens actually used, with no rate to charge them at.
240
- assert_eq!(
273
+
274
+ // Cache tokens used with no rate of their own: charged at 15.0, the
275
+ // highest rate this model publishes — an over-estimate, never an under.
276
+ close(
241
277
  price(
242
- &json!({"uncached_input_tokens": 1_000_000, "cache_read_tokens": 1}),
278
+ &json!({"uncached_input_tokens": 1_000_000, "cache_read_tokens": 1_000_000}),
243
279
  &partial,
244
280
  ),
245
- None,
246
- "an unpriced bucket that was used must not round down to a partial sum"
281
+ 18.0,
282
+ );
283
+
284
+ // The bound must never fall below what a complete price would charge.
285
+ let complete = Cost {
286
+ cache_read: Some(0.3),
287
+ ..partial
288
+ };
289
+ let usage = json!({"uncached_input_tokens": 1_000_000, "cache_read_tokens": 1_000_000});
290
+ assert!(
291
+ price(&usage, &partial).unwrap() >= price(&usage, &complete).unwrap(),
292
+ "a fallback rate must bound the real one from above, or a cap under-charges"
247
293
  );
248
294
  }
249
295
 
@@ -119,7 +119,12 @@ pub struct Usage {
119
119
  pub cache_read_tokens: u64,
120
120
  pub cache_write_tokens: u64,
121
121
  /// USD charged for this response. `null` means the catalog carries no price
122
- /// for the model — it never means free. See `llmshim::cost`.
122
+ /// for the model at all — it never means free.
123
+ ///
124
+ /// Where a model prices some token classes and not others, the unpriced ones
125
+ /// are charged at its highest published rate, so this is an **upper bound**
126
+ /// rather than `null`. Under-reporting would let a spend cap stop binding;
127
+ /// over-reporting merely spends a budget slightly early. See `llmshim::cost`.
123
128
  pub cost_usd: Option<f64>,
124
129
  }
125
130
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes