Skip to content

Commit ff21568

Browse files
committed
Add OVHcloud AI Endpoints provider
1 parent 316db10 commit ff21568

File tree

8 files changed

+269
-33
lines changed

8 files changed

+269
-33
lines changed

docs/source/en/guides/inference.md

Lines changed: 31 additions & 31 deletions
Large diffs are not rendered by default.

src/huggingface_hub/inference/_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class InferenceClient:
135135
Note: for better compatibility with OpenAI's client, `model` has been aliased as `base_url`. Those 2
136136
arguments are mutually exclusive. If a URL is passed as `model` or `base_url` for chat completion, the `(/v1)/chat/completions` suffix path will be appended to the URL.
137137
provider (`str`, *optional*):
138-
Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"clarifai"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `"publicai"`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"`, `"wavespeed"` or `"zai-org"`.
138+
Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"clarifai"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `"ovhcloud"`, `"publicai"`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"`, `"wavespeed"` or `"zai-org"`.
139139
Defaults to "auto" i.e. the first of the providers available for the model, sorted by the user's order in https://hf.co/settings/inference-providers.
140140
If model is a URL or `base_url` is passed, then `provider` is not used.
141141
token (`str`, *optional*):

src/huggingface_hub/inference/_generated/_async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class AsyncInferenceClient:
126126
Note: for better compatibility with OpenAI's client, `model` has been aliased as `base_url`. Those 2
127127
arguments are mutually exclusive. If a URL is passed as `model` or `base_url` for chat completion, the `(/v1)/chat/completions` suffix path will be appended to the URL.
128128
provider (`str`, *optional*):
129-
Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"clarifai"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `"publicai"`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"`, `"wavespeed"` or `"zai-org"`.
129+
Name of the provider to use for inference. Can be `"black-forest-labs"`, `"cerebras"`, `"clarifai"`, `"cohere"`, `"fal-ai"`, `"featherless-ai"`, `"fireworks-ai"`, `"groq"`, `"hf-inference"`, `"hyperbolic"`, `"nebius"`, `"novita"`, `"nscale"`, `"openai"`, `"ovhcloud"`, `"publicai"`, `"replicate"`, `"sambanova"`, `"scaleway"`, `"together"`, `"wavespeed"` or `"zai-org"`.
130130
Defaults to "auto" i.e. the first of the providers available for the model, sorted by the user's order in https://hf.co/settings/inference-providers.
131131
If model is a URL or `base_url` is passed, then `provider` is not used.
132132
token (`str`, *optional*):

src/huggingface_hub/inference/_providers/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .novita import NovitaConversationalTask, NovitaTextGenerationTask, NovitaTextToVideoTask
3939
from .nscale import NscaleConversationalTask, NscaleTextToImageTask
4040
from .openai import OpenAIConversationalTask
41+
from .ovhcloud import OVHcloudAIEndpointsAutomaticSpeechRecognitionTask, OVHcloudAIEndpointsConversationalTask, OVHcloudAIEndpointsFeatureExtractionTask, OVHcloudAIEndpointsTextToImageTask
4142
from .publicai import PublicAIConversationalTask
4243
from .replicate import ReplicateImageToImageTask, ReplicateTask, ReplicateTextToImageTask, ReplicateTextToSpeechTask
4344
from .sambanova import SambanovaConversationalTask, SambanovaFeatureExtractionTask
@@ -70,6 +71,7 @@
7071
"novita",
7172
"nscale",
7273
"openai",
74+
"ovhcloud",
7375
"publicai",
7476
"replicate",
7577
"sambanova",
@@ -166,6 +168,12 @@
166168
"openai": {
167169
"conversational": OpenAIConversationalTask(),
168170
},
171+
"ovhcloud": {
172+
"conversational": OVHcloudAIEndpointsConversationalTask(),
173+
"text-to-image": OVHcloudAIEndpointsTextToImageTask(),
174+
"feature-extraction": OVHcloudAIEndpointsFeatureExtractionTask(),
175+
"automatic-speech-recognition": OVHcloudAIEndpointsAutomaticSpeechRecognitionTask(),
176+
},
169177
"publicai": {
170178
"conversational": PublicAIConversationalTask(),
171179
},

src/huggingface_hub/inference/_providers/_common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"hyperbolic": {},
3333
"nebius": {},
3434
"nscale": {},
35+
"ovhcloud": {},
3536
"replicate": {},
3637
"sambanova": {},
3738
"scaleway": {},
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import base64
2+
from abc import ABC
3+
from typing import Any, Dict, Optional, Union
4+
5+
from huggingface_hub.hf_api import InferenceProviderMapping
6+
from huggingface_hub.inference._common import RequestParameters, _as_dict
7+
from huggingface_hub.inference._providers._common import (
8+
TaskProviderHelper,
9+
filter_none,
10+
)
11+
12+
_PROVIDER = "ovhcloud"
13+
_BASE_URL = "https://oai.endpoints.kepler.ai.cloud.ovh.net"
14+
15+
class OVHcloudAIEndpointsTask(TaskProviderHelper, ABC):
16+
def __init__(self, task: str):
17+
super().__init__(provider=_PROVIDER, base_url=_BASE_URL, task=task)
18+
19+
def _prepare_route(self, mapped_model: str, api_key: str) -> str:
20+
if self.task == "text-to-image":
21+
return "/v1/images/generations"
22+
elif self.task == "conversational":
23+
return "/v1/chat/completions"
24+
elif self.task == "feature-extraction":
25+
return "/v1/embeddings"
26+
elif self.task == "automatic-speech-recognition":
27+
return "/v1/audio/transcriptions"
28+
raise ValueError(f"Unsupported task '{self.task}' for OVHcloud AI Endpoints.")
29+
30+
def _prepare_payload_as_dict(
31+
self, messages: Any, parameters: Dict, provider_mapping_info: InferenceProviderMapping
32+
) -> Optional[Dict]:
33+
return {"messages": messages, "model": provider_mapping_info.provider_id, **filter_none(parameters)}
34+
35+
36+
class OVHcloudAIEndpointsConversationalTask(OVHcloudAIEndpointsTask):
37+
def __init__(self):
38+
super().__init__("conversational")
39+
40+
def _prepare_payload_as_dict(
41+
self, messages: Any, parameters: dict, provider_mapping_info: InferenceProviderMapping
42+
) -> Optional[dict]:
43+
return super()._prepare_payload_as_dict(messages, parameters, provider_mapping_info)
44+
45+
46+
class OVHcloudAIEndpointsTextToImageTask(OVHcloudAIEndpointsTask):
47+
def __init__(self):
48+
super().__init__("text-to-image")
49+
50+
def _prepare_payload_as_dict(
51+
self, inputs: Any, parameters: dict, provider_mapping_info: InferenceProviderMapping
52+
) -> Optional[dict]:
53+
mapped_model = provider_mapping_info.provider_id
54+
return {"prompt": inputs, "model": mapped_model, **filter_none(parameters)}
55+
56+
def get_response(self, response: Union[bytes, dict], request_params: Optional[RequestParameters] = None) -> Any:
57+
response_dict = _as_dict(response)
58+
return base64.b64decode(response_dict["data"][0]["b64_json"])
59+
60+
class OVHcloudAIEndpointsFeatureExtractionTask(OVHcloudAIEndpointsTask):
61+
def __init__(self):
62+
super().__init__("feature-extraction")
63+
64+
def _prepare_payload_as_dict(
65+
self, inputs: Any, parameters: Dict, provider_mapping_info: InferenceProviderMapping
66+
) -> Optional[Dict]:
67+
return {"input": inputs, "model": provider_mapping_info.provider_id, **filter_none(parameters)}
68+
69+
def get_response(self, response: Union[bytes, dict], request_params: Optional[RequestParameters] = None) -> Any:
70+
embeddings = _as_dict(response)["data"]
71+
return [embedding["embedding"] for embedding in embeddings]
72+
73+
class OVHcloudAIEndpointsAutomaticSpeechRecognitionTask(OVHcloudAIEndpointsTask):
74+
def __init__(self):
75+
super().__init__("automatic-speech-recognition")
76+
77+
def _prepare_payload_as_dict(
78+
self, inputs: Any, parameters: dict, provider_mapping_info: InferenceProviderMapping
79+
) -> Optional[dict]:
80+
return {"file": inputs, "model": provider_mapping_info.provider_id, **filter_none(parameters)}
81+
82+
def get_response(self, response: Union[bytes, dict], request_params: Optional[RequestParameters] = None) -> Any:
83+
response_dict = _as_dict(response)
84+
return response_dict["text"]

tests/test_inference_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@
117117
"text-generation": "NousResearch/Nous-Hermes-Llama2-13b",
118118
"conversational": "meta-llama/Llama-3.1-8B-Instruct",
119119
},
120+
"ovhcloud": {
121+
"automatic-speech-recognition": "openai/whisper-large-v3",
122+
"conversational": "meta-llama/Llama-3.1-8B-Instruct",
123+
"feature-extraction": "BAAI/bge-m3",
124+
"text-to-image": "stabilityai/stable-diffusion-xl-base-1.0",
125+
},
120126
"replicate": {
121127
"text-to-image": "ByteDance/SDXL-Lightning",
122128
},

tests/test_inference_providers.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from huggingface_hub.inference._providers.novita import NovitaConversationalTask, NovitaTextGenerationTask
4747
from huggingface_hub.inference._providers.nscale import NscaleConversationalTask, NscaleTextToImageTask
4848
from huggingface_hub.inference._providers.openai import OpenAIConversationalTask
49+
from huggingface_hub.inference._providers.ovhcloud import OVHcloudAIEndpointsAutomaticSpeechRecognitionTask, OVHcloudAIEndpointsConversationalTask, OVHcloudAIEndpointsFeatureExtractionTask, OVHcloudAIEndpointsTextToImageTask
4950
from huggingface_hub.inference._providers.publicai import PublicAIConversationalTask
5051
from huggingface_hub.inference._providers.replicate import (
5152
ReplicateImageToImageTask,
@@ -1422,6 +1423,142 @@ def test_prepare_url(self):
14221423
assert helper._prepare_url("sk-XXXXXX", "gpt-4o-mini") == "https://api.openai.com/v1/chat/completions"
14231424

14241425

1426+
class TestOVHcloudAIEndpointsProvider:
1427+
def test_prepare_hf_url_conversational(self):
1428+
helper = OVHcloudAIEndpointsConversationalTask()
1429+
url = helper._prepare_url("hf_token", "username/repo_name")
1430+
assert url == "https://router.huggingface.co/ovhcloud/v1/chat/completions"
1431+
1432+
def test_prepare_url_conversational(self):
1433+
helper = OVHcloudAIEndpointsConversationalTask()
1434+
url = helper._prepare_url("ovhcloud_token", "username/repo_name")
1435+
assert url == "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions"
1436+
1437+
def test_prepare_payload_as_dict(self):
1438+
helper = OVHcloudAIEndpointsConversationalTask()
1439+
payload = helper._prepare_payload_as_dict(
1440+
[
1441+
{"role": "system", "content": "You are a helpful assistant"},
1442+
{"role": "user", "content": "Hello!"},
1443+
],
1444+
{
1445+
"max_tokens": 512,
1446+
"temperature": 0.15,
1447+
"top_p": 1,
1448+
"presence_penalty": 0,
1449+
"stream": True,
1450+
},
1451+
InferenceProviderMapping(
1452+
provider="ovhcloud",
1453+
hf_model_id="meta-llama/Llama-3.1-8B-Instruct",
1454+
providerId="Llama-3.1-8B-Instruct",
1455+
task="conversational",
1456+
status="live",
1457+
),
1458+
)
1459+
assert payload == {
1460+
"max_tokens": 512,
1461+
"messages": [
1462+
{"content": "You are a helpful assistant", "role": "system"},
1463+
{"role": "user", "content": "Hello!"},
1464+
],
1465+
"model": "Llama-3.1-8B-Instruct",
1466+
"presence_penalty": 0,
1467+
"stream": True,
1468+
"temperature": 0.15,
1469+
"top_p": 1,
1470+
}
1471+
1472+
def test_prepare_url_feature_extraction(self):
1473+
helper = OVHcloudAIEndpointsFeatureExtractionTask()
1474+
assert (
1475+
helper._prepare_url("hf_token", "username/repo_name")
1476+
== "https://router.huggingface.co/ovhcloud/v1/embeddings"
1477+
)
1478+
1479+
def test_prepare_payload_as_dict_feature_extraction(self):
1480+
helper = OVHcloudAIEndpointsFeatureExtractionTask()
1481+
payload = helper._prepare_payload_as_dict(
1482+
"Example text to embed",
1483+
{"truncate": True},
1484+
InferenceProviderMapping(
1485+
provider="ovhcloud",
1486+
hf_model_id="BAAI/bge-m3",
1487+
providerId="BGE-M3",
1488+
task="feature-extraction",
1489+
status="live",
1490+
),
1491+
)
1492+
assert payload == {"input": "Example text to embed", "model": "BGE-M3", "truncate": True}
1493+
1494+
def test_prepare_url_text_to_image(self):
1495+
helper = OVHcloudAIEndpointsTextToImageTask()
1496+
assert (
1497+
helper._prepare_url("hf_token", "username/repo_name")
1498+
== "https://router.huggingface.co/ovhcloud/v1/images/generations"
1499+
)
1500+
1501+
url = helper._prepare_url("ovhcloud_token", "username/repo_name")
1502+
assert url == "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/images/generations"
1503+
1504+
def test_prepare_payload_as_dict_text_to_image(self):
1505+
helper = OVHcloudAIEndpointsTextToImageTask()
1506+
payload = helper._prepare_payload_as_dict(
1507+
inputs="a beautiful cat",
1508+
provider_mapping_info=InferenceProviderMapping(
1509+
provider="ovhcloud",
1510+
hf_model_id="stabilityai/stable-diffusion-xl-base-1.0",
1511+
providerId="stable-diffusion-xl-base-v10",
1512+
task="text-to-image",
1513+
status="live",
1514+
),
1515+
parameters={}
1516+
)
1517+
assert payload == {
1518+
"prompt": "a beautiful cat",
1519+
"model": "stable-diffusion-xl-base-v10",
1520+
}
1521+
1522+
def test_text_to_image_get_response(self):
1523+
helper = OVHcloudAIEndpointsTextToImageTask()
1524+
response = helper.get_response({"data": [{"b64_json": base64.b64encode(b"image_bytes").decode()}]})
1525+
assert response == b"image_bytes"
1526+
1527+
def test_prepare_url_automatic_speech_recognition(self):
1528+
helper = OVHcloudAIEndpointsAutomaticSpeechRecognitionTask()
1529+
assert (
1530+
helper._prepare_url("hf_token", "username/repo_name")
1531+
== "https://router.huggingface.co/ovhcloud/v1/audio/transcriptions"
1532+
)
1533+
1534+
url = helper._prepare_url("ovhcloud_token", "username/repo_name")
1535+
assert url == "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/audio/transcriptions"
1536+
1537+
def test_prepare_payload_as_dict_automatic_speech_recognition(self):
1538+
helper = OVHcloudAIEndpointsAutomaticSpeechRecognitionTask()
1539+
1540+
payload = helper._prepare_payload_as_dict(
1541+
f"data:audio/mpeg;base64,{base64.b64encode(b'dummy_audio_data').decode()}",
1542+
{},
1543+
InferenceProviderMapping(
1544+
provider="ovhcloud",
1545+
hf_model_id="openai/whisper-large-v3",
1546+
providerId="whisper-large-v3",
1547+
task="automatic-speech-recognition",
1548+
status="live",
1549+
),
1550+
)
1551+
assert payload == {
1552+
"file": f"data:audio/mpeg;base64,{base64.b64encode(b'dummy_audio_data').decode()}",
1553+
"model": "whisper-large-v3",
1554+
}
1555+
1556+
def test_automatic_speech_recognition_get_response(self):
1557+
helper = OVHcloudAIEndpointsAutomaticSpeechRecognitionTask()
1558+
response = helper.get_response({"text": "Hello world"})
1559+
assert response == "Hello world"
1560+
1561+
14251562
class TestReplicateProvider:
14261563
def test_prepare_headers(self):
14271564
helper = ReplicateTask("text-to-image")

0 commit comments

Comments
 (0)