mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
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.
This commit is contained in:
parent
a292fd409f
commit
3ee6a70d04
14 changed files with 778 additions and 0 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
379
tests/unit/llms/openai_like/test_quicksilverpro_provider.py
Normal file
379
tests/unit/llms/openai_like/test_quicksilverpro_provider.py
Normal file
|
|
@ -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"
|
||||
14
ui/litellm-dashboard/public/assets/logos/quicksilverpro.svg
Normal file
14
ui/litellm-dashboard/public/assets/logos/quicksilverpro.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
|||
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<Record<Providers, string>> = {
|
|||
[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<Record<Providers, string>> = {
|
|||
[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",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const TREATMENT_BY_ASSET: Readonly<Record<string, LogoTreatment>> = {
|
|||
"fireworks.svg": "plate",
|
||||
"llm_guard.png": "plate",
|
||||
"pangea.png": "plate",
|
||||
"quicksilverpro.svg": "plate",
|
||||
"repelloai.png": "plate",
|
||||
"sambanova.svg": "plate",
|
||||
"sentry.svg": "plate",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue