Skip to content

ChatOpenAI (Responses API) mutates model_kwargs["text"] in place: one JSON mode call permanently forces JSON mode on later plain calls #38869

Description

Submission checklist

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-anthropic
  • langchain-classic
  • langchain-core
  • langchain-model-profiles
  • langchain-tests
  • langchain-text-splitters
  • langchain-chroma
  • langchain-deepseek
  • langchain-exa
  • langchain-fireworks
  • langchain-groq
  • langchain-huggingface
  • langchain-mistralai
  • langchain-nomic
  • langchain-ollama
  • langchain-openrouter
  • langchain-perplexity
  • langchain-qdrant
  • langchain-xai
  • Other / not sure / general

Related Issues / PRs

Reproduction Steps / Example Code (Python)

from langchain_openai import ChatOpenAI

# Caller-owned options dict, passed by reference via model_kwargs.
my_text_opts = {"verbosity": "low"}

llm = ChatOpenAI(
    model="gpt-4.1",
    api_key="test",
    use_responses_api=True,
    model_kwargs={"text": my_text_opts},
)

print("caller dict BEFORE any call:        ", my_text_opts)

# Call 1: a JSON mode / structured output request (what with_structured_output
# with method="json_mode" binds: response_format={"type": "json_object"}).
# Offline: only builds the request payload, never touches the network.
payload1 = llm._get_request_payload(
    "give me json", response_format={"type": "json_object"}
)
print("payload1['text'] (expected format): ", payload1["text"])

print("caller dict AFTER structured call:  ", my_text_opts)
print("llm.model_kwargs AFTER:             ", llm.model_kwargs)

# Call 2: a PLAIN call, no response_format anywhere.
payload2 = llm._get_request_payload("just chat normally")
print("payload2['text'] (plain call!):     ", payload2["text"])

assert "format" not in my_text_opts, "caller's dict was mutated in place"

Error Message and Stack Trace (if applicable)

Observed output (langchain-openai 1.3.5 and current master):

caller dict BEFORE any call:         {'verbosity': 'low'}
payload1['text'] (expected format):  {'verbosity': 'low', 'format': {'type': 'json_object'}}
caller dict AFTER structured call:   {'verbosity': 'low', 'format': {'type': 'json_object'}}
llm.model_kwargs AFTER:              {'text': {'verbosity': 'low', 'format': {'type': 'json_object'}}}
payload2['text'] (plain call!):      {'verbosity': 'low', 'format': {'type': 'json_object'}}
AssertionError: caller's dict was mutated in place

Description

With use_responses_api=True, _construct_responses_api_payload writes the response format and verbosity straight into payload["text"]:

  • payload["text"]["format"] = {"type": "json_object"} (JSON mode)
  • payload["text"]["format"] = format_value (json_schema)
  • payload["text"]["verbosity"] = verbosity

(libs/partners/openai/langchain_openai/chat_models/base.py, around lines 4356-4383 on current master.)

The problem is that payload is built by shallow-merging self.model_kwargs (and per-call kwargs), so payload["text"] is the very same dict object the caller passed as model_kwargs={"text": {...}}. Those writes therefore land in the instance's model_kwargs and in the caller's own dict.

What I expect: request payload construction should not modify the model instance or the caller's inputs. Each call's text.format should apply to that call only.

What happens instead: after one JSON mode or structured output call, model_kwargs["text"] permanently contains format: {"type": "json_object"}, so every later plain call on the same instance is silently forced into JSON mode (see payload2 in the repro, which never asked for any response format). The caller's dict is also corrupted, so sharing one options dict across two ChatOpenAI instances spreads the pollution.

The fix looks small: copy the text dict once during payload construction before writing into it, mirroring what was done for the same bug class in langchain-perplexity (#38840). I have the fix and a regression test ready. I'd like to work on this, could you assign it to me?

System Info

System Information

OS: Windows
OS Version: 10.0.26200
Python Version: 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)]

Package Information

langchain_core: 1.4.9
langchain: 1.3.13
langsmith: 0.10.5
langchain_openai: 1.3.5
langgraph_sdk: 0.4.2

Also reproduced against current master (commit 7bf8fe2), where the same in-place writes are present in _construct_responses_api_payload.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugRelated to a bug, vulnerability, unexpected error with an existing featureexternalopenai`langchain-openai` package issues & PRs

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions