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.
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/workflows/release.yml +1 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/CLAUDE.md +28 -4
- {llmshim-0.8.0 → llmshim-0.9.0}/Cargo.lock +70 -36
- {llmshim-0.8.0 → llmshim-0.9.0}/Cargo.toml +2 -2
- {llmshim-0.8.0 → llmshim-0.9.0}/PKG-INFO +1 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/README.md +15 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/Cargo.toml +1 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/README.md +22 -0
- llmshim-0.9.0/crates/llmshim-catalog/data/README.md +26 -0
- llmshim-0.9.0/crates/llmshim-catalog/data/verified.json +43 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/builtin.rs +13 -3
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/lib.rs +17 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/merge.rs +2 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/parse.rs +20 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/types.rs +56 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/tests/catalog.rs +101 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/caching.md +22 -6
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/reasoning.md +14 -3
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/models.md +25 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/types.py +13 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/client.rs +1 -1
- {llmshim-0.8.0 → llmshim-0.9.0}/src/config.rs +31 -5
- {llmshim-0.8.0 → llmshim-0.9.0}/src/cost.rs +96 -5
- llmshim-0.9.0/src/error.rs +72 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/log.rs +25 -9
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/openrouter.rs +10 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/xai.rs +10 -6
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/convert.rs +1 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/types.rs +8 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/shim.rs +63 -1
- llmshim-0.9.0/tests/fixtures/openrouter-usage-include-stream.sse +92 -0
- llmshim-0.9.0/tests/fixtures/openrouter-usage-include.json +1 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_current_models.rs +2 -2
- llmshim-0.9.0/tests/integration_grok_4_7.rs +298 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_openrouter.rs +106 -0
- llmshim-0.9.0/tests/security_gemini_error_redaction.rs +136 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_advertised_models.rs +1 -1
- llmshim-0.9.0/tests/unit_config.rs +83 -0
- llmshim-0.9.0/tests/unit_grok_4_7.rs +78 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_log.rs +39 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openrouter.rs +231 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_proxy.rs +3 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_proxy_convert.rs +3 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_xai.rs +3 -3
- llmshim-0.8.0/crates/llmshim-catalog/data/README.md +0 -8
- llmshim-0.8.0/src/error.rs +0 -40
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/scripts/npm-verify-served.sh +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/workflows/catalog-refresh.yml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/.github/workflows/pages.yml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/.gitignore +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/CODE_OF_CONDUCT.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/CONTRIBUTING.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/LICENSE-APACHE +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/LICENSE-MIT +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/NOTICE +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/SECURITY.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/bench.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/bench_python.py +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/gateway_loadtest.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/benchmarks/loadtest.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/LICENSE-APACHE +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/LICENSE-MIT +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/data/LICENSE.models.dev +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/data/models.dev.json +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/aliases.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/capabilities.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/src/refresh.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/crates/llmshim-catalog/tests/refresh.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/.gitignore +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/book.toml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/mermaid-init.js +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/mermaid.min.js +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/SUMMARY.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/contracts.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/conversations.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/portability.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/routing.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/concepts/translation-flow.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/capabilities.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/fallbacks.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/images.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/native-controls.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/schemas.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/streaming.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/guides/tools.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/introduction.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/deployment.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/http-api.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/native-apis.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/proxy/scaling.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/api.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/cli.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/configuration.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/errors.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/providers.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/request-fields.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/reference/surfaces.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/choose.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/cli.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/clients.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/configure.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/proxy.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/docs/src/start/rust.md +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/examples/chat.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/examples/stream.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/__init__.py +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/_client.py +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/llmshim/_server.py +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/pyproject.toml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/breaker.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/cache.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/cli.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/env.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/error/normalize.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/fallback.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/auth.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/distributed.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/http.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/idempotency.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/metrics.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/mod.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/gateway/quota.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/lib.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/main.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/models.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/provider.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/anthropic.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/anthropic_reasoning.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/anthropic_signature.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/chatgpt/auth.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/chatgpt/mod.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/chatgpt/streaming.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/gemini.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/mod.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/openai.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/providers/openai_compat.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/error.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/handlers.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/health.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/mod.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/ratelimit.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/wire/mod.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/proxy/wire/receipts.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/reasoning/normalize.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/reasoning.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/router.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/memo.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/mod.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/validate.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/schema/walk.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/streaming.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/toolcall/streaming.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/toolcall.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/usage.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/src/vision.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/chatgpt-red.png +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/sglang-models.toml +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/sglang-responses-stream.sse +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/fixtures/sglang-responses-turn1.json +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_chatgpt.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_chatgpt_proxy.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_fallback.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_gemini.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_gemini_tools.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_long_context.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_multimodel.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_proxy.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_sglang.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_thinking.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_tool_roundtrip.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_vision.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/integration_xai.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/support/completion_status.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_anthropic.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_cache.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_chatgpt.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_cli.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_client_breaker.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_client_retry_after.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_fable.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_fallback.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_fast_mode.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_gemini.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_models.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_multimodel.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openai.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openai_compat.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_openai_compat_responses.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_provider_contracts.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_reasoning.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_reasoning_profile.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_router.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_schema.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_shim.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_signature.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_sse.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_toolcall.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_tools.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_usage.rs +0 -0
- {llmshim-0.8.0 → llmshim-0.9.0}/tests/unit_vision.rs +0 -0
- {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.
|
|
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.
|
|
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.
|
|
332
|
-
the
|
|
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.
|
|
544
|
+
version = "5.4.2"
|
|
540
545
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
541
|
-
checksum = "
|
|
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.
|
|
799
|
+
version = "0.4.19"
|
|
793
800
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
794
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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.
|
|
1553
|
+
version = "0.11.18"
|
|
1547
1554
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1548
|
-
checksum = "
|
|
1555
|
+
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
|
|
1549
1556
|
dependencies = [
|
|
1550
1557
|
"bytes",
|
|
1551
|
-
"getrandom 0.3
|
|
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.
|
|
1610
|
+
version = "0.9.5"
|
|
1603
1611
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1604
|
-
checksum = "
|
|
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.
|
|
1869
|
+
version = "0.23.45"
|
|
1836
1870
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1837
|
-
checksum = "
|
|
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.
|
|
1905
|
+
version = "0.103.15"
|
|
1872
1906
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1873
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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"] }
|
|
@@ -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.
|
|
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
|
|
@@ -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.
|
|
264
|
+
id: "xai/grok-4.7",
|
|
265
265
|
provider: "xai",
|
|
266
266
|
family: Some(ModelFamily::Grok),
|
|
267
|
-
name: "grok-4.
|
|
268
|
-
label: "Grok 4.
|
|
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,
|
|
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
|
}
|