Files
wehub-resource-sync 9201ef759e
Harness Compat / harness compat (push) Failing after 0s
CI / test on 3.12 (standard) (push) Has been cancelled
CI / test on 3.13 (standard) (push) Has been cancelled
CI / test on 3.14 (standard) (push) Has been cancelled
CI / test on 3.10 (all-extras) (push) Has been cancelled
CI / test on 3.11 (all-extras) (push) Has been cancelled
CI / test on 3.12 (all-extras) (push) Has been cancelled
CI / test on 3.14 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.10 (pydantic-evals) (push) Has been cancelled
CI / test on 3.11 (pydantic-evals) (push) Has been cancelled
CI / test on 3.12 (pydantic-evals) (push) Has been cancelled
CI / deploy-docs-preview (push) Has been cancelled
CI / build release artifacts (push) Has been cancelled
CI / publish to PyPI (push) Has been cancelled
CI / Send tweet (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / mypy (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / test on 3.10 (standard) (push) Has been cancelled
CI / test on 3.11 (standard) (push) Has been cancelled
CI / test on 3.13 (all-extras) (push) Has been cancelled
CI / test on 3.14 (all-extras) (push) Has been cancelled
CI / test on 3.10 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.11 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.12 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-evals) (push) Has been cancelled
CI / test on 3.14 (pydantic-evals) (push) Has been cancelled
CI / test on 3.10 (lowest-versions) (push) Has been cancelled
CI / test on 3.11 (lowest-versions) (push) Has been cancelled
CI / test on 3.12 (lowest-versions) (push) Has been cancelled
CI / test on 3.13 (lowest-versions) (push) Has been cancelled
CI / test on 3.14 (lowest-versions) (push) Has been cancelled
CI / test examples on 3.11 (push) Has been cancelled
CI / test examples on 3.12 (push) Has been cancelled
CI / test examples on 3.13 (push) Has been cancelled
CI / test examples on 3.14 (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / check (push) Has been cancelled
CI / deploy-docs (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:27:52 +08:00

179 lines
6.9 KiB
Python

from __future__ import annotations as _annotations
from datetime import datetime, timezone
import pytest
from pydantic_ai import (
Agent,
ModelRequest,
ModelResponse,
RunContext,
TextPart,
ThinkingPart,
ToolCallPart,
UserPromptPart,
)
from pydantic_ai.capabilities import Capability
from pydantic_ai.run import AgentRunResult, AgentRunResultEvent
from pydantic_ai.usage import RequestUsage
from .._inline_snapshot import snapshot
from ..conftest import IsDatetime, IsStr, try_import
with try_import() as imports_successful:
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.deepseek import DeepSeekProvider
pytestmark = [
pytest.mark.skipif(not imports_successful(), reason='openai not installed'),
pytest.mark.anyio,
pytest.mark.vcr,
]
async def test_deepseek_deferred_capability_with_thinking(allow_model_requests: None, deepseek_api_key: str):
"""Regression test for #5829: real-API check that deferred capabilities work on a DeepSeek thinking model.
Loading a deferred capability injects a framework-synthesized `search_tools` assistant turn with
tool calls but no thinking; before the fix DeepSeek rejected it with a 400. A successful
recording confirms DeepSeek accepts the empty `reasoning_content` the fix sends. The
deterministic mapping guard is in
`test_openai.py::test_field_mode_thinking_backfill_on_synthetic_tool_search_turn`.
"""
model = OpenAIChatModel('deepseek-reasoner', provider=DeepSeekProvider(api_key=deepseek_api_key))
def roll_dice() -> str:
"""Roll a six-sided die and return the result."""
return '4'
def get_player_name(ctx: RunContext[str]) -> str:
"""Get the player's name."""
return ctx.deps
agent = Agent(
model,
deps_type=str,
instructions=(
"You're a dice game, you should roll the die and see if the number you get back "
"matches the user's guess. If so, tell them they're a winner. Use the player's name "
'in the response.'
),
capabilities=[Capability[str](id='DICE_ROLL', tools=[get_player_name, roll_dice], defer_loading=True)],
)
result = await agent.run('My guess is 4', deps='Anne')
# The run completing at all is the core regression signal — it 400'd before the fix. The
# structural checks make sure the recording exercised the deferred + thinking path rather than
# the model answering directly (which would leave the bug untested).
assert isinstance(result.output, str) and result.output
messages = result.all_messages()
assert any(
isinstance(part, ToolCallPart) and part.tool_name == 'load_capability'
for message in messages
for part in message.parts
), 'expected the model to call `load_capability`; the deferred path was not exercised'
assert any(isinstance(part, ThinkingPart) for message in messages for part in message.parts), (
'expected a `ThinkingPart`; thinking was not exercised, so the reasoning_content round-trip is untested'
)
async def test_deepseek_model_thinking_part(allow_model_requests: None, deepseek_api_key: str):
deepseek_model = OpenAIChatModel('deepseek-reasoner', provider=DeepSeekProvider(api_key=deepseek_api_key))
agent = Agent(model=deepseek_model)
result = await agent.run('How do I cross the street?')
assert result.all_messages() == snapshot(
[
ModelRequest(
parts=[UserPromptPart(content='How do I cross the street?', timestamp=IsDatetime())],
timestamp=IsDatetime(),
run_id=IsStr(),
conversation_id=IsStr(),
),
ModelResponse(
parts=[
ThinkingPart(content=IsStr(), id='reasoning_content', provider_name='deepseek'),
TextPart(content=IsStr()),
],
usage=RequestUsage(
input_tokens=12,
output_tokens=789,
details={
'prompt_cache_hit_tokens': 0,
'prompt_cache_miss_tokens': 12,
'reasoning_tokens': 415,
},
),
model_name='deepseek-reasoner',
timestamp=IsDatetime(),
provider_name='deepseek',
provider_url='https://api.deepseek.com',
provider_details={
'finish_reason': 'stop',
'timestamp': datetime(2025, 4, 22, 14, 9, 11, tzinfo=timezone.utc),
},
provider_response_id='181d9669-2b3a-445e-bd13-2ebff2c378f6',
finish_reason='stop',
run_id=IsStr(),
conversation_id=IsStr(),
),
]
)
async def test_deepseek_model_thinking_stream(allow_model_requests: None, deepseek_api_key: str):
deepseek_model = OpenAIChatModel('deepseek-reasoner', provider=DeepSeekProvider(api_key=deepseek_api_key))
agent = Agent(model=deepseek_model)
result: AgentRunResult | None = None
async with agent.run_stream_events(user_prompt='How do I cross the street?') as event_stream:
async for event in event_stream:
if isinstance(event, AgentRunResultEvent):
result = event.result
assert result is not None
assert result.all_messages() == snapshot(
[
ModelRequest(
parts=[
UserPromptPart(
content='How do I cross the street?',
timestamp=IsDatetime(),
)
],
timestamp=IsDatetime(),
run_id=IsStr(),
conversation_id=IsStr(),
),
ModelResponse(
parts=[
ThinkingPart(
content=IsStr(),
id='reasoning_content',
provider_name='deepseek',
),
TextPart(content='Hello there! 😊 How can I help you today?'),
],
usage=RequestUsage(
input_tokens=6,
output_tokens=212,
details={'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 6, 'reasoning_tokens': 198},
),
model_name='deepseek-reasoner',
timestamp=IsDatetime(),
provider_name='deepseek',
provider_url='https://api.deepseek.com',
provider_details={
'finish_reason': 'stop',
'timestamp': datetime(2025, 7, 10, 17, 41, 44, tzinfo=timezone.utc),
},
provider_response_id='33be18fc-3842-486c-8c29-dd8e578f7f20',
finish_reason='stop',
run_id=IsStr(),
conversation_id=IsStr(),
),
]
)