Skip to content

Commit a89c549

Browse files
authored
feat(ollama): add basic auth support (#32328)
support for URL authentication in the format `https://user:password@host:port` for all LangChain Ollama clients. Related to #32327 and #25055
1 parent a336afa commit a89c549

File tree

6 files changed

+409
-61
lines changed

6 files changed

+409
-61
lines changed

libs/partners/ollama/langchain_ollama/_utils.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
"""Utility function to validate Ollama models."""
22

3+
from __future__ import annotations
4+
5+
import base64
6+
from typing import Optional
7+
from urllib.parse import unquote, urlparse
8+
39
from httpx import ConnectError
410
from ollama import Client, ResponseError
511

@@ -40,3 +46,70 @@ def validate_model(client: Client, model_name: str) -> None:
4046
"Please check your Ollama server logs."
4147
)
4248
raise ValueError(msg) from e
49+
50+
51+
def parse_url_with_auth(
52+
url: Optional[str],
53+
) -> tuple[Optional[str], Optional[dict[str, str]]]:
54+
"""Parse URL and extract `userinfo` credentials for headers.
55+
56+
Handles URLs of the form: `https://user:password@host:port/path`
57+
58+
Args:
59+
url: The URL to parse.
60+
61+
Returns:
62+
A tuple of ``(cleaned_url, headers_dict)`` where:
63+
- ``cleaned_url`` is the URL without authentication credentials if any were
64+
found. Otherwise, returns the original URL.
65+
- ``headers_dict`` contains Authorization header if credentials were found.
66+
"""
67+
if not url:
68+
return None, None
69+
70+
parsed = urlparse(url)
71+
if not parsed.scheme or not parsed.netloc or not parsed.hostname:
72+
return None, None
73+
if not parsed.username:
74+
return url, None
75+
76+
# Handle case where password might be empty string or None
77+
password = parsed.password or ""
78+
79+
# Create basic auth header (decode percent-encoding)
80+
username = unquote(parsed.username)
81+
password = unquote(password)
82+
credentials = f"{username}:{password}"
83+
encoded_credentials = base64.b64encode(credentials.encode()).decode()
84+
headers = {"Authorization": f"Basic {encoded_credentials}"}
85+
86+
# Strip credentials from URL
87+
cleaned_netloc = parsed.hostname or ""
88+
if parsed.port:
89+
cleaned_netloc += f":{parsed.port}"
90+
91+
cleaned_url = f"{parsed.scheme}://{cleaned_netloc}"
92+
if parsed.path:
93+
cleaned_url += parsed.path
94+
if parsed.query:
95+
cleaned_url += f"?{parsed.query}"
96+
if parsed.fragment:
97+
cleaned_url += f"#{parsed.fragment}"
98+
99+
return cleaned_url, headers
100+
101+
102+
def merge_auth_headers(
103+
client_kwargs: dict,
104+
auth_headers: Optional[dict[str, str]],
105+
) -> None:
106+
"""Merge authentication headers into client kwargs in-place.
107+
108+
Args:
109+
client_kwargs: The client kwargs dict to update.
110+
auth_headers: Headers to merge (typically from ``parse_url_with_auth``).
111+
"""
112+
if auth_headers:
113+
headers = client_kwargs.get("headers", {})
114+
headers.update(auth_headers)
115+
client_kwargs["headers"] = headers

libs/partners/ollama/langchain_ollama/chat_models.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,10 @@
77
import logging
88
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
99
from operator import itemgetter
10-
from typing import (
11-
Any,
12-
Callable,
13-
Literal,
14-
Optional,
15-
Union,
16-
cast,
17-
)
10+
from typing import Any, Callable, Literal, Optional, Union, cast
1811
from uuid import uuid4
1912

20-
from langchain_core.callbacks import (
21-
CallbackManagerForLLMRun,
22-
)
13+
from langchain_core.callbacks import CallbackManagerForLLMRun
2314
from langchain_core.callbacks.manager import AsyncCallbackManagerForLLMRun
2415
from langchain_core.exceptions import OutputParserException
2516
from langchain_core.language_models import LanguageModelInput
@@ -57,7 +48,7 @@
5748
from pydantic.v1 import BaseModel as BaseModelV1
5849
from typing_extensions import Self, is_typeddict
5950

60-
from ._utils import validate_model
51+
from ._utils import merge_auth_headers, parse_url_with_auth, validate_model
6152

6253
log = logging.getLogger(__name__)
6354

@@ -592,32 +583,50 @@ class Multiply(BaseModel):
592583
"""How long the model will stay loaded into memory."""
593584

594585
base_url: Optional[str] = None
595-
"""Base url the model is hosted under."""
586+
"""Base url the model is hosted under.
587+
588+
If none, defaults to the Ollama client default.
589+
590+
Supports `userinfo` auth in the format `http://username:password@localhost:11434`.
591+
Useful if your Ollama server is behind a proxy.
592+
593+
!!! warning
594+
`userinfo` is not secure and should only be used for local testing or
595+
in secure environments. Avoid using it in production or over unsecured
596+
networks.
597+
598+
!!! note
599+
If using `userinfo`, ensure that the Ollama server is configured to
600+
accept and validate these credentials.
601+
602+
!!! note
603+
`userinfo` headers are passed to both sync and async clients.
604+
605+
"""
596606

597607
client_kwargs: Optional[dict] = {}
598-
"""Additional kwargs to pass to the httpx clients.
608+
"""Additional kwargs to pass to the httpx clients. Pass headers in here.
599609
600610
These arguments are passed to both synchronous and async clients.
601611
602612
Use ``sync_client_kwargs`` and ``async_client_kwargs`` to pass different arguments
603613
to synchronous and asynchronous clients.
604-
605614
"""
606615

607616
async_client_kwargs: Optional[dict] = {}
608-
"""Additional kwargs to merge with ``client_kwargs`` before
609-
passing to the httpx AsyncClient.
617+
"""Additional kwargs to merge with ``client_kwargs`` before passing to httpx client.
610618
611-
`Full list of params. <https://www.python-httpx.org/api/#asyncclient>`__
619+
These are clients unique to the async client; for shared args use ``client_kwargs``.
612620
621+
For a full list of the params, see the `httpx documentation <https://www.python-httpx.org/api/#asyncclient>`__.
613622
"""
614623

615624
sync_client_kwargs: Optional[dict] = {}
616-
"""Additional kwargs to merge with ``client_kwargs`` before
617-
passing to the httpx Client.
625+
"""Additional kwargs to merge with ``client_kwargs`` before passing to httpx client.
618626
619-
`Full list of params. <https://www.python-httpx.org/api/#client>`__
627+
These are clients unique to the sync client; for shared args use ``client_kwargs``.
620628
629+
For a full list of the params, see the `httpx documentation <https://www.python-httpx.org/api/#client>`__.
621630
"""
622631

623632
_client: Client = PrivateAttr()
@@ -682,6 +691,9 @@ def _set_clients(self) -> Self:
682691
"""Set clients to use for ollama."""
683692
client_kwargs = self.client_kwargs or {}
684693

694+
cleaned_url, auth_headers = parse_url_with_auth(self.base_url)
695+
merge_auth_headers(client_kwargs, auth_headers)
696+
685697
sync_client_kwargs = client_kwargs
686698
if self.sync_client_kwargs:
687699
sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}
@@ -690,8 +702,8 @@ def _set_clients(self) -> Self:
690702
if self.async_client_kwargs:
691703
async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}
692704

693-
self._client = Client(host=self.base_url, **sync_client_kwargs)
694-
self._async_client = AsyncClient(host=self.base_url, **async_client_kwargs)
705+
self._client = Client(host=cleaned_url, **sync_client_kwargs)
706+
self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)
695707
if self.validate_model_on_init:
696708
validate_model(self._client, self.model)
697709
return self

libs/partners/ollama/langchain_ollama/embeddings.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@
66

77
from langchain_core.embeddings import Embeddings
88
from ollama import AsyncClient, Client
9-
from pydantic import (
10-
BaseModel,
11-
ConfigDict,
12-
PrivateAttr,
13-
model_validator,
14-
)
9+
from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator
1510
from typing_extensions import Self
1611

17-
from ._utils import validate_model
12+
from ._utils import merge_auth_headers, parse_url_with_auth, validate_model
1813

1914

2015
class OllamaEmbeddings(BaseModel, Embeddings):
@@ -134,32 +129,50 @@ class OllamaEmbeddings(BaseModel, Embeddings):
134129
"""
135130

136131
base_url: Optional[str] = None
137-
"""Base url the model is hosted under."""
132+
"""Base url the model is hosted under.
133+
134+
If none, defaults to the Ollama client default.
135+
136+
Supports `userinfo` auth in the format `http://username:password@localhost:11434`.
137+
Useful if your Ollama server is behind a proxy.
138+
139+
!!! warning
140+
`userinfo` is not secure and should only be used for local testing or
141+
in secure environments. Avoid using it in production or over unsecured
142+
networks.
143+
144+
!!! note
145+
If using `userinfo`, ensure that the Ollama server is configured to
146+
accept and validate these credentials.
147+
148+
!!! note
149+
`userinfo` headers are passed to both sync and async clients.
150+
151+
"""
138152

139153
client_kwargs: Optional[dict] = {}
140-
"""Additional kwargs to pass to the httpx clients.
154+
"""Additional kwargs to pass to the httpx clients. Pass headers in here.
141155
142156
These arguments are passed to both synchronous and async clients.
143157
144158
Use ``sync_client_kwargs`` and ``async_client_kwargs`` to pass different arguments
145159
to synchronous and asynchronous clients.
146-
147160
"""
148161

149162
async_client_kwargs: Optional[dict] = {}
150-
"""Additional kwargs to merge with ``client_kwargs`` before passing to the httpx
151-
AsyncClient.
163+
"""Additional kwargs to merge with ``client_kwargs`` before passing to httpx client.
152164
153-
For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#asyncclient>`__.
165+
These are clients unique to the async client; for shared args use ``client_kwargs``.
154166
167+
For a full list of the params, see the `httpx documentation <https://www.python-httpx.org/api/#asyncclient>`__.
155168
"""
156169

157170
sync_client_kwargs: Optional[dict] = {}
158-
"""Additional kwargs to merge with ``client_kwargs`` before
159-
passing to the HTTPX Client.
171+
"""Additional kwargs to merge with ``client_kwargs`` before passing to httpx client.
160172
161-
For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#client>`__.
173+
These are clients unique to the sync client; for shared args use ``client_kwargs``.
162174
175+
For a full list of the params, see the `httpx documentation <https://www.python-httpx.org/api/#client>`__.
163176
"""
164177

165178
_client: Optional[Client] = PrivateAttr(default=None)
@@ -261,6 +274,9 @@ def _set_clients(self) -> Self:
261274
"""Set clients to use for Ollama."""
262275
client_kwargs = self.client_kwargs or {}
263276

277+
cleaned_url, auth_headers = parse_url_with_auth(self.base_url)
278+
merge_auth_headers(client_kwargs, auth_headers)
279+
264280
sync_client_kwargs = client_kwargs
265281
if self.sync_client_kwargs:
266282
sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}
@@ -269,8 +285,8 @@ def _set_clients(self) -> Self:
269285
if self.async_client_kwargs:
270286
async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}
271287

272-
self._client = Client(host=self.base_url, **sync_client_kwargs)
273-
self._async_client = AsyncClient(host=self.base_url, **async_client_kwargs)
288+
self._client = Client(host=cleaned_url, **sync_client_kwargs)
289+
self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)
274290
if self.validate_model_on_init:
275291
validate_model(self._client, self.model)
276292
return self

libs/partners/ollama/langchain_ollama/llms.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
from __future__ import annotations
44

55
from collections.abc import AsyncIterator, Iterator, Mapping
6-
from typing import (
7-
Any,
8-
Literal,
9-
Optional,
10-
Union,
11-
)
6+
from typing import Any, Literal, Optional, Union
127

138
from langchain_core.callbacks import (
149
AsyncCallbackManagerForLLMRun,
@@ -20,7 +15,7 @@
2015
from pydantic import PrivateAttr, model_validator
2116
from typing_extensions import Self
2217

23-
from ._utils import validate_model
18+
from ._utils import merge_auth_headers, parse_url_with_auth, validate_model
2419

2520

2621
class OllamaLLM(BaseLLM):
@@ -213,32 +208,50 @@ class OllamaLLM(BaseLLM):
213208
"""How long the model will stay loaded into memory."""
214209

215210
base_url: Optional[str] = None
216-
"""Base url the model is hosted under."""
211+
"""Base url the model is hosted under.
212+
213+
If none, defaults to the Ollama client default.
214+
215+
Supports `userinfo` auth in the format `http://username:password@localhost:11434`.
216+
Useful if your Ollama server is behind a proxy.
217+
218+
!!! warning
219+
`userinfo` is not secure and should only be used for local testing or
220+
in secure environments. Avoid using it in production or over unsecured
221+
networks.
222+
223+
!!! note
224+
If using `userinfo`, ensure that the Ollama server is configured to
225+
accept and validate these credentials.
226+
227+
!!! note
228+
`userinfo` headers are passed to both sync and async clients.
229+
230+
"""
217231

218232
client_kwargs: Optional[dict] = {}
219-
"""Additional kwargs to pass to the httpx clients.
233+
"""Additional kwargs to pass to the httpx clients. Pass headers in here.
220234
221235
These arguments are passed to both synchronous and async clients.
222236
223237
Use ``sync_client_kwargs`` and ``async_client_kwargs`` to pass different arguments
224238
to synchronous and asynchronous clients.
225-
226239
"""
227240

228241
async_client_kwargs: Optional[dict] = {}
229-
"""Additional kwargs to merge with ``client_kwargs`` before passing to the HTTPX
230-
AsyncClient.
242+
"""Additional kwargs to merge with ``client_kwargs`` before passing to httpx client.
231243
232-
For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#asyncclient>`__.
244+
These are clients unique to the async client; for shared args use ``client_kwargs``.
233245
246+
For a full list of the params, see the `httpx documentation <https://www.python-httpx.org/api/#asyncclient>`__.
234247
"""
235248

236249
sync_client_kwargs: Optional[dict] = {}
237-
"""Additional kwargs to merge with ``client_kwargs`` before
238-
passing to the HTTPX Client.
250+
"""Additional kwargs to merge with ``client_kwargs`` before passing to httpx client.
239251
240-
For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#client>`__.
252+
These are clients unique to the sync client; for shared args use ``client_kwargs``.
241253
254+
For a full list of the params, see the `httpx documentation <https://www.python-httpx.org/api/#client>`__.
242255
"""
243256

244257
_client: Optional[Client] = PrivateAttr(default=None)
@@ -310,6 +323,9 @@ def _set_clients(self) -> Self:
310323
"""Set clients to use for ollama."""
311324
client_kwargs = self.client_kwargs or {}
312325

326+
cleaned_url, auth_headers = parse_url_with_auth(self.base_url)
327+
merge_auth_headers(client_kwargs, auth_headers)
328+
313329
sync_client_kwargs = client_kwargs
314330
if self.sync_client_kwargs:
315331
sync_client_kwargs = {**sync_client_kwargs, **self.sync_client_kwargs}
@@ -318,8 +334,8 @@ def _set_clients(self) -> Self:
318334
if self.async_client_kwargs:
319335
async_client_kwargs = {**async_client_kwargs, **self.async_client_kwargs}
320336

321-
self._client = Client(host=self.base_url, **sync_client_kwargs)
322-
self._async_client = AsyncClient(host=self.base_url, **async_client_kwargs)
337+
self._client = Client(host=cleaned_url, **sync_client_kwargs)
338+
self._async_client = AsyncClient(host=cleaned_url, **async_client_kwargs)
323339
if self.validate_model_on_init:
324340
validate_model(self._client, self.model)
325341
return self

0 commit comments

Comments
 (0)