Skip to content

Commit 0189c50

Browse files
authored
openai[fix]: Correctly set usage metadata for OpenAI Responses API (#31756)
1 parent 9aa75ea commit 0189c50

File tree

3 files changed

+68
-6
lines changed

3 files changed

+68
-6
lines changed

libs/partners/openai/langchain_openai/chat_models/base.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3271,19 +3271,23 @@ def _create_usage_metadata_responses(oai_token_usage: dict) -> UsageMetadata:
32713271
input_tokens = oai_token_usage.get("input_tokens", 0)
32723272
output_tokens = oai_token_usage.get("output_tokens", 0)
32733273
total_tokens = oai_token_usage.get("total_tokens", input_tokens + output_tokens)
3274-
32753274
output_token_details: dict = {
3276-
"audio": (oai_token_usage.get("completion_tokens_details") or {}).get(
3277-
"audio_tokens"
3278-
),
3279-
"reasoning": (oai_token_usage.get("output_token_details") or {}).get(
3275+
"reasoning": (oai_token_usage.get("output_tokens_details") or {}).get(
32803276
"reasoning_tokens"
3281-
),
3277+
)
3278+
}
3279+
input_token_details: dict = {
3280+
"cache_read": (oai_token_usage.get("input_tokens_details") or {}).get(
3281+
"cached_tokens"
3282+
)
32823283
}
32833284
return UsageMetadata(
32843285
input_tokens=input_tokens,
32853286
output_tokens=output_tokens,
32863287
total_tokens=total_tokens,
3288+
input_token_details=InputTokenDetails(
3289+
**{k: v for k, v in input_token_details.items() if v is not None}
3290+
),
32873291
output_token_details=OutputTokenDetails(
32883292
**{k: v for k, v in output_token_details.items() if v is not None}
32893293
),

libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
"""Standard LangChain interface tests for Responses API"""
22

3+
from pathlib import Path
4+
from typing import cast
5+
36
import pytest
47
from langchain_core.language_models import BaseChatModel
8+
from langchain_core.messages import AIMessage
59

610
from langchain_openai import ChatOpenAI
711
from tests.integration_tests.chat_models.test_base_standard import TestOpenAIStandard
812

13+
REPO_ROOT_DIR = Path(__file__).parents[6]
14+
915

1016
class TestOpenAIResponses(TestOpenAIStandard):
1117
@property
@@ -19,3 +25,35 @@ def chat_model_params(self) -> dict:
1925
@pytest.mark.xfail(reason="Unsupported.")
2026
def test_stop_sequence(self, model: BaseChatModel) -> None:
2127
super().test_stop_sequence(model)
28+
29+
def invoke_with_cache_read_input(self, *, stream: bool = False) -> AIMessage:
30+
with open(REPO_ROOT_DIR / "README.md") as f:
31+
readme = f.read()
32+
33+
input_ = f"""What's langchain? Here's the langchain README:
34+
35+
{readme}
36+
"""
37+
llm = ChatOpenAI(model="gpt-4.1-mini", output_version="responses/v1")
38+
_invoke(llm, input_, stream)
39+
# invoke twice so first invocation is cached
40+
return _invoke(llm, input_, stream)
41+
42+
def invoke_with_reasoning_output(self, *, stream: bool = False) -> AIMessage:
43+
llm = ChatOpenAI(
44+
model="o4-mini",
45+
reasoning={"effort": "medium", "summary": "auto"},
46+
output_version="responses/v1",
47+
)
48+
input_ = "What was the 3rd highest building in 2000?"
49+
return _invoke(llm, input_, stream)
50+
51+
52+
def _invoke(llm: ChatOpenAI, input_: str, stream: bool) -> AIMessage:
53+
if stream:
54+
full = None
55+
for chunk in llm.stream(input_):
56+
full = full + chunk if full else chunk # type: ignore[operator]
57+
return cast(AIMessage, full)
58+
else:
59+
return cast(AIMessage, llm.invoke(input_))

libs/partners/openai/tests/unit_tests/chat_models/test_base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
_convert_message_to_dict,
6161
_convert_to_openai_response_format,
6262
_create_usage_metadata,
63+
_create_usage_metadata_responses,
6364
_format_message_content,
6465
_get_last_messages,
6566
_oai_structured_outputs_parser,
@@ -948,6 +949,25 @@ def test__create_usage_metadata() -> None:
948949
)
949950

950951

952+
def test__create_usage_metadata_responses() -> None:
953+
response_usage_metadata = {
954+
"input_tokens": 100,
955+
"input_tokens_details": {"cached_tokens": 50},
956+
"output_tokens": 50,
957+
"output_tokens_details": {"reasoning_tokens": 10},
958+
"total_tokens": 150,
959+
}
960+
result = _create_usage_metadata_responses(response_usage_metadata)
961+
962+
assert result == UsageMetadata(
963+
output_tokens=50,
964+
input_tokens=100,
965+
total_tokens=150,
966+
input_token_details={"cache_read": 50},
967+
output_token_details={"reasoning": 10},
968+
)
969+
970+
951971
def test__convert_to_openai_response_format() -> None:
952972
# Test response formats that aren't tool-like.
953973
response_format: dict = {

0 commit comments

Comments
 (0)