From 3ee6a70d04a789905ce07f67466719939e569e36 Mon Sep 17 00:00:00 2001 From: Raullen Chai Date: Fri, 2 Oct 2026 17:56:43 -0700 Subject: [PATCH 1/2] feat: add QuickSilver Pro as a JSON-configured OpenAI-compatible provider Adds QuickSilver Pro (MachineFi Inc., https://quicksilverpro.io) as a JSON-configured OpenAI-compatible provider, following the SCX.ai provider pattern (PR #34752). - litellm/llms/openai_like/providers.json: `quicksilverpro` entry with base_url https://api.quicksilverpro.io/v1, env vars QUICKSILVERPRO_API_KEY / QUICKSILVERPRO_API_BASE, param_mappings {max_completion_tokens -> max_tokens}, and supported endpoints /v1/chat/completions, /v1/responses, /v1/messages (Claude models). - constants.py: appended to openai_compatible_endpoints and openai_compatible_providers. - types/utils.py: LlmProviders.QUICKSILVERPRO. - model_prices_and_context_window.json (+ backup, kept in sync): 8 models with prices from https://quicksilverpro.io/pricing.json (catalog_version 40, updated 2026-09-30), USD per 1M tokens converted to USD per token: claude-opus-5-5 (1.60/8.00), claude-sonnet-5-5 (1.00/5.00), gpt-6.1-sol (2.00/10.00), deepseek-v4.1-flash (0.125/0.55), glm-5.3-flash (0.06/0.20), mimo-v2.6-pro (0.348/0.696), gemini-3.8-flash (0.6375/3.1875), qwen3.8-27b (0.34/2.04), with cache-read/cache-write prices and context windows from the same catalog. Where pricing.json lists no output cap (deepseek-v4.1-flash, mimo-v2.6-pro), the legacy max_tokens falls back to the context window. - provider_endpoints_support.json (+ backup): chat_completions, messages and responses enabled; embeddings, image_generations, audio_transcriptions, audio_speech, moderations, batches, rerank and a2a disabled (no JSON-provider mechanism exists for images). - proxy/public_endpoints/provider_create_fields.json: QUICKSILVERPRO form entry (required api_key, optional api_base with default placeholder), default model quicksilverpro/claude-sonnet-5-5. - UI: provider enum + slug + logo + placeholder maps (provider_info_helpers.tsx), plate logo treatment (logoTreatments.ts), quicksilverpro.svg in public assets and the built proxy assets. - tests/unit/llms/openai_like/test_quicksilverpro_provider.py (25 tests): registry/config, provider resolution + api_base override, param mapping, router config, dashboard registration, and per-model behaviour checks that charge/read each entry's own prices via cost_per_token + get_model_info instead of pinning upstream numbers. respx-mocked (no network) request tests cover all three declared endpoints: chat/completions (with max_completion_tokens -> max_tokens mapping asserted on the wire), /v1/responses, and /v1/messages. Local validation: 290 passed (tests/unit/llms/openai_like/ + tests/unit/test_model_prices_schema.py), 90 UI tests passed (provider_info_helpers.test.tsx, logoTreatments.test.ts) with no type errors. --- litellm/constants.py | 2 + litellm/llms/openai_like/providers.json | 9 + ...odel_prices_and_context_window_backup.json | 138 +++++++ .../provider_endpoints_support_backup.json | 17 + .../out/assets/logos/quicksilverpro.svg | 14 + .../provider_create_fields.json | 28 ++ litellm/types/utils.py | 1 + model_prices_and_context_window.json | 138 +++++++ provider_endpoints_support.json | 17 + .../test_quicksilverpro_provider.py | 379 ++++++++++++++++++ .../public/assets/logos/quicksilverpro.svg | 14 + .../components/provider_info_helpers.test.tsx | 15 + .../src/components/provider_info_helpers.tsx | 5 + .../src/lib/logoTreatments.ts | 1 + 14 files changed, 778 insertions(+) create mode 100644 litellm/proxy/_experimental/out/assets/logos/quicksilverpro.svg create mode 100644 tests/unit/llms/openai_like/test_quicksilverpro_provider.py create mode 100644 ui/litellm-dashboard/public/assets/logos/quicksilverpro.svg diff --git a/litellm/constants.py b/litellm/constants.py index 18fc6aa7e74..b00b220a252 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -965,6 +965,7 @@ openai_compatible_endpoints: Final[list] = [ "https://api.scx.ai/v1", "https://api.prisminference.com/v1", "https://gigachat.devices.sberbank.ru/api/v1", + "https://api.quicksilverpro.io/v1", ] @@ -1040,6 +1041,7 @@ openai_compatible_providers: Final[list] = [ "scx-ai", "prism", "sail", + "quicksilverpro", ] OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS: Final = frozenset({"openai"} | frozenset(openai_compatible_providers)) diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 61ff4be3a46..da122375b6b 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -218,5 +218,14 @@ "api_key_env": "SAIL_API_KEY", "api_base_env": "SAIL_API_BASE", "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"] + }, + "quicksilverpro": { + "base_url": "https://api.quicksilverpro.io/v1", + "api_key_env": "QUICKSILVERPRO_API_KEY", + "api_base_env": "QUICKSILVERPRO_API_BASE", + "param_mappings": { + "max_completion_tokens": "max_tokens" + }, + "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"] } } diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 12e4760ed3a..ad745aeac10 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -17209,6 +17209,144 @@ "/v1/images/generations" ] }, + "quicksilverpro/claude-opus-5-5": { + "cache_creation_input_token_cost": 2e-06, + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 1.6e-06, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/claude-sonnet-5-5": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/deepseek-v4.1-flash": { + "cache_read_input_token_cost": 1.25e-08, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 5.5e-07, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "quicksilverpro/gemini-3.8-flash": { + "cache_read_input_token_cost": 6.375e-08, + "input_cost_per_token": 6.375e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 3.1875e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/glm-5.3-flash": { + "cache_read_input_token_cost": 1.2e-08, + "input_cost_per_token": 6e-08, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": false + }, + "quicksilverpro/gpt-6.1-sol": { + "cache_creation_input_token_cost": 2.5e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/mimo-v2.6-pro": { + "cache_read_input_token_cost": 2.88e-09, + "input_cost_per_token": 3.48e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 6.96e-07, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/qwen3.8-27b": { + "cache_creation_input_token_cost": 4.25e-07, + "cache_read_input_token_cost": 6.8e-08, + "input_cost_per_token": 3.4e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1000000, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.04e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": false + }, "qwencloud/deepseek-v4-flash": { "cache_read_input_token_cost": 4e-08, "input_cost_per_token": 2e-07, diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index c9635587eeb..af330a7b3a0 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -1926,6 +1926,23 @@ "interactions": true } }, + "quicksilverpro": { + "display_name": "QuickSilver Pro (`quicksilverpro`)", + "url": "https://quicksilverpro.io/docs", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "darkbloom": { "display_name": "Darkbloom (`darkbloom`)", "url": "https://docs.litellm.ai/docs/providers/darkbloom", diff --git a/litellm/proxy/_experimental/out/assets/logos/quicksilverpro.svg b/litellm/proxy/_experimental/out/assets/logos/quicksilverpro.svg new file mode 100644 index 00000000000..ce98c22bcd3 --- /dev/null +++ b/litellm/proxy/_experimental/out/assets/logos/quicksilverpro.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 6e96d6ad0ec..e580598bf65 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -2883,6 +2883,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "QUICKSILVERPRO", + "provider_display_name": "QuickSilver Pro", + "litellm_provider": "quicksilverpro", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.quicksilverpro.io/v1", + "tooltip": null, + "required": false, + "field_type": "text", + "options": null, + "default_value": null + }, + { + "key": "api_key", + "label": "API Key", + "placeholder": null, + "tooltip": null, + "required": true, + "field_type": "password", + "options": null, + "default_value": null + } + ], + "default_model_placeholder": "quicksilverpro/claude-sonnet-5-5" + }, { "provider": "PRISM", "provider_display_name": "Prism", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 8c10b9e3497..28e5b6afa2a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4158,6 +4158,7 @@ class LlmProviders(str, Enum): DARKBLOOM = "darkbloom" META = "meta" SAIL = "sail" + QUICKSILVERPRO = "quicksilverpro" LITELLM_AGENT = "litellm_agent" CURSOR = "cursor" BEDROCK_MANTLE = "bedrock_mantle" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 12e4760ed3a..ad745aeac10 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -17209,6 +17209,144 @@ "/v1/images/generations" ] }, + "quicksilverpro/claude-opus-5-5": { + "cache_creation_input_token_cost": 2e-06, + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 1.6e-06, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/claude-sonnet-5-5": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/deepseek-v4.1-flash": { + "cache_read_input_token_cost": 1.25e-08, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 5.5e-07, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "quicksilverpro/gemini-3.8-flash": { + "cache_read_input_token_cost": 6.375e-08, + "input_cost_per_token": 6.375e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 3.1875e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/glm-5.3-flash": { + "cache_read_input_token_cost": 1.2e-08, + "input_cost_per_token": 6e-08, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": false + }, + "quicksilverpro/gpt-6.1-sol": { + "cache_creation_input_token_cost": 2.5e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/mimo-v2.6-pro": { + "cache_read_input_token_cost": 2.88e-09, + "input_cost_per_token": 3.48e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 6.96e-07, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "quicksilverpro/qwen3.8-27b": { + "cache_creation_input_token_cost": 4.25e-07, + "cache_read_input_token_cost": 6.8e-08, + "input_cost_per_token": 3.4e-07, + "litellm_provider": "quicksilverpro", + "max_input_tokens": 1000000, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.04e-06, + "source": "https://quicksilverpro.io/pricing.json", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": false + }, "qwencloud/deepseek-v4-flash": { "cache_read_input_token_cost": 4e-08, "input_cost_per_token": 2e-07, diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index d18f8d2e6d1..0ea1a9124d4 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2186,6 +2186,23 @@ "interactions": true } }, + "quicksilverpro": { + "display_name": "QuickSilver Pro (`quicksilverpro`)", + "url": "https://quicksilverpro.io/docs", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "darkbloom": { "display_name": "Darkbloom (`darkbloom`)", "url": "https://docs.litellm.ai/docs/providers/darkbloom", diff --git a/tests/unit/llms/openai_like/test_quicksilverpro_provider.py b/tests/unit/llms/openai_like/test_quicksilverpro_provider.py new file mode 100644 index 00000000000..c0387fcd6ae --- /dev/null +++ b/tests/unit/llms/openai_like/test_quicksilverpro_provider.py @@ -0,0 +1,379 @@ +""" +Tests for the QuickSilver Pro provider configuration and integration. + +QuickSilver Pro (`quicksilverpro`) is a JSON-configured OpenAI-compatible +provider (https://quicksilverpro.io, operated by MachineFi Inc.). Its price / +context catalog is committed to the model cost map; these tests check +LiteLLM's own behaviour against the committed entries rather than pinning +upstream numbers. +""" + +import json +from pathlib import Path +from typing import Final + +import pytest +import respx + +import litellm +from litellm.caching.llm_caching_handler import LLMClientCache + +QSP_MODELS: Final = tuple( + sorted(name for name in litellm.model_cost if name.startswith("quicksilverpro/")) +) + + +class TestQuickSilverProProviderConfig: + def test_quicksilverpro_in_provider_list(self): + from litellm import LlmProviders + + assert hasattr(LlmProviders, "QUICKSILVERPRO") + assert LlmProviders.QUICKSILVERPRO.value == "quicksilverpro" + assert "quicksilverpro" in litellm.provider_list + + def test_quicksilverpro_json_config_exists(self): + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + assert JSONProviderRegistry.exists("quicksilverpro") + + provider = JSONProviderRegistry.get("quicksilverpro") + assert provider is not None + assert provider.base_url == "https://api.quicksilverpro.io/v1" + assert provider.api_key_env == "QUICKSILVERPRO_API_KEY" + assert provider.api_base_env == "QUICKSILVERPRO_API_BASE" + assert provider.param_mappings.get("max_completion_tokens") == "max_tokens" + + def test_quicksilverpro_supported_endpoints(self): + """chat completions, Responses API and Anthropic /v1/messages are + declared; image generations have no JSON-provider mechanism and are + not.""" + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + provider = JSONProviderRegistry.get("quicksilverpro") + assert provider is not None + assert provider.supported_endpoints == [ + "/v1/chat/completions", + "/v1/responses", + "/v1/messages", + ] + + def test_quicksilverpro_in_openai_compatible_providers(self): + from litellm.constants import openai_compatible_providers + + assert "quicksilverpro" in openai_compatible_providers + + def test_quicksilverpro_provider_resolution(self): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="quicksilverpro/claude-sonnet-5-5", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert model == "claude-sonnet-5-5" + assert provider == "quicksilverpro" + assert api_base == "https://api.quicksilverpro.io/v1" + + def test_quicksilverpro_api_base_override(self): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="quicksilverpro/claude-sonnet-5-5", + custom_llm_provider=None, + api_base="https://custom.quicksilverpro.example/v1", + api_key="sk-test", + ) + + assert provider == "quicksilverpro" + assert api_base == "https://custom.quicksilverpro.example/v1" + assert api_key == "sk-test" + + def test_quicksilverpro_url_autodetection(self): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="claude-sonnet-5-5", + custom_llm_provider=None, + api_base="https://api.quicksilverpro.io/v1", + api_key=None, + ) + assert provider == "quicksilverpro" + assert api_base == "https://api.quicksilverpro.io/v1" + + def test_quicksilverpro_max_completion_tokens_mapped(self): + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + provider = JSONProviderRegistry.get("quicksilverpro") + assert provider is not None + config = create_config_class(provider)() + + optional_params = config.map_openai_params( + non_default_params={"max_completion_tokens": 256}, + optional_params={}, + model="claude-sonnet-5-5", + drop_params=False, + ) + assert optional_params["max_tokens"] == 256 + assert "max_completion_tokens" not in optional_params + + def test_quicksilverpro_router_config(self): + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "quicksilverpro-chat", + "litellm_params": { + "model": "quicksilverpro/claude-sonnet-5-5", + "api_key": "test-key", + }, + } + ] + ) + + assert len(router.model_list) == 1 + assert router.model_list[0]["model_name"] == "quicksilverpro-chat" + + +class TestQuickSilverProModelMetadata: + @staticmethod + def _load(path_parts): + json_path = Path(__file__).parents[4].joinpath(*path_parts) + with open(json_path) as f: + return json.load(f) + + def test_all_models_present_in_model_cost(self): + model_cost = self._load(("model_prices_and_context_window.json",)) + for model in QSP_MODELS: + assert model in model_cost, f"{model} missing from model cost map" + assert model_cost[model]["litellm_provider"] == "quicksilverpro" + assert model_cost[model]["mode"] == "chat" + + def test_models_synced_to_backup(self): + model_cost = self._load(("model_prices_and_context_window.json",)) + backup = self._load( + ("litellm", "model_prices_and_context_window_backup.json") + ) + for model in QSP_MODELS: + assert model in backup, f"{model} missing from backup json" + assert backup[model] == model_cost[model], ( + f"{model} differs between root and backup json" + ) + + @pytest.mark.parametrize("model", QSP_MODELS) + def test_quicksilverpro_model_cost_and_capabilities(self, model: str): + """Behaviour against the committed entries: cost_per_token must charge + each model's own input/output price, and get_model_info must expose a + coherent entry. Expected values are read from the loaded cost map, not + hard-coded.""" + from litellm.cost_calculator import cost_per_token + + prompt_cost, completion_cost = cost_per_token( + model=model, + prompt_tokens=1_000_000, + completion_tokens=1_000_000, + custom_llm_provider="quicksilverpro", + ) + model_info = litellm.get_model_info(model) + + assert prompt_cost == pytest.approx(model_info["input_cost_per_token"] * 1_000_000) + assert completion_cost == pytest.approx(model_info["output_cost_per_token"] * 1_000_000) + assert model_info["output_cost_per_token"] > 0 + assert 0 < model_info["cache_read_input_token_cost"] < model_info["input_cost_per_token"] + # Entries without a provider-specified output cap come back with + # max_output_tokens=None; the legacy max_tokens then carries the cap. + max_output = model_info.get("max_output_tokens") or model_info["max_input_tokens"] + assert ( + model_info["max_tokens"] + == max_output + <= model_info["max_input_tokens"] + ) + assert model_info["litellm_provider"] == "quicksilverpro" + assert model_info["mode"] == "chat" + assert litellm.supports_vision(model) is model_info["supports_vision"] + + def test_quicksilverpro_cost_map_is_queryable(self): + """The entries must be consumable through litellm's own cost map, not + just present in the JSON file.""" + from litellm import model_cost + + for model in QSP_MODELS: + assert model in model_cost, model + assert model_cost[model]["litellm_provider"] == "quicksilverpro" + + +class TestQuickSilverProDashboardRegistration: + @staticmethod + def _provider_create_fields(): + path = ( + Path(litellm.__file__).parent + / "proxy" + / "public_endpoints" + / "provider_create_fields.json" + ) + with open(path) as f: + return json.load(f) + + def test_quicksilverpro_is_selectable_in_the_add_model_form(self): + entries = [ + e + for e in self._provider_create_fields() + if e["litellm_provider"] == "quicksilverpro" + ] + assert ( + len(entries) == 1 + ), "quicksilverpro must appear exactly once in provider_create_fields.json" + + entry = entries[0] + assert entry["provider"] == "QUICKSILVERPRO" + assert entry["provider_display_name"] == "QuickSilver Pro" + assert entry["default_model_placeholder"].startswith("quicksilverpro/") + + fields = {f["key"]: f for f in entry["credential_fields"]} + assert fields["api_key"]["required"] is True + assert fields["api_key"]["field_type"] == "password" + assert fields["api_base"]["required"] is False + + def test_quicksilverpro_supported_endpoints_matrix(self): + matrix = json.loads( + ( + Path(litellm.__file__).parent + / "provider_endpoints_support_backup.json" + ).read_text() + ) + + endpoints = matrix["providers"]["quicksilverpro"]["endpoints"] + assert endpoints["chat_completions"] is True + assert endpoints["messages"] is True + assert endpoints["responses"] is True + assert endpoints["embeddings"] is False + assert endpoints["image_generations"] is False + + +def test_quicksilverpro_chat_completion_request(): + """chat/completions hits the provider's base URL with bearer auth and maps + max_completion_tokens -> max_tokens.""" + with respx.mock() as upstream: + route: Final = upstream.post( + "https://api.quicksilverpro.io/v1/chat/completions" + ).respond( + 200, + json={ + "id": "chatcmpl_quicksilverpro", + "object": "chat.completion", + "created": 1_789_550_000, + "model": "claude-sonnet-5-5", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello from QuickSilver Pro"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7}, + }, + ) + response: Final = litellm.completion( + model="quicksilverpro/claude-sonnet-5-5", + messages=[{"role": "user", "content": "Say hello"}], + max_completion_tokens=128, + api_key="quicksilverpro-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://api.quicksilverpro.io/v1/chat/completions" + assert request.headers["authorization"] == "Bearer quicksilverpro-test-key" + assert body["model"] == "claude-sonnet-5-5" + assert body["messages"] == [{"role": "user", "content": "Say hello"}] + assert body["max_tokens"] == 128 + assert "max_completion_tokens" not in body + assert response.choices[0].message.content == "Hello from QuickSilver Pro" + + +def test_quicksilverpro_responses_request(): + with respx.mock() as upstream: + route: Final = upstream.post( + "https://api.quicksilverpro.io/v1/responses" + ).respond( + 200, + json={ + "id": "resp_quicksilverpro", + "object": "response", + "created_at": 1_789_550_000, + "model": "claude-sonnet-5-5", + "status": "completed", + "output": [ + { + "id": "msg_quicksilverpro", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [ + { + "type": "output_text", + "text": "Hello from QuickSilver Pro", + "annotations": [], + } + ], + } + ], + "usage": {"input_tokens": 4, "output_tokens": 3, "total_tokens": 7}, + }, + ) + response: Final = litellm.responses( + model="quicksilverpro/claude-sonnet-5-5", + input="Say hello", + api_key="quicksilverpro-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://api.quicksilverpro.io/v1/responses" + assert request.headers["authorization"] == "Bearer quicksilverpro-test-key" + assert body["model"] == "claude-sonnet-5-5" + assert body["input"] == "Say hello" + assert response.output[0].content[0].text == "Hello from QuickSilver Pro" + + +@pytest.mark.asyncio +async def test_quicksilverpro_anthropic_messages_request(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache()) + with respx.mock() as upstream: + route: Final = upstream.post( + "https://api.quicksilverpro.io/v1/messages" + ).respond( + 200, + json={ + "id": "msg_quicksilverpro", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-5-5", + "content": [{"type": "text", "text": "Hello from QuickSilver Pro"}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 4, "output_tokens": 3}, + }, + ) + response: Final = await litellm.anthropic.messages.acreate( + model="quicksilverpro/claude-sonnet-5-5", + messages=[{"role": "user", "content": "Say hello"}], + max_tokens=32, + api_key="quicksilverpro-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://api.quicksilverpro.io/v1/messages" + assert request.headers["authorization"] == "Bearer quicksilverpro-test-key" + assert request.headers["anthropic-version"] == "2023-06-01" + assert body["model"] == "claude-sonnet-5-5" + assert body["messages"] == [{"role": "user", "content": "Say hello"}] + assert response["content"][0]["text"] == "Hello from QuickSilver Pro" diff --git a/ui/litellm-dashboard/public/assets/logos/quicksilverpro.svg b/ui/litellm-dashboard/public/assets/logos/quicksilverpro.svg new file mode 100644 index 00000000000..ce98c22bcd3 --- /dev/null +++ b/ui/litellm-dashboard/public/assets/logos/quicksilverpro.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx index 7cfdaf3275d..0b32132d0a1 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -73,6 +73,17 @@ describe("provider_info_helpers", () => { expect(fromEnumKey.logo).toBe(providerLogoMap[Providers.SCX_AI]); }); + it("should map quicksilverpro slug and QUICKSILVERPRO enum key to the QuickSilver Pro display name and logo", () => { + const fromSlug = getProviderLogoAndName("quicksilverpro"); + expect(fromSlug.displayName).toBe(Providers.QUICKSILVERPRO); + expect(fromSlug.logo).toBe(providerLogoMap[Providers.QUICKSILVERPRO]); + expect(fromSlug.logo).toBeTruthy(); + + const fromEnumKey = getProviderLogoAndName("QUICKSILVERPRO"); + expect(fromEnumKey.displayName).toBe(Providers.QUICKSILVERPRO); + expect(fromEnumKey.logo).toBe(providerLogoMap[Providers.QUICKSILVERPRO]); + }); + it("should map bedrock_mantle slug to Bedrock Mantle display name and logo", () => { const result = getProviderLogoAndName("bedrock_mantle"); expect(result.displayName).toBe(Providers.BedrockMantle); @@ -229,6 +240,10 @@ describe("provider_info_helpers", () => { expect(getPlaceholder(Providers.SCX_AI)).toBe("scx-ai/GLM-5.2"); }); + it("should return a quicksilverpro model placeholder for QUICKSILVERPRO provider", () => { + expect(getPlaceholder(Providers.QUICKSILVERPRO)).toBe("quicksilverpro/claude-sonnet-5-5"); + }); + it("should return an edenai model placeholder for EDENAI provider", () => { expect(getPlaceholder(Providers.EDENAI)).toBe("edenai/openai/gpt-mini-latest"); }); diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 5ea693bea10..53be3a359e6 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -47,6 +47,7 @@ import openrouterLogo from "../../public/assets/logos/openrouter.svg"; import oracleLogo from "../../public/assets/logos/oracle.svg"; import perplexityAiLogo from "../../public/assets/logos/perplexity-ai.svg"; import qwenLogo from "../../public/assets/logos/qwen.png"; +import quicksilverproLogo from "../../public/assets/logos/quicksilverpro.svg"; import recraftLogo from "../../public/assets/logos/recraft.svg"; import replicateLogo from "../../public/assets/logos/replicate.svg"; import runwayLogo from "../../public/assets/logos/runway.png"; @@ -155,6 +156,7 @@ export enum Providers { PETALS = "Petals", PG_VECTOR = "Pg Vector", PREDIBASE = "Predibase", + QUICKSILVERPRO = "QuickSilver Pro", Qwen_AI_Platform = "Qianwen AI Platform", QwenCloud = "QwenCloud", RECRAFT = "Recraft", @@ -274,6 +276,7 @@ export const provider_map: Record = { PETALS: "petals", PG_VECTOR: "pg_vector", PREDIBASE: "predibase", + QUICKSILVERPRO: "quicksilverpro", Qwen_AI_Platform: "qwen_ai_platform", QwenCloud: "qwencloud", RECRAFT: "recraft", @@ -376,6 +379,7 @@ export const providerLogoMap: Partial> = { [Providers.Openrouter]: openrouterLogo.src, [Providers.Oracle]: oracleLogo.src, [Providers.Perplexity]: perplexityAiLogo.src, + [Providers.QUICKSILVERPRO]: quicksilverproLogo.src, [Providers.Qwen_AI_Platform]: qwenLogo.src, [Providers.QwenCloud]: qwenLogo.src, [Providers.RECRAFT]: recraftLogo.src, @@ -456,6 +460,7 @@ const providerPlaceholderMap: Partial> = { [Providers.SageMaker]: "sagemaker/jumpstart-dft-meta-textgeneration-llama-2-7b", [Providers.Sail]: "sail/openai/gpt-oss-120b", [Providers.SCX_AI]: "scx-ai/GLM-5.2", + [Providers.QUICKSILVERPRO]: "quicksilverpro/claude-sonnet-5-5", [Providers.Snowflake]: "snowflake/mistral-7b", [Providers.Tencent]: "tencent/deepseek-v4-pro", [Providers.Vertex_AI]: "gemini-pro", diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.ts b/ui/litellm-dashboard/src/lib/logoTreatments.ts index fc19c5c364d..68a5a9f418e 100644 --- a/ui/litellm-dashboard/src/lib/logoTreatments.ts +++ b/ui/litellm-dashboard/src/lib/logoTreatments.ts @@ -39,6 +39,7 @@ const TREATMENT_BY_ASSET: Readonly> = { "fireworks.svg": "plate", "llm_guard.png": "plate", "pangea.png": "plate", + "quicksilverpro.svg": "plate", "repelloai.png": "plate", "sambanova.svg": "plate", "sentry.svg": "plate", From 24fe3cec87662d9d31baf9c11d39070d87002f56 Mon Sep 17 00:00:00 2001 From: Raullen Chai Date: Fri, 2 Oct 2026 18:11:40 -0700 Subject: [PATCH 2/2] test(quicksilverpro): drop vendor-price relation assert, guard non-empty cost map --- tests/unit/llms/openai_like/test_quicksilverpro_provider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/llms/openai_like/test_quicksilverpro_provider.py b/tests/unit/llms/openai_like/test_quicksilverpro_provider.py index c0387fcd6ae..7bdb33096fa 100644 --- a/tests/unit/llms/openai_like/test_quicksilverpro_provider.py +++ b/tests/unit/llms/openai_like/test_quicksilverpro_provider.py @@ -24,6 +24,9 @@ QSP_MODELS: Final = tuple( class TestQuickSilverProProviderConfig: + def test_quicksilverpro_has_cost_map_entries(self): + assert QSP_MODELS + def test_quicksilverpro_in_provider_list(self): from litellm import LlmProviders @@ -182,9 +185,6 @@ class TestQuickSilverProModelMetadata: assert prompt_cost == pytest.approx(model_info["input_cost_per_token"] * 1_000_000) assert completion_cost == pytest.approx(model_info["output_cost_per_token"] * 1_000_000) assert model_info["output_cost_per_token"] > 0 - assert 0 < model_info["cache_read_input_token_cost"] < model_info["input_cost_per_token"] - # Entries without a provider-specified output cap come back with - # max_output_tokens=None; the legacy max_tokens then carries the cap. max_output = model_info.get("max_output_tokens") or model_info["max_input_tokens"] assert ( model_info["max_tokens"]