openai[minor]: release 0.3 (#29100)

## Goal

Solve the following problems with `langchain-openai`:

- Structured output with `o1` [breaks out of the
box](https://langchain.slack.com/archives/C050X0VTN56/p1735232400232099).
- `with_structured_output` by default does not use OpenAI’s [structured
output
feature](https://platform.openai.com/docs/guides/structured-outputs).
- We override API defaults for temperature and other parameters.

## Breaking changes:

- Default method for structured output is changing to OpenAI’s dedicated
[structured output
feature](https://platform.openai.com/docs/guides/structured-outputs).
For schemas specified via TypedDict or JSON schema, strict schema
validation is disabled by default but can be enabled by specifying
`strict=True`.
- To recover previous default, pass `method="function_calling"` into
`with_structured_output`.
- Models that don’t support `method="json_schema"` (e.g., `gpt-4` and
`gpt-3.5-turbo`, currently the default model for ChatOpenAI) will raise
an error unless `method` is explicitly specified.
- To recover previous default, pass `method="function_calling"` into
`with_structured_output`.
- Schemas specified via Pydantic `BaseModel` that have fields with
non-null defaults or metadata (like min/max constraints) will raise an
error.
- To recover previous default, pass `method="function_calling"` into
`with_structured_output`.
- `strict` now defaults to False for `method="json_schema"` when schemas
are specified via TypedDict or JSON schema.
- To recover previous behavior, use `with_structured_output(schema,
strict=True)`
- Schemas specified via Pydantic V1 will raise a warning (and use
`method="function_calling"`) unless `method` is explicitly specified.
- To remove the warning, pass `method="function_calling"` into
`with_structured_output`.
- Streaming with default structured output method / Pydantic schema no
longer generates intermediate streamed chunks.
- To recover previous behavior, pass `method="function_calling"` into
`with_structured_output`.
- We no longer override default temperature (was 0.7 in LangChain, now
will follow OpenAI, currently 1.0).
- To recover previous behavior, initialize `ChatOpenAI` or
`AzureChatOpenAI` with `temperature=0.7`.
- Note: conceptually there is a difference between forcing a tool call
and forcing a response format. Tool calls may have more concise
arguments vs. generating content adhering to a schema. Prompts may need
to be adjusted to recover desired behavior.

---------

Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: Bagatur <baskaryan@gmail.com>
This commit is contained in:
ccurme
2025-01-10 10:50:32 -05:00
committed by GitHub
parent facfd42768
commit 6e63ccba84
14 changed files with 912 additions and 295 deletions

View File

@@ -55,6 +55,10 @@ class TestAzureOpenAIStandardLegacy(ChatModelIntegrationTests):
"azure_endpoint": OPENAI_API_BASE,
}
@property
def structured_output_kwargs(self) -> dict:
return {"method": "function_calling"}
@pytest.mark.xfail(reason="Not yet supported.")
def test_usage_metadata_streaming(self, model: BaseChatModel) -> None:
super().test_usage_metadata_streaming(model)

View File

@@ -630,20 +630,39 @@ def test_bind_tools_tool_choice() -> None:
assert not msg.tool_calls
def test_openai_structured_output() -> None:
@pytest.mark.parametrize("model", ["gpt-4o-mini", "o1"])
def test_openai_structured_output(model: str) -> None:
class MyModel(BaseModel):
"""A Person"""
name: str
age: int
llm = ChatOpenAI().with_structured_output(MyModel)
llm = ChatOpenAI(model=model).with_structured_output(MyModel)
result = llm.invoke("I'm a 27 year old named Erick")
assert isinstance(result, MyModel)
assert result.name == "Erick"
assert result.age == 27
def test_structured_output_errors_with_legacy_models() -> None:
class MyModel(BaseModel):
"""A Person"""
name: str
age: int
llm = ChatOpenAI(model="gpt-4").with_structured_output(MyModel)
with pytest.warns(UserWarning, match="with_structured_output"):
with pytest.raises(openai.BadRequestError):
_ = llm.invoke("I'm a 27 year old named Erick")
with pytest.warns(UserWarning, match="with_structured_output"):
with pytest.raises(openai.BadRequestError):
_ = list(llm.stream("I'm a 27 year old named Erick"))
def test_openai_proxy() -> None:
"""Test ChatOpenAI with proxy."""
chat_openai = ChatOpenAI(openai_proxy="http://localhost:8080")
@@ -820,20 +839,18 @@ def test_tool_calling_strict() -> None:
@pytest.mark.parametrize(
("model", "method", "strict"),
[("gpt-4o", "function_calling", True), ("gpt-4o-2024-08-06", "json_schema", None)],
("model", "method"),
[("gpt-4o", "function_calling"), ("gpt-4o-2024-08-06", "json_schema")],
)
def test_structured_output_strict(
model: str,
method: Literal["function_calling", "json_schema"],
strict: Optional[bool],
model: str, method: Literal["function_calling", "json_schema"]
) -> None:
"""Test to verify structured output with strict=True."""
from pydantic import BaseModel as BaseModelProper
from pydantic import Field as FieldProper
llm = ChatOpenAI(model=model, temperature=0)
llm = ChatOpenAI(model=model)
class Joke(BaseModelProper):
"""Joke to tell user."""
@@ -842,10 +859,7 @@ def test_structured_output_strict(
punchline: str = FieldProper(description="answer to resolve the joke")
# Pydantic class
# Type ignoring since the interface only officially supports pydantic 1
# or pydantic.v1.BaseModel but not pydantic.BaseModel from pydantic 2.
# We'll need to do a pass updating the type signatures.
chat = llm.with_structured_output(Joke, method=method, strict=strict)
chat = llm.with_structured_output(Joke, method=method, strict=True)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, Joke)
@@ -854,7 +868,7 @@ def test_structured_output_strict(
# Schema
chat = llm.with_structured_output(
Joke.model_json_schema(), method=method, strict=strict
Joke.model_json_schema(), method=method, strict=True
)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, dict)
@@ -875,14 +889,14 @@ def test_structured_output_strict(
default="foo", description="answer to resolve the joke"
)
chat = llm.with_structured_output(InvalidJoke, method=method, strict=strict)
chat = llm.with_structured_output(InvalidJoke, method=method, strict=True)
with pytest.raises(openai.BadRequestError):
chat.invoke("Tell me a joke about cats.")
with pytest.raises(openai.BadRequestError):
next(chat.stream("Tell me a joke about cats."))
chat = llm.with_structured_output(
InvalidJoke.model_json_schema(), method=method, strict=strict
InvalidJoke.model_json_schema(), method=method, strict=True
)
with pytest.raises(openai.BadRequestError):
chat.invoke("Tell me a joke about cats.")
@@ -890,11 +904,9 @@ def test_structured_output_strict(
next(chat.stream("Tell me a joke about cats."))
@pytest.mark.parametrize(
("model", "method", "strict"), [("gpt-4o-2024-08-06", "json_schema", None)]
)
@pytest.mark.parametrize(("model", "method"), [("gpt-4o-2024-08-06", "json_schema")])
def test_nested_structured_output_strict(
model: str, method: Literal["json_schema"], strict: Optional[bool]
model: str, method: Literal["json_schema"]
) -> None:
"""Test to verify structured output with strict=True for nested object."""
@@ -914,7 +926,7 @@ def test_nested_structured_output_strict(
self_evaluation: SelfEvaluation
# Schema
chat = llm.with_structured_output(JokeWithEvaluation, method=method, strict=strict)
chat = llm.with_structured_output(JokeWithEvaluation, method=method, strict=True)
result = chat.invoke("Tell me a joke about cats.")
assert isinstance(result, dict)
assert set(result.keys()) == {"setup", "punchline", "self_evaluation"}
@@ -927,6 +939,46 @@ def test_nested_structured_output_strict(
assert set(chunk["self_evaluation"].keys()) == {"score", "text"}
@pytest.mark.parametrize(
("strict", "method"),
[
(True, "json_schema"),
(False, "json_schema"),
(True, "function_calling"),
(False, "function_calling"),
],
)
def test_json_schema_openai_format(
strict: bool, method: Literal["json_schema", "function_calling"]
) -> None:
"""Test we can pass in OpenAI schema format specifying strict."""
llm = ChatOpenAI(model="gpt-4o-mini")
schema = {
"name": "get_weather",
"description": "Fetches the weather in the given location",
"strict": strict,
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The location to get the weather for",
},
"unit": {
"type": "string",
"description": "The unit to return the temperature in",
"enum": ["F", "C"],
},
},
"additionalProperties": False,
"required": ["location", "unit"],
},
}
chat = llm.with_structured_output(schema, method=method)
result = chat.invoke("What is the weather in New York?")
assert isinstance(result, dict)
def test_json_mode() -> None:
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = llm.invoke(