Submission checklist
Package (Required)
Related Issues / PRs
#38899 (same defect, reported 2026-07-17 and auto-closed without review)
Reproduction Steps / Example Code (Python)
from langchain_ollama import ChatOllama
# A config dict the caller owns and reuses, with their own headers nested inside.
caller_headers = {"X-Tenant": "acme"}
shared_kwargs = {"headers": caller_headers}
ChatOllama(
model="llama3",
base_url="https://alice:s3cret@ollama.internal:11434",
client_kwargs=shared_kwargs,
validate_model_on_init=False,
)
print("caller's dict: ", shared_kwargs)
print("nested headers: ", caller_headers)
# The same dict reused for a second model whose URL has no credentials at all.
second = ChatOllama(
model="llama3",
base_url="http://localhost:11434",
client_kwargs=shared_kwargs,
validate_model_on_init=False,
)
print("second client sends Authorization:", second._client._client.headers.get("authorization"))
Output on current `master` (`a9780cd`):
caller's dict: {'headers': {'X-Tenant': 'acme', 'Authorization': 'Basic YWxpY2U6czNjcmV0'}}
nested headers: {'X-Tenant': 'acme', 'Authorization': 'Basic YWxpY2U6czNjcmV0'}
second client sends Authorization: Basic YWxpY2U6czNjcmV0
Expected: `client_kwargs` and the dict nested inside it are left as the caller passed them, and the second model — whose `base_url` carries no credentials — sends no `Authorization` header.
Error Message and Stack Trace (if applicable)
No exception is raised. The credential is silently written into the caller's dict.
Description
client_kwargs is documented as configuration the caller supplies ("Additional kwargs to pass to the httpx clients. Pass headers in here."). Constructing a model writes into it instead of reading from it.
Where. All three model classes share the same block in their _set_clients validator:
langchain_ollama/chat_models.py:951
langchain_ollama/llms.py:332
langchain_ollama/embeddings.py:298
client_kwargs = self.client_kwargs or {}
cleaned_url, auth_headers = parse_url_with_auth(self.base_url)
merge_auth_headers(client_kwargs, auth_headers)
self.client_kwargs or {} is not a copy — when the field is a non-empty dict it is the same object the caller passed. merge_auth_headers (langchain_ollama/_utils.py:142) then writes into it, and into the dict nested under "headers":
if auth_headers:
headers = client_kwargs.get("headers", {})
headers.update(auth_headers) # mutates the caller's nested dict
client_kwargs["headers"] = headers
The helper documents itself as in-place, so the defect is that caller-owned data reaches it uncopied.
Three consequences, in increasing order of severity.
-
The caller's client_kwargs gains an Authorization entry it never set, and so does the nested headers dict if they kept a reference to it. Anything that later logs or serialises that config exposes the credential.
-
The credential is also on the model object afterwards, since self.client_kwargs is the mutated dict — so model_dump() and similar carry it.
-
Reusing one client_kwargs dict across instances carries the first URL's credential to every later one. In the example above the second model points at localhost with no credentials in its URL, and its client still sends Alice's Authorization header — a credential for one host transmitted to another. Sharing a single config dict across several models is an ordinary pattern, and nothing in the field's documentation suggests it is unsafe.
Only a non-empty client_kwargs triggers it: an empty or None field makes or {} produce a fresh dict, so the mutation lands somewhere harmless. In other words it misfires exactly when the caller has supplied something worth corrupting.
Proposed fix. Two small changes:
- take a copy at the three call sites —
client_kwargs = dict(self.client_kwargs or {})
- in
merge_auth_headers, build a new mapping instead of updating the caller's nested one — client_kwargs["headers"] = {**client_kwargs.get("headers", {}), **auth_headers}
Both are needed: the shallow copy alone still leaves the nested headers dict shared. That is +6/-6 across the four files, plus regression tests. The module already applies this discipline elsewhere — _convert_messages_to_ollama_messages copies with the comment "shallow copy to avoid mutating caller's list".
Verification of the proposed fix, on master a9780cd in libs/partners/ollama:
- the repro above leaves both dicts untouched and the second client sends no
Authorization
- auth still reaches the client it belongs to: with credentials in
base_url, all three classes still send Authorization alongside the caller's own headers (covered by a test, so the copy cannot be "fixed" into dropping the feature)
- nine regression tests added (three cases across the three classes); six of them fail on current
master and pass with the fix
make test — 105 passed, 2 skipped (96 passed on master, plus the 9 new)
make lint — lint_imports, ruff check, ruff format --diff and ty check all clean on langchain_ollama and tests
pytest -m compile tests/integration_tests — passes
- also ran the auth suite on Python 3.10 and 3.14, and the full unit suite on Python 3.12 with
pydantic~=2.7.0, matching this package's CI matrix
uv.lock untouched
Prior report. This was diagnosed first by Ayaan Gazali (@ayaangazali) in #38899, with the same root cause, the same three affected classes and a fix already prepared on a fork. That issue was closed the same day by the automated triage bot because it carried no issue type, i.e. it had been filed through the API rather than the web form — so it was never read on its merits, and the defect is still present two and a half months later, including in the released langchain-ollama 1.1.0. I am filing this through the web form so the report survives triage. If Ayaan Gazali (@ayaangazali) would like to carry the fix themselves I am glad to step aside; otherwise I would like to be assigned and will open the PR with the change and the regression tests described above.
System Info
master @ a9780cd (2026-09-29)
langchain-ollama 1.1.0 (as released)
Python 3.12.13
langchain-core 1.6.6
Social handles (optional)
No response
Submission checklist
Package (Required)
Related Issues / PRs
#38899 (same defect, reported 2026-07-17 and auto-closed without review)
Reproduction Steps / Example Code (Python)
Error Message and Stack Trace (if applicable)
No exception is raised. The credential is silently written into the caller's dict.Description
client_kwargsis documented as configuration the caller supplies ("Additional kwargs to pass to the httpx clients. Pass headers in here."). Constructing a model writes into it instead of reading from it.Where. All three model classes share the same block in their
_set_clientsvalidator:langchain_ollama/chat_models.py:951langchain_ollama/llms.py:332langchain_ollama/embeddings.py:298self.client_kwargs or {}is not a copy — when the field is a non-empty dict it is the same object the caller passed.merge_auth_headers(langchain_ollama/_utils.py:142) then writes into it, and into the dict nested under"headers":The helper documents itself as in-place, so the defect is that caller-owned data reaches it uncopied.
Three consequences, in increasing order of severity.
The caller's
client_kwargsgains anAuthorizationentry it never set, and so does the nestedheadersdict if they kept a reference to it. Anything that later logs or serialises that config exposes the credential.The credential is also on the model object afterwards, since
self.client_kwargsis the mutated dict — somodel_dump()and similar carry it.Reusing one
client_kwargsdict across instances carries the first URL's credential to every later one. In the example above the second model points atlocalhostwith no credentials in its URL, and its client still sends Alice'sAuthorizationheader — a credential for one host transmitted to another. Sharing a single config dict across several models is an ordinary pattern, and nothing in the field's documentation suggests it is unsafe.Only a non-empty
client_kwargstriggers it: an empty orNonefield makesor {}produce a fresh dict, so the mutation lands somewhere harmless. In other words it misfires exactly when the caller has supplied something worth corrupting.Proposed fix. Two small changes:
client_kwargs = dict(self.client_kwargs or {})merge_auth_headers, build a new mapping instead of updating the caller's nested one —client_kwargs["headers"] = {**client_kwargs.get("headers", {}), **auth_headers}Both are needed: the shallow copy alone still leaves the nested
headersdict shared. That is +6/-6 across the four files, plus regression tests. The module already applies this discipline elsewhere —_convert_messages_to_ollama_messagescopies with the comment "shallow copy to avoid mutating caller's list".Verification of the proposed fix, on
mastera9780cdinlibs/partners/ollama:Authorizationbase_url, all three classes still sendAuthorizationalongside the caller's own headers (covered by a test, so the copy cannot be "fixed" into dropping the feature)masterand pass with the fixmake test— 105 passed, 2 skipped (96 passed on master, plus the 9 new)make lint—lint_imports,ruff check,ruff format --diffandty checkall clean onlangchain_ollamaandtestspytest -m compile tests/integration_tests— passespydantic~=2.7.0, matching this package's CI matrixuv.lockuntouchedPrior report. This was diagnosed first by Ayaan Gazali (@ayaangazali) in #38899, with the same root cause, the same three affected classes and a fix already prepared on a fork. That issue was closed the same day by the automated triage bot because it carried no issue type, i.e. it had been filed through the API rather than the web form — so it was never read on its merits, and the defect is still present two and a half months later, including in the released
langchain-ollama1.1.0. I am filing this through the web form so the report survives triage. If Ayaan Gazali (@ayaangazali) would like to carry the fix themselves I am glad to step aside; otherwise I would like to be assigned and will open the PR with the change and the regression tests described above.System Info
Social handles (optional)
No response