llmshim 0.8.0__tar.gz → 0.9.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 (205) hide show
  1. {llmshim-0.8.0 → llmshim-0.9.0}/.github/workflows/release.yml +1 -1
  2. {llmshim-0.8.0 → llmshim-0.9.0}/CLAUDE.md +28 -4
  3. {llmshim-0.8.0 → llmshim-0.9.0}/Cargo.lock +70 -36
  4. {llmshim-0.8.0 → llmshim-0.9.0}/Cargo.toml +2 -2
  5. {llmshim-0.8.0 → llmshim-0.9.0}/PKG-INFO +1 -1
  6. {llmshim-0.8.0 → llmshim-0.9.0}/README.md +15 -1
  7. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/Cargo.toml +1 -1
  8. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/README.md +22 -0
  9. llmshim-0.9.0/crates/llmshim-catalog/data/README.md +26 -0
  10. llmshim-0.9.0/crates/llmshim-catalog/data/verified.json +43 -0
  11. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/builtin.rs +13 -3
  12. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/lib.rs +17 -0
  13. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/merge.rs +2 -1
  14. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/parse.rs +20 -1
  15. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/types.rs +56 -0
  16. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/tests/catalog.rs +101 -0
  17. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/caching.md +22 -6
  18. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/reasoning.md +14 -3
  19. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/models.md +25 -1
  20. {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/types.py +13 -0
  21. {llmshim-0.8.0 → llmshim-0.9.0}/src/client.rs +1 -1
  22. {llmshim-0.8.0 → llmshim-0.9.0}/src/config.rs +31 -5
  23. {llmshim-0.8.0 → llmshim-0.9.0}/src/cost.rs +96 -5
  24. llmshim-0.9.0/src/error.rs +72 -0
  25. {llmshim-0.8.0 → llmshim-0.9.0}/src/log.rs +25 -9
  26. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/openrouter.rs +10 -0
  27. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/xai.rs +10 -6
  28. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/convert.rs +1 -0
  29. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/types.rs +8 -0
  30. {llmshim-0.8.0 → llmshim-0.9.0}/src/shim.rs +63 -1
  31. llmshim-0.9.0/tests/fixtures/openrouter-usage-include-stream.sse +92 -0
  32. llmshim-0.9.0/tests/fixtures/openrouter-usage-include.json +1 -0
  33. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_current_models.rs +2 -2
  34. llmshim-0.9.0/tests/integration_grok_4_7.rs +298 -0
  35. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_openrouter.rs +106 -0
  36. llmshim-0.9.0/tests/security_gemini_error_redaction.rs +136 -0
  37. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_advertised_models.rs +1 -1
  38. llmshim-0.9.0/tests/unit_config.rs +83 -0
  39. llmshim-0.9.0/tests/unit_grok_4_7.rs +78 -0
  40. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_log.rs +39 -0
  41. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openrouter.rs +231 -0
  42. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_proxy.rs +3 -0
  43. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_proxy_convert.rs +3 -0
  44. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_xai.rs +3 -3
  45. llmshim-0.8.0/crates/llmshim-catalog/data/README.md +0 -8
  46. llmshim-0.8.0/src/error.rs +0 -40
  47. {llmshim-0.8.0 → llmshim-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  48. {llmshim-0.8.0 → llmshim-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  49. {llmshim-0.8.0 → llmshim-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  50. {llmshim-0.8.0 → llmshim-0.9.0}/.github/scripts/npm-verify-served.sh +0 -0
  51. {llmshim-0.8.0 → llmshim-0.9.0}/.github/workflows/catalog-refresh.yml +0 -0
  52. {llmshim-0.8.0 → llmshim-0.9.0}/.github/workflows/pages.yml +0 -0
  53. {llmshim-0.8.0 → llmshim-0.9.0}/.gitignore +0 -0
  54. {llmshim-0.8.0 → llmshim-0.9.0}/CODE_OF_CONDUCT.md +0 -0
  55. {llmshim-0.8.0 → llmshim-0.9.0}/CONTRIBUTING.md +0 -0
  56. {llmshim-0.8.0 → llmshim-0.9.0}/LICENSE-APACHE +0 -0
  57. {llmshim-0.8.0 → llmshim-0.9.0}/LICENSE-MIT +0 -0
  58. {llmshim-0.8.0 → llmshim-0.9.0}/NOTICE +0 -0
  59. {llmshim-0.8.0 → llmshim-0.9.0}/SECURITY.md +0 -0
  60. {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/bench.rs +0 -0
  61. {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/bench_python.py +0 -0
  62. {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/gateway_loadtest.rs +0 -0
  63. {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/loadtest.rs +0 -0
  64. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/LICENSE-APACHE +0 -0
  65. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/LICENSE-MIT +0 -0
  66. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/data/LICENSE.models.dev +0 -0
  67. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/data/models.dev.json +0 -0
  68. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/aliases.rs +0 -0
  69. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/capabilities.rs +0 -0
  70. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/refresh.rs +0 -0
  71. {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/tests/refresh.rs +0 -0
  72. {llmshim-0.8.0 → llmshim-0.9.0}/docs/.gitignore +0 -0
  73. {llmshim-0.8.0 → llmshim-0.9.0}/docs/book.toml +0 -0
  74. {llmshim-0.8.0 → llmshim-0.9.0}/docs/mermaid-init.js +0 -0
  75. {llmshim-0.8.0 → llmshim-0.9.0}/docs/mermaid.min.js +0 -0
  76. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/SUMMARY.md +0 -0
  77. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/contracts.md +0 -0
  78. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/conversations.md +0 -0
  79. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/portability.md +0 -0
  80. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/routing.md +0 -0
  81. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/translation-flow.md +0 -0
  82. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/capabilities.md +0 -0
  83. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/fallbacks.md +0 -0
  84. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/images.md +0 -0
  85. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/native-controls.md +0 -0
  86. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/schemas.md +0 -0
  87. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/streaming.md +0 -0
  88. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/tools.md +0 -0
  89. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/introduction.md +0 -0
  90. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/deployment.md +0 -0
  91. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/http-api.md +0 -0
  92. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/native-apis.md +0 -0
  93. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/scaling.md +0 -0
  94. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/api.md +0 -0
  95. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/cli.md +0 -0
  96. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/configuration.md +0 -0
  97. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/errors.md +0 -0
  98. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/providers.md +0 -0
  99. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/request-fields.md +0 -0
  100. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/surfaces.md +0 -0
  101. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/choose.md +0 -0
  102. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/cli.md +0 -0
  103. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/clients.md +0 -0
  104. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/configure.md +0 -0
  105. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/proxy.md +0 -0
  106. {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/rust.md +0 -0
  107. {llmshim-0.8.0 → llmshim-0.9.0}/examples/chat.rs +0 -0
  108. {llmshim-0.8.0 → llmshim-0.9.0}/examples/stream.rs +0 -0
  109. {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/__init__.py +0 -0
  110. {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/_client.py +0 -0
  111. {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/_server.py +0 -0
  112. {llmshim-0.8.0 → llmshim-0.9.0}/pyproject.toml +0 -0
  113. {llmshim-0.8.0 → llmshim-0.9.0}/src/breaker.rs +0 -0
  114. {llmshim-0.8.0 → llmshim-0.9.0}/src/cache.rs +0 -0
  115. {llmshim-0.8.0 → llmshim-0.9.0}/src/cli.rs +0 -0
  116. {llmshim-0.8.0 → llmshim-0.9.0}/src/env.rs +0 -0
  117. {llmshim-0.8.0 → llmshim-0.9.0}/src/error/normalize.rs +0 -0
  118. {llmshim-0.8.0 → llmshim-0.9.0}/src/fallback.rs +0 -0
  119. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/auth.rs +0 -0
  120. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/distributed.rs +0 -0
  121. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/http.rs +0 -0
  122. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/idempotency.rs +0 -0
  123. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/metrics.rs +0 -0
  124. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/mod.rs +0 -0
  125. {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/quota.rs +0 -0
  126. {llmshim-0.8.0 → llmshim-0.9.0}/src/lib.rs +0 -0
  127. {llmshim-0.8.0 → llmshim-0.9.0}/src/main.rs +0 -0
  128. {llmshim-0.8.0 → llmshim-0.9.0}/src/models.rs +0 -0
  129. {llmshim-0.8.0 → llmshim-0.9.0}/src/provider.rs +0 -0
  130. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/anthropic.rs +0 -0
  131. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/anthropic_reasoning.rs +0 -0
  132. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/anthropic_signature.rs +0 -0
  133. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/chatgpt/auth.rs +0 -0
  134. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/chatgpt/mod.rs +0 -0
  135. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/chatgpt/streaming.rs +0 -0
  136. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/gemini.rs +0 -0
  137. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/mod.rs +0 -0
  138. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/openai.rs +0 -0
  139. {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/openai_compat.rs +0 -0
  140. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/error.rs +0 -0
  141. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/handlers.rs +0 -0
  142. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/health.rs +0 -0
  143. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/mod.rs +0 -0
  144. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/ratelimit.rs +0 -0
  145. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/wire/mod.rs +0 -0
  146. {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/wire/receipts.rs +0 -0
  147. {llmshim-0.8.0 → llmshim-0.9.0}/src/reasoning/normalize.rs +0 -0
  148. {llmshim-0.8.0 → llmshim-0.9.0}/src/reasoning.rs +0 -0
  149. {llmshim-0.8.0 → llmshim-0.9.0}/src/router.rs +0 -0
  150. {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/memo.rs +0 -0
  151. {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/mod.rs +0 -0
  152. {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/validate.rs +0 -0
  153. {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/walk.rs +0 -0
  154. {llmshim-0.8.0 → llmshim-0.9.0}/src/streaming.rs +0 -0
  155. {llmshim-0.8.0 → llmshim-0.9.0}/src/toolcall/streaming.rs +0 -0
  156. {llmshim-0.8.0 → llmshim-0.9.0}/src/toolcall.rs +0 -0
  157. {llmshim-0.8.0 → llmshim-0.9.0}/src/usage.rs +0 -0
  158. {llmshim-0.8.0 → llmshim-0.9.0}/src/vision.rs +0 -0
  159. {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/chatgpt-red.png +0 -0
  160. {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/sglang-models.toml +0 -0
  161. {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/sglang-responses-stream.sse +0 -0
  162. {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/sglang-responses-turn1.json +0 -0
  163. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration.rs +0 -0
  164. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_chatgpt.rs +0 -0
  165. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_chatgpt_proxy.rs +0 -0
  166. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_fallback.rs +0 -0
  167. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_gemini.rs +0 -0
  168. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_gemini_tools.rs +0 -0
  169. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_long_context.rs +0 -0
  170. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_multimodel.rs +0 -0
  171. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_proxy.rs +0 -0
  172. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_sglang.rs +0 -0
  173. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_thinking.rs +0 -0
  174. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_tool_roundtrip.rs +0 -0
  175. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_vision.rs +0 -0
  176. {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_xai.rs +0 -0
  177. {llmshim-0.8.0 → llmshim-0.9.0}/tests/support/completion_status.rs +0 -0
  178. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_anthropic.rs +0 -0
  179. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_cache.rs +0 -0
  180. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_chatgpt.rs +0 -0
  181. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_cli.rs +0 -0
  182. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_client_breaker.rs +0 -0
  183. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_client_retry_after.rs +0 -0
  184. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_fable.rs +0 -0
  185. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_fallback.rs +0 -0
  186. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_fast_mode.rs +0 -0
  187. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_gemini.rs +0 -0
  188. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_models.rs +0 -0
  189. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_multimodel.rs +0 -0
  190. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openai.rs +0 -0
  191. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openai_compat.rs +0 -0
  192. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openai_compat_responses.rs +0 -0
  193. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_provider_contracts.rs +0 -0
  194. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_reasoning.rs +0 -0
  195. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_reasoning_profile.rs +0 -0
  196. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_router.rs +0 -0
  197. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_schema.rs +0 -0
  198. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_shim.rs +0 -0
  199. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_signature.rs +0 -0
  200. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_sse.rs +0 -0
  201. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_toolcall.rs +0 -0
  202. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_tools.rs +0 -0
  203. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_usage.rs +0 -0
  204. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_vision.rs +0 -0
  205. {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_wire.rs +0 -0
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: Check formatting
19
19
  run: cargo fmt --all --check
20
20
  - name: Clippy
21
- run: cargo clippy --workspace --features proxy -- -D warnings
21
+ run: cargo clippy --workspace --all-targets --features proxy -- -D warnings
22
22
  - name: Unit tests
23
23
  env:
24
24
  LLMSHIM_CATALOG_OFFLINE: '1'
@@ -16,7 +16,7 @@ This is a public crate on crates.io. Do NOT make breaking changes to `pub` items
16
16
  - **ChatGPT subscription (OAuth):** only `chatgpt/gpt-6-astra`, `chatgpt/gpt-5.6-sol`, `chatgpt/gpt-5.6-terra`, and `chatgpt/gpt-5.6-luna`. `CHATGPT_MODELS` in `src/models.rs` is shared by discovery, CLI selection, and validation; older/unlisted models fail before authentication or network calls.
17
17
  - **Anthropic:** `claude-fable-5-1`, `claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5-20251001`
18
18
  - **Gemini:** `gemini-3.8-flash`, `gemini-3.5-flash-lite`
19
- - **xAI:** `grok-4.6`
19
+ - **xAI:** `grok-4.7`
20
20
  - **OpenRouter:** not enumerated (huge/dynamic catalog) — any `openrouter/<vendor>/<model>` slug routes through, e.g. `openrouter/anthropic/claude-sonnet-5`.
21
21
  - **vLLM / SGLang:** not enumerated (self-hosted) — any `vllm/<served-model>` or `sglang/<served-model>` routes through to the configured server, e.g. `sglang/Qwen/Qwen3.6-35B-A3B-FP8`.
22
22
 
@@ -73,6 +73,30 @@ for whichever stream chunk carries usage; `log.rs`, `proxy::types::Usage`, both
73
73
  native facades and the four bundled clients carry it through as a nullable
74
74
  field. `null` means unknown, not free.
75
75
 
76
+ **A provider-reported bill outranks the catalog.** Where a response carries
77
+ `usage.cost` — OpenRouter returns it on every call, with no parameter to set —
78
+ `stamp` uses that number and records `usage.cost_source: "provider"`;
79
+ otherwise it prices from the catalog and records `"catalog"`. Measured
80
+ 2026-09-22 on `deepseek/deepseek-v4.1-flash`: the catalog's rate for the
81
+ OpenRouter slug was half what OpenRouter billed, so the estimate this replaces
82
+ was under-reporting 2:1. A catalog cannot detect that from the inside. The
83
+ catalog product is an estimate of that bill and deliberately an upper bound, so
84
+ it must never overwrite one. `cost_source` rides beside `cost_usd` everywhere
85
+ it goes (`LogEntry`, `proxy::types::Usage`, OpenAPI, the typed clients).
86
+ `shim::add_usage` sums a reported `cost` across a repair's two attempts, because
87
+ two dispatches are two charges.
88
+
89
+ `ModelInfo.context_cost_tiers` adds context-dependent standard rates without
90
+ changing the public `Cost` struct. `cost_for_input_tokens` includes cached input
91
+ in tier selection; `src/cost.rs` applies the resulting rates to the whole
92
+ response. Local per-field prices outrank lower-source tier rates. Builtin
93
+ launch metadata lives in `crates/llmshim-catalog/data/verified.json`, separate
94
+ from the unmodified models.dev snapshot. Grok 4.7 live regression tests are in
95
+ `tests/integration_grok_4_7.rs`; they use an ephemeral proxy port, configured
96
+ keys, and billed API calls. `none` clamps to `low`; named tool choice must be
97
+ flat on the Responses wire. Preserve encrypted reasoning on both normal and
98
+ streaming tool round trips.
99
+
76
100
  `src/gateway/quota.rs` adds a per-identity dollar cap beside the RPM/TPM
77
101
  buckets: `budget_usd` + `budget_window_secs` on an `Identity`, checked before
78
102
  dispatch and charged after (cost is only knowable once a response exists, so
@@ -321,15 +345,15 @@ accounts exercise enforcement too. The provider handles incompatible
321
345
  thinking on a switch to older models; do not infer signatures from text.
322
346
 
323
347
  `tests/unit_fable.rs` pins these rules. Live checks for Fable 5, Fable 5.1,
324
- Opus 5, Gemini 3.8 Flash, and Grok 4.6:
348
+ Opus 5, Gemini 3.8 Flash, and Grok 4.7:
325
349
 
326
350
  ```bash
327
351
  cargo test --features proxy --test integration_current_models -- --ignored --nocapture
328
352
  ```
329
353
 
330
354
  The live tests consume API usage and are ignored during offline preflight.
331
- Gemini 3.8 Flash and Opus 5 already had catalog/adapter support; Grok 4.6 is
332
- the verified xAI model ID. Keep the ChatGPT four-model allowlist independent.
355
+ Gemini 3.8 Flash and Opus 5 already had catalog/adapter support; Grok 4.7 is
356
+ the current xAI model ID (verified live 2026-09-21); retain Grok 4.6 for explicit routing. Keep the ChatGPT four-model allowlist independent.
333
357
 
334
358
  Two knobs work across every provider: `reasoning_effort` (`none|low|medium|high|xhigh|max`) and `reasoning_mode` (`standard|pro`). A third, `reasoning_summary` (`auto|none`), controls reasoning-text visibility → Anthropic `thinking.display` (`auto`→`summarized`, the default when `reasoning_effort` is present so newer models like Sonnet 5 / Opus 4.7-4.8 return reasoning text instead of the API-default `omitted`; `none`→`omitted` for lower latency). Applies to both the adaptive and pre-4.6 enabled thinking builders; a caller-supplied `thinking` block bypasses it. Each provider transform maps them to its native dialect, **clamping to the nearest tier the target model accepts** (all boundaries verified live — e.g. `max` is native on OpenAI gpt-5.6 and GPT-6 Astra; Anthropic 4.6 rejects `xhigh` but has `max`; Gemini's enum tops out at `high`; xAI grok-4.20 models reject any reasoning param). `mode: "pro"` is native on OpenAI gpt-5.6/-pro models (`reasoning.mode`), emulated as a one-tier effort bump elsewhere; explicit `none` always wins. Native passthrough (`x-openai.reasoning`, `x-anthropic.thinking`, `x-gemini.thinkingConfig`) bypasses the mapping entirely and always takes precedence. **Full per-provider mapping tables: `docs/src/guides/reasoning.md`** — update it and the pinning tests in `tests/unit_*.rs` together whenever a mapping changes.
335
359
 
@@ -320,6 +320,17 @@ version = "0.2.1"
320
320
  source = "registry+https://github.com/rust-lang/crates.io-index"
321
321
  checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
322
322
 
323
+ [[package]]
324
+ name = "chacha20"
325
+ version = "0.10.2"
326
+ source = "registry+https://github.com/rust-lang/crates.io-index"
327
+ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
328
+ dependencies = [
329
+ "cfg-if",
330
+ "cpufeatures 0.3.1",
331
+ "rand_core 0.10.1",
332
+ ]
333
+
323
334
  [[package]]
324
335
  name = "chrono"
325
336
  version = "0.4.44"
@@ -377,15 +388,6 @@ version = "0.4.31"
377
388
  source = "registry+https://github.com/rust-lang/crates.io-index"
378
389
  checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
379
390
 
380
- [[package]]
381
- name = "concurrent-queue"
382
- version = "2.5.0"
383
- source = "registry+https://github.com/rust-lang/crates.io-index"
384
- checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
385
- dependencies = [
386
- "crossbeam-utils",
387
- ]
388
-
389
391
  [[package]]
390
392
  name = "core-foundation"
391
393
  version = "0.10.1"
@@ -411,6 +413,15 @@ dependencies = [
411
413
  "libc",
412
414
  ]
413
415
 
416
+ [[package]]
417
+ name = "cpufeatures"
418
+ version = "0.3.1"
419
+ source = "registry+https://github.com/rust-lang/crates.io-index"
420
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
421
+ dependencies = [
422
+ "libc",
423
+ ]
424
+
414
425
  [[package]]
415
426
  name = "crc32fast"
416
427
  version = "1.5.0"
@@ -420,12 +431,6 @@ dependencies = [
420
431
  "cfg-if",
421
432
  ]
422
433
 
423
- [[package]]
424
- name = "crossbeam-utils"
425
- version = "0.8.21"
426
- source = "registry+https://github.com/rust-lang/crates.io-index"
427
- checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
428
-
429
434
  [[package]]
430
435
  name = "crossterm"
431
436
  version = "0.28.1"
@@ -536,11 +541,10 @@ dependencies = [
536
541
 
537
542
  [[package]]
538
543
  name = "event-listener"
539
- version = "5.4.1"
544
+ version = "5.4.2"
540
545
  source = "registry+https://github.com/rust-lang/crates.io-index"
541
- checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
546
+ checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
542
547
  dependencies = [
543
- "concurrent-queue",
544
548
  "parking",
545
549
  "pin-project-lite",
546
550
  ]
@@ -783,15 +787,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
783
787
  checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
784
788
  dependencies = [
785
789
  "cfg-if",
790
+ "js-sys",
786
791
  "libc",
787
792
  "r-efi 6.0.0",
793
+ "rand_core 0.10.1",
794
+ "wasm-bindgen",
788
795
  ]
789
796
 
790
797
  [[package]]
791
798
  name = "h2"
792
- version = "0.4.13"
799
+ version = "0.4.19"
793
800
  source = "registry+https://github.com/rust-lang/crates.io-index"
794
- checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
801
+ checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
795
802
  dependencies = [
796
803
  "atomic-waker",
797
804
  "bytes",
@@ -1180,7 +1187,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
1180
1187
 
1181
1188
  [[package]]
1182
1189
  name = "llmshim"
1183
- version = "0.8.0"
1190
+ version = "0.9.0"
1184
1191
  dependencies = [
1185
1192
  "async-stream",
1186
1193
  "async-trait",
@@ -1215,7 +1222,7 @@ dependencies = [
1215
1222
 
1216
1223
  [[package]]
1217
1224
  name = "llmshim-catalog"
1218
- version = "0.1.0"
1225
+ version = "0.1.1"
1219
1226
  dependencies = [
1220
1227
  "chrono",
1221
1228
  "dirs",
@@ -1321,7 +1328,7 @@ dependencies = [
1321
1328
  "hyper-util",
1322
1329
  "log",
1323
1330
  "pin-project-lite",
1324
- "rand",
1331
+ "rand 0.9.5",
1325
1332
  "regex",
1326
1333
  "serde_json",
1327
1334
  "serde_urlencoded",
@@ -1543,14 +1550,15 @@ dependencies = [
1543
1550
 
1544
1551
  [[package]]
1545
1552
  name = "quinn-proto"
1546
- version = "0.11.13"
1553
+ version = "0.11.18"
1547
1554
  source = "registry+https://github.com/rust-lang/crates.io-index"
1548
- checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
1555
+ checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
1549
1556
  dependencies = [
1550
1557
  "bytes",
1551
- "getrandom 0.3.4",
1558
+ "getrandom 0.4.3",
1552
1559
  "lru-slab",
1553
- "rand",
1560
+ "rand 0.10.3",
1561
+ "rand_pcg",
1554
1562
  "ring",
1555
1563
  "rustc-hash",
1556
1564
  "rustls",
@@ -1599,12 +1607,23 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1599
1607
 
1600
1608
  [[package]]
1601
1609
  name = "rand"
1602
- version = "0.9.2"
1610
+ version = "0.9.5"
1603
1611
  source = "registry+https://github.com/rust-lang/crates.io-index"
1604
- checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
1612
+ checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
1605
1613
  dependencies = [
1606
1614
  "rand_chacha",
1607
- "rand_core",
1615
+ "rand_core 0.9.5",
1616
+ ]
1617
+
1618
+ [[package]]
1619
+ name = "rand"
1620
+ version = "0.10.3"
1621
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1622
+ checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
1623
+ dependencies = [
1624
+ "chacha20",
1625
+ "getrandom 0.4.3",
1626
+ "rand_core 0.10.1",
1608
1627
  ]
1609
1628
 
1610
1629
  [[package]]
@@ -1614,7 +1633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1614
1633
  checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1615
1634
  dependencies = [
1616
1635
  "ppv-lite86",
1617
- "rand_core",
1636
+ "rand_core 0.9.5",
1618
1637
  ]
1619
1638
 
1620
1639
  [[package]]
@@ -1626,6 +1645,21 @@ dependencies = [
1626
1645
  "getrandom 0.3.4",
1627
1646
  ]
1628
1647
 
1648
+ [[package]]
1649
+ name = "rand_core"
1650
+ version = "0.10.1"
1651
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1652
+ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
1653
+
1654
+ [[package]]
1655
+ name = "rand_pcg"
1656
+ version = "0.10.2"
1657
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1658
+ checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
1659
+ dependencies = [
1660
+ "rand_core 0.10.1",
1661
+ ]
1662
+
1629
1663
  [[package]]
1630
1664
  name = "redis"
1631
1665
  version = "1.2.4"
@@ -1832,9 +1866,9 @@ dependencies = [
1832
1866
 
1833
1867
  [[package]]
1834
1868
  name = "rustls"
1835
- version = "0.23.37"
1869
+ version = "0.23.45"
1836
1870
  source = "registry+https://github.com/rust-lang/crates.io-index"
1837
- checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
1871
+ checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
1838
1872
  dependencies = [
1839
1873
  "once_cell",
1840
1874
  "ring",
@@ -1868,9 +1902,9 @@ dependencies = [
1868
1902
 
1869
1903
  [[package]]
1870
1904
  name = "rustls-webpki"
1871
- version = "0.103.9"
1905
+ version = "0.103.15"
1872
1906
  source = "registry+https://github.com/rust-lang/crates.io-index"
1873
- checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
1907
+ checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
1874
1908
  dependencies = [
1875
1909
  "ring",
1876
1910
  "rustls-pki-types",
@@ -2015,7 +2049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2015
2049
  checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
2016
2050
  dependencies = [
2017
2051
  "cfg-if",
2018
- "cpufeatures",
2052
+ "cpufeatures 0.2.17",
2019
2053
  "digest",
2020
2054
  ]
2021
2055
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "llmshim"
3
- version = "0.8.0"
3
+ version = "0.9.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"
@@ -24,7 +24,7 @@ gateway-redis = ["gateway", "redis-coordination"]
24
24
  redis-coordination = ["proxy", "dep:redis"]
25
25
 
26
26
  [dependencies]
27
- llmshim-catalog = { version = "0.1.0", path = "crates/llmshim-catalog" }
27
+ llmshim-catalog = { version = "0.1.1", path = "crates/llmshim-catalog" }
28
28
  tokio = { version = "1", features = ["full"] }
29
29
  reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "http2", "gzip", "brotli", "zstd", "deflate"] }
30
30
  serde = { version = "1", features = ["derive"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llmshim
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -88,6 +88,20 @@ OpenRouter is OpenAI Chat Completions-compatible, so tools, vision, streaming,
88
88
  and `reasoning_effort` all pass through; OpenRouter-only controls (provider
89
89
  routing, model fallbacks, transforms) go under an `x-openrouter` key.
90
90
 
91
+ OpenRouter also reports **what it actually charged**, on every call and with no
92
+ parameter to set. llmshim keeps it: the response carries `usage.cost_usd` with
93
+ `usage.cost_source: "provider"` — the bill, not a catalog estimate — alongside
94
+ `usage.cost_details` and `usage.is_byok`. On a stream it rides on the terminal
95
+ chunk. Because OpenRouter is an aggregator, the response's top-level
96
+ **`provider`** names the upstream that actually served the call (`"Together"`,
97
+ `"Fireworks"`, …) and `id` is the OpenRouter generation id; both survive the
98
+ streaming and buffered paths.
99
+
100
+ This matters more than a rounding difference. Measured on
101
+ `deepseek/deepseek-v4.1-flash`, the catalog's own rate for the OpenRouter slug
102
+ was **half** what OpenRouter actually billed, so the estimate it replaces was
103
+ under-reporting 2:1.
104
+
91
105
  Point at a **self-hosted vLLM or SGLang** server (local or remote) by setting its
92
106
  base URL — no key needed unless the server was launched with one:
93
107
 
@@ -473,7 +487,7 @@ Standard library only. Full docs: [`clients/ruby/README.md`](clients/ruby/README
473
487
  | **OpenAI** | `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | Yes (summaries) |
474
488
  | **Anthropic** | `claude-fable-5-1`, `claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5-20251001` | Yes (thinking summaries) |
475
489
  | **Google Gemini** | `gemini-3.8-flash`, `gemini-3.5-flash-lite` | Yes (thought summaries) |
476
- | **xAI** | `grok-4.6` | No (hidden) |
490
+ | **xAI** | `grok-4.7` | No (hidden) |
477
491
 
478
492
  The CLI and server advertise these current tiers. ChatGPT subscription access
479
493
  uses the same four OpenAI models under `chatgpt/`. OpenRouter and self-hosted
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "llmshim-catalog"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  description = "Offline-first model metadata with verified defaults, models.dev, and local overrides"
6
6
  license = "MIT OR Apache-2.0"
@@ -18,9 +18,31 @@ input/output modalities, knowledge/release dates, and field-level sources.
18
18
  Missing data remains unknown. A date given only as a month is not turned into
19
19
  an invented day. Unknown family keys remain absent and must not authorize replay.
20
20
 
21
+ `ModelInfo::cost_for_input_tokens(total)` selects context-dependent rates from
22
+ `context_cost_tiers`; `cost` remains the base rates. The total includes cached
23
+ input. A tier applies to the entire request strictly above its threshold, with
24
+ missing rates inherited from the preceding tier. Models.dev `cost.tiers` entries
25
+ whose type is `context` are imported. Local policy can set
26
+ `context_cost_tiers = []` to disable inherited tiers, or use:
27
+
28
+ ```toml
29
+ [[models."xai/grok-4.7".context_cost_tiers]]
30
+ above_input_tokens = 200000
31
+ [models."xai/grok-4.7".context_cost_tiers.cost]
32
+ input = 4.0
33
+ cache_read = 1.0
34
+ output = 12.0
35
+ ```
36
+
37
+ The strongest source replaces the tier list as a whole. Higher-priority base
38
+ price overrides still win per field over lower-priority tier rates. Provider
39
+ discovery cannot supply either base prices or tiers.
40
+
21
41
  The compiled `builtin` module retains the small verified discovery table,
22
42
  historical metadata, and the separate ChatGPT subscription allowlist. Catalog
23
43
  coverage does not authorize or configure an API route or a subscription model.
44
+ `data/verified.json` supplies sourced launch metadata independently of the
45
+ models.dev snapshot. It does not add models to the curated discovery list.
24
46
 
25
47
  ## Layers and policy
26
48
 
@@ -0,0 +1,26 @@
1
+ `models.dev.json` is the unmodified response downloaded from
2
+ https://models.dev/api.json on 2026-09-16. It is third-party data under the MIT
3
+ license in `LICENSE.models.dev`. No credentials, local overrides, provider
4
+ account discovery, or private fixtures belong in this directory.
5
+
6
+ The refresh workflow replaces only this public snapshot. Tests validate its
7
+ structure and minimum coverage before proposing an update. The builtin table
8
+ continues to win conflicts for the fields it asserts.
9
+
10
+ `verified.json` contains separate verified builtin assertions, checked on
11
+ 2026-09-21. It is maintained here, not copied into the models.dev artifact:
12
+
13
+ - Native Grok 4.7 capabilities, reasoning efforts, dates, and standard token
14
+ prices: https://docs.x.ai/developers/grok-4-7 and
15
+ https://docs.x.ai/developers/release-notes (including Grok 4.6 context pricing).
16
+ - Grok 4.7 streaming, image input, structured output, named tool selection,
17
+ and encrypted-reasoning round trips: `tests/integration_grok_4_7.rs` in the
18
+ parent repository, verified against the live xAI Responses API.
19
+ - OpenRouter's Grok 4.7 identity, capabilities, limits and standard endpoint
20
+ prices: https://openrouter.ai/api/v1/models/x-ai/grok-4.7/endpoints . Priority
21
+ endpoint rates are different; the catalog entries describe standard rates.
22
+ The `:nitro` entry supplies replay metadata without a fixed price because
23
+ standard and priority endpoints can compete for that route.
24
+
25
+ No public `grok-4.7-fast` API route is asserted. Its launch availability is
26
+ limited to Cursor and Grok Build.
@@ -0,0 +1,43 @@
1
+ {
2
+ "xai/grok-4.7": {
3
+ "name": "Grok 4.7",
4
+ "family": "grok",
5
+ "limit": {"context": 500000},
6
+ "release_date": "2026-09-21",
7
+ "modalities": {"input": ["text", "image"], "output": ["text"]},
8
+ "reasoning_options": [{"type": "effort", "values": ["low", "medium", "high", "xhigh"]}],
9
+ "streaming": true,
10
+ "forced_tool_choice": true,
11
+ "cost": {"input": 2, "cache_read": 0.5, "output": 6},
12
+ "context_cost_tiers": [{"above_input_tokens": 200000, "cost": {"input": 4, "cache_read": 1, "output": 12}}]
13
+ },
14
+ "xai/grok-4.6": {
15
+ "cost": {"input": 2, "cache_read": 0.5, "output": 6},
16
+ "context_cost_tiers": [{"above_input_tokens": 200000, "cost": {"input": 4, "cache_read": 1, "output": 12}}]
17
+ },
18
+ "openrouter/x-ai/grok-4.7:nitro": {
19
+ "name": "Grok 4.7 (Nitro)",
20
+ "family": "grok",
21
+ "limit": {"context": 500000, "output": 450000},
22
+ "modalities": {"input": ["text", "image", "file"], "output": ["text"]},
23
+ "reasoning_options": [{"type": "effort", "values": ["low", "medium", "high", "xhigh"]}],
24
+ "reasoning": true,
25
+ "tool_call": true,
26
+ "structured_output": true,
27
+ "forced_tool_choice": true
28
+ },
29
+ "openrouter/x-ai/grok-4.7": {
30
+ "name": "Grok 4.7",
31
+ "family": "grok",
32
+ "limit": {"context": 500000, "output": 450000},
33
+ "release_date": "2026-09-21",
34
+ "modalities": {"input": ["text", "image", "file"], "output": ["text"]},
35
+ "reasoning_options": [{"type": "effort", "values": ["low", "medium", "high", "xhigh"]}],
36
+ "reasoning": true,
37
+ "tool_call": true,
38
+ "structured_output": true,
39
+ "forced_tool_choice": true,
40
+ "cost": {"input": 1.6, "cache_read": 0.4, "output": 4.8},
41
+ "context_cost_tiers": [{"above_input_tokens": 200000, "cost": {"input": 3.2, "cache_read": 0.8, "output": 9.6}}]
42
+ }
43
+ }
@@ -261,11 +261,11 @@ pub const MODELS: &[BuiltinModelInfo] = &[
261
261
  capabilities: CAPS_STD,
262
262
  },
263
263
  BuiltinModelInfo {
264
- id: "xai/grok-4.6",
264
+ id: "xai/grok-4.7",
265
265
  provider: "xai",
266
266
  family: Some(ModelFamily::Grok),
267
- name: "grok-4.6",
268
- label: "Grok 4.6",
267
+ name: "grok-4.7",
268
+ label: "Grok 4.7",
269
269
  context_window_tokens: Some(500_000),
270
270
  max_output_tokens: None,
271
271
  capabilities: CAPS_XAI,
@@ -279,6 +279,16 @@ pub const MODELS: &[BuiltinModelInfo] = &[
279
279
  /// Historical metadata remains available to explicit `spec()` lookups without
280
280
  /// appearing in discovery or model pickers. Routing is provider-owned.
281
281
  const LEGACY_MODELS: &[BuiltinModelInfo] = &[
282
+ BuiltinModelInfo {
283
+ id: "xai/grok-4.6",
284
+ provider: "xai",
285
+ family: Some(ModelFamily::Grok),
286
+ name: "grok-4.6",
287
+ label: "Grok 4.6",
288
+ context_window_tokens: Some(500_000),
289
+ max_output_tokens: None,
290
+ capabilities: CAPS_XAI,
291
+ },
282
292
  BuiltinModelInfo {
283
293
  id: "openai/gpt-5.5",
284
294
  provider: "openai",
@@ -165,6 +165,20 @@ impl Catalog {
165
165
  model.source = CatalogSource::Builtin;
166
166
  self.merge_model(model);
167
167
  }
168
+ // Verified launch-day metadata is separate from the unmodified
169
+ // models.dev snapshot and does not expand curated discovery.
170
+ let entries: Value = serde_json::from_str(include_str!("../data/verified.json"))
171
+ .expect("validated builtin metadata");
172
+ for (id, value) in entries.as_object().expect("builtin metadata object") {
173
+ let (provider, name) = id.split_once('/').expect("qualified builtin id");
174
+ self.merge_model(parse::model_from_value(
175
+ provider,
176
+ name,
177
+ value,
178
+ CatalogSource::Builtin,
179
+ None,
180
+ ));
181
+ }
168
182
  }
169
183
 
170
184
  pub fn merge_models_dev(
@@ -235,6 +249,9 @@ impl Catalog {
235
249
  ))?;
236
250
  let mut model =
237
251
  parse::model_from_value(provider, name, v, CatalogSource::Local, None);
252
+ if v.get("context_cost_tiers").is_some() && model.context_cost_tiers.is_none() {
253
+ return Err(CatalogError::Invalid("invalid context_cost_tiers"));
254
+ }
238
255
  if let Some(label) = v["label"].as_str() {
239
256
  model.label = label.into();
240
257
  }
@@ -1,6 +1,6 @@
1
1
  use crate::{CatalogSource, Cost, ModelInfo, Support};
2
2
 
3
- fn rank(source: CatalogSource) -> u8 {
3
+ pub(crate) fn rank(source: CatalogSource) -> u8 {
4
4
  match source {
5
5
  CatalogSource::ModelsDev => 1,
6
6
  CatalogSource::Builtin => 2,
@@ -76,6 +76,7 @@ pub(crate) fn merge(target: &mut ModelInfo, incoming: &ModelInfo) {
76
76
  }
77
77
  }
78
78
  if source != CatalogSource::ProviderApi {
79
+ field!(context_cost_tiers, incoming.context_cost_tiers.is_some());
79
80
  if let Some(cost) = incoming.cost {
80
81
  let mut merged = target.cost.unwrap_or_default();
81
82
  for (key, value, slot) in [
@@ -1,5 +1,6 @@
1
1
  use crate::{
2
- CatalogError, CatalogSource, Cost, ModelCapabilities, ModelFamily, ModelInfo, Support,
2
+ CatalogError, CatalogSource, ContextCostTier, Cost, ModelCapabilities, ModelFamily, ModelInfo,
3
+ Support,
3
4
  };
4
5
  use chrono::{DateTime, NaiveDate, Utc};
5
6
  use serde_json::Value;
@@ -77,6 +78,24 @@ pub(crate) fn model_from_value(
77
78
  };
78
79
  }
79
80
  m.cost = cost(&v["cost"]);
81
+ m.context_cost_tiers = if let Some(tiers) = v.get("context_cost_tiers") {
82
+ serde_json::from_value(tiers.clone()).ok()
83
+ } else {
84
+ v["cost"]["tiers"].as_array().map(|tiers| {
85
+ tiers
86
+ .iter()
87
+ .filter_map(|tier| {
88
+ if tier["tier"]["type"] != "context" {
89
+ return None;
90
+ }
91
+ Some(ContextCostTier {
92
+ above_input_tokens: tier["tier"]["size"].as_u64()?,
93
+ cost: cost(tier)?,
94
+ })
95
+ })
96
+ .collect()
97
+ })
98
+ };
80
99
  m.reasoning_options = v["reasoning_options"]
81
100
  .as_array()
82
101
  .into_iter()
@@ -81,6 +81,14 @@ pub struct Cost {
81
81
  pub cache_write: Option<f64>,
82
82
  }
83
83
 
84
+ /// Rates for the entire request when total input (including cached input)
85
+ /// exceeds `above_input_tokens`. Missing rates inherit the previous tier.
86
+ #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
87
+ pub struct ContextCostTier {
88
+ pub above_input_tokens: u64,
89
+ pub cost: Cost,
90
+ }
91
+
84
92
  /// Media types are strings so new catalog modalities survive without a release.
85
93
  #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
86
94
  #[serde(default)]
@@ -123,6 +131,9 @@ pub struct ModelInfo {
123
131
  pub capabilities: crate::ModelCapabilities,
124
132
  pub family: Option<ModelFamily>,
125
133
  pub cost: Option<Cost>,
134
+ /// None means unspecified; an explicit empty list disables inherited tiers.
135
+ #[serde(default)]
136
+ pub context_cost_tiers: Option<Vec<ContextCostTier>>,
126
137
  pub reasoning_options: Vec<ReasoningOption>,
127
138
  pub modalities: Modalities,
128
139
  pub knowledge_cutoff: Option<NaiveDate>,
@@ -148,6 +159,7 @@ impl ModelInfo {
148
159
  capabilities: crate::ModelCapabilities::unknown(),
149
160
  family: None,
150
161
  cost: None,
162
+ context_cost_tiers: None,
151
163
  reasoning_options: Vec::new(),
152
164
  modalities: Modalities::default(),
153
165
  knowledge_cutoff: None,
@@ -158,4 +170,48 @@ impl ModelInfo {
158
170
  field_sources: BTreeMap::new(),
159
171
  }
160
172
  }
173
+
174
+ /// Resolve context-dependent rates without changing the base `Cost` API.
175
+ /// Higher-priority per-field base overrides also override lower-priority
176
+ /// tier rates. Tier lists are replaced atomically by a stronger source.
177
+ pub fn cost_for_input_tokens(&self, input_tokens: u64) -> Option<Cost> {
178
+ let mut cost = self.cost?;
179
+ let mut tiers: Vec<_> = self
180
+ .context_cost_tiers
181
+ .as_deref()
182
+ .unwrap_or_default()
183
+ .iter()
184
+ .filter(|t| input_tokens > t.above_input_tokens)
185
+ .collect();
186
+ tiers.sort_by_key(|t| t.above_input_tokens);
187
+ let tier_source = self
188
+ .field_sources
189
+ .get("context_cost_tiers")
190
+ .copied()
191
+ .unwrap_or(self.source);
192
+ for tier in tiers {
193
+ for (key, rate, slot) in [
194
+ ("cost.input", tier.cost.input, &mut cost.input),
195
+ ("cost.output", tier.cost.output, &mut cost.output),
196
+ (
197
+ "cost.cache_read",
198
+ tier.cost.cache_read,
199
+ &mut cost.cache_read,
200
+ ),
201
+ (
202
+ "cost.cache_write",
203
+ tier.cost.cache_write,
204
+ &mut cost.cache_write,
205
+ ),
206
+ ] {
207
+ let base_source = self.field_sources.get(key).copied().unwrap_or(self.source);
208
+ if rate.is_some_and(|n| n.is_finite() && n >= 0.0)
209
+ && crate::merge::rank(tier_source) >= crate::merge::rank(base_source)
210
+ {
211
+ *slot = rate;
212
+ }
213
+ }
214
+ }
215
+ Some(cost)
216
+ }
161
217
  }