Skip to content

Commit 6ab0b75

Browse files
desmondcheongzxvenkateshdb
authored andcommitted
feat(embed_text): Support LM Studio as a provider (Eventual-Inc#5103)
## Changes Made Add LM Studio as a text embedding provider. For example ``` import daft from daft.ai.provider import load_provider from daft.functions.ai import embed_text provider = load_provider("lm_studio", base_url="http://127.0.0.1:1234") # This base_url parameter is optional if you're using the defaults for LM Studio. You can modify this as needed. model = "text-embedding-nomic-embed-text-v1.5" # Select a text embedding model that you've loaded into LM Studio. ( daft.read_huggingface("Open-Orca/OpenOrca") .with_column("embedding", embed_text(daft.col("response"), provider=provider, model=model)) .show() ) ```
1 parent 9ff124c commit 6ab0b75

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed

daft/ai/openai/__init__.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from daft.ai.provider import Provider
44

5-
from daft.ai.openai.text_embedder import OpenAITextEmbedderDescriptor
5+
from daft.ai.openai.text_embedder import OpenAITextEmbedderDescriptor, LMStudioTextEmbedderDescriptor
66
from typing import TYPE_CHECKING, Any, TypedDict
77

88
from typing_extensions import Unpack
@@ -13,6 +13,7 @@
1313
from daft.ai.typing import Options
1414

1515
__all__ = [
16+
"LMStudioProvider",
1617
"OpenAIProvider",
1718
]
1819

@@ -39,3 +40,38 @@ def get_text_embedder(self, model: str | None = None, **options: Any) -> TextEmb
3940

4041
def get_image_embedder(self, model: str | None = None, **options: Any) -> ImageEmbedderDescriptor:
4142
raise NotImplementedError("embed_image is not currently implemented for the OpenAI provider")
43+
44+
45+
class LMStudioProvider(OpenAIProvider):
46+
"""LM Studio provider that extends OpenAI provider with local server configuration.
47+
48+
LM Studio runs a local server that's API-compatible with OpenAI, so we can reuse
49+
all the OpenAI logic and just configure the base URL to point to the local instance.
50+
"""
51+
52+
def __init__(
53+
self,
54+
name: str | None = None,
55+
**options: Unpack[OpenAIProviderOptions],
56+
):
57+
if "api_key" not in options:
58+
options["api_key"] = "not-needed-for-lm-studio"
59+
if "base_url" not in options:
60+
options["base_url"] = "http://localhost:1234/v1"
61+
else:
62+
# Ensure base_url ends with /v1 for LM Studio compatibility.
63+
base_url = options["base_url"]
64+
if base_url is not None and not base_url.endswith("/v1"):
65+
options["base_url"] = base_url.rstrip("/") + "/v1"
66+
super().__init__(name or "lm_studio", **options)
67+
68+
def get_text_embedder(self, model: str | None = None, **options: Any) -> TextEmbedderDescriptor:
69+
return LMStudioTextEmbedderDescriptor(
70+
provider_name=self._name,
71+
provider_options=self._options,
72+
model_name=(model or "text-embedding-3-small"),
73+
model_options=options,
74+
)
75+
76+
def get_image_embedder(self, model: str | None = None, **options: Any) -> ImageEmbedderDescriptor:
77+
raise NotImplementedError("embed_image is not currently implemented for the LM Studio provider")

daft/ai/openai/text_embedder.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,48 @@ def instantiate(self) -> TextEmbedder:
8585
)
8686

8787

88+
@dataclass
89+
class LMStudioTextEmbedderDescriptor(TextEmbedderDescriptor):
90+
"""LM Studio text embedder descriptor that dynamically discovers model dimensions.
91+
92+
Unlike OpenAI, LM Studio can load different models with varying embedding dimensions.
93+
This descriptor queries the local server to get the actual model dimensions.
94+
"""
95+
96+
provider_name: str
97+
provider_options: OpenAIProviderOptions
98+
model_name: str
99+
model_options: Options
100+
101+
def get_provider(self) -> str:
102+
return "lm_studio"
103+
104+
def get_model(self) -> str:
105+
return self.model_name
106+
107+
def get_options(self) -> Options:
108+
return self.model_options
109+
110+
def get_dimensions(self) -> EmbeddingDimensions:
111+
try:
112+
client = OpenAI(**self.provider_options)
113+
response = client.embeddings.create(
114+
input="dimension probe",
115+
model=self.model_name,
116+
encoding_format="float",
117+
)
118+
size = len(response.data[0].embedding)
119+
return EmbeddingDimensions(size=size, dtype=DataType.float32())
120+
except Exception as ex:
121+
raise ValueError("Failed to determine embedding dimensions from LM Studio.") from ex
122+
123+
def instantiate(self) -> TextEmbedder:
124+
return OpenAITextEmbedder(
125+
client=OpenAI(**self.provider_options),
126+
model=self.model_name,
127+
)
128+
129+
88130
class OpenAITextEmbedder(TextEmbedder):
89131
"""The OpenAI TextEmbedder will batch across rows, and split a large row into a batch request when necessary.
90132

daft/ai/provider.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ def __init__(self, dependencies: list[str]):
1616
super().__init__(f"Missing required dependencies: {deps}. " f"Please install {deps} to use this provider.")
1717

1818

19+
def load_lm_studio(name: str | None = None, **options: Any) -> Provider:
20+
try:
21+
from daft.ai.openai import LMStudioProvider
22+
23+
return LMStudioProvider(name, **options)
24+
except ImportError as e:
25+
raise ProviderImportError(["openai"]) from e
26+
27+
1928
def load_openai(name: str | None = None, **options: Unpack[OpenAIProviderOptions]) -> Provider:
2029
try:
2130
from daft.ai.openai import OpenAIProvider
@@ -44,6 +53,7 @@ def load_transformers(name: str | None = None, **options: Any) -> Provider:
4453

4554

4655
PROVIDERS: dict[str, Callable[..., Provider]] = {
56+
"lm_studio": load_lm_studio,
4757
"openai": load_openai,
4858
"sentence_transformers": load_sentence_transformers,
4959
"transformers": load_transformers,

docs/modalities/text.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,32 @@ model = "text-embedding-3-small"
9595

9696
In this case you could either use a different model with a larger maximum context length, or could chunk your text into smaller segments before generating embeddings. See our [text embeddings guide](../examples/text-embeddings.md) for examples of text chunking strategies, or refer to the section below on [text chunking](#chunk-text-into-smaller-pieces).
9797

98+
#### Using LM Studio
99+
100+
[LM Studio](https://lmstudio.ai/) is a local AI model platform that lets you run Large Language Models like Qwen, Mistral, Gemma, or gpt-oss on your own machine. If you're running an LM studio server, Daft can use it as a provider for computing embeddings.
101+
102+
First install the optional OpenAI dependency for Daft. This is needed because LM studio uses an OpenAI-compatible API.
103+
104+
```bash
105+
pip install -U "daft[openai]"
106+
```
107+
108+
LM Studio runs on `localhost` port `1234` by default, but you can customize the `base_url` as needed in Daft. In this example, we use the [`nomic-ai/nomic-embed-text-v1.5`](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5) embedding model.
109+
110+
```python
111+
import daft
112+
from daft.ai.provider import load_provider
113+
from daft.functions.ai import embed_text
114+
115+
provider = load_provider("lm_studio", base_url="http://127.0.0.1:1235") # This base_url parameter is optional if you're using the defaults for LM Studio. You can modify this as needed.
116+
model = "text-embedding-nomic-embed-text-v1.5" # Select a text embedding model that you've loaded into LM Studio.
117+
118+
(
119+
daft.read_huggingface("Open-Orca/OpenOrca")
120+
.with_column("embedding", embed_text(daft.col("response"), provider=provider, model=model))
121+
.show()
122+
)
123+
```
98124

99125
### How to work with embeddings
100126

tests/ai/test_lm_studio.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
pytest.importorskip("openai")
6+
7+
from unittest.mock import patch
8+
9+
import numpy as np
10+
from openai.types.create_embedding_response import CreateEmbeddingResponse
11+
from openai.types.embedding import Embedding as OpenAIEmbedding
12+
13+
from daft.ai.openai import LMStudioProvider
14+
from daft.ai.protocols import TextEmbedder, TextEmbedderDescriptor
15+
16+
17+
@pytest.mark.parametrize(
18+
"model, embedding_dim",
19+
[
20+
("text-embedding-qwen3-embedding-0.6b", 1024),
21+
("text-embedding-nomic-embed-text-v1.5", 768),
22+
],
23+
)
24+
def test_lm_studio_text_embedder(model, embedding_dim):
25+
text_data = [
26+
"Alice was beginning to get very tired of sitting by her sister on the bank.",
27+
"So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid),",
28+
"whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies,",
29+
"when suddenly a White Rabbit with pink eyes ran close by her.",
30+
"There was nothing so very remarkable in that;",
31+
"nor did Alice think it so very much out of the way to hear the Rabbit say to itself, 'Oh dear! Oh dear! I shall be late!'",
32+
]
33+
34+
def mock_embedding_response(input_data):
35+
if isinstance(input_data, list):
36+
num_texts = len(input_data)
37+
else:
38+
num_texts = 1
39+
40+
embeddings = []
41+
for i in range(num_texts):
42+
embedding_values = [0.1] * embedding_dim
43+
embedding_obj = OpenAIEmbedding(embedding=embedding_values, index=i, object="embedding")
44+
embeddings.append(embedding_obj)
45+
46+
response = CreateEmbeddingResponse(
47+
data=embeddings, model=model, object="list", usage={"prompt_tokens": 0, "total_tokens": 0}
48+
)
49+
return response
50+
51+
with patch("openai.resources.embeddings.Embeddings.create") as mock_embed:
52+
mock_embed.side_effect = lambda **kwargs: mock_embedding_response(kwargs.get("input"))
53+
54+
descriptor = LMStudioProvider().get_text_embedder(model=model)
55+
assert isinstance(descriptor, TextEmbedderDescriptor)
56+
assert descriptor.get_provider() == "lm_studio"
57+
assert descriptor.get_model() == model
58+
assert descriptor.get_dimensions().size == embedding_dim
59+
60+
embedder = descriptor.instantiate()
61+
assert isinstance(embedder, TextEmbedder)
62+
embeddings = embedder.embed_text(text_data)
63+
assert len(embeddings) == len(text_data)
64+
assert all(isinstance(embedding, np.ndarray) for embedding in embeddings)
65+
assert all(len(embedding) == embedding_dim for embedding in embeddings)

0 commit comments

Comments
 (0)