mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge 144dce7815 into c5da69cd13
This commit is contained in:
commit
0ca221d7d5
12 changed files with 203 additions and 1 deletions
|
|
@ -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) | ✅ | ✅ | ✅ | ✅ | | | | | | |
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ const OPENAI_COMPATIBLE_PROVIDERS: &[&str] = &[
|
|||
"cognition",
|
||||
"scx-ai",
|
||||
"zerogpu",
|
||||
"lokutor",
|
||||
];
|
||||
|
||||
impl ExceptionFamily {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4774,6 +4774,7 @@ class LlmProviders(str, Enum):
|
|||
INFERENCEAPIS = "inferenceapis"
|
||||
HOSTINGER_ROUTER = "hostinger_router"
|
||||
ZEROGPU = "zerogpu"
|
||||
LOKUTOR = "lokutor"
|
||||
DARKBLOOM = "darkbloom"
|
||||
META = "meta"
|
||||
SAIL = "sail"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
99
tests/unit/llms/openai_like/test_lokutor_provider.py
Normal file
99
tests/unit/llms/openai_like/test_lokutor_provider.py
Normal 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"
|
||||
Loading…
Add table
Reference in a new issue