diff --git a/litellm/__init__.py b/litellm/__init__.py index 1784deeb797..779341bf726 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -681,6 +681,7 @@ publicai_models: Set = set() darkbloom_models: Set = set() openinfer_models: Final[Set[str]] = set() v0_models: Set = set() +vispark_models: Set[str] = set() morph_models: Set = set() lambda_ai_models: Set = set() inception_models: Set = set() @@ -968,6 +969,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje openinfer_models.add(key) elif value.get("litellm_provider") == "v0": v0_models.add(key) + elif value.get("litellm_provider") == "vispark": + vispark_models.add(key) elif value.get("litellm_provider") == "morph": morph_models.add(key) elif value.get("litellm_provider") == "lambda_ai": @@ -1136,6 +1139,7 @@ model_list = list( | darkbloom_models | openinfer_models | v0_models + | vispark_models | morph_models | lambda_ai_models | inception_models @@ -1253,6 +1257,7 @@ def _build_models_by_provider() -> dict: "darkbloom": darkbloom_models, "openinfer": openinfer_models, "v0": v0_models, + "vispark": vispark_models, "morph": morph_models, "lambda_ai": lambda_ai_models, "inception": inception_models, diff --git a/litellm/constants.py b/litellm/constants.py index c0fe0491775..74c93c5c692 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1056,6 +1056,7 @@ openai_compatible_endpoints: Final[list[str]] = [ "https://inference.coralbricks.ai/v1", "https://api.cortecs.ai/v1", "https://api.scx.ai/v1", + "https://api.lab.vispark.in/v1", "https://api.prisminference.com/v1", "https://api.reka.ai/v1", "https://gigachat.devices.sberbank.ru/api/v1", @@ -1130,6 +1131,7 @@ openai_compatible_providers: Final[list[str]] = [ "pinstripes", # Pinstripes - JSON-configured provider "openinfer", "darkbloom", + "vispark", # Vispark Lab - JSON-configured provider "meta", # Meta Model API (Muse Spark) - JSON-configured provider "cognition", "coralbricks", @@ -1163,6 +1165,7 @@ openai_text_completion_compatible_providers: Final[list] = [ # providers that s "poe", "chutes", "v0", + "vispark", "lambda_ai", "hyperbolic", "wandb", diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 36337fddbdc..a520c4e7012 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -249,5 +249,14 @@ "/v1/responses", "/v1/messages" ] + }, + "vispark": { + "base_url": "https://api.lab.vispark.in/v1", + "api_key_env": "VISPARK_LAB_API_KEY", + "api_base_env": "VISPARK_LAB_API_BASE", + "param_mappings": { + "max_completion_tokens": "max_tokens" + }, + "supported_endpoints": ["/v1/chat/completions", "/v1/completions"] } } diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 90a0763f786..fd62738a03f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -49883,6 +49883,102 @@ "supports_tool_choice": true, "supports_vision": true }, + "vispark/vision-small": { + "input_cost_per_token": 1.05e-06, + "litellm_provider": "vispark", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 3.16e-06, + "source": "https://api.lab.vispark.in/v1/models", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vispark/vision-medium": { + "input_cost_per_token": 4.21e-06, + "litellm_provider": "vispark", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.263e-05, + "source": "https://api.lab.vispark.in/v1/models", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vispark/vision-large": { + "input_cost_per_token": 7.37e-06, + "litellm_provider": "vispark", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 2.211e-05, + "source": "https://api.lab.vispark.in/v1/models", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "vercel_ai_gateway/alibaba/qwen-3-14b": { "input_cost_per_token": 8e-08, "litellm_provider": "vercel_ai_gateway", diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index f95b553c2b4..a6c058102cd 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -2352,6 +2352,24 @@ "interactions": true } }, + "vispark": { + "display_name": "Vispark (`vispark`)", + "url": "https://lab.vispark.in/#vision", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false, + "interactions": false + } + }, "vercel_ai_gateway": { "display_name": "Vercel AI Gateway (`vercel_ai_gateway`)", "url": "https://docs.litellm.ai/docs/providers/vercel_ai_gateway", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 4bec42a7868..dfeb462dee4 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -4070,5 +4070,33 @@ } ], "default_model_placeholder": "cursor/claude-4-sonnet" + }, + { + "provider": "VISPARK", + "provider_display_name": "Vispark", + "litellm_provider": "vispark", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.lab.vispark.in/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": "vispark/vision-medium" } ] diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 5939a350970..f2007e93f5f 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4453,6 +4453,7 @@ class LlmProviders(str, Enum): MOONSHOT = "moonshot" PUBLICAI = "publicai" V0 = "v0" + VISPARK = "vispark" MORPH = "morph" LAMBDA_AI = "lambda_ai" INCEPTION = "inception" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 90a0763f786..fd62738a03f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -49883,6 +49883,102 @@ "supports_tool_choice": true, "supports_vision": true }, + "vispark/vision-small": { + "input_cost_per_token": 1.05e-06, + "litellm_provider": "vispark", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 3.16e-06, + "source": "https://api.lab.vispark.in/v1/models", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vispark/vision-medium": { + "input_cost_per_token": 4.21e-06, + "litellm_provider": "vispark", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.263e-05, + "source": "https://api.lab.vispark.in/v1/models", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vispark/vision-large": { + "input_cost_per_token": 7.37e-06, + "litellm_provider": "vispark", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 2.211e-05, + "source": "https://api.lab.vispark.in/v1/models", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "vercel_ai_gateway/alibaba/qwen-3-14b": { "input_cost_per_token": 8e-08, "litellm_provider": "vercel_ai_gateway", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index d65da3f41fd..f669e3a9e7d 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2722,6 +2722,24 @@ "interactions": true } }, + "vispark": { + "display_name": "Vispark (`vispark`)", + "url": "https://lab.vispark.in/#vision", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false, + "interactions": false + } + }, "vercel_ai_gateway": { "display_name": "Vercel AI Gateway (`vercel_ai_gateway`)", "url": "https://docs.litellm.ai/docs/providers/vercel_ai_gateway", diff --git a/tests/unit/llms/openai_like/test_vispark_provider.py b/tests/unit/llms/openai_like/test_vispark_provider.py new file mode 100644 index 00000000000..e1e2f79cbd4 --- /dev/null +++ b/tests/unit/llms/openai_like/test_vispark_provider.py @@ -0,0 +1,177 @@ +import json +from pathlib import Path +from typing import Final + +import pytest +import respx + +import litellm + +_CHAT_COMPLETION: Final = { + "id": "chatcmpl_vispark", + "object": "chat.completion", + "created": 1_790_000_000, + "model": "vision-medium", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello from Vispark"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7}, +} + +_TEXT_COMPLETION: Final = { + "id": "cmpl_vispark", + "object": "text_completion", + "created": 1_790_000_000, + "model": "vision-small", + "choices": [{"index": 0, "text": "Hello from Vispark", "finish_reason": "stop", "logprobs": None}], + "usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7}, +} + + +def test_vispark_provider_resolution(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("VISPARK_LAB_API_KEY", "vispark-env-key") + monkeypatch.delenv("VISPARK_LAB_API_BASE", raising=False) + + model, provider, api_key, api_base = get_llm_provider( + model="vispark/vision-medium", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert model == "vision-medium" + assert provider == "vispark" + assert api_key == "vispark-env-key" + assert api_base == "https://api.lab.vispark.in/v1" + + +def test_vispark_provider_keeps_explicit_credentials(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("VISPARK_LAB_API_KEY", "vispark-env-key") + monkeypatch.setenv("VISPARK_LAB_API_BASE", "https://vispark.env.example/v1") + + _, provider, api_key, api_base = get_llm_provider( + model="vispark/vision-medium", + custom_llm_provider=None, + api_base="https://vispark.internal.example/v1", + api_key="vispark-explicit-key", + ) + + assert provider == "vispark" + assert api_key == "vispark-explicit-key" + assert api_base == "https://vispark.internal.example/v1" + + +def test_vispark_api_base_env_overrides_default(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("VISPARK_LAB_API_KEY", "vispark-env-key") + monkeypatch.setenv("VISPARK_LAB_API_BASE", "https://vispark.env.example/v1") + + _, provider, _, api_base = get_llm_provider( + model="vispark/vision-medium", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert provider == "vispark" + assert api_base == "https://vispark.env.example/v1" + + +def test_vispark_api_base_autodetects_provider(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("VISPARK_LAB_API_KEY", "vispark-env-key") + + model, provider, api_key, api_base = get_llm_provider( + model="vision-medium", + custom_llm_provider=None, + api_base="https://api.lab.vispark.in/v1", + api_key=None, + ) + + assert model == "vision-medium" + assert provider == "vispark" + assert api_key == "vispark-env-key" + assert api_base == "https://api.lab.vispark.in/v1" + + +def test_vispark_chat_completion_request_maps_max_completion_tokens(): + with respx.mock() as upstream: + route: Final = upstream.post("https://api.lab.vispark.in/v1/chat/completions").respond( + 200, json=_CHAT_COMPLETION + ) + response: Final = litellm.completion( + model="vispark/vision-medium", + messages=[{"role": "user", "content": "Say hello"}], + max_completion_tokens=64, + api_key="vispark-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert request.headers["authorization"] == "Bearer vispark-test-key" + assert body["model"] == "vision-medium" + assert body["max_tokens"] == 64 + assert "max_completion_tokens" not in body + assert response.choices[0].message.content == "Hello from Vispark" + + +def test_vispark_text_completion_request(): + with respx.mock() as upstream: + route: Final = upstream.post("https://api.lab.vispark.in/v1/completions").respond(200, json=_TEXT_COMPLETION) + response: Final = litellm.text_completion( + model="vispark/vision-small", + prompt="Say hello", + api_key="vispark-test-key", + ) + + body: Final = json.loads(route.calls.last.request.content) + assert route.call_count == 1 + assert body["model"] == "vision-small" + assert body["prompt"] == "Say hello" + assert response.choices[0].text == "Hello from Vispark" + + +def test_vispark_cost_follows_the_cost_map(): + cost_entry: Final = litellm.model_cost["vispark/vision-large"] + + prompt_cost, completion_cost = litellm.cost_per_token( + model="vision-large", + custom_llm_provider="vispark", + prompt_tokens=1_000, + completion_tokens=500, + ) + + assert prompt_cost == pytest.approx(1_000 * cost_entry["input_cost_per_token"]) + assert completion_cost == pytest.approx(500 * cost_entry["output_cost_per_token"]) + + +def test_vispark_models_are_listed_for_the_provider(): + from litellm.utils import get_valid_models + + expected: Final = {name for name, entry in litellm.model_cost.items() if entry.get("litellm_provider") == "vispark"} + + assert expected + assert set(litellm.models_by_provider["vispark"]) == expected + assert expected <= set(get_valid_models(custom_llm_provider="vispark")) + + +def test_vispark_endpoint_matrix_matches_between_root_and_backup(): + backup_path: Final = Path(litellm.__file__).parent / "provider_endpoints_support_backup.json" + root_path: Final = Path(litellm.__file__).parent.parent / "provider_endpoints_support.json" + + backup_entry: Final = json.loads(backup_path.read_text())["providers"]["vispark"] + root_entry: Final = json.loads(root_path.read_text())["providers"]["vispark"] + + assert backup_entry == root_entry + assert root_entry["endpoints"]["chat_completions"] is True