feat(providers): add Phala as an OpenAI-compatible provider

This commit is contained in:
Kingsley 2026-10-25 12:05:08 +00:00
parent dddbaa803d
commit 412c5f82cd
No known key found for this signature in database
7 changed files with 204 additions and 0 deletions

View file

@ -1057,6 +1057,7 @@ openai_compatible_endpoints: Final[list[str]] = [
"https://api.cognition.ai/v1",
"https://inference.coralbricks.ai/v1",
"https://api.cortecs.ai/v1",
"https://inference.phala.com/v1",
"https://api.scx.ai/v1",
"https://api.prisminference.com/v1",
"https://api.reka.ai/v1",
@ -1137,6 +1138,7 @@ openai_compatible_providers: Final[list[str]] = [
"cognition",
"coralbricks",
"cortecs",
"phala",
"scx-ai",
"prism",
"sail",

View file

@ -192,6 +192,15 @@
"api_base_env": "CORTECS_API_BASE",
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
},
"phala": {
"base_url": "https://inference.phala.com/v1",
"api_key_env": "PHALA_AI_API_KEY",
"api_base_env": "PHALA_API_BASE",
"provider_info": {
"supports_reasoning": true
},
"supported_endpoints": ["/v1/chat/completions"]
},
"pinstripes": {
"base_url": "https://pinstripes.io/v1",
"api_key_env": "PINSTRIPES_API_KEY",

View file

@ -636,6 +636,24 @@
"interactions": false
}
},
"phala": {
"display_name": "Phala (`phala`)",
"url": "https://docs.phala.com/phala-cloud/confidential-ai/confidential-model/confidential-ai-api",
"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
}
},
"cortecs": {
"display_name": "Cortecs (`cortecs`)",
"url": "https://docs.litellm.ai/docs/providers/cortecs",

View file

@ -3119,6 +3119,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "PHALA",
"provider_display_name": "Phala",
"litellm_provider": "phala",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://inference.phala.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": "phala/z-ai/glm-5.3"
},
{
"provider": "PREDIBASE",
"provider_display_name": "Predibase",

View file

@ -4578,6 +4578,7 @@ class LlmProviders(str, Enum):
COGNITION = "cognition"
CORALBRICKS = "coralbricks"
CORTECS = "cortecs"
PHALA = "phala"
SCX_AI = "scx-ai"
PRISM = "prism"
OPENINFER = "openinfer"

View file

@ -2166,6 +2166,24 @@
"a2a": false
}
},
"phala": {
"display_name": "Phala (`phala`)",
"url": "https://docs.phala.com/phala-cloud/confidential-ai/confidential-model/confidential-ai-api",
"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
}
},
"pinstripes": {
"display_name": "Pinstripes (`pinstripes`)",
"url": "https://docs.litellm.ai/docs/providers/pinstripes",

View file

@ -0,0 +1,128 @@
import json
from typing import Final
import pytest
import respx
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
PHALA_API_BASE: Final = "https://inference.phala.com/v1"
PHALA_MODEL_ID: Final = "z-ai/glm-5.3"
PHALA_MODEL: Final = f"phala/{PHALA_MODEL_ID}"
@pytest.mark.parametrize("custom_provider", (None, "phala"))
def test_phala_resolves_environment_credentials(monkeypatch: pytest.MonkeyPatch, custom_provider: str | None) -> None:
monkeypatch.setenv("PHALA_AI_API_KEY", "phala-env-key")
model, provider, api_key, api_base = get_llm_provider(
model=PHALA_MODEL if custom_provider is None else PHALA_MODEL_ID,
custom_llm_provider=custom_provider,
)
assert (model, provider, api_key, api_base) == (PHALA_MODEL_ID, "phala", "phala-env-key", PHALA_API_BASE)
def test_phala_detected_from_api_base(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PHALA_AI_API_KEY", "phala-env-key")
model, provider, api_key, api_base = get_llm_provider(model=PHALA_MODEL_ID, api_base=PHALA_API_BASE)
assert (model, provider, api_key, api_base) == (PHALA_MODEL_ID, "phala", "phala-env-key", PHALA_API_BASE)
def test_phala_explicit_credentials_override_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PHALA_AI_API_KEY", "phala-env-key")
monkeypatch.setenv("PHALA_API_BASE", "https://environment.example/v1")
model, provider, api_key, api_base = get_llm_provider(
model=PHALA_MODEL,
api_base="https://explicit.example/v1",
api_key="phala-explicit-key",
)
assert (model, provider, api_key, api_base) == (
PHALA_MODEL_ID,
"phala",
"phala-explicit-key",
"https://explicit.example/v1",
)
@pytest.mark.asyncio
@pytest.mark.parametrize("is_async", (False, True))
@pytest.mark.parametrize("api_base", (PHALA_API_BASE, "https://override.example/v1"))
async def test_phala_chat_completion_request(monkeypatch: pytest.MonkeyPatch, is_async: bool, api_base: str) -> None:
monkeypatch.setenv("PHALA_AI_API_KEY", "phala-test-key")
monkeypatch.setenv("PHALA_API_BASE", api_base)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
with respx.mock() as upstream:
route: Final = upstream.post(f"{api_base}/chat/completions").respond(
200,
json={
"id": "chatcmpl_phala",
"object": "chat.completion",
"created": 1_789_550_000,
"model": PHALA_MODEL_ID,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello from Phala"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7},
},
)
response: Final = (
await litellm.acompletion(
model=PHALA_MODEL,
messages=[{"role": "user", "content": "Say hello"}],
reasoning_effort="low",
reasoning={"effort": "low"},
)
if is_async
else litellm.completion(
model=PHALA_MODEL,
messages=[{"role": "user", "content": "Say hello"}],
reasoning_effort="low",
reasoning={"effort": "low"},
)
)
request: Final = route.calls.last.request
assert route.call_count == 1
assert request.headers["authorization"] == "Bearer phala-test-key"
assert json.loads(request.content) == {
"model": PHALA_MODEL_ID,
"messages": [{"role": "user", "content": "Say hello"}],
"reasoning_effort": "low",
"reasoning": {"effort": "low"},
}
assert isinstance(response, litellm.ModelResponse)
assert response.choices[0].message.content == "Hello from Phala"
assert response.usage.total_tokens == 7
@pytest.mark.asyncio
async def test_phala_available_in_add_model() -> None:
from litellm.proxy.public_endpoints.public_endpoints import get_provider_fields, get_supported_providers
assert "phala" in await get_supported_providers()
providers: Final = await get_provider_fields()
phala: Final = next(provider for provider in providers if provider.litellm_provider == "phala")
assert phala.provider_display_name == "Phala"
assert phala.default_model_placeholder == PHALA_MODEL
assert {field.key: field.required for field in phala.credential_fields} == {"api_base": False, "api_key": True}
@pytest.mark.asyncio
async def test_phala_listed_for_chat_completions() -> None:
from litellm.proxy.public_endpoints.public_endpoints import get_supported_endpoints
endpoints: Final = await get_supported_endpoints()
chat: Final = next(endpoint for endpoint in endpoints.endpoints if endpoint.key == "chat_completions")
assert any(provider.slug == "phala" and provider.display_name == "Phala" for provider in chat.providers)