Howwise RSS
AI & Automation5 min read

OpenAI vs ChatOpenAI in LangChain: Which Class to Use

Short answer

In the openai vs chatopenai langchain choice, OpenAI wraps the legacy /v1/completions endpoint (string in, string out) and ChatOpenAI wraps /v1/chat/completions (messages in, AIMessage out). Use ChatOpenAI for nearly every modern model.

Key takeaways
  • Use ChatOpenAI for gpt-4o, gpt-4.1, gpt-3.5-turbo and other chat models; OpenAI only fits completion-style models such as gpt-3.5-turbo-instruct.
  • OpenAI.invoke() returns a str, while ChatOpenAI.invoke() returns an AIMessage whose text is in .content.
  • The error 'This is a chat model and not supported in the v1/completions endpoint' means you passed a chat model to the OpenAI class; switch the import to ChatOpenAI.
  • Both classes implement the Runnable interface, so invoke, stream, batch and LCEL pipes work the same way, and StrOutputParser converts an AIMessage to a string.
  • Install langchain-openai and import from langchain_openai; the old langchain.llms and langchain.chat_models paths are legacy.

What is the difference between OpenAI and ChatOpenAI in LangChain?

Both classes live in the langchain-openai package, but they call different OpenAI HTTP endpoints. OpenAI sends a single prompt string to /v1/completions and returns generated text. ChatOpenAI sends a list of role-tagged messages to /v1/chat/completions and returns a message object.

In LangChain's type hierarchy, OpenAI is a BaseLLM (text in, text out) and ChatOpenAI is a BaseChatModel (messages in, AIMessage out). That difference in input and output types is the source of nearly every confusion and error.

AspectOpenAIChatOpenAI
LangChain base classBaseLLMBaseChatModel
OpenAI endpoint/v1/completions (legacy)/v1/chat/completions (or Responses API if enabled)
Inputstr promptlist of messages (a plain str is wrapped as a HumanMessage)
invoke() returnsstrAIMessage (text in .content)
Typical modelsgpt-3.5-turbo-instruct, legacy davinci-style modelsgpt-4o, gpt-4.1, gpt-3.5-turbo, o-series and newer chat models
Tool calling and structured outputNot designed for itSupported (bind_tools, with_structured_output)
Use whenYou specifically need a completion-style modelAlmost always

How do I install and import them correctly?

Both classes now ship in a dedicated partner package. The older imports from langchain.llms and langchain.chat_models, and later langchain_community, are legacy paths that tutorials still show.

Install the package and import from langchain_openai. The API key is read from the OPENAI_API_KEY environment variable unless you pass api_key.

pip install -U langchain-openai
export OPENAI_API_KEY="sk-..."

# Old (avoid in new code)
# from langchain.llms import OpenAI
# from langchain.chat_models import ChatOpenAI

# Current
# from langchain_openai import OpenAI, ChatOpenAI
Tip: If an old tutorial's import fails with a deprecation warning or ImportError, replace the module path with langchain_openai rather than pinning an ancient langchain version.

Side by side: what do invoke() calls return?

The example below runs both classes. The completion model takes a string and returns a string. The chat model takes messages and returns an AIMessage, so you read .content.

Roles map to classes: system (SystemMessage) sets behavior, human (HumanMessage) is the user turn, and ai (AIMessage) is a previous assistant turn. OpenAI's API calls these system, user and assistant. Chat models use this structure so the model can distinguish instructions from user input and keep multi-turn history.

from langchain_openai import OpenAI, ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.output_parsers import StrOutputParser

# Completion-style LLM
llm = OpenAI(model="gpt-3.5-turbo-instruct", temperature=0)
text = llm.invoke("Name one French cheese.")
print(type(text), text)          # <class 'str'> ...

# Chat model
chat = ChatOpenAI(model="gpt-4o-mini", temperature=0)
msg = chat.invoke([
    SystemMessage(content="You answer in one word."),
    HumanMessage(content="Name one French cheese."),
])
print(type(msg), msg.content)    # AIMessage, 'Brie'

# A plain string also works on a chat model
msg2 = chat.invoke("Name one French cheese.")

# Get a string back
chain = chat | StrOutputParser()
print(chain.invoke("Name one French cheese."))   # str

Why do both seem interchangeable in the Quickstart?

Both classes implement LangChain's Runnable interface: invoke, ainvoke, stream, batch. That is why a prompt template piped into either one works with LCEL (prompt | model | parser).

The interchangeability is only on the surface. Output types differ, so downstream code that expects a str breaks when you swap in a chat model unless you add StrOutputParser. Features such as tool calling, structured output and token-by-token message chunks are built on the chat message format, so they exist only on chat models.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
    ("system", "Translate to {lang}."),
    ("human", "{text}"),
])
chain = prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
print(chain.invoke({"lang": "German", "text": "Good morning"}))

for chunk in chain.stream({"lang": "German", "text": "Good morning"}):
    print(chunk, end="")

Why do I get 'This is a chat model and not supported in the v1/completions endpoint'?

The OpenAI class always posts to /v1/completions. Chat models such as gpt-4, gpt-4o and gpt-3.5-turbo are served only on the chat endpoint, so the API rejects the request. Related messages include 'model not found' or 'not supported in the v1/completions endpoint' and, in older versions, a LangChain warning that the OpenAI class is deprecated for gpt-4.

SymptomCauseFix
'This is a chat model and not supported in the v1/completions endpoint'OpenAI(model="gpt-4o") or another chat modelUse ChatOpenAI(model=...)
'This is not a chat model and thus not supported in the v1/chat/completions endpoint'ChatOpenAI(model="gpt-3.5-turbo-instruct")Use OpenAI(model=...), or switch to a chat model
AttributeError: 'str' object has no attribute 'content'Using .content on an OpenAI resultDrop .content, or move to ChatOpenAI
AttributeError: 'AIMessage' object has no attribute 'strip' or similarString code receiving an AIMessageRead .content or add StrOutputParser()
ImportError or deprecation from langchain.llms / langchain.chat_modelsLegacy import pathpip install langchain-openai and import from langchain_openai
  1. Find where the model is created and note the model name string.
  2. If it is a chat model (gpt-4o, gpt-4.1, gpt-3.5-turbo, o-series), change the class to ChatOpenAI.
  3. Update downstream code to read .content, or append StrOutputParser() in the chain.
  4. Switch prompts to ChatPromptTemplate if you want system and human roles.
Tip: Model availability changes. If a model name is rejected, confirm which endpoint it supports in OpenAI's model reference instead of guessing.

Which parameters differ, and what should I use today?

Both accept model (alias model_name in older code), temperature, max_tokens, api_key (alias openai_api_key), timeout and max_retries. Prefer model and api_key in current code. Some newer reasoning models reject temperature or use max_completion_tokens instead of max_tokens, so set only what the model accepts.

Defaults differ by version, so set model and temperature explicitly instead of relying on them. Recent ChatOpenAI versions default to a chat model, while OpenAI has historically defaulted to a legacy completion model; pinning removes the surprise.

Rule of thumb: use ChatOpenAI unless you specifically need a completion-style model such as gpt-3.5-turbo-instruct. The completions endpoint is legacy, and agents, tools, structured output and chat history all assume chat messages.

Two caveats to verify against current LangChain and OpenAI docs: Azure has the same split (AzureChatOpenAI for chat deployments, AzureOpenAI for completion deployments, where you pass your deployment name), and ChatOpenAI can be switched to OpenAI's newer Responses API with use_responses_api=True. With that option the class stays a chat model, so your code still receives AIMessage objects, but the content may be structured blocks rather than a plain string.

from langchain_openai import ChatOpenAI

chat = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    max_tokens=300,
    timeout=30,
    max_retries=2,
)

# Structured output and tools need a chat model
from pydantic import BaseModel
class City(BaseModel):
    name: str
    country: str

print(chat.with_structured_output(City).invoke("Where is the Eiffel Tower?"))

Common questions

Can I pass a plain string to ChatOpenAI?

Yes. LangChain converts a string into a single HumanMessage before calling the chat endpoint. You still get an AIMessage back, so use .content or StrOutputParser to get text.

Is the OpenAI class deprecated?

The class still exists in langchain-openai, but it targets a legacy endpoint and few current models. Treat it as a niche option for completion-style models and default to ChatOpenAI.

Why does ChatOpenAI return an AIMessage instead of a string?

The message carries more than text: response_metadata, usage_metadata (token counts), tool_calls and an id. Returning an object keeps that data available; StrOutputParser discards it when you only want text.

Does gpt-3.5-turbo work with the OpenAI class?

No. gpt-3.5-turbo is a chat model, so use ChatOpenAI. The completion-compatible sibling is gpt-3.5-turbo-instruct, which works with OpenAI.

Do I need to change my chain when switching from OpenAI to ChatOpenAI?

Usually only the class and any code that treats the result as a string. Add StrOutputParser at the end of the chain, and consider ChatPromptTemplate so system and user text are sent as separate roles.

Related topics:langchainopenaichatopenaillmpython