Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4d4fcf4
Refactoring and stabilizing Harmony/MCP response tests and pin system…
AndreasKaratzas Feb 5, 2026
865882b
Adding a timeout and alternative response for weather tool
AndreasKaratzas Feb 6, 2026
ab97fa1
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 6, 2026
2d3a45b
Increasing debugging verbosity
AndreasKaratzas Feb 6, 2026
81dfafb
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 9, 2026
a32bf26
Fix empty input routing and add missing tool recipient in harmony mes…
AndreasKaratzas Feb 10, 2026
48dcb2c
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 10, 2026
4e650bb
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 12, 2026
0e8737f
Added back the structured content format (input_text type in system m…
AndreasKaratzas Feb 12, 2026
7d07998
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 12, 2026
42ec1d9
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 15, 2026
81e15c1
Stabilize test_parsable_context tests remove hardcoded indices, apply…
AndreasKaratzas Feb 15, 2026
812e665
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 15, 2026
02df1ba
Gate builtin tool execution on requested tool types
AndreasKaratzas Feb 15, 2026
88049a1
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 16, 2026
801b219
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 17, 2026
47a44e6
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 18, 2026
69d1b81
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 19, 2026
1d29d73
Merge remote-tracking branch 'origin/main' into akaratza_refactor_ent…
AndreasKaratzas Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions tests/entrypoints/openai/responses/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations

import json
import logging
from collections.abc import Callable
from typing import Any

import pytest

logger = logging.getLogger(__name__)

BASE_TEST_ENV = {
# The day vLLM said "hello world" on arxiv 🚀
"VLLM_GPT_OSS_SYSTEM_START_DATE": "2023-09-12",
}
DEFAULT_MAX_RETRIES = 3


@pytest.fixture
def pairs_of_event_types() -> dict[str, str]:
Expand All @@ -28,3 +43,159 @@ def pairs_of_event_types() -> dict[str, str]:
}
# fmt: on
return event_pairs


async def retry_for_tool_call(
client,
*,
model: str,
expected_tool_type: str = "function_call",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i think it's better to explicitly set this var, so we shouldn't have a default val

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean pass the default inside the function? Something like max_retries: int = 3?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it should be expected_tool_type: str, so the caller always has to explicitly set it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, yeah, didn't think about that. Will commit that.

max_retries: int = DEFAULT_MAX_RETRIES,
**create_kwargs: Any,
):
"""Call ``client.responses.create`` up to *max_retries* times, returning
the first response that contains an output item of *expected_tool_type*.

Returns the **last** response if none match so the caller's assertions
fire with a clear diagnostic.
"""
last_response = None
for attempt in range(max_retries):
response = await client.responses.create(model=model, **create_kwargs)
last_response = response
if any(
getattr(item, "type", None) == expected_tool_type
for item in response.output
):
return response
assert last_response is not None
return last_response
Comment thread
robertgshaw2-redhat marked this conversation as resolved.


async def retry_streaming_for(
client,
*,
model: str,
validate_events: Callable[[list], bool],
max_retries: int = DEFAULT_MAX_RETRIES,
**create_kwargs: Any,
) -> list:
"""Call ``client.responses.create(stream=True)`` up to *max_retries*
times, returning the first event list where *validate_events* returns
``True``.
"""
last_events: list = []
for attempt in range(max_retries):
stream = await client.responses.create(
model=model, stream=True, **create_kwargs
)
events: list = []
async for event in stream:
events.append(event)
last_events = events
if validate_events(events):
return events
return last_events


def has_output_type(response, type_name: str) -> bool:
"""Return True if *response* has at least one output item of *type_name*."""
return any(getattr(item, "type", None) == type_name for item in response.output)


def events_contain_type(events: list, type_substring: str) -> bool:
"""Return True if any event's type contains *type_substring*."""
return any(type_substring in getattr(e, "type", "") for e in events)


def validate_streaming_event_stack(
events: list, pairs_of_event_types: dict[str, str]
) -> None:
"""Validate that streaming events are properly nested/paired."""
stack: list[str] = []
for event in events:
etype = event.type
if etype == "response.created":
stack.append(etype)
elif etype == "response.completed":
assert stack and stack[-1] == pairs_of_event_types[etype], (
f"Unexpected stack top for {etype}: "
f"got {stack[-1] if stack else '<empty>'}"
)
stack.pop()
elif etype.endswith("added") or etype == "response.mcp_call.in_progress":
stack.append(etype)
elif etype.endswith("delta"):
if stack and stack[-1] == etype:
continue
stack.append(etype)
elif etype.endswith("done") or etype == "response.mcp_call.completed":
assert etype in pairs_of_event_types, f"Unknown done event: {etype}"
expected_start = pairs_of_event_types[etype]
assert stack and stack[-1] == expected_start, (
f"Stack mismatch for {etype}: "
f"expected {expected_start}, "
f"got {stack[-1] if stack else '<empty>'}"
)
stack.pop()
assert len(stack) == 0, f"Unclosed events on stack: {stack}"


def log_response_diagnostics(
response,
*,
label: str = "Response Diagnostics",
) -> dict[str, Any]:
"""Extract and log diagnostic info from a Responses API response.

Logs reasoning, tool-call attempts, MCP items, and output types so
that CI output (``pytest -s`` or ``--log-cli-level=INFO``) gives
full visibility into model behaviour even on passing runs.

Returns the extracted data so callers can make additional assertions
if needed.
"""
reasoning_texts = [
text
for item in response.output
if getattr(item, "type", None) == "reasoning"
for content in getattr(item, "content", [])
if (text := getattr(content, "text", None))
]

tool_call_attempts = [
{
"recipient": msg.get("recipient"),
"channel": msg.get("channel"),
}
for msg in response.output_messages
if (msg.get("recipient") or "").startswith("python")
]

mcp_items = [
{
"name": getattr(item, "name", None),
"status": getattr(item, "status", None),
}
for item in response.output
if getattr(item, "type", None) == "mcp_call"
]

output_types = [getattr(o, "type", None) for o in response.output]

diagnostics = {
"model_attempted_tool_calls": bool(tool_call_attempts),
"tool_call_attempts": tool_call_attempts,
"mcp_items": mcp_items,
"reasoning": reasoning_texts,
"output_text": response.output_text,
"output_types": output_types,
}

logger.info(
"\n====== %s ======\n%s\n==============================",
label,
json.dumps(diagnostics, indent=2, default=str),
)

return diagnostics
Loading
Loading