This commit is contained in:
Daniel Varela 2026-11-05 03:24:39 +00:00 • committed by GitHub
commit 0ca221d7d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 203 additions and 1 deletions

View file

@ -425,6 +425,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th
| [LiteLLM Proxy (`litellm_proxy`)](https://docs.litellm.ai/docs/providers/litellm_proxy) | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | |
| [Llamafile (`llamafile`)](https://docs.litellm.ai/docs/providers/llamafile) | ✅ | ✅ | ✅ | | | | | | | |
| [LM Studio (`lm_studio`)](https://docs.litellm.ai/docs/providers/lm_studio) | ✅ | ✅ | ✅ | | | | | | | |
| [Lokutor (`lokutor`)](https://docs.litellm.ai/docs/providers/lokutor) | | | | | | ✅ | ✅ | | | |
| [Maritalk (`maritalk`)](https://docs.litellm.ai/docs/providers/maritalk) | ✅ | ✅ | ✅ | | | | | | | |
| [Meta - Llama API (`meta_llama`)](https://docs.litellm.ai/docs/providers/meta_llama) | ✅ | ✅ | ✅ | | | | | | | |
| [Mistral AI API (`mistral`)](https://docs.litellm.ai/docs/providers/mistral) | ✅ | ✅ | ✅ | ✅ | | | | | | |

View file

@ -114,6 +114,7 @@ const OPENAI_COMPATIBLE_PROVIDERS: &[&str] = &[
"cognition",
"scx-ai",
"zerogpu",
"lokutor",
];
impl ExceptionFamily {

View file

@ -690,6 +690,7 @@ kyma_models: Final[set[str]] = set() # mutable-ok: populated when the shared mo
streamlake_models: Final[Set[str]] = set()
wally_models: Final[Set[str]] = set()
zerogpu_models: Final[Set[str]] = set()
lokutor_models: Final[Set[str]] = set()
v0_models: Set = set()
morph_models: Set = set()
opencode_go_models: Final[set[str]] = set() # mutable-ok: populated when the shared model catalog loads
@ -989,6 +990,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
wally_models.add(key)
elif value.get("litellm_provider") == "zerogpu":
zerogpu_models.add(key)
elif value.get("litellm_provider") == "lokutor":
lokutor_models.add(key)
elif value.get("litellm_provider") == "v0":
v0_models.add(key)
elif value.get("litellm_provider") == "morph":
@ -1170,6 +1173,7 @@ model_list = list(
| streamlake_models
| wally_models
| zerogpu_models
| lokutor_models
| v0_models
| morph_models
| opencode_go_models
@ -1304,6 +1308,7 @@ def _build_models_by_provider() -> dict:
"streamlake": streamlake_models,
"wally": wally_models,
"zerogpu": zerogpu_models,
"lokutor": lokutor_models,
"v0": v0_models,
"morph": morph_models,
"opencode_go": opencode_go_models,

View file

@ -1131,6 +1131,7 @@ openai_compatible_providers: Final[list[str]] = [
"lm_studio",
"galadriel",
"zerogpu",
"lokutor", # Lokutor - JSON-configured provider (audio speech and transcription)
"github_copilot", # GitHub Copilot Chat API
"chatgpt", # ChatGPT subscription API
"novita",

View file

@ -58,6 +58,12 @@
"max_completion_tokens": "max_tokens"
}
},
"lokutor": {
"base_url": "https://api.lokutor.com/v1",
"api_key_env": "LOKUTOR_API_KEY",
"api_base_env": "LOKUTOR_API_BASE",
"supported_endpoints": ["/v1/audio/speech", "/v1/audio/transcriptions"]
},
"zerogpu": {
"base_url": "https://api.zerogpu.ai/v1",
"api_key_env": "ZEROGPU_API_KEY",

View file

@ -8988,7 +8988,8 @@ def speech(
# set API KEY
api_key = (
api_key
or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there
or dynamic_api_key # key resolved by get_llm_provider for openai-compatible providers (e.g. LOKUTOR_API_KEY)
or litellm.api_key
or litellm.openai_key
or get_secret("OPENAI_API_KEY")
)

View file

@ -4541,6 +4541,32 @@
"generateContent": true
}
},
"lokutor": {
"display_name": "Lokutor (`lokutor`)",
"url": "https://docs.litellm.ai/docs/providers/lokutor",
"endpoints": {
"chat_completions": false,
"responses": false,
"text_completion": false,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": true,
"audio_speech": true,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": false,
"interactions": false,
"systemone": false,
"a2a": false,
"generateContent": false
}
},
"zerogpu": {
"display_name": "ZeroGPU (`zerogpu`)",
"url": "https://docs.litellm.ai/docs/providers/zerogpu",

View file

@ -4425,6 +4425,34 @@
],
"default_model_placeholder": "zai/glm-4.5"
},
{
"provider": "Lokutor",
"provider_display_name": "Lokutor",
"litellm_provider": "lokutor",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.lokutor.com/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": "lokutor/tts-1"
},
{
"provider": "ZeroGPU",
"provider_display_name": "ZeroGPU",

View file

@ -4774,6 +4774,7 @@ class LlmProviders(str, Enum):
INFERENCEAPIS = "inferenceapis"
HOSTINGER_ROUTER = "hostinger_router"
ZEROGPU = "zerogpu"
LOKUTOR = "lokutor"
DARKBLOOM = "darkbloom"
META = "meta"
SAIL = "sail"

View file

@ -4484,11 +4484,13 @@ def get_optional_params_transcription(
**kwargs,
):
from litellm.constants import OPENAI_TRANSCRIPTION_PARAMS
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
# retrieve all parameters passed to the function
passed_params: Final = locals().copy()
passed_params.pop("OPENAI_TRANSCRIPTION_PARAMS")
passed_params.pop("JSONProviderRegistry")
passed_params.pop("model")
custom_llm_provider = passed_params.pop("custom_llm_provider")
drop_params = normalize_drop_params(passed_params.pop("drop_params"))
@ -4569,6 +4571,11 @@ def get_optional_params_transcription(
raise UnsupportedParamsError(message=message, llm_provider="bedrock")
case mapped_params:
optional_params = dict(mapped_params)
elif provider_config is None and JSONProviderRegistry.serves_endpoint(
custom_llm_provider, model, "/v1/audio/transcriptions"
):
# JSON-configured OpenAI-compatible provider that serves /audio/transcriptions: it takes the OpenAI form fields as-is
optional_params = non_default_params
elif provider_config is not None: # custom audio transcription config
supported_params = provider_config.get_supported_openai_params(model=model)
_check_valid_arg(supported_params=supported_params)

View file

@ -4541,6 +4541,32 @@
"generateContent": true
}
},
"lokutor": {
"display_name": "Lokutor (`lokutor`)",
"url": "https://docs.litellm.ai/docs/providers/lokutor",
"endpoints": {
"chat_completions": false,
"responses": false,
"text_completion": false,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": true,
"audio_speech": true,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": false,
"interactions": false,
"systemone": false,
"a2a": false,
"generateContent": false
}
},
"zerogpu": {
"display_name": "ZeroGPU (`zerogpu`)",
"url": "https://docs.litellm.ai/docs/providers/zerogpu",

View file

@ -0,0 +1,99 @@
import json
from typing import Final
import httpx
import pytest
import respx
from openai import AsyncOpenAI
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
LOKUTOR_API_BASE: Final = "https://api.lokutor.com/v1"
WAV_BYTES: Final = b"RIFF\x24\x00\x00\x00WAVEfmt "
@pytest.fixture(autouse=True)
def _lokutor_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LOKUTOR_API_KEY", "lokutor-test-key")
monkeypatch.delenv("LOKUTOR_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
def test_prefixed_model_resolves_to_lokutor_base_and_env_key():
model, provider, api_key, api_base = get_llm_provider(model="lokutor/tts-1")
assert (model, provider, api_key, api_base) == ("tts-1", "lokutor", "lokutor-test-key", LOKUTOR_API_BASE)
def test_lokutor_is_a_registered_openai_compatible_provider():
assert "lokutor" in litellm.openai_compatible_providers
assert litellm.LlmProviders("lokutor") is litellm.LlmProviders.LOKUTOR
def test_speech_posts_to_lokutor_audio_speech_with_lokutor_key():
with respx.mock() as upstream:
route: Final = upstream.post(f"{LOKUTOR_API_BASE}/audio/speech").respond(
200, content=WAV_BYTES, headers={"content-type": "audio/wav"}
)
response: Final = litellm.speech(
model="lokutor/tts-1",
voice="alloy",
input="Hello from Lokutor.",
)
assert response.content == WAV_BYTES
request: Final = route.calls.last.request
assert request.headers["authorization"] == "Bearer lokutor-test-key"
assert json.loads(request.content) == {
"model": "tts-1",
"input": "Hello from Lokutor.",
"voice": "alloy",
}
def test_explicit_api_key_wins_over_the_provider_env_key():
with respx.mock() as upstream:
route: Final = upstream.post(f"{LOKUTOR_API_BASE}/audio/speech").respond(200, content=WAV_BYTES)
litellm.speech(model="lokutor/tts-1", voice="F1", input="Hi", api_key="explicit-key")
assert route.calls.last.request.headers["authorization"] == "Bearer explicit-key"
def test_transcription_posts_multipart_to_lokutor_audio_transcriptions():
with respx.mock() as upstream:
route: Final = upstream.post(f"{LOKUTOR_API_BASE}/audio/transcriptions").respond(
200, json={"text": "hello from lokutor"}
)
response: Final = litellm.transcription(
model="lokutor/whisper-1",
file=("hello.wav", WAV_BYTES, "audio/wav"),
language="en",
)
assert response.text == "hello from lokutor"
request: Final = route.calls.last.request
assert request.headers["authorization"] == "Bearer lokutor-test-key"
assert request.headers["content-type"].startswith("multipart/form-data")
assert b'name="language"' in request.content
assert b"\r\n\r\nen\r\n" in request.content
assert b"hello.wav" in request.content
@pytest.mark.asyncio
async def test_async_speech_uses_lokutor_endpoint():
requests: Final[list[httpx.Request]] = []
def respond(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, content=WAV_BYTES, headers={"content-type": "audio/wav"})
async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http_client:
client: Final = AsyncOpenAI(api_key="lokutor-test-key", base_url=LOKUTOR_API_BASE, http_client=http_client)
response: Final = await litellm.aspeech(model="lokutor/tts-1", voice="nova", input="Hola", client=client)
assert response.content == WAV_BYTES
assert str(requests[0].url) == f"{LOKUTOR_API_BASE}/audio/speech"