feat(providers): add Y-API as a JSON-configured OpenAI-compatible provider

Y-API is an OpenAI-compatible relay fronting DeepSeek, Z.ai, Moonshot,
Tencent, Xiaomi, Qwen and OpenAI models behind one key. It also serves the
Anthropic Messages API and the OpenAI Responses API.

Registered through the declarative JSON registry, so no Python provider
module is needed:

- litellm/llms/openai_like/providers.json: base URL, YAPI_API_KEY /
  YAPI_API_BASE, a max_tokens -> max_completion_tokens mapping, and the
  three supported endpoints.
- litellm/types/utils.py: LlmProviders.Y_API.
- litellm/constants.py: added to openai_compatible_providers,
  openai_compatible_endpoints and openai_text_completion_compatible_providers.
- provider_endpoints_support.json + the runtime backup: chat_completions,
  messages and responses true; embeddings false.
- provider_create_fields.json: an Add Model form entry, without which the
  provider would be invisible in the dashboard.
- tests: resolution, api_base override, router config, both endpoint
  matrices, dashboard registration, and the token-parameter rewrite.

Model ids keep the upstream organization prefix, so every model string has
two slashes (`y-api/deepseek/deepseek-v4-flash`). Provider resolution splits
on the first slash and forwards the rest unchanged; there is a test for it.

The `openai/*` models this relay fronts reject `max_tokens` outright and
require `max_completion_tokens`. LiteLLM does not translate the parameter for
this provider, so the mapping rewrites the caller's `max_tokens` to the name
the relay expects. Both spellings then arrive identically, and a caller who
sets no cap still sends none.
This commit is contained in:
jiweiyeah 2026-09-17 11:58:31 +08:00
parent f4308bc124
commit 3c6a6c0cc1
7 changed files with 318 additions and 0 deletions

View file

@ -950,6 +950,7 @@ openai_compatible_endpoints: Final[list] = [
"https://api.cognition.ai/v1",
"https://api.scx.ai/v1",
"https://gigachat.devices.sberbank.ru/api/v1",
"https://api.y-api.bestvirtualgoods.com/v1",
]
@ -1023,6 +1024,7 @@ openai_compatible_providers: Final[list] = [
"cognition",
"scx-ai",
"sail",
"y-api", # Y-API - JSON-configured provider
]
OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS: Final = frozenset({"openai"} | frozenset(openai_compatible_providers))
@ -1051,6 +1053,7 @@ openai_text_completion_compatible_providers: Final[list] = [ # providers that s
"lambda_ai",
"hyperbolic",
"wandb",
"y-api",
]
_openai_like_providers: Final[list] = [
"predibase",

View file

@ -206,5 +206,18 @@
"api_key_env": "SAIL_API_KEY",
"api_base_env": "SAIL_API_BASE",
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
},
"y-api": {
"base_url": "https://api.y-api.bestvirtualgoods.com/v1",
"api_key_env": "YAPI_API_KEY",
"api_base_env": "YAPI_API_BASE",
"param_mappings": {
"max_tokens": "max_completion_tokens"
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses",
"/v1/messages"
]
}
}

View file

@ -2624,6 +2624,15 @@
"messages": true,
"responses": true
}
},
"y-api": {
"display_name": "Y-API (`y-api`)",
"url": "https://docs.litellm.ai/docs/providers/y-api",
"endpoints": {
"chat_completions": true,
"messages": true,
"responses": true
}
}
},
"endpoints": {

View file

@ -3601,6 +3601,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "Y_API",
"provider_display_name": "Y-API",
"litellm_provider": "y-api",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.y-api.bestvirtualgoods.com/v1",
"tooltip": "Y-API base URL (defaults to https://api.y-api.bestvirtualgoods.com/v1)",
"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": "y-api/deepseek/deepseek-v4-flash"
},
{
"provider": "CURSOR",
"provider_display_name": "Cursor",

View file

@ -4142,6 +4142,7 @@ class LlmProviders(str, Enum):
DARKBLOOM = "darkbloom"
META = "meta"
SAIL = "sail"
Y_API = "y-api"
LITELLM_AGENT = "litellm_agent"
CURSOR = "cursor"
BEDROCK_MANTLE = "bedrock_mantle"

View file

@ -3087,6 +3087,23 @@
"rerank": false,
"a2a": false
}
},
"y-api": {
"display_name": "Y-API (`y-api`)",
"url": "https://docs.litellm.ai/docs/providers/y-api",
"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
}
}
},
"endpoints": {

View file

@ -0,0 +1,247 @@
"""
Tests for Y-API provider configuration and integration.
Y-API is a JSON-configured OpenAI-compatible relay. Its model ids keep the
upstream organization prefix, so they contain a slash of their own
(`y-api/deepseek/deepseek-v4-flash`) -- provider resolution must split on the
first slash only.
"""
import litellm
BASE_URL = "https://api.y-api.bestvirtualgoods.com/v1"
class TestYApiProviderConfig:
"""Test Y-API provider configuration"""
def test_y_api_in_provider_list(self):
"""Test that y-api is in the provider list"""
from litellm import LlmProviders
assert hasattr(LlmProviders, "Y_API")
assert LlmProviders.Y_API.value == "y-api"
assert "y-api" in litellm.provider_list
def test_y_api_json_config_exists(self):
"""Test that y-api is configured in providers.json"""
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert JSONProviderRegistry.exists("y-api")
y_api = JSONProviderRegistry.get("y-api")
assert y_api is not None
assert y_api.base_url == BASE_URL
assert y_api.api_key_env == "YAPI_API_KEY"
assert y_api.api_base_env == "YAPI_API_BASE"
assert y_api.param_mappings == {"max_tokens": "max_completion_tokens"}
def test_y_api_supported_endpoints(self):
"""Test the endpoints declared for y-api"""
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
y_api = JSONProviderRegistry.get("y-api")
assert y_api is not None
assert "/v1/chat/completions" in y_api.supported_endpoints
assert "/v1/responses" in y_api.supported_endpoints
assert "/v1/messages" in y_api.supported_endpoints
assert JSONProviderRegistry.supports_responses_api("y-api") is True
def test_y_api_provider_resolution_with_nested_model_id(self):
"""Test that provider resolution finds y-api and keeps the nested model id"""
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, api_key, api_base = get_llm_provider(
model="y-api/deepseek/deepseek-v4-flash",
custom_llm_provider=None,
api_base=None,
api_key=None,
)
assert model == "deepseek/deepseek-v4-flash"
assert provider == "y-api"
assert api_base == BASE_URL
def test_y_api_api_base_override(self):
"""Test that an explicit api_base / api_key overrides the default"""
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, api_key, api_base = get_llm_provider(
model="y-api/deepseek/deepseek-v4-flash",
custom_llm_provider=None,
api_base="https://custom.example.com/v1",
api_key="sk-test",
)
assert provider == "y-api"
assert api_base == "https://custom.example.com/v1"
assert api_key == "sk-test"
def test_y_api_router_config(self):
"""Test that y-api can be used in Router configuration"""
from litellm import Router
router = Router(
model_list=[
{
"model_name": "y-api-chat",
"litellm_params": {
"model": "y-api/deepseek/deepseek-v4-flash",
"api_key": "test-key",
},
}
]
)
assert len(router.model_list) == 1
assert router.model_list[0]["model_name"] == "y-api-chat"
def test_y_api_supported_endpoints_matrix(self):
"""The documented matrix lists y-api with the endpoints it actually serves."""
import json
from pathlib import Path
import litellm as _litellm
matrix_path = (
Path(_litellm.__file__).parent.parent / "provider_endpoints_support.json"
)
matrix = json.loads(matrix_path.read_text())
assert "y-api" in matrix["providers"]
endpoints = matrix["providers"]["y-api"]["endpoints"]
assert endpoints["chat_completions"] is True
assert endpoints["messages"] is True
assert endpoints["responses"] is True
# embeddings is advertised false: GET /v1/models returns no embedding
# model, and POST /v1/embeddings answers
# "No available channel for model ... under group y-api (distributor)".
assert endpoints["embeddings"] is False
def test_y_api_runtime_supported_endpoints_matrix(self):
"""The runtime-served backup matrix (GET /public/supported_endpoints) lists y-api."""
import json
from pathlib import Path
import litellm as _litellm
backup_path = (
Path(_litellm.__file__).parent / "provider_endpoints_support_backup.json"
)
matrix = json.loads(backup_path.read_text())
assert "y-api" in matrix["providers"]
endpoints = matrix["providers"]["y-api"]["endpoints"]
assert endpoints["chat_completions"] is True
assert endpoints["messages"] is True
assert endpoints["responses"] is True
class TestYApiDashboardRegistration:
"""The Add Model form must offer Y-API.
`test_every_backend_provider_is_listed_in_add_model_or_frozen_as_unlisted`
fails if a provider exists in `LlmProviders` without an entry here, so this
is the difference between "selectable in the UI" and "silently dropped into
the frozen unlisted set".
"""
@staticmethod
def _provider_create_fields():
import json
from pathlib import Path
import litellm
path = (
Path(litellm.__file__).parent
/ "proxy"
/ "public_endpoints"
/ "provider_create_fields.json"
)
with open(path) as f:
return json.load(f)
def test_y_api_is_selectable_in_the_add_model_form(self):
entries = [
e
for e in self._provider_create_fields()
if e["litellm_provider"] == "y-api"
]
assert (
len(entries) == 1
), "y-api must appear exactly once in provider_create_fields.json"
entry = entries[0]
assert entry["provider"] == "Y_API"
assert entry["provider_display_name"] == "Y-API"
assert entry["default_model_placeholder"].startswith("y-api/")
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
# The base URL is surfaced as the placeholder so admins can see the
# default without it being written into config.
assert fields["api_base"]["placeholder"] == BASE_URL
def test_y_api_is_returned_by_the_public_providers_fields_endpoint(self):
"""The endpoint the Add Model dropdown actually reads from."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from litellm.proxy.public_endpoints.public_endpoints import router
app_instance = FastAPI()
app_instance.include_router(router)
test_client = TestClient(app_instance)
response = test_client.get("/public/providers/fields")
assert response.status_code == 200
providers = {p["litellm_provider"] for p in response.json()}
assert "y-api" in providers
class TestYApiTokenParamMapping:
"""`max_tokens` has to reach this relay as `max_completion_tokens`.
The `openai/*` models y-api fronts reject the legacy name outright:
Unsupported parameter: 'max_tokens' is not supported with this model.
Use 'max_completion_tokens' instead.
LiteLLM's `openai_gpt` base class does not translate it for this provider --
the model ids are not in the OpenAI cost map, so the caller's `max_tokens`
is forwarded verbatim. These tests drive the transformation the provider
actually applies to caller params, rather than reading the config, because
the config alone cannot show which parameter name leaves the process.
"""
@staticmethod
def _map(**params):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
provider = JSONProviderRegistry.get("y-api")
config = create_config_class(provider)()
return config.map_openai_params(
non_default_params=dict(params),
optional_params={},
model="gpt-5.6-luna",
drop_params=False,
)
def test_max_tokens_is_rewritten(self):
mapped = self._map(max_tokens=16)
assert mapped["max_completion_tokens"] == 16
assert "max_tokens" not in mapped
def test_max_completion_tokens_passes_through(self):
mapped = self._map(max_completion_tokens=16)
assert mapped["max_completion_tokens"] == 16
assert "max_tokens" not in mapped
def test_absent_cap_sends_neither(self):
mapped = self._map()
assert "max_tokens" not in mapped
assert "max_completion_tokens" not in mapped