mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(perplexity): emit search_context_size once per stream in _astream (#39924)
This commit is contained in:
1 parent
7f5b32402d
commit
c9dcf6eb96
2 files changed
+91
-2
No files matched your search
@@ -1318,6 +1318,7 @@ class ChatPerplexity(BaseChatModel):
|
||||
|
||||
added_model_name: bool = False
|
||||
added_search_queries: bool = False
|
||||
added_search_context_size: bool = False
|
||||
async for chunk in stream_resp:
|
||||
if not isinstance(chunk, dict):
|
||||
chunk = chunk.model_dump()
|
||||
@@ -1341,8 +1342,10 @@ class ChatPerplexity(BaseChatModel):
|
||||
if not added_search_queries:
|
||||
generation_info["num_search_queries"] = num_search_queries
|
||||
added_search_queries = True
|
||||
if search_context_size := total_usage.get("search_context_size"):
|
||||
generation_info["search_context_size"] = search_context_size
|
||||
if not added_search_context_size:
|
||||
if search_context_size := total_usage.get("search_context_size"):
|
||||
generation_info["search_context_size"] = search_context_size
|
||||
added_search_context_size = True
|
||||
|
||||
choices = chunk.get("choices") or []
|
||||
if len(choices) == 0:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any, cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -8,6 +9,7 @@ from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AIMessageChunk,
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.runnables import RunnableBinding
|
||||
@@ -159,6 +161,90 @@ def test_perplexity_stream_includes_videos_and_reasoning(mocker: MockerFixture)
|
||||
)
|
||||
|
||||
|
||||
def _usage_bearing_chunks() -> list[dict[str, Any]]:
|
||||
"""Three chunks, each carrying cumulative usage.
|
||||
|
||||
Perplexity reports aggregate usage on every chunk -- that is what the
|
||||
`prev_total_usage` / `subtract_usage` bookkeeping in `_stream` and `_astream`
|
||||
exists for -- so single-valued metadata must only be emitted once.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"model": "sonar",
|
||||
"choices": [{"delta": {"content": "Hello "}, "finish_reason": None}],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 11,
|
||||
"num_search_queries": 2,
|
||||
"search_context_size": "low",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model": "sonar",
|
||||
"choices": [{"delta": {"content": "world"}, "finish_reason": None}],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 2,
|
||||
"total_tokens": 12,
|
||||
"num_search_queries": 2,
|
||||
"search_context_size": "low",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model": "sonar",
|
||||
"choices": [{"delta": {}, "finish_reason": "stop"}],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 3,
|
||||
"total_tokens": 13,
|
||||
"num_search_queries": 2,
|
||||
"search_context_size": "low",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_perplexity_stream_emits_single_valued_usage_metadata_once() -> None:
|
||||
llm = ChatPerplexity(model="sonar", api_key="test", timeout=30)
|
||||
mock_stream = MagicMock()
|
||||
mock_stream.__iter__.return_value = _usage_bearing_chunks()
|
||||
llm.client.chat.completions.create = MagicMock(return_value=mock_stream)
|
||||
|
||||
full: BaseMessageChunk | None = None
|
||||
for chunk in llm.stream("Hello"):
|
||||
full = chunk if full is None else full + chunk
|
||||
|
||||
assert full is not None
|
||||
assert full.response_metadata["search_context_size"] == "low"
|
||||
assert full.response_metadata["num_search_queries"] == 2
|
||||
assert full.response_metadata["model_name"] == "sonar"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_perplexity_astream_emits_single_valued_usage_metadata_once() -> None:
|
||||
"""`search_context_size` is a string, so repeating it concatenates on merge."""
|
||||
llm = ChatPerplexity(model="sonar", api_key="test", timeout=30)
|
||||
|
||||
async def _chunk_iter() -> AsyncIterator[dict[str, Any]]:
|
||||
for chunk in _usage_bearing_chunks():
|
||||
yield chunk
|
||||
|
||||
async def _create(**kwargs: Any) -> AsyncIterator[dict[str, Any]]:
|
||||
return _chunk_iter()
|
||||
|
||||
llm.async_client.chat.completions.create = _create
|
||||
|
||||
full: BaseMessageChunk | None = None
|
||||
async for chunk in llm.astream("Hello"):
|
||||
full = chunk if full is None else full + chunk
|
||||
|
||||
assert full is not None
|
||||
assert full.response_metadata["search_context_size"] == "low"
|
||||
assert full.response_metadata["num_search_queries"] == 2
|
||||
assert full.response_metadata["model_name"] == "sonar"
|
||||
|
||||
|
||||
def test_create_usage_metadata_basic() -> None:
|
||||
"""Test _create_usage_metadata with basic token counts."""
|
||||
token_usage = {
|
||||
|
||||
Reference in new issue
Block a user