feat(parasail): add Parasail as a JSON-configured OpenAI-compatible provider (#29842)

* feat(parasail): add Parasail as a JSON-configured OpenAI-compatible provider

Registers parasail in the openai_like JSON provider loader with both
/v1/chat/completions and /v1/responses support. Parasail's Responses API
rejects store:true and any request that omits store, so the loader gains a
force_store_false special_handling flag; the parasail entry sets it and
the generated Responses config overrides store=false on every call. This
keeps callers from hitting "State storage not supported" and matches what
Parasail's docs require.

Adds the PARASAIL enum value, listing under openai_compatible_providers,
provider documentation at docs/my-website/docs/providers/parasail.md, and
a focused unit test file under tests/test_litellm/llms/parasail/ that
covers JSON registration, chat URL construction, Responses URL
construction with PARASAIL_API_BASE override, and the force_store_false
regression in both the caller-sent-store=true and caller-omitted cases.

* fix(parasail): register in provider_endpoints_support, drop in-repo docs

Greptile review feedback. The provider doc belongs in the litellm-docs
repo, not this one's docs/my-website tree; removing it here. Adds the
parasail entry to provider_endpoints_support.json so the
check_provider_folders_documented.py CI check passes (chat_completions
and responses true; others false).
This commit is contained in:
Kai Huang 2026-06-08 05:22:16 -07:00 committed by GitHub
parent 0806a3e54f
commit f087de5c8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 219 additions and 0 deletions

View file

@ -831,6 +831,7 @@ openai_compatible_providers: List = [
"nano-gpt", # Nano-GPT - JSON-configured provider
"poe", # Poe - JSON-configured provider
"chutes", # Chutes - JSON-configured provider
"parasail", # Parasail - JSON-configured provider
"featherless_ai",
"nscale",
"nebius",

View file

@ -187,6 +187,7 @@ def create_responses_config_class(provider: SimpleProviderConfig):
from litellm.llms.openai_like.responses.transformation import (
OpenAILikeResponsesConfig,
)
from litellm.types.llms.openai import ResponseInputParam
from litellm.types.router import GenericLiteLLMParams
class JSONProviderResponsesConfig(OpenAILikeResponsesConfig):
@ -223,5 +224,23 @@ def create_responses_config_class(provider: SimpleProviderConfig):
api_base = api_base.rstrip("/")
return f"{api_base}/responses"
def transform_responses_api_request(
self,
model: str,
input: Union[str, ResponseInputParam],
response_api_optional_request_params: dict,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> dict:
if provider.special_handling.get("force_store_false"):
response_api_optional_request_params["store"] = False
return super().transform_responses_api_request(
model=model,
input=input,
response_api_optional_request_params=response_api_optional_request_params,
litellm_params=litellm_params,
headers=headers,
)
_responses_config_cache[provider.slug] = JSONProviderResponsesConfig
return JSONProviderResponsesConfig

View file

@ -132,5 +132,14 @@
"param_mappings": {
"max_completion_tokens": "max_tokens"
}
},
"parasail": {
"base_url": "https://api.parasail.io/v1",
"api_key_env": "PARASAIL_API_KEY",
"api_base_env": "PARASAIL_API_BASE",
"supported_endpoints": ["/v1/chat/completions", "/v1/responses"],
"special_handling": {
"force_store_false": true
}
}
}

View file

@ -3392,6 +3392,7 @@ class LlmProviders(str, Enum):
POE = "poe"
CHUTES = "chutes"
NEOSANTARA = "neosantara"
PARASAIL = "parasail"
XIAOMI_MIMO = "xiaomi_mimo"
TENSORMESH = "tensormesh"
LITELLM_AGENT = "litellm_agent"

View file

@ -1834,6 +1834,23 @@
"search": true
}
},
"parasail": {
"display_name": "Parasail (`parasail`)",
"url": "https://docs.litellm.ai/docs/providers/parasail",
"endpoints": {
"chat_completions": true,
"messages": false,
"responses": true,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false
}
},
"perplexity": {
"display_name": "Perplexity AI (`perplexity`)",
"url": "https://docs.litellm.ai/docs/providers/perplexity",

View file

@ -0,0 +1,172 @@
import os
from unittest.mock import patch
PARASAIL_API_BASE = "https://api.parasail.io/v1"
PARASAIL_RESPONSES_GATEWAY = "https://api-webflux.saas.parasail.io/v1"
def test_parasail_json_registry():
import litellm
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert litellm.LlmProviders.PARASAIL.value == "parasail"
assert litellm.LlmProviders("parasail") == litellm.LlmProviders.PARASAIL
assert JSONProviderRegistry.exists("parasail")
config = JSONProviderRegistry.get("parasail")
assert config is not None
assert config.base_url == PARASAIL_API_BASE
assert config.api_key_env == "PARASAIL_API_KEY"
assert config.api_base_env == "PARASAIL_API_BASE"
assert "/v1/chat/completions" in config.supported_endpoints
assert "/v1/responses" in config.supported_endpoints
assert config.special_handling.get("force_store_false") is True
def test_parasail_listed_in_openai_compatible_providers():
from litellm.constants import openai_compatible_providers
assert "parasail" in openai_compatible_providers
def test_parasail_dynamic_config_env_vars():
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
config = create_config_class(JSONProviderRegistry.get("parasail"))()
with patch.dict(
os.environ,
{
"PARASAIL_API_KEY": "test-key",
"PARASAIL_API_BASE": PARASAIL_RESPONSES_GATEWAY,
},
):
api_base, api_key = config._get_openai_compatible_provider_info(None, None)
assert api_base == PARASAIL_RESPONSES_GATEWAY
assert api_key == "test-key"
def test_parasail_provider_detection_by_prefix():
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, _, api_base = get_llm_provider(
"parasail/parasail-llama-33-70b-fp8"
)
assert model == "parasail-llama-33-70b-fp8"
assert provider == "parasail"
assert api_base == PARASAIL_API_BASE
def test_parasail_chat_complete_url():
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
config = create_config_class(JSONProviderRegistry.get("parasail"))()
assert (
config.get_complete_url(
api_base=None,
api_key=None,
model="parasail-llama-33-70b-fp8",
optional_params={},
litellm_params={},
)
== f"{PARASAIL_API_BASE}/chat/completions"
)
def test_parasail_responses_api_config():
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.utils import ProviderConfigManager
config = ProviderConfigManager.get_provider_responses_api_config(
provider="parasail",
model="parasail-kimi-k25-elicit",
)
assert isinstance(config, OpenAIResponsesAPIConfig)
assert config.custom_llm_provider == "parasail"
assert (
config.get_complete_url(api_base=None, litellm_params={})
== f"{PARASAIL_API_BASE}/responses"
)
def test_parasail_responses_api_honors_api_base_override():
from litellm.utils import ProviderConfigManager
config = ProviderConfigManager.get_provider_responses_api_config(
provider="parasail",
model="parasail-kimi-k25-elicit",
)
with patch.dict(
os.environ,
{"PARASAIL_API_BASE": PARASAIL_RESPONSES_GATEWAY},
):
url = config.get_complete_url(api_base=None, litellm_params={})
assert url == f"{PARASAIL_RESPONSES_GATEWAY}/responses"
def test_parasail_responses_api_forces_store_false_when_caller_sets_true():
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager
config = ProviderConfigManager.get_provider_responses_api_config(
provider="parasail",
model="parasail-kimi-k25-elicit",
)
request_params: dict = {"store": True, "temperature": 0.2}
transformed = config.transform_responses_api_request(
model="parasail-kimi-k25-elicit",
input="hello",
response_api_optional_request_params=request_params,
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert transformed["store"] is False
assert transformed["temperature"] == 0.2
def test_parasail_responses_api_forces_store_false_when_caller_omits_store():
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager
config = ProviderConfigManager.get_provider_responses_api_config(
provider="parasail",
model="parasail-kimi-k25-elicit",
)
transformed = config.transform_responses_api_request(
model="parasail-kimi-k25-elicit",
input="hello",
response_api_optional_request_params={},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert transformed["store"] is False
def test_parasail_responses_api_validate_environment_sets_bearer_token():
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager
config = ProviderConfigManager.get_provider_responses_api_config(
provider="parasail",
model="parasail-kimi-k25-elicit",
)
with patch.dict(os.environ, {"PARASAIL_API_KEY": "secret-from-env"}):
headers = config.validate_environment(
headers={},
model="parasail-kimi-k25-elicit",
litellm_params=GenericLiteLLMParams(),
)
assert headers["Authorization"] == "Bearer secret-from-env"